From f36f426fdbf5888c8ef4b6eb28ba4b174fde8822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Wed, 8 Apr 2026 10:19:21 +0200 Subject: [PATCH 1/6] [dart_support] add dart support --- src/cli/refs.ts | 1 + src/source-parser.ts | 163 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/src/cli/refs.ts b/src/cli/refs.ts index 77b8949..d5f3826 100644 --- a/src/cli/refs.ts +++ b/src/cli/refs.ts @@ -43,6 +43,7 @@ const SOURCE_EXTS = new Set([ '.go', '.c', '.h', + '.dart', ]); /** diff --git a/src/source-parser.ts b/src/source-parser.ts index 46f95d8..f4b8c20 100644 --- a/src/source-parser.ts +++ b/src/source-parser.ts @@ -60,6 +60,7 @@ const grammarMap: Record = { '.go': 'tree-sitter-go.wasm', '.c': 'tree-sitter-c.wasm', '.h': 'tree-sitter-c.wasm', + '.dart': 'tree-sitter-dart.wasm', }; /** All source file extensions that lat can parse (derived from grammarMap). */ @@ -517,6 +518,165 @@ function extractGoSymbols(tree: Tree): SourceSymbol[] { return symbols; } +/** + * Extract the name from a Dart node. The tree-sitter-dart grammar doesn't + * always expose a `name` field — for mixin_declaration the name is a plain + * `identifier` child. For method_signature the name lives inside a nested + * function_signature. This helper tries the field first, then falls back + * to the first `identifier` named child. + */ +function dartName(node: SyntaxNode): string | null { + const field = node.childForFieldName('name'); + if (field) return field.text; + const ident = node.namedChildren.find((c) => c.type === 'identifier'); + return ident ? ident.text : null; +} + +function extractDartSymbols(tree: Tree): SourceSymbol[] { + const symbols: SourceSymbol[] = []; + const root = tree.rootNode; + + for (let i = 0; i < root.childCount; i++) { + const node = root.child(i)!; + const startLine = node.startPosition.row + 1; + const endLine = node.endPosition.row + 1; + + if (node.type === 'function_signature') { + const name = dartName(node); + if (name) { + symbols.push({ + name, + kind: 'function', + startLine, + endLine, + signature: firstLine(node.text), + }); + } + } else if (node.type === 'class_definition') { + const name = dartName(node); + if (name) { + symbols.push({ + name, + kind: 'class', + startLine, + endLine, + signature: firstLine(node.text), + }); + const body = node.childForFieldName('body'); + if (body) { + extractDartClassMembers(body, name, symbols); + } + } + } else if (node.type === 'mixin_declaration') { + const name = dartName(node); + if (name) { + symbols.push({ + name, + kind: 'interface', + startLine, + endLine, + signature: firstLine(node.text), + }); + const body = node.namedChildren.find((c) => c.type === 'class_body'); + if (body) { + extractDartClassMembers(body, name, symbols); + } + } + } else if (node.type === 'extension_declaration') { + const name = dartName(node); + if (name) { + symbols.push({ + name, + kind: 'class', + startLine, + endLine, + signature: firstLine(node.text), + }); + const body = node.namedChildren.find((c) => c.type === 'class_body'); + if (body) { + extractDartClassMembers(body, name, symbols); + } + } + } else if (node.type === 'enum_declaration') { + const name = dartName(node); + if (name) { + symbols.push({ + name, + kind: 'class', + startLine, + endLine, + signature: firstLine(node.text), + }); + const body = node.namedChildren.find( + (c) => c.type === 'enum_body' || c.type === 'class_body', + ); + if (body) { + extractDartClassMembers(body, name, symbols); + } + } + } else if (node.type === 'static_final_declaration_list') { + // Top-level `final x = ...` or `const x = ...` — the list contains + // static_final_declaration children, each with an identifier. + for (let j = 0; j < node.namedChildCount; j++) { + const decl = node.namedChild(j)!; + if (decl.type === 'static_final_declaration') { + const name = dartName(decl); + if (name) { + symbols.push({ + name, + kind: 'variable', + startLine: decl.startPosition.row + 1, + endLine: decl.endPosition.row + 1, + signature: firstLine(decl.text), + }); + } + } + } + } + } + + return symbols; +} + +function extractDartClassMembers( + body: SyntaxNode, + className: string, + symbols: SourceSymbol[], +): void { + for (let i = 0; i < body.namedChildCount; i++) { + const member = body.namedChild(i)!; + if (member.type === 'method_signature') { + // method_signature wraps a function_signature child that holds the name + const funcSig = member.namedChildren.find( + (c) => c.type === 'function_signature', + ); + const name = funcSig ? dartName(funcSig) : null; + if (name) { + symbols.push({ + name, + kind: 'method', + parent: className, + startLine: member.startPosition.row + 1, + endLine: member.endPosition.row + 1, + signature: firstLine(member.text), + }); + } + } else if (member.type === 'function_signature') { + const name = dartName(member); + if (name) { + symbols.push({ + name, + kind: 'method', + parent: className, + startLine: member.startPosition.row + 1, + endLine: member.endPosition.row + 1, + signature: firstLine(member.text), + }); + } + } + } +} + /** * Extract the declarator name from a C function_declarator node. * Handles plain identifiers and pointer declarators (*name). @@ -862,6 +1022,9 @@ export async function parseSourceSymbols( if (ext === '.c' || ext === '.h') { return extractCSymbols(tree); } + if (ext === '.dart') { + return extractDartSymbols(tree); + } return extractTsSymbols(tree); } finally { tree.delete(); From 5458f5a53c442bd1f0e7c70f42e1bfc7629f81c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Wed, 8 Apr 2026 10:19:29 +0200 Subject: [PATCH 2/6] [dart_support] add tests --- tests/cases.test.ts | 36 +++++++++++++++++++ .../lat.md/docs.md | 9 +++++ .../src/app.dart | 15 ++++++++ .../source-ref-dart-valid/lat.md/docs.md | 13 +++++++ .../cases/source-ref-dart-valid/src/app.dart | 25 +++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 tests/cases/error-source-ref-dart-missing/lat.md/docs.md create mode 100644 tests/cases/error-source-ref-dart-missing/src/app.dart create mode 100644 tests/cases/source-ref-dart-valid/lat.md/docs.md create mode 100644 tests/cases/source-ref-dart-valid/src/app.dart diff --git a/tests/cases.test.ts b/tests/cases.test.ts index b9e0bb8..0ab8158 100644 --- a/tests/cases.test.ts +++ b/tests/cases.test.ts @@ -900,6 +900,42 @@ describe('error-source-ref-go-missing', () => { }); }); +describe('source-ref-dart-valid', () => { + it('resolves Dart function, class, method, mixin, enum, and variable refs without errors', async () => { + // docs.md links: greet (func), Greeter (class), Greeter#greet (method), + // createGreeter (func), Greeting (mixin), defaultName (var), Color (enum) + const { errors } = await checkMd(latDir('source-ref-dart-valid')); + expect(errors).toHaveLength(0); + }); +}); + +describe('error-source-ref-dart-missing', () => { + it('check md reports all missing Dart symbols', async () => { + const { errors } = await checkMd(latDir('error-source-ref-dart-missing')); + expect(errors).toHaveLength(4); + + const byTarget = new Map(errors.map((e) => [e.target, e])); + + const fn = byTarget.get('src/app.dart#nonexistent')!; + expect(fn).toBeDefined(); + expect(fn.message).toContain('symbol "nonexistent" not found'); + + const cls = byTarget.get('src/app.dart#MissingClass')!; + expect(cls).toBeDefined(); + expect(cls.message).toContain('symbol "MissingClass" not found'); + + const cnst = byTarget.get('src/app.dart#MISSING_CONST')!; + expect(cnst).toBeDefined(); + expect(cnst.message).toContain('symbol "MISSING_CONST" not found'); + + const method = byTarget.get('src/app.dart#Greeter#missingMethod')!; + expect(method).toBeDefined(); + expect(method.message).toContain( + 'symbol "Greeter#missingMethod" not found', + ); + }); +}); + describe('source-ref-c-valid', () => { // @lat: [[tests/check-md#Passes with valid links#Passes with C enum value links]] it('resolves C function, struct, struct field, enum, typedef, define, variable, pointer-returning, and array refs without errors', async () => { diff --git a/tests/cases/error-source-ref-dart-missing/lat.md/docs.md b/tests/cases/error-source-ref-dart-missing/lat.md/docs.md new file mode 100644 index 0000000..c917692 --- /dev/null +++ b/tests/cases/error-source-ref-dart-missing/lat.md/docs.md @@ -0,0 +1,9 @@ +# Docs + +Missing function: [[src/app.dart#nonexistent]]. + +Missing class: [[src/app.dart#MissingClass]]. + +Missing const: [[src/app.dart#MISSING_CONST]]. + +Missing method: [[src/app.dart#Greeter#missingMethod]]. diff --git a/tests/cases/error-source-ref-dart-missing/src/app.dart b/tests/cases/error-source-ref-dart-missing/src/app.dart new file mode 100644 index 0000000..acb90a3 --- /dev/null +++ b/tests/cases/error-source-ref-dart-missing/src/app.dart @@ -0,0 +1,15 @@ +String greet(String name) { + return 'Hello, $name!'; +} + +class Greeter { + final String prefix; + + Greeter(this.prefix); + + String greet(String name) { + return '$prefix $name!'; + } +} + +final defaultName = 'World'; diff --git a/tests/cases/source-ref-dart-valid/lat.md/docs.md b/tests/cases/source-ref-dart-valid/lat.md/docs.md new file mode 100644 index 0000000..0f9cfa0 --- /dev/null +++ b/tests/cases/source-ref-dart-valid/lat.md/docs.md @@ -0,0 +1,13 @@ +# Docs + +See [[src/app.dart#greet]] for the greeting function. + +The [[src/app.dart#Greeter]] class has a method [[src/app.dart#Greeter#greet]]. + +Create one with [[src/app.dart#createGreeter]]. + +The [[src/app.dart#Greeting]] mixin defines shared behavior. + +Default name is [[src/app.dart#defaultName]]. + +The [[src/app.dart#Color]] enum lists available colors. diff --git a/tests/cases/source-ref-dart-valid/src/app.dart b/tests/cases/source-ref-dart-valid/src/app.dart new file mode 100644 index 0000000..f914576 --- /dev/null +++ b/tests/cases/source-ref-dart-valid/src/app.dart @@ -0,0 +1,25 @@ +String greet(String name) { + return 'Hello, $name!'; +} + +class Greeter { + final String prefix; + + Greeter(this.prefix); + + String greet(String name) { + return '$prefix $name!'; + } +} + +Greeter createGreeter(String prefix) { + return Greeter(prefix); +} + +mixin Greeting { + String hello() => 'Hi there!'; +} + +final defaultName = 'World'; + +enum Color { red, green, blue } From 332074c16d70cdbb3d4bddbbc5a9bdf7aa84acc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Wed, 8 Apr 2026 10:20:07 +0200 Subject: [PATCH 3/6] [dart_support] update docs --- AGENTS.md | 4 ++-- CLAUDE.md | 4 ++-- lat.md/markdown.md | 7 +++++-- templates/AGENTS.md | 4 ++-- templates/cursor-rules.md | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9c015aa..c699b6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,8 @@ If `lat search` fails because no API key is configured, explain to the user that - **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). - **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. -- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. -- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C/Dart files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct), `[[src/app.dart#Greeter#greet]]` (Dart method). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C/Dart) or `# @lat: [[section-id]]` (Python) — ties source code to concepts # Test specs diff --git a/CLAUDE.md b/CLAUDE.md index 9c015aa..c699b6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,8 +36,8 @@ If `lat search` fails because no API key is configured, explain to the user that - **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). - **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. -- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. -- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C/Dart files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct), `[[src/app.dart#Greeter#greet]]` (Dart method). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C/Dart) or `# @lat: [[section-id]]` (Python) — ties source code to concepts # Test specs diff --git a/lat.md/markdown.md b/lat.md/markdown.md index bdb21b1..0113833 100644 --- a/lat.md/markdown.md +++ b/lat.md/markdown.md @@ -32,7 +32,7 @@ Resolution is handled by [[src/lattice.ts#resolveRef]]. See [[parser#Short Ref R ### Source Code Links -Wiki links can reference symbols in TypeScript, JavaScript, Python, Rust, Go, and C source files: +Wiki links can reference symbols in TypeScript, JavaScript, Python, Rust, Go, C, and Dart source files: - **`[[src/config.ts#getConfigDir]]`** — the `getConfigDir` function in `src/config.ts` - **`[[src/server.ts#App#listen]]`** — the `listen` method on class `App` in `src/server.ts` @@ -40,9 +40,10 @@ Wiki links can reference symbols in TypeScript, JavaScript, Python, Rust, Go, an - **`[[src/app.go#Greeter#Greet]]`** — the `Greet` method on type `Greeter` in Go - **`[[src/app.h#Greeter]]`** — the `Greeter` struct in a C header - **`[[src/app.h#Greeter#prefix]]`** — the `prefix` field of struct `Greeter` in C +- **`[[src/app.dart#Greeter#greet]]`** — the `greet` method on class `Greeter` in Dart - **`[[src/config.ts]]`** — link to the file itself (no symbol) -Supported extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.rs`, `.go`, `.c`, `.h`. +Supported extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.rs`, `.go`, `.c`, `.h`, `.dart`. Python symbols: functions, classes, methods, module-level variables. Decorated definitions (`@decorator`) are unwrapped transparently — `[[file.py#my_func]]` resolves whether or not `my_func` has decorators, and `# @lat:` comments placed between decorators and the `def`/`class` line are scanned normally. @@ -52,6 +53,8 @@ Go symbols: functions, types (structs, interfaces, type aliases), methods (with C symbols: functions (including pointer-returning like `char *func()`), structs, struct fields/members, enums, enum values (including anonymous enums and `typedef enum` members), typedefs, `#define` macros (both object-like and function-like), variables (including arrays). Struct fields are resolved via the parent struct — `[[file.h#Struct#field]]` matches any `field_declaration` inside `struct Struct { ... }`, including fields nested inside anonymous unions and structs. Enum values can be referenced standalone (`[[file.h#GREEN]]`) or qualified by their enum name (`[[file.h#Color#GREEN]]`); both forms work for named enums, `typedef enum`, and named `typedef enum`. Both `.c` and `.h` files are supported — include guards (`#ifndef`/`#endif`) are walked through transparently. +Dart symbols: functions, classes, methods, mixins, enums, extensions, top-level variables. Methods are resolved inside the class body — `[[file.dart#Class#method]]` matches any method declaration inside `class Class { ... }`. Mixins are emitted as interface-kind symbols. + Source code is parsed lazily with tree-sitter (via `web-tree-sitter`). Only files referenced by wiki links are parsed — no up-front scanning. [[cli#check#md]] validates that the file exists and the symbol is defined. ### Strict vs Lenient Contexts diff --git a/templates/AGENTS.md b/templates/AGENTS.md index c544150..b98afc5 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -35,8 +35,8 @@ If `lat search` fails because no API key is configured, explain to the user that - **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). - **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. -- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. -- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C/Dart files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct), `[[src/app.dart#Greeter#greet]]` (Dart method). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C/Dart) or `# @lat: [[section-id]]` (Python) — ties source code to concepts # Test specs diff --git a/templates/cursor-rules.md b/templates/cursor-rules.md index 7bab9a1..ed0e298 100644 --- a/templates/cursor-rules.md +++ b/templates/cursor-rules.md @@ -33,8 +33,8 @@ If `lat_search` fails because `LAT_LLM_KEY` is not set, explain to the user that - **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). - **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. -- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. -- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C/Dart files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct), `[[src/app.dart#Greeter#greet]]` (Dart method). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C/Dart) or `# @lat: [[section-id]]` (Python) — ties source code to concepts # Test specs From 79d13faae708c556c5f1ce1f32be5fc656ac2c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 16 Apr 2026 10:43:39 +0200 Subject: [PATCH 4/6] [dart_support] add support for dot shorthands --- src/source-parser.ts | 34 +++++++++++++++++++ tests/cases.test.ts | 12 +++++++ .../lat.md/docs.md | 3 ++ .../src/app.dart | 13 +++++++ 4 files changed, 62 insertions(+) create mode 100644 tests/cases/source-ref-dart-error-recovery/lat.md/docs.md create mode 100644 tests/cases/source-ref-dart-error-recovery/src/app.dart diff --git a/src/source-parser.ts b/src/source-parser.ts index f4b8c20..df5a1e4 100644 --- a/src/source-parser.ts +++ b/src/source-parser.ts @@ -614,6 +614,40 @@ function extractDartSymbols(tree: Tree): SourceSymbol[] { extractDartClassMembers(body, name, symbols); } } + } else if (node.type === 'ERROR') { + // Graceful degradation: when the grammar cannot fully parse a node + // (e.g. Dart 3.7+ dot shorthand), extract at least the top-level + // declaration name from the raw text so code refs still resolve. + const text = node.text; + const m = text.match( + /^(?:abstract\s+)?(?:class|mixin|enum|extension(?:\s+type)?)\s+(\w+)/, + ); + if (m) { + const kind = + text.match(/^(?:abstract\s+)?class\s/) || text.match(/^enum\s/) + ? 'class' + : text.match(/^mixin\s/) + ? 'interface' + : 'class'; + symbols.push({ + name: m[1], + kind, + startLine, + endLine, + signature: firstLine(text), + }); + } else { + const fm = text.match(/^(?:\w+\s+)*(\w+)\s*\(/); + if (fm) { + symbols.push({ + name: fm[1], + kind: 'function', + startLine, + endLine, + signature: firstLine(text), + }); + } + } } else if (node.type === 'static_final_declaration_list') { // Top-level `final x = ...` or `const x = ...` — the list contains // static_final_declaration children, each with an identifier. diff --git a/tests/cases.test.ts b/tests/cases.test.ts index 0ab8158..6c304cb 100644 --- a/tests/cases.test.ts +++ b/tests/cases.test.ts @@ -909,6 +909,18 @@ describe('source-ref-dart-valid', () => { }); }); +describe('source-ref-dart-error-recovery', () => { + it('recovers class names from Dart files with parse errors', async () => { + // app.dart uses Dart 3.7 dot shorthand (.start) which tree-sitter-dart + // cannot parse — the class becomes an ERROR node. The fallback regex + // should still extract the class name so the wiki link resolves. + const { errors } = await checkMd( + latDir('source-ref-dart-error-recovery'), + ); + expect(errors).toHaveLength(0); + }); +}); + describe('error-source-ref-dart-missing', () => { it('check md reports all missing Dart symbols', async () => { const { errors } = await checkMd(latDir('error-source-ref-dart-missing')); diff --git a/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md b/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md new file mode 100644 index 0000000..328ed6a --- /dev/null +++ b/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md @@ -0,0 +1,3 @@ +# Docs + +The [[src/app.dart#MyWidget]] class uses Dart 3.7 dot shorthand that the grammar cannot fully parse. diff --git a/tests/cases/source-ref-dart-error-recovery/src/app.dart b/tests/cases/source-ref-dart-error-recovery/src/app.dart new file mode 100644 index 0000000..cb09d5f --- /dev/null +++ b/tests/cases/source-ref-dart-error-recovery/src/app.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: .start, + children: [ + const Text('hello'), + ], + ); +} From eb05b61b7e70d6f81f6bf1fe4bb0d0e9343a7d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Wed, 22 Apr 2026 09:12:13 +0200 Subject: [PATCH 5/6] [dart_support] cleanup dart support with updated tree-sitter-wasms library --- package.json | 2 +- pnpm-lock.yaml | 10 +++--- src/source-parser.ts | 34 ------------------- tests/cases.test.ts | 15 ++------ .../lat.md/docs.md | 3 -- .../src/app.dart | 13 ------- .../source-ref-dart-valid/lat.md/docs.md | 2 ++ .../cases/source-ref-dart-valid/src/app.dart | 4 +++ 8 files changed, 14 insertions(+), 69 deletions(-) delete mode 100644 tests/cases/source-ref-dart-error-recovery/lat.md/docs.md delete mode 100644 tests/cases/source-ref-dart-error-recovery/src/app.dart diff --git a/package.json b/package.json index 9edbd4c..34722fb 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@folder/xdg": "^4.0.1", "@libsql/client": "^0.17.0", "@modelcontextprotocol/sdk": "^1.27.1", - "@repomix/tree-sitter-wasms": "^0.1.16", + "@repomix/tree-sitter-wasms": "^0.1.17", "commander": "^14.0.3", "ignore-walk": "^8.0.0", "mdast-util-to-markdown": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3759de..b8775a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.27.1 version: 1.27.1(zod@4.3.6) '@repomix/tree-sitter-wasms': - specifier: ^0.1.16 - version: 0.1.16 + specifier: ^0.1.17 + version: 0.1.17 commander: specifier: ^14.0.3 version: 14.0.3 @@ -316,8 +316,8 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@repomix/tree-sitter-wasms@0.1.16': - resolution: {integrity: sha512-CIINozBWFwjhH4DQALN/b4n1S08fHhXQOdjX2G7s4w+Urew37aLU0AHVyCjHM5Pbnh63tDYt4YyUkS6vRUV38A==} + '@repomix/tree-sitter-wasms@0.1.17': + resolution: {integrity: sha512-tc3HnFqdMF1pXhIMzG3aTaBDpIiHK2tPfn3fwqA6P3WTbHa+1EuuTubbKshvmN7xCHP5Ojz0/VW4R+XvR88KOw==} '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} @@ -1547,7 +1547,7 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@repomix/tree-sitter-wasms@0.1.16': {} + '@repomix/tree-sitter-wasms@0.1.17': {} '@rollup/rollup-android-arm-eabi@4.59.0': optional: true diff --git a/src/source-parser.ts b/src/source-parser.ts index df5a1e4..f4b8c20 100644 --- a/src/source-parser.ts +++ b/src/source-parser.ts @@ -614,40 +614,6 @@ function extractDartSymbols(tree: Tree): SourceSymbol[] { extractDartClassMembers(body, name, symbols); } } - } else if (node.type === 'ERROR') { - // Graceful degradation: when the grammar cannot fully parse a node - // (e.g. Dart 3.7+ dot shorthand), extract at least the top-level - // declaration name from the raw text so code refs still resolve. - const text = node.text; - const m = text.match( - /^(?:abstract\s+)?(?:class|mixin|enum|extension(?:\s+type)?)\s+(\w+)/, - ); - if (m) { - const kind = - text.match(/^(?:abstract\s+)?class\s/) || text.match(/^enum\s/) - ? 'class' - : text.match(/^mixin\s/) - ? 'interface' - : 'class'; - symbols.push({ - name: m[1], - kind, - startLine, - endLine, - signature: firstLine(text), - }); - } else { - const fm = text.match(/^(?:\w+\s+)*(\w+)\s*\(/); - if (fm) { - symbols.push({ - name: fm[1], - kind: 'function', - startLine, - endLine, - signature: firstLine(text), - }); - } - } } else if (node.type === 'static_final_declaration_list') { // Top-level `final x = ...` or `const x = ...` — the list contains // static_final_declaration children, each with an identifier. diff --git a/tests/cases.test.ts b/tests/cases.test.ts index 6c304cb..580b64c 100644 --- a/tests/cases.test.ts +++ b/tests/cases.test.ts @@ -903,24 +903,13 @@ describe('error-source-ref-go-missing', () => { describe('source-ref-dart-valid', () => { it('resolves Dart function, class, method, mixin, enum, and variable refs without errors', async () => { // docs.md links: greet (func), Greeter (class), Greeter#greet (method), - // createGreeter (func), Greeting (mixin), defaultName (var), Color (enum) + // createGreeter (func), Greeting (mixin), defaultName (var), Color (enum), + // DotShorthand (class using Dart 3.7 dot shorthand), DotShorthand#pick (method) const { errors } = await checkMd(latDir('source-ref-dart-valid')); expect(errors).toHaveLength(0); }); }); -describe('source-ref-dart-error-recovery', () => { - it('recovers class names from Dart files with parse errors', async () => { - // app.dart uses Dart 3.7 dot shorthand (.start) which tree-sitter-dart - // cannot parse — the class becomes an ERROR node. The fallback regex - // should still extract the class name so the wiki link resolves. - const { errors } = await checkMd( - latDir('source-ref-dart-error-recovery'), - ); - expect(errors).toHaveLength(0); - }); -}); - describe('error-source-ref-dart-missing', () => { it('check md reports all missing Dart symbols', async () => { const { errors } = await checkMd(latDir('error-source-ref-dart-missing')); diff --git a/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md b/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md deleted file mode 100644 index 328ed6a..0000000 --- a/tests/cases/source-ref-dart-error-recovery/lat.md/docs.md +++ /dev/null @@ -1,3 +0,0 @@ -# Docs - -The [[src/app.dart#MyWidget]] class uses Dart 3.7 dot shorthand that the grammar cannot fully parse. diff --git a/tests/cases/source-ref-dart-error-recovery/src/app.dart b/tests/cases/source-ref-dart-error-recovery/src/app.dart deleted file mode 100644 index cb09d5f..0000000 --- a/tests/cases/source-ref-dart-error-recovery/src/app.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; - -class MyWidget extends StatelessWidget { - const MyWidget({super.key}); - - @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: .start, - children: [ - const Text('hello'), - ], - ); -} diff --git a/tests/cases/source-ref-dart-valid/lat.md/docs.md b/tests/cases/source-ref-dart-valid/lat.md/docs.md index 0f9cfa0..1a62e7b 100644 --- a/tests/cases/source-ref-dart-valid/lat.md/docs.md +++ b/tests/cases/source-ref-dart-valid/lat.md/docs.md @@ -11,3 +11,5 @@ The [[src/app.dart#Greeting]] mixin defines shared behavior. Default name is [[src/app.dart#defaultName]]. The [[src/app.dart#Color]] enum lists available colors. + +The [[src/app.dart#DotShorthand]] class exercises Dart 3.7 dot shorthand via [[src/app.dart#DotShorthand#pick]]. diff --git a/tests/cases/source-ref-dart-valid/src/app.dart b/tests/cases/source-ref-dart-valid/src/app.dart index f914576..502c5d3 100644 --- a/tests/cases/source-ref-dart-valid/src/app.dart +++ b/tests/cases/source-ref-dart-valid/src/app.dart @@ -23,3 +23,7 @@ mixin Greeting { final defaultName = 'World'; enum Color { red, green, blue } + +class DotShorthand { + Color pick() => .red; +} From f5e1f8d284ac750f2b162c20498391be4ee94579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 4 May 2026 12:07:14 +0200 Subject: [PATCH 6/6] [dart_support] add dart code ref test --- tests/cases.test.ts | 28 +++++++++++++++++++++++ tests/cases/dart-code-ref/app.dart | 9 ++++++++ tests/cases/dart-code-ref/lat.md/specs.md | 9 ++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/cases/dart-code-ref/app.dart create mode 100644 tests/cases/dart-code-ref/lat.md/specs.md diff --git a/tests/cases.test.ts b/tests/cases.test.ts index 580b64c..220b036 100644 --- a/tests/cases.test.ts +++ b/tests/cases.test.ts @@ -390,6 +390,34 @@ describe('python-code-ref', () => { }); }); +// --- dart-code-ref --- + +describe('dart-code-ref', () => { + it('scans @lat refs from Dart // comments including between annotations', async () => { + const { refs } = await scanCodeRefs(caseDir('dart-code-ref')); + expect(refs).toHaveLength(3); + + expect(refs[0].target).toBe('Specs#Feature A'); + expect(refs[0].file).toContain('app.dart'); + expect(refs[0].line).toBe(1); + + expect(refs[1].target).toBe('Specs#Feature B'); + expect(refs[1].file).toContain('app.dart'); + expect(refs[1].line).toBe(5); + + expect(refs[2].target).toBe('Specs#Nonexistent'); + expect(refs[2].line).toBe(8); + }); + + it('detects dangling @lat ref in Dart file', async () => { + const { errors, files } = await checkCodeRefs(latDir('dart-code-ref')); + expect(errors).toHaveLength(1); + expect(errors[0].target).toBe('Specs#Nonexistent'); + expect(errors[0].message).toContain('no matching section found'); + expect(files).toEqual({ '.dart': 1 }); + }); +}); + // --- gitignore-filtering --- describe('gitignore-filtering', () => { diff --git a/tests/cases/dart-code-ref/app.dart b/tests/cases/dart-code-ref/app.dart new file mode 100644 index 0000000..59435b0 --- /dev/null +++ b/tests/cases/dart-code-ref/app.dart @@ -0,0 +1,9 @@ +// @lat: [[Specs#Feature A]] +void doFeatureA() {} + +@deprecated +// @lat: [[Specs#Feature B]] +void doFeatureB() {} + +// @lat: [[Specs#Nonexistent]] +void doMissing() {} diff --git a/tests/cases/dart-code-ref/lat.md/specs.md b/tests/cases/dart-code-ref/lat.md/specs.md new file mode 100644 index 0000000..cab3d79 --- /dev/null +++ b/tests/cases/dart-code-ref/lat.md/specs.md @@ -0,0 +1,9 @@ +# Specs + +## Feature A + +Some text. + +## Feature B + +More text.