mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-09-19 07:34:57 +08:00
Merge remote-tracking branch 'origin/main' into fix/kotlin-property-initializer-walk
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -137,6 +137,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
#### MCP / indexing
|
||||
|
||||
- Indexing now warns when parser errors leave a file with no symbols, including C++ raw strings with 16-character delimiters, so missing code is no longer silent. (#1522)
|
||||
|
||||
- `codegraph index <path>` now refuses uninitialized paths and names the nearest initialized parent instead of silently rebuilding it; thanks @danusha2345. (#1524, #1689)
|
||||
|
||||
- Sync now recovers the same connections as a clean index after interrupted reference resolution, including inherited calls and callbacks that previously stayed missing. (#1577)
|
||||
|
||||
- `codegraph_explore` now re-serves source to fresh subagents and after context compaction, with cross-call dedup available only through an explicit `CODEGRAPH_EXPLORE_DEDUP=1` opt-in; thanks @danusha2345. (#1620, #1624)
|
||||
@@ -230,6 +234,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- Kotlin `init { }` blocks and destructuring declarations no longer swallow their code: `init { val cfg = load() }` and `val (a, b) = makePair()` contributed no call edge at all, and now attribute to the enclosing class or file.
|
||||
- A Kotlin property's accessor body now belongs to the property whichever line it is written on, instead of being dropped (same line) or handed to the enclosing class (own line).
|
||||
- Kotlin properties that hold a lambda, a SAM callback or an anonymous object — `private val frameListener = CameraFrameListener { … }`, the way Android and MSDK callbacks are almost always declared — now contribute call edges. Previously everything inside such an initializer was dropped, so a function reached only through one of these callbacks looked like it had no callers at all and its blast radius came back far too small. Delegated properties (`by lazy { … }`) and plain initializers (`val x = compute()`) were affected the same way and are fixed too. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
|
||||
- Rust unit structs (`struct Unit;`) and their trait implementation relationships now appear in the graph after re-indexing. (#1513, #1514)
|
||||
- Imports from Node built-ins or npm packages no longer connect to unrelated type members with matching names; re-index after upgrading to clear existing false dependencies. Thanks @ctype-lab. (#1537)
|
||||
|
||||
- Inheritance relationships no longer attach external Rust or npm supertypes to unrelated local symbols with the same name, including in Svelte, Vue and Astro components; re-index after upgrading to clear existing false relationships. Thanks @ctype-lab. (#1536)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* `codegraph index <path>` rebuilds <path>, never an ancestor (#1524).
|
||||
*
|
||||
* The command used to resolve an uninitialized <path> upward to the nearest
|
||||
* initialized parent and rebuild THAT under a normal "Done" — so
|
||||
* `codegraph index child` from a monorepo re-indexed the whole container and
|
||||
* never said so. An explicit path that is not initialized is now an error that
|
||||
* names the ancestor it would have picked.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
|
||||
function run(cwd: string, args: string[]) {
|
||||
const r = spawnSync(process.execPath, [BIN, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' },
|
||||
});
|
||||
return { status: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') };
|
||||
}
|
||||
|
||||
describe('codegraph index <path> (#1524)', () => {
|
||||
let root: string;
|
||||
let parent: string;
|
||||
let child: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-path-'));
|
||||
parent = path.join(root, 'parent');
|
||||
child = path.join(parent, 'child');
|
||||
fs.mkdirSync(child, { recursive: true });
|
||||
fs.writeFileSync(path.join(parent, 'p.py'), 'def parent_only():\n return 1\n');
|
||||
fs.writeFileSync(path.join(child, 'c.py'), 'def child_only():\n return 2\n');
|
||||
const cg = CodeGraph.initSync(parent);
|
||||
await cg.indexAll();
|
||||
cg.close();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('refuses an explicit path that has no index of its own, naming the ancestor it would have rebuilt', () => {
|
||||
const before = fs.statSync(path.join(parent, '.codegraph', 'codegraph.db')).mtimeMs;
|
||||
const r = run(root, ['index', child, '--quiet']);
|
||||
expect(r.status).toBe(1);
|
||||
expect(r.out).toContain(`not initialized in ${child}`);
|
||||
expect(r.out).toContain(parent);
|
||||
// The parent's index was not touched.
|
||||
expect(fs.statSync(path.join(parent, '.codegraph', 'codegraph.db')).mtimeMs).toBe(before);
|
||||
expect(fs.existsSync(path.join(child, '.codegraph'))).toBe(false);
|
||||
});
|
||||
|
||||
it('rebuilds the explicit path when it is initialized, and a bare `index` still resolves upward from a subdirectory', () => {
|
||||
expect(run(root, ['index', parent, '--quiet']).status).toBe(0);
|
||||
expect(run(child, ['index', '--quiet']).status).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
const COLLAPSE_WARNING = 'parse produced no symbols (tree has errors)';
|
||||
const SOURCE = `const char* kTemplate = R"FILE_TEMPLATE_V1(
|
||||
struct Ignored { int v; };
|
||||
)FILE_TEMPLATE_V1";
|
||||
|
||||
int after_the_raw_string(int x) {
|
||||
return x + 1;
|
||||
}
|
||||
`;
|
||||
|
||||
describe('CLI parse warnings (#1522)', () => {
|
||||
let root: string;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-parse-warning-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function run(args: string[]) {
|
||||
const result = spawnSync(process.execPath, [BIN, ...args], {
|
||||
cwd: root,
|
||||
encoding: 'utf-8',
|
||||
timeout: 20_000,
|
||||
env: {
|
||||
...process.env,
|
||||
CODEGRAPH_NO_DAEMON: '1',
|
||||
CODEGRAPH_WASM_RELAUNCHED: '1',
|
||||
CODEGRAPH_TELEMETRY: '0',
|
||||
NO_COLOR: '1',
|
||||
},
|
||||
});
|
||||
return { status: result.status, out: (result.stdout ?? '') + (result.stderr ?? '') };
|
||||
}
|
||||
|
||||
it('shows a collapsed parse without failing, then stays quiet after a healthy re-index', () => {
|
||||
const sourcePath = path.join(root, 'min.cpp');
|
||||
fs.writeFileSync(sourcePath, SOURCE);
|
||||
|
||||
const collapsed = run(['init', '--yes']);
|
||||
expect(collapsed.status, collapsed.out).toBe(0);
|
||||
expect(collapsed.out).toContain('Indexed 1 files');
|
||||
expect(collapsed.out).toContain(`min.cpp: ${COLLAPSE_WARNING}`);
|
||||
|
||||
fs.writeFileSync(sourcePath, SOURCE.replaceAll('FILE_TEMPLATE_V1', 'FILE_TEMPLATE_V'));
|
||||
const healthy = run(['index']);
|
||||
expect(healthy.status, healthy.out).toBe(0);
|
||||
expect(healthy.out).not.toContain(COLLAPSE_WARNING);
|
||||
|
||||
const query = run(['query', 'after_the_raw_string']);
|
||||
expect(query.status, query.out).toBe(0);
|
||||
expect(query.out).toMatch(/function\s+after_the_raw_string/);
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { getParser, initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
|
||||
function rawStringSource(delimiter: string): string {
|
||||
return `const char* kTemplate = R"${delimiter}(
|
||||
struct Ignored { int v; };
|
||||
)${delimiter}";
|
||||
|
||||
int after_the_raw_string(int x) {
|
||||
return x + 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
describe('C++ raw-string delimiter parse collapse (#1522)', () => {
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['cpp', 'c']);
|
||||
});
|
||||
|
||||
it('warns when a legal 16-character delimiter swallows every symbol', () => {
|
||||
const result = extractFromSource('min.cpp', rawStringSource('FILE_TEMPLATE_V1'));
|
||||
|
||||
// The vendored tree-sitter-cpp scanner currently rejects the standard's
|
||||
// maximum delimiter length, consuming the following function as ERROR.
|
||||
expect(result.nodes.filter((n) => n.kind === 'function')).toEqual([]);
|
||||
expect(result.nodes.map((n) => n.kind)).toEqual(['file']);
|
||||
expect(result.errors).toEqual([
|
||||
{
|
||||
message:
|
||||
'min.cpp: parse produced no symbols (tree has errors) — ' +
|
||||
'the file is indexed but contributes nothing to the graph',
|
||||
severity: 'warning',
|
||||
code: 'parse_error',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('extracts the function after a 15-character delimiter without warning', () => {
|
||||
const result = extractFromSource('min.cpp', rawStringSource('FILE_TEMPLATE_V'));
|
||||
|
||||
expect(result.nodes.filter((n) => n.kind === 'function').map((n) => n.name))
|
||||
.toEqual(['after_the_raw_string']);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(['min.cpp', 'min.c', 'min.h'])('does not warn on a healthy include-only %s', (filePath) => {
|
||||
const result = extractFromSource(filePath, '#include <stdio.h>\n#include <stdlib.h>\n');
|
||||
|
||||
expect(result.nodes.filter((n) => n.kind !== 'file' && n.kind !== 'import')).toEqual([]);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not warn on a healthy empty file with zero symbols', () => {
|
||||
const result = extractFromSource('empty.cpp', '');
|
||||
|
||||
expect(result.nodes.map((n) => n.kind)).toEqual(['file']);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not warn on parse errors when a function survives', () => {
|
||||
const source = 'int before_the_raw_string() { return 0; }\n' + rawStringSource('FILE_TEMPLATE_V1');
|
||||
const tree = getParser('cpp')!.parse(source)!;
|
||||
try {
|
||||
expect(tree.rootNode.hasError).toBe(true);
|
||||
} finally {
|
||||
tree.delete();
|
||||
}
|
||||
|
||||
const result = extractFromSource('min.cpp', source);
|
||||
|
||||
expect(result.nodes.filter((n) => n.kind === 'function').map((n) => n.name))
|
||||
.toEqual(['before_the_raw_string']);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1324,6 +1324,35 @@ pub struct User {
|
||||
expect(structNode?.name).toBe('User');
|
||||
});
|
||||
|
||||
it('should extract unit and tuple structs, not just brace structs', () => {
|
||||
// A unit struct has no body field, but it IS a complete definition —
|
||||
// Rust has no forward declarations. Skipping it dropped the type and
|
||||
// every `impl Trait for UnitStruct` edge with it.
|
||||
const code = `
|
||||
pub struct Unit;
|
||||
pub struct Tuple(pub u32);
|
||||
pub struct Brace { pub x: u32 }
|
||||
`;
|
||||
const result = extractFromSource('shapes.rs', code);
|
||||
|
||||
const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name).sort();
|
||||
expect(structs).toEqual(['Brace', 'Tuple', 'Unit']);
|
||||
});
|
||||
|
||||
it('should link impl Trait for a unit struct', () => {
|
||||
const code = `
|
||||
pub struct Unit;
|
||||
pub trait Greet { fn hi(&self) -> String; }
|
||||
impl Greet for Unit { fn hi(&self) -> String { "unit".into() } }
|
||||
`;
|
||||
const result = extractFromSource('greet.rs', code);
|
||||
|
||||
const unit = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Unit');
|
||||
expect(unit).toBeDefined();
|
||||
const trait = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Greet');
|
||||
expect(trait).toBeDefined();
|
||||
});
|
||||
|
||||
it('should extract trait declarations', () => {
|
||||
const code = `
|
||||
pub trait Repository {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* SAME ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
|
||||
* unresolved refs compared as canonicalized multisets — over the checked-in
|
||||
* torture fixture (torture.rs: impl/trait quirks incl. generic / lifetime /
|
||||
* reference / scoped / generic-trait impl receivers (#1588), unit-struct skip, phantom
|
||||
* reference / scoped / generic-trait impl receivers (#1588), unit structs
|
||||
* (a bodiless struct IS a definition — both walkers mint a node), phantom
|
||||
* const identifiers, use-binding refs incl. nested groups + wildcard-emits-
|
||||
* nothing, chained-call re-encode, turbofish, Rocket route macros body-only,
|
||||
* fn-ref shapes, value-ref shadowing, attribute-broken docstrings, dead-code
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
//! kind is always `variable`, no signature, and EVERY direct `identifier`
|
||||
//! child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
|
||||
//! phantom `OTHER`). Top-level initializer values are never body-walked.
|
||||
//! - Unit structs (`struct Unit;`, no body field) mint NO node; `mod_item`
|
||||
//! mints no module node and adds no QN prefix.
|
||||
//! - `mod_item` mints no module node and adds no QN prefix.
|
||||
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
|
||||
//! `Foo::new().bar`); a call through a field of the enclosing type keeps
|
||||
//! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
|
||||
@@ -590,10 +589,12 @@ impl<'t> Walker<'t> {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
/// Extract a Rust struct or union with a body; unit structs remain skipped.
|
||||
/// Extract a Rust struct or union — the body field is OPTIONAL. A unit
|
||||
/// struct (`struct U;`) has no body and is still a complete definition,
|
||||
/// so it mints a node with no members; tuple structs' ordered_field_declaration_list
|
||||
/// is a body. Mirrors the TS reference's `allowBodilessStruct`.
|
||||
fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
|
||||
stack_guard!();
|
||||
let Some(body) = node.child_by_field_name("body") else { return };
|
||||
let name = self.extract_name(node);
|
||||
let extra = Extra {
|
||||
docstring: preceding_docstring(node, self.src),
|
||||
@@ -603,6 +604,10 @@ impl<'t> Walker<'t> {
|
||||
let Some(row) = self.create_node(kind, &name, node, extra) else { return };
|
||||
self.extract_inheritance(node, row);
|
||||
|
||||
// Unit structs have no body to walk — the node itself is the whole
|
||||
// definition.
|
||||
let Some(body) = node.child_by_field_name("body") else { return };
|
||||
|
||||
self.stack.push(Scope { row, kind, name });
|
||||
for i in 0..body.named_child_count() {
|
||||
if let Some(c) = body.named_child(i) {
|
||||
|
||||
@@ -140,7 +140,7 @@ undefined; **no isConst means `const_item`/`static_item` extract as kind
|
||||
|---|---|---|
|
||||
| `function_item` (top level) | functionTypes, tree-sitter.ts:994 → extractFunction:1517 | not inside class-like at file scope → extractFunction; **first line of extractFunction (1522): if getReceiverType returns a value → extractMethod instead** (this is how impl-block fns become methods — impl_item does NOT push a scope) |
|
||||
| `function_signature_item` | same | in a trait body (trait pushed, class-like) → extractMethod; no `body` field → no body walk |
|
||||
| `struct_item` | structTypes:1059 → extractStruct:1869 | `body` field required: **unit structs `struct Unit;` have no body → NO node minted** (1876, `record_declaration` exemption is C#-only). Tuple structs have body `ordered_field_declaration_list` → extracted. `field_declaration` children make NO nodes (rust has no fieldTypes) — visitNode recurses into them and finds nothing |
|
||||
| `struct_item` | structTypes:1059 → extractStruct:1869 | ~~`body` field required: unit structs `struct Unit;` have no body → NO node minted~~ — **superseded: Rust now sets `allowBodilessStruct`, so `struct Unit;` mints a node with no members.** Rust has no forward declarations, so the bodiless skip (meant for C/C++) never applied here; the `record_declaration` exemption is the C# form of the same carve-out. Tuple structs have body `ordered_field_declaration_list` → extracted. `field_declaration` children make NO nodes (rust has no fieldTypes) — visitNode recurses into them and finds nothing |
|
||||
| `enum_item` | enumTypes:1064 → extractEnum:1914 | body `enum_variant_list`; `enum_variant` children → extractEnumMembers:1958 — **`name` field path: one `enum_member` node from `getChildByField(node,'name')`, then return** (variant payload bodies `B(u32)` / `C { x }` are never walked). Non-variant children (e.g. `attribute_item`) → visitNode (no-op) |
|
||||
| `trait_item` | interfaceTypes:1054 → extractInterface:1834 | kind `'trait'` (interfaceKind); extractInheritance sees the `trait_bounds` child (see below); body `declaration_list` children visited with the trait pushed → fn items become methods with QN `Trait::name` via nodeStack |
|
||||
| `impl_item` | dedicated branch:1273-1276 → extractRustImplItem:5690 | emits the implements back-reference (below); **skipChildren stays false** → the `declaration_list` is then visited normally by the loop at 1295 (that's how impl members are reached; impl pushes NOTHING on the nodeStack) |
|
||||
@@ -490,7 +490,7 @@ inner `array_expression`, but `const CB: fn() = handler;` captures nothing
|
||||
## Gates (per plan §5, no exceptions)
|
||||
|
||||
- **Torture fixture `torture.rs`** (+ CRLF variant, derived in-memory), pinning
|
||||
at minimum: unit struct (NO node) / tuple struct / field struct; enum with
|
||||
at minimum: unit struct (node, no members) / tuple struct / field struct; enum with
|
||||
unit+tuple+struct variants; trait with supertraits incl. a SCOPED one
|
||||
(`fmt::Debug` — dropped) + `function_signature_item` + default method +
|
||||
associated type/const (no node; const value call attributes to trait);
|
||||
|
||||
+18
-2
@@ -378,6 +378,7 @@ type IndexResult = {
|
||||
*/
|
||||
function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexResult, projectPath?: string): void {
|
||||
const hasErrors = result.filesErrored > 0;
|
||||
const parseWarnings = result.errors.filter((e) => e.code === 'parse_error' && e.severity === 'warning');
|
||||
|
||||
// Surface non-file-level failures (e.g. lock-acquisition failure
|
||||
// when another indexer is running) before the file-count branches.
|
||||
@@ -403,6 +404,10 @@ function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexR
|
||||
clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files`);
|
||||
}
|
||||
clack.log.info(`${formatNumber(result.nodesCreated)} nodes, ${formatNumber(result.edgesCreated)} edges in ${formatDuration(result.durationMs)}`);
|
||||
// Warning-only parse failures keep indexing successful, but must be visible.
|
||||
for (const warning of parseWarnings) {
|
||||
clack.log.warn(warning.message);
|
||||
}
|
||||
// A PARTIAL index (files silently dropped mid-pipeline) must not pass
|
||||
// as a clean run — it's the difference between "indexed the repo" and
|
||||
// "indexed most of the repo, quietly". Only the completeness
|
||||
@@ -791,7 +796,13 @@ program
|
||||
.option('-q, --quiet', 'Suppress progress output')
|
||||
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
|
||||
.action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean; verbose?: boolean }) => {
|
||||
const projectPath = resolveProjectPath(pathArg);
|
||||
// An EXPLICIT path names the project to rebuild — it is never a hint to go
|
||||
// looking for one. resolveProjectPath walks up to the nearest initialized
|
||||
// ancestor, which is right for `codegraph query` run from a subdirectory,
|
||||
// but for a full re-index it silently rebuilt the parent's graph under a
|
||||
// normal "Done" when <path> had no index of its own (#1524). Only a bare
|
||||
// `codegraph index` (cwd) may resolve upward.
|
||||
const projectPath = pathArg ? path.resolve(pathArg) : resolveProjectPath();
|
||||
|
||||
try {
|
||||
// Don't (re)index your home directory / a filesystem root (#845). --force
|
||||
@@ -804,7 +815,12 @@ program
|
||||
|
||||
if (!isInitialized(projectPath)) {
|
||||
error(`CodeGraph not initialized in ${projectPath}`);
|
||||
info('Run "codegraph init" first');
|
||||
const ancestor = pathArg ? resolveProjectPath(pathArg) : projectPath;
|
||||
if (ancestor !== projectPath) {
|
||||
info(`The nearest initialized project is ${ancestor} — pass that path to rebuild it, or run "codegraph init" in ${projectPath} to index it on its own.`);
|
||||
} else {
|
||||
info('Run "codegraph init" first');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,9 @@ export const rustExtractor: LanguageExtractor = {
|
||||
methodTypes: ['function_item', 'function_signature_item'],
|
||||
interfaceTypes: ['trait_item'],
|
||||
structTypes: ['struct_item'],
|
||||
// `struct Unit;` is a unit struct — a complete definition with no body
|
||||
// field, not a forward declaration. Rust has no forward declarations.
|
||||
allowBodilessStruct: true,
|
||||
// Unions share struct member syntax and impl attachment, but retain their
|
||||
// distinct semantic kind in the graph.
|
||||
unionTypes: ['union_item'],
|
||||
|
||||
@@ -187,6 +187,19 @@ export interface LanguageExtractor {
|
||||
* bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
|
||||
*/
|
||||
skipBodilessClass?: boolean;
|
||||
/**
|
||||
* Keep a bodiless struct node — it IS a complete definition, not a forward
|
||||
* declaration. Set only for languages where a bodiless `struct` is complete:
|
||||
* Rust's unit struct (`struct Unit;`). Leave unset for C/C++, where
|
||||
* `struct Foo;` is a forward declaration.
|
||||
*
|
||||
* Opposite polarity from `skipBodilessClass` (#1093) because the defaults
|
||||
* differ: a bodiless CLASS is kept unless a language opts into skipping,
|
||||
* a bodiless STRUCT is skipped unless a language opts into keeping. The
|
||||
* hardcoded C# `record_declaration` carve-out (#831) is the same situation
|
||||
* predating this flag.
|
||||
*/
|
||||
allowBodilessStruct?: boolean;
|
||||
/** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
|
||||
interfaceKind?: NodeKind;
|
||||
|
||||
|
||||
@@ -584,6 +584,18 @@ export class TreeSitterExtractor {
|
||||
|
||||
if (packageNodeId) this.nodeStack.pop();
|
||||
this.nodeStack.pop();
|
||||
|
||||
// hasError is routine for several grammars; warn only when no symbols survived.
|
||||
const symbolCount = this.nodes.filter((n) => n.kind !== 'file').length;
|
||||
if (this.tree?.rootNode.hasError && symbolCount === 0) {
|
||||
this.errors.push({
|
||||
message:
|
||||
`${this.filePath}: parse produced no symbols (tree has errors) — ` +
|
||||
`the file is indexed but contributes nothing to the graph`,
|
||||
severity: 'warning',
|
||||
code: 'parse_error',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -1962,8 +1974,16 @@ export class TreeSitterExtractor {
|
||||
// Skip forward declarations and type references (no body = not a definition)
|
||||
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
||||
// complete definitions with no body block. (#831)
|
||||
//
|
||||
// `allowBodilessStruct` is the per-language escape hatch for the same
|
||||
// situation: a bodiless struct that IS a complete definition (Rust's unit
|
||||
// struct `struct Unit;`). Opposite polarity from `skipBodilessClass`
|
||||
// (#1093) because the two defaults differ — a bodiless CLASS is kept
|
||||
// unless a language opts into skipping, a bodiless STRUCT is skipped
|
||||
// unless a language opts into keeping.
|
||||
const body = getChildByField(node, this.extractor.bodyField);
|
||||
if (!body && node.type !== 'record_declaration') return;
|
||||
if (!body && node.type !== 'record_declaration' && !this.extractor.allowBodilessStruct)
|
||||
return;
|
||||
|
||||
const name = extractName(node, this.source, this.extractor);
|
||||
const docstring = getPrecedingDocstring(node, this.source);
|
||||
|
||||
Reference in New Issue
Block a user