@optave/codegraph 3.0.4 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +59 -52
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +9 -10
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +274 -159
  19. package/src/cfg.js +111 -341
  20. package/src/check.js +3 -3
  21. package/src/cli.js +122 -167
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +274 -697
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -392
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +160 -228
  35. package/src/index.js +36 -2
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +97 -20
  39. package/src/owners.js +132 -132
  40. package/src/parser.js +58 -131
  41. package/src/queries-cli.js +866 -0
  42. package/src/queries.js +1356 -2261
  43. package/src/resolve.js +11 -2
  44. package/src/result-formatter.js +21 -0
  45. package/src/sequence.js +364 -0
  46. package/src/structure.js +200 -199
  47. package/src/test-filter.js +7 -0
  48. package/src/triage.js +120 -162
  49. package/src/viewer.js +1 -1
package/src/mcp.js CHANGED
@@ -113,6 +113,11 @@ const BASE_TOOLS = [
113
113
  properties: {
114
114
  file: { type: 'string', description: 'File path (partial match supported)' },
115
115
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
116
+ unused: {
117
+ type: 'boolean',
118
+ description: 'Show only exports with zero consumers',
119
+ default: false,
120
+ },
116
121
  ...PAGINATION_PROPS,
117
122
  },
118
123
  required: ['file'],
@@ -418,6 +423,43 @@ const BASE_TOOLS = [
418
423
  },
419
424
  },
420
425
  },
426
+ {
427
+ name: 'sequence',
428
+ description:
429
+ 'Generate a Mermaid sequence diagram from call graph edges. Participants are files, messages are function calls between them.',
430
+ inputSchema: {
431
+ type: 'object',
432
+ properties: {
433
+ name: {
434
+ type: 'string',
435
+ description: 'Entry point or function name to trace from (partial match)',
436
+ },
437
+ depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
438
+ format: {
439
+ type: 'string',
440
+ enum: ['mermaid', 'json'],
441
+ description: 'Output format (default: mermaid)',
442
+ },
443
+ dataflow: {
444
+ type: 'boolean',
445
+ description: 'Annotate with parameter names and return arrows',
446
+ default: false,
447
+ },
448
+ file: {
449
+ type: 'string',
450
+ description: 'Scope search to functions in this file (partial match)',
451
+ },
452
+ kind: {
453
+ type: 'string',
454
+ enum: EVERY_SYMBOL_KIND,
455
+ description: 'Filter to a specific symbol kind',
456
+ },
457
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
458
+ ...PAGINATION_PROPS,
459
+ },
460
+ required: ['name'],
461
+ },
462
+ },
421
463
  {
422
464
  name: 'complexity',
423
465
  description:
@@ -794,26 +836,27 @@ export async function startMCPServer(customDbPath, options = {}) {
794
836
  process.exit(1);
795
837
  }
796
838
 
797
- // Lazy import query functions to avoid circular deps at module load
798
- const {
799
- impactAnalysisData,
800
- moduleMapData,
801
- fileDepsData,
802
- exportsData,
803
- fnDepsData,
804
- fnImpactData,
805
- pathData,
806
- contextData,
807
- childrenData,
808
- explainData,
809
- whereData,
810
- diffImpactData,
811
- listFunctionsData,
812
- rolesData,
813
- } = await import('./queries.js');
839
+ // Connect transport FIRST so the server can receive the client's
840
+ // `initialize` request while heavy modules (queries, better-sqlite3)
841
+ // are still loading. These are lazy-loaded on the first tool call
842
+ // and cached for subsequent calls.
843
+ let _queries;
844
+ let _Database;
814
845
 
815
- const require = createRequire(import.meta.url);
816
- const Database = require('better-sqlite3');
846
+ async function getQueries() {
847
+ if (!_queries) {
848
+ _queries = await import('./queries.js');
849
+ }
850
+ return _queries;
851
+ }
852
+
853
+ function getDatabase() {
854
+ if (!_Database) {
855
+ const require = createRequire(import.meta.url);
856
+ _Database = require('better-sqlite3');
857
+ }
858
+ return _Database;
859
+ }
817
860
 
818
861
  const server = new Server(
819
862
  { name: 'codegraph', version: '1.0.0' },
@@ -826,8 +869,24 @@ export async function startMCPServer(customDbPath, options = {}) {
826
869
 
827
870
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
828
871
  const { name, arguments: args } = request.params;
829
-
830
872
  try {
873
+ const {
874
+ impactAnalysisData,
875
+ moduleMapData,
876
+ fileDepsData,
877
+ exportsData,
878
+ fnDepsData,
879
+ fnImpactData,
880
+ pathData,
881
+ contextData,
882
+ childrenData,
883
+ explainData,
884
+ whereData,
885
+ diffImpactData,
886
+ listFunctionsData,
887
+ rolesData,
888
+ } = await getQueries();
889
+ const Database = getDatabase();
831
890
  if (!multiRepo && args.repo) {
832
891
  throw new Error(
833
892
  'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
@@ -902,6 +961,7 @@ export async function startMCPServer(customDbPath, options = {}) {
902
961
  case 'file_exports':
903
962
  result = exportsData(args.file, dbPath, {
904
963
  noTests: args.no_tests,
964
+ unused: args.unused,
905
965
  limit: Math.min(args.limit ?? MCP_DEFAULTS.file_exports, MCP_MAX_LIMIT),
906
966
  offset: args.offset ?? 0,
907
967
  });
@@ -1165,6 +1225,23 @@ export async function startMCPServer(customDbPath, options = {}) {
1165
1225
  }
1166
1226
  break;
1167
1227
  }
1228
+ case 'sequence': {
1229
+ const { sequenceData, sequenceToMermaid } = await import('./sequence.js');
1230
+ const seqResult = sequenceData(args.name, dbPath, {
1231
+ depth: args.depth,
1232
+ file: args.file,
1233
+ kind: args.kind,
1234
+ dataflow: args.dataflow,
1235
+ noTests: args.no_tests,
1236
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1237
+ offset: args.offset ?? 0,
1238
+ });
1239
+ result =
1240
+ args.format === 'json'
1241
+ ? seqResult
1242
+ : { text: sequenceToMermaid(seqResult), ...seqResult };
1243
+ break;
1244
+ }
1168
1245
  case 'complexity': {
1169
1246
  const { complexityData } = await import('./complexity.js');
1170
1247
  result = complexityData(dbPath, {
package/src/owners.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { findDbPath, openReadonlyOrFail } from './db.js';
4
- import { isTestFile } from './queries.js';
4
+ import { outputResult } from './result-formatter.js';
5
+ import { isTestFile } from './test-filter.js';
5
6
 
6
7
  // ─── CODEOWNERS Parsing ──────────────────────────────────────────────
7
8
 
@@ -163,142 +164,144 @@ export function ownersForFiles(filePaths, repoRoot) {
163
164
  */
164
165
  export function ownersData(customDbPath, opts = {}) {
165
166
  const db = openReadonlyOrFail(customDbPath);
166
- const dbPath = findDbPath(customDbPath);
167
- const repoRoot = path.resolve(path.dirname(dbPath), '..');
168
-
169
- const parsed = parseCodeowners(repoRoot);
170
- if (!parsed) {
171
- db.close();
172
- return {
173
- codeownersFile: null,
174
- files: [],
175
- symbols: [],
176
- boundaries: [],
177
- summary: {
178
- totalFiles: 0,
179
- ownedFiles: 0,
180
- unownedFiles: 0,
181
- coveragePercent: 0,
182
- ownerCount: 0,
183
- byOwner: [],
184
- },
185
- };
186
- }
167
+ try {
168
+ const dbPath = findDbPath(customDbPath);
169
+ const repoRoot = path.resolve(path.dirname(dbPath), '..');
170
+
171
+ const parsed = parseCodeowners(repoRoot);
172
+ if (!parsed) {
173
+ return {
174
+ codeownersFile: null,
175
+ files: [],
176
+ symbols: [],
177
+ boundaries: [],
178
+ summary: {
179
+ totalFiles: 0,
180
+ ownedFiles: 0,
181
+ unownedFiles: 0,
182
+ coveragePercent: 0,
183
+ ownerCount: 0,
184
+ byOwner: [],
185
+ },
186
+ };
187
+ }
187
188
 
188
- // Get all distinct files from nodes
189
- let allFiles = db
190
- .prepare('SELECT DISTINCT file FROM nodes')
191
- .all()
192
- .map((r) => r.file);
189
+ // Get all distinct files from nodes
190
+ let allFiles = db
191
+ .prepare('SELECT DISTINCT file FROM nodes')
192
+ .all()
193
+ .map((r) => r.file);
193
194
 
194
- if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
195
- if (opts.file) {
196
- const filter = opts.file;
197
- allFiles = allFiles.filter((f) => f.includes(filter));
198
- }
195
+ if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
196
+ if (opts.file) {
197
+ const filter = opts.file;
198
+ allFiles = allFiles.filter((f) => f.includes(filter));
199
+ }
199
200
 
200
- // Map files to owners
201
- const fileOwners = allFiles.map((file) => ({
202
- file,
203
- owners: matchOwners(file, parsed.rules),
204
- }));
205
-
206
- // Build owner-to-files index
207
- const ownerIndex = new Map();
208
- let ownedCount = 0;
209
- for (const fo of fileOwners) {
210
- if (fo.owners.length > 0) ownedCount++;
211
- for (const o of fo.owners) {
212
- if (!ownerIndex.has(o)) ownerIndex.set(o, []);
213
- ownerIndex.get(o).push(fo.file);
201
+ // Map files to owners
202
+ const fileOwners = allFiles.map((file) => ({
203
+ file,
204
+ owners: matchOwners(file, parsed.rules),
205
+ }));
206
+
207
+ // Build owner-to-files index
208
+ const ownerIndex = new Map();
209
+ let ownedCount = 0;
210
+ for (const fo of fileOwners) {
211
+ if (fo.owners.length > 0) ownedCount++;
212
+ for (const o of fo.owners) {
213
+ if (!ownerIndex.has(o)) ownerIndex.set(o, []);
214
+ ownerIndex.get(o).push(fo.file);
215
+ }
214
216
  }
215
- }
216
217
 
217
- // Filter files if --owner specified
218
- let filteredFiles = fileOwners;
219
- if (opts.owner) {
220
- filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner));
221
- }
218
+ // Filter files if --owner specified
219
+ let filteredFiles = fileOwners;
220
+ if (opts.owner) {
221
+ filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner));
222
+ }
222
223
 
223
- // Get symbols for filtered files
224
- const fileSet = new Set(filteredFiles.map((fo) => fo.file));
225
- let symbols = db
226
- .prepare('SELECT name, kind, file, line FROM nodes')
227
- .all()
228
- .filter((n) => fileSet.has(n.file));
229
-
230
- if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
231
- if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
232
-
233
- const symbolsWithOwners = symbols.map((s) => ({
234
- ...s,
235
- owners: matchOwners(s.file, parsed.rules),
236
- }));
237
-
238
- // Boundary analysis — cross-owner call edges
239
- const boundaries = [];
240
- if (opts.boundary) {
241
- const edges = db
242
- .prepare(
243
- `SELECT e.id, e.kind AS edgeKind,
244
- s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
245
- t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
246
- FROM edges e
247
- JOIN nodes s ON e.source_id = s.id
248
- JOIN nodes t ON e.target_id = t.id
249
- WHERE e.kind = 'calls'`,
250
- )
251
- .all();
252
-
253
- for (const e of edges) {
254
- if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
255
- const srcOwners = matchOwners(e.srcFile, parsed.rules);
256
- const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
257
- // Cross-boundary: different owner sets
258
- const srcKey = srcOwners.sort().join(',');
259
- const tgtKey = tgtOwners.sort().join(',');
260
- if (srcKey !== tgtKey) {
261
- boundaries.push({
262
- from: {
263
- name: e.srcName,
264
- kind: e.srcKind,
265
- file: e.srcFile,
266
- line: e.srcLine,
267
- owners: srcOwners,
268
- },
269
- to: {
270
- name: e.tgtName,
271
- kind: e.tgtKind,
272
- file: e.tgtFile,
273
- line: e.tgtLine,
274
- owners: tgtOwners,
275
- },
276
- edgeKind: e.edgeKind,
277
- });
224
+ // Get symbols for filtered files
225
+ const fileSet = new Set(filteredFiles.map((fo) => fo.file));
226
+ let symbols = db
227
+ .prepare('SELECT name, kind, file, line FROM nodes')
228
+ .all()
229
+ .filter((n) => fileSet.has(n.file));
230
+
231
+ if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
232
+ if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
233
+
234
+ const symbolsWithOwners = symbols.map((s) => ({
235
+ ...s,
236
+ owners: matchOwners(s.file, parsed.rules),
237
+ }));
238
+
239
+ // Boundary analysis — cross-owner call edges
240
+ const boundaries = [];
241
+ if (opts.boundary) {
242
+ const edges = db
243
+ .prepare(
244
+ `SELECT e.id, e.kind AS edgeKind,
245
+ s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
246
+ t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
247
+ FROM edges e
248
+ JOIN nodes s ON e.source_id = s.id
249
+ JOIN nodes t ON e.target_id = t.id
250
+ WHERE e.kind = 'calls'`,
251
+ )
252
+ .all();
253
+
254
+ for (const e of edges) {
255
+ if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
256
+ const srcOwners = matchOwners(e.srcFile, parsed.rules);
257
+ const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
258
+ // Cross-boundary: different owner sets
259
+ const srcKey = srcOwners.sort().join(',');
260
+ const tgtKey = tgtOwners.sort().join(',');
261
+ if (srcKey !== tgtKey) {
262
+ boundaries.push({
263
+ from: {
264
+ name: e.srcName,
265
+ kind: e.srcKind,
266
+ file: e.srcFile,
267
+ line: e.srcLine,
268
+ owners: srcOwners,
269
+ },
270
+ to: {
271
+ name: e.tgtName,
272
+ kind: e.tgtKind,
273
+ file: e.tgtFile,
274
+ line: e.tgtLine,
275
+ owners: tgtOwners,
276
+ },
277
+ edgeKind: e.edgeKind,
278
+ });
279
+ }
278
280
  }
279
281
  }
280
- }
281
282
 
282
- // Summary
283
- const byOwner = [...ownerIndex.entries()]
284
- .map(([owner, files]) => ({ owner, fileCount: files.length }))
285
- .sort((a, b) => b.fileCount - a.fileCount);
286
-
287
- db.close();
288
- return {
289
- codeownersFile: parsed.path,
290
- files: filteredFiles,
291
- symbols: symbolsWithOwners,
292
- boundaries,
293
- summary: {
294
- totalFiles: allFiles.length,
295
- ownedFiles: ownedCount,
296
- unownedFiles: allFiles.length - ownedCount,
297
- coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
298
- ownerCount: ownerIndex.size,
299
- byOwner,
300
- },
301
- };
283
+ // Summary
284
+ const byOwner = [...ownerIndex.entries()]
285
+ .map(([owner, files]) => ({ owner, fileCount: files.length }))
286
+ .sort((a, b) => b.fileCount - a.fileCount);
287
+
288
+ return {
289
+ codeownersFile: parsed.path,
290
+ files: filteredFiles,
291
+ symbols: symbolsWithOwners,
292
+ boundaries,
293
+ summary: {
294
+ totalFiles: allFiles.length,
295
+ ownedFiles: ownedCount,
296
+ unownedFiles: allFiles.length - ownedCount,
297
+ coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
298
+ ownerCount: ownerIndex.size,
299
+ byOwner,
300
+ },
301
+ };
302
+ } finally {
303
+ db.close();
304
+ }
302
305
  }
303
306
 
304
307
  // ─── CLI Display ─────────────────────────────────────────────────────
@@ -310,10 +313,7 @@ export function ownersData(customDbPath, opts = {}) {
310
313
  */
311
314
  export function owners(customDbPath, opts = {}) {
312
315
  const data = ownersData(customDbPath, opts);
313
- if (opts.json) {
314
- console.log(JSON.stringify(data, null, 2));
315
- return;
316
- }
316
+ if (outputResult(data, null, opts)) return;
317
317
 
318
318
  if (!data.codeownersFile) {
319
319
  console.log('No CODEOWNERS file found.');
package/src/parser.js CHANGED
@@ -183,133 +183,55 @@ function resolveEngine(opts = {}) {
183
183
  }
184
184
 
185
185
  /**
186
- * Normalize native engine output to match the camelCase convention
187
- * used by the WASM extractors.
186
+ * Patch native engine output in-place for the few remaining semantic transforms.
187
+ * With #[napi(js_name)] on Rust types, most fields already arrive as camelCase.
188
+ * This only handles:
189
+ * - _lineCount compat for builder.js
190
+ * - Backward compat for older native binaries missing js_name annotations
191
+ * - dataflow argFlows/mutations bindingType → binding wrapper
188
192
  */
189
- function normalizeNativeSymbols(result) {
190
- return {
191
- _lineCount: result.lineCount ?? result.line_count ?? null,
192
- definitions: (result.definitions || []).map((d) => ({
193
- name: d.name,
194
- kind: d.kind,
195
- line: d.line,
196
- endLine: d.endLine ?? d.end_line ?? null,
197
- decorators: d.decorators,
198
- complexity: d.complexity
199
- ? {
200
- cognitive: d.complexity.cognitive,
201
- cyclomatic: d.complexity.cyclomatic,
202
- maxNesting: d.complexity.maxNesting,
203
- halstead: d.complexity.halstead ?? null,
204
- loc: d.complexity.loc ?? null,
205
- maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
206
- }
207
- : null,
208
- cfg: d.cfg?.blocks?.length
209
- ? {
210
- blocks: d.cfg.blocks.map((b) => ({
211
- index: b.index,
212
- type: b.type,
213
- startLine: b.startLine,
214
- endLine: b.endLine,
215
- label: b.label ?? null,
216
- })),
217
- edges: d.cfg.edges.map((e) => ({
218
- sourceIndex: e.sourceIndex,
219
- targetIndex: e.targetIndex,
220
- kind: e.kind,
221
- })),
222
- }
223
- : null,
224
- children: d.children?.length
225
- ? d.children.map((c) => ({
226
- name: c.name,
227
- kind: c.kind,
228
- line: c.line,
229
- endLine: c.endLine ?? c.end_line ?? null,
230
- }))
231
- : undefined,
232
- })),
233
- calls: (result.calls || []).map((c) => ({
234
- name: c.name,
235
- line: c.line,
236
- dynamic: c.dynamic,
237
- receiver: c.receiver,
238
- })),
239
- imports: (result.imports || []).map((i) => ({
240
- source: i.source,
241
- names: i.names || [],
242
- line: i.line,
243
- typeOnly: i.typeOnly ?? i.type_only,
244
- reexport: i.reexport,
245
- wildcardReexport: i.wildcardReexport ?? i.wildcard_reexport,
246
- pythonImport: i.pythonImport ?? i.python_import,
247
- goImport: i.goImport ?? i.go_import,
248
- rustUse: i.rustUse ?? i.rust_use,
249
- javaImport: i.javaImport ?? i.java_import,
250
- csharpUsing: i.csharpUsing ?? i.csharp_using,
251
- rubyRequire: i.rubyRequire ?? i.ruby_require,
252
- phpUse: i.phpUse ?? i.php_use,
253
- })),
254
- classes: (result.classes || []).map((c) => ({
255
- name: c.name,
256
- extends: c.extends,
257
- implements: c.implements,
258
- line: c.line,
259
- })),
260
- exports: (result.exports || []).map((e) => ({
261
- name: e.name,
262
- kind: e.kind,
263
- line: e.line,
264
- })),
265
- astNodes: (result.astNodes ?? result.ast_nodes ?? []).map((n) => ({
266
- kind: n.kind,
267
- name: n.name,
268
- line: n.line,
269
- text: n.text ?? null,
270
- receiver: n.receiver ?? null,
271
- })),
272
- dataflow: result.dataflow
273
- ? {
274
- parameters: (result.dataflow.parameters || []).map((p) => ({
275
- funcName: p.funcName,
276
- paramName: p.paramName,
277
- paramIndex: p.paramIndex,
278
- line: p.line,
279
- })),
280
- returns: (result.dataflow.returns || []).map((r) => ({
281
- funcName: r.funcName,
282
- expression: r.expression ?? '',
283
- referencedNames: r.referencedNames ?? [],
284
- line: r.line,
285
- })),
286
- assignments: (result.dataflow.assignments || []).map((a) => ({
287
- varName: a.varName,
288
- callerFunc: a.callerFunc ?? null,
289
- sourceCallName: a.sourceCallName,
290
- expression: a.expression ?? '',
291
- line: a.line,
292
- })),
293
- argFlows: (result.dataflow.argFlows ?? []).map((f) => ({
294
- callerFunc: f.callerFunc ?? null,
295
- calleeName: f.calleeName,
296
- argIndex: f.argIndex,
297
- argName: f.argName ?? null,
298
- binding: f.bindingType ? { type: f.bindingType } : null,
299
- confidence: f.confidence,
300
- expression: f.expression ?? '',
301
- line: f.line,
302
- })),
303
- mutations: (result.dataflow.mutations || []).map((m) => ({
304
- funcName: m.funcName ?? null,
305
- receiverName: m.receiverName,
306
- binding: m.bindingType ? { type: m.bindingType } : null,
307
- mutatingExpr: m.mutatingExpr,
308
- line: m.line,
309
- })),
310
- }
311
- : null,
312
- };
193
+ function patchNativeResult(r) {
194
+ // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count"
195
+ r.lineCount = r.lineCount ?? r.line_count ?? null;
196
+ r._lineCount = r.lineCount;
197
+
198
+ // Backward compat for older binaries missing js_name annotations
199
+ if (r.definitions) {
200
+ for (const d of r.definitions) {
201
+ if (d.endLine === undefined && d.end_line !== undefined) {
202
+ d.endLine = d.end_line;
203
+ }
204
+ }
205
+ }
206
+ if (r.imports) {
207
+ for (const i of r.imports) {
208
+ if (i.typeOnly === undefined) i.typeOnly = i.type_only;
209
+ if (i.wildcardReexport === undefined) i.wildcardReexport = i.wildcard_reexport;
210
+ if (i.pythonImport === undefined) i.pythonImport = i.python_import;
211
+ if (i.goImport === undefined) i.goImport = i.go_import;
212
+ if (i.rustUse === undefined) i.rustUse = i.rust_use;
213
+ if (i.javaImport === undefined) i.javaImport = i.java_import;
214
+ if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
215
+ if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
216
+ if (i.phpUse === undefined) i.phpUse = i.php_use;
217
+ }
218
+ }
219
+
220
+ // dataflow: wrap bindingType into binding object for argFlows and mutations
221
+ if (r.dataflow) {
222
+ if (r.dataflow.argFlows) {
223
+ for (const f of r.dataflow.argFlows) {
224
+ f.binding = f.bindingType ? { type: f.bindingType } : null;
225
+ }
226
+ }
227
+ if (r.dataflow.mutations) {
228
+ for (const m of r.dataflow.mutations) {
229
+ m.binding = m.bindingType ? { type: m.bindingType } : null;
230
+ }
231
+ }
232
+ }
233
+
234
+ return r;
313
235
  }
314
236
 
315
237
  /**
@@ -440,8 +362,8 @@ export async function parseFileAuto(filePath, source, opts = {}) {
440
362
  const { native } = resolveEngine(opts);
441
363
 
442
364
  if (native) {
443
- const result = native.parseFile(filePath, source, !!opts.dataflow);
444
- return result ? normalizeNativeSymbols(result) : null;
365
+ const result = native.parseFile(filePath, source, !!opts.dataflow, opts.ast !== false);
366
+ return result ? patchNativeResult(result) : null;
445
367
  }
446
368
 
447
369
  // WASM path
@@ -463,11 +385,16 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
463
385
  const result = new Map();
464
386
 
465
387
  if (native) {
466
- const nativeResults = native.parseFiles(filePaths, rootDir, !!opts.dataflow);
388
+ const nativeResults = native.parseFiles(
389
+ filePaths,
390
+ rootDir,
391
+ !!opts.dataflow,
392
+ opts.ast !== false,
393
+ );
467
394
  for (const r of nativeResults) {
468
395
  if (!r) continue;
469
396
  const relPath = path.relative(rootDir, r.file).split(path.sep).join('/');
470
- result.set(relPath, normalizeNativeSymbols(r));
397
+ result.set(relPath, patchNativeResult(r));
471
398
  }
472
399
  return result;
473
400
  }
@@ -532,7 +459,7 @@ export function createParseTreeCache() {
532
459
  export async function parseFileIncremental(cache, filePath, source, opts = {}) {
533
460
  if (cache) {
534
461
  const result = cache.parseFile(filePath, source);
535
- return result ? normalizeNativeSymbols(result) : null;
462
+ return result ? patchNativeResult(result) : null;
536
463
  }
537
464
  return parseFileAuto(filePath, source, opts);
538
465
  }