From 2d3804908c99803eb0cd3cf9094f4881ad2e1a72 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Wed, 7 Oct 2026 00:29:02 +0900 Subject: [PATCH 1/4] fix: separate following with statements from import attributes Co-authored-by: WillBooster (Codex CLI) --- src/scanner.c | 3 +- test/unit/importAttributeBoundary.test.ts | 63 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 test/unit/importAttributeBoundary.test.ts diff --git a/src/scanner.c b/src/scanner.c index 807d9c1..c4c5554 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -443,7 +443,8 @@ static bool scan_after_line_break(TSLexer *lexer, bool after_block_arrow, LineBr case LINE_BREAK_AFTER_MODIFIER_WORD: return !before_slash && (lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->eof(lexer)); case LINE_BREAK_BEFORE_IMPORT_ATTRIBUTES: - return before_slash || !scan_word(lexer, "with"); + if (before_slash || !scan_word(lexer, "with")) return true; + return scan_whitespace_and_comments(lexer, scanned_content, true, true) == REJECT || lexer->lookahead != '{'; case LINE_BREAK_AFTER_ACCESSOR_WORD: return !before_slash && (lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*' || lexer->eof(lexer)); diff --git a/test/unit/importAttributeBoundary.test.ts b/test/unit/importAttributeBoundary.test.ts new file mode 100644 index 0000000..7f5e8ba --- /dev/null +++ b/test/unit/importAttributeBoundary.test.ts @@ -0,0 +1,63 @@ +import { Edit, Language, Parser } from '@willbooster/web-tree-sitter'; +import { expect, test } from 'vitest'; + +test('distinguishes import attributes from following with statements', async () => { + await Parser.init(); + const parser = new Parser().setLanguage(await Language.load('tree-sitter-javascript.wasm')); + try { + for (const declaration of ['import { A }', 'import A', 'export { A }', 'export *', 'export * as ns']) { + for (const trivia of [' ', '\n', ' /* c */ ', ' // c\n']) { + const prefix = `${declaration} from "m"\n`; + for (const [suffix, attribute] of [ + ['(x) { use(x); }', false], + ['{ type: "json" };', true], + ] as const) { + const source = `${prefix}with${trivia}${suffix}\nconst after = 1;`; + const tree = parser.parse(source)!; + try { + expect(tree.rootNode.hasError, source).toBe(false); + const statements = tree.rootNode.namedChildren.filter((node) => node.type !== 'comment'); + expect(statements[0]?.descendantsOfType('import_attribute')).toHaveLength(attribute ? 1 : 0); + expect(statements[attribute ? 1 : 2]?.text).toBe('const after = 1;'); + if (!attribute) expect(statements[1]?.type).toBe('with_statement'); + const index = prefix.length; + const end = source.indexOf('\nconst after'); + const replacement = `with${trivia}${attribute ? '(x) { use(x); }' : '{ type: "json" };'}`; + const updated = source.slice(0, index) + replacement + source.slice(end); + tree.edit( + new Edit({ + startIndex: index, + oldEndIndex: end, + newEndIndex: index + replacement.length, + startPosition: position(source, index), + oldEndPosition: position(source, end), + newEndPosition: position(updated, index + replacement.length), + }) + ); + const incremental = parser.parse(updated, tree)!; + const fresh = parser.parse(updated)!; + try { + expect(incremental.rootNode.toString()).toBe(fresh.rootNode.toString()); + expect(incremental.rootNode.hasError).toBe(false); + expect(incremental.rootNode.namedChildren[0]?.descendantsOfType('import_attribute')).toHaveLength( + attribute ? 0 : 1 + ); + } finally { + incremental.delete(); + fresh.delete(); + } + } finally { + tree.delete(); + } + } + } + } + } finally { + parser.delete(); + } +}); + +function position(source: string, index: number): { row: number; column: number } { + const lines = source.slice(0, index).split('\n'); + return { row: lines.length - 1, column: lines.at(-1)!.length }; +} From 2a340250c96a41d82fe5e0235023b201e789e1d3 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Wed, 7 Oct 2026 00:40:26 +0900 Subject: [PATCH 2/4] fix: preserve HTML trivia before import attributes Co-authored-by: WillBooster (Codex CLI) --- src/scanner.c | 2 +- test/unit/importAttributeBoundary.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scanner.c b/src/scanner.c index c4c5554..35e2537 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -444,7 +444,7 @@ static bool scan_after_line_break(TSLexer *lexer, bool after_block_arrow, LineBr return !before_slash && (lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->eof(lexer)); case LINE_BREAK_BEFORE_IMPORT_ATTRIBUTES: if (before_slash || !scan_word(lexer, "with")) return true; - return scan_whitespace_and_comments(lexer, scanned_content, true, true) == REJECT || lexer->lookahead != '{'; + return !scan_default_trivia(lexer, true) || lexer->lookahead != '{'; case LINE_BREAK_AFTER_ACCESSOR_WORD: return !before_slash && (lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*' || lexer->eof(lexer)); diff --git a/test/unit/importAttributeBoundary.test.ts b/test/unit/importAttributeBoundary.test.ts index 7f5e8ba..28f1f0c 100644 --- a/test/unit/importAttributeBoundary.test.ts +++ b/test/unit/importAttributeBoundary.test.ts @@ -6,7 +6,7 @@ test('distinguishes import attributes from following with statements', async () const parser = new Parser().setLanguage(await Language.load('tree-sitter-javascript.wasm')); try { for (const declaration of ['import { A }', 'import A', 'export { A }', 'export *', 'export * as ns']) { - for (const trivia of [' ', '\n', ' /* c */ ', ' // c\n']) { + for (const trivia of [' ', '\n', ' /* c */ ', ' // c\n', ' c\n', ' /* a */ c\n', ' /* a */ c\n', ' /* a */