Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions lat.md/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,18 @@ 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`
- **`[[src/lib.rs#Greeter#greet]]`** — the `greet` method on struct `Greeter` in Rust
- **`[[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.

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/cli/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const SOURCE_EXTS = new Set([
'.go',
'.c',
'.h',
'.dart',
]);

/**
Expand Down
163 changes: 163 additions & 0 deletions src/source-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const grammarMap: Record<string, string> = {
'.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). */
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can extend the extraction to var-types var x = ..., typed such as String x = ... and late int x = ... declarations?

// 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).
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions templates/cursor-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 65 additions & 0 deletions tests/cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -900,6 +928,43 @@ 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),
// 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('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 () => {
Expand Down
Loading