@veewo/gitnexus 1.3.11 → 1.4.6-rc
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.
- package/README.md +37 -80
- package/dist/benchmark/agent-context/tool-runner.js +2 -2
- package/dist/benchmark/neonspark-candidates.js +3 -3
- package/dist/benchmark/tool-runner.js +2 -2
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +16 -12
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +68 -48
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +8 -1
- package/dist/cli/eval-server.js +30 -13
- package/dist/cli/index.js +28 -82
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +3 -1
- package/dist/cli/setup.js +87 -48
- package/dist/cli/setup.test.js +18 -13
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +50 -16
- package/dist/cli/wiki.js +8 -4
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +4 -1
- package/dist/config/supported-languages.js +3 -2
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +7 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +8 -6
- package/dist/core/ingestion/call-processor.js +468 -206
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
- package/dist/core/ingestion/entry-point-scoring.js +116 -23
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.d.ts +19 -4
- package/dist/core/ingestion/framework-detection.js +182 -6
- package/dist/core/ingestion/heritage-processor.d.ts +13 -5
- package/dist/core/ingestion/heritage-processor.js +109 -55
- package/dist/core/ingestion/import-processor.d.ts +16 -20
- package/dist/core/ingestion/import-processor.js +199 -579
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +4 -1
- package/dist/core/ingestion/parsing-processor.js +107 -109
- package/dist/core/ingestion/pipeline.d.ts +6 -3
- package/dist/core/ingestion/pipeline.js +208 -114
- package/dist/core/ingestion/process-processor.js +8 -2
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/symbol-table.d.ts +21 -1
- package/dist/core/ingestion/symbol-table.js +40 -12
- package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
- package/dist/core/ingestion/tree-sitter-queries.js +297 -7
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +611 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +406 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +449 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
- package/dist/core/ingestion/type-extractors/shared.js +703 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +103 -0
- package/dist/core/ingestion/utils.js +1085 -4
- package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
- package/dist/core/ingestion/workers/parse-worker.js +634 -222
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
- package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +23 -22
- package/dist/core/lbug/schema.test.d.ts +1 -0
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +12 -11
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +6 -6
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
- package/dist/mcp/core/lbug-adapter.js +327 -0
- package/dist/mcp/local/local-backend.d.ts +21 -16
- package/dist/mcp/local/local-backend.js +306 -706
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
- package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
- package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
- package/dist/mcp/resources.js +2 -2
- package/dist/mcp/server.js +28 -13
- package/dist/mcp/staleness.js +2 -2
- package/dist/mcp/tools.js +12 -3
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +74 -4
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +149 -46
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +20 -4
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/skills/gitnexus-cli.md +8 -8
- package/skills/gitnexus-debugging.md +1 -1
- package/skills/gitnexus-exploring.md +1 -1
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-impact-analysis.md +1 -1
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/mcp/core/kuzu-adapter.js +0 -231
- /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
- /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
- /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
- /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
- /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
2
|
+
/**
|
|
3
|
+
* Walk a named-binding re-export chain through NamedImportMap.
|
|
4
|
+
*
|
|
5
|
+
* When file A imports { User } from B, and B re-exports { User } from C,
|
|
6
|
+
* the NamedImportMap for A points to B, but B has no User definition.
|
|
7
|
+
* This function follows the chain: A→B→C until a definition is found.
|
|
8
|
+
*
|
|
9
|
+
* Returns the definitions found at the end of the chain, or null if the
|
|
10
|
+
* chain breaks (missing binding, circular reference, or depth exceeded).
|
|
11
|
+
* Max depth 5 to prevent infinite loops.
|
|
12
|
+
*
|
|
13
|
+
* @param allDefs Pre-computed `symbolTable.lookupFuzzy(name)` result — must be the
|
|
14
|
+
* complete unfiltered result. Passing a file-filtered subset will cause
|
|
15
|
+
* silent misses at depth=0 for non-aliased bindings.
|
|
16
|
+
*/
|
|
17
|
+
export function walkBindingChain(name, currentFilePath, symbolTable, namedImportMap, allDefs) {
|
|
18
|
+
let lookupFile = currentFilePath;
|
|
19
|
+
let lookupName = name;
|
|
20
|
+
const visited = new Set();
|
|
21
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
22
|
+
const bindings = namedImportMap.get(lookupFile);
|
|
23
|
+
if (!bindings)
|
|
24
|
+
return null;
|
|
25
|
+
const binding = bindings.get(lookupName);
|
|
26
|
+
if (!binding)
|
|
27
|
+
return null;
|
|
28
|
+
const key = `${binding.sourcePath}:${binding.exportedName}`;
|
|
29
|
+
if (visited.has(key))
|
|
30
|
+
return null; // circular
|
|
31
|
+
visited.add(key);
|
|
32
|
+
const targetName = binding.exportedName;
|
|
33
|
+
const resolvedDefs = targetName !== lookupName || depth > 0
|
|
34
|
+
? symbolTable.lookupFuzzy(targetName).filter(def => def.filePath === binding.sourcePath)
|
|
35
|
+
: allDefs.filter(def => def.filePath === binding.sourcePath);
|
|
36
|
+
if (resolvedDefs.length > 0)
|
|
37
|
+
return resolvedDefs;
|
|
38
|
+
// No definition in source file → follow re-export chain
|
|
39
|
+
lookupFile = binding.sourcePath;
|
|
40
|
+
lookupName = targetName;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract named bindings from an import AST node.
|
|
46
|
+
* Returns undefined if the import is not a named import (e.g., import * or default).
|
|
47
|
+
*
|
|
48
|
+
* TS: import { User, Repo as R } from './models'
|
|
49
|
+
* → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
|
|
50
|
+
*
|
|
51
|
+
* Python: from models import User, Repo as R
|
|
52
|
+
* → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
|
|
53
|
+
*/
|
|
54
|
+
export function extractNamedBindings(importNode, language) {
|
|
55
|
+
if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
|
|
56
|
+
return extractTsNamedBindings(importNode);
|
|
57
|
+
}
|
|
58
|
+
if (language === SupportedLanguages.Python) {
|
|
59
|
+
return extractPythonNamedBindings(importNode);
|
|
60
|
+
}
|
|
61
|
+
if (language === SupportedLanguages.Kotlin) {
|
|
62
|
+
return extractKotlinNamedBindings(importNode);
|
|
63
|
+
}
|
|
64
|
+
if (language === SupportedLanguages.Rust) {
|
|
65
|
+
return extractRustNamedBindings(importNode);
|
|
66
|
+
}
|
|
67
|
+
if (language === SupportedLanguages.PHP) {
|
|
68
|
+
return extractPhpNamedBindings(importNode);
|
|
69
|
+
}
|
|
70
|
+
if (language === SupportedLanguages.CSharp) {
|
|
71
|
+
return extractCsharpNamedBindings(importNode);
|
|
72
|
+
}
|
|
73
|
+
if (language === SupportedLanguages.Java) {
|
|
74
|
+
return extractJavaNamedBindings(importNode);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
export function extractTsNamedBindings(importNode) {
|
|
79
|
+
// import_statement > import_clause > named_imports > import_specifier*
|
|
80
|
+
const importClause = findChild(importNode, 'import_clause');
|
|
81
|
+
if (importClause) {
|
|
82
|
+
const namedImports = findChild(importClause, 'named_imports');
|
|
83
|
+
if (!namedImports)
|
|
84
|
+
return undefined; // default import, namespace import, or side-effect
|
|
85
|
+
const bindings = [];
|
|
86
|
+
for (let i = 0; i < namedImports.namedChildCount; i++) {
|
|
87
|
+
const specifier = namedImports.namedChild(i);
|
|
88
|
+
if (specifier?.type !== 'import_specifier')
|
|
89
|
+
continue;
|
|
90
|
+
const identifiers = [];
|
|
91
|
+
for (let j = 0; j < specifier.namedChildCount; j++) {
|
|
92
|
+
const child = specifier.namedChild(j);
|
|
93
|
+
if (child?.type === 'identifier')
|
|
94
|
+
identifiers.push(child.text);
|
|
95
|
+
}
|
|
96
|
+
if (identifiers.length === 1) {
|
|
97
|
+
bindings.push({ local: identifiers[0], exported: identifiers[0] });
|
|
98
|
+
}
|
|
99
|
+
else if (identifiers.length === 2) {
|
|
100
|
+
// import { Foo as Bar } → exported='Foo', local='Bar'
|
|
101
|
+
bindings.push({ local: identifiers[1], exported: identifiers[0] });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
105
|
+
}
|
|
106
|
+
// Re-export: export { X } from './y' → export_statement > export_clause > export_specifier
|
|
107
|
+
const exportClause = findChild(importNode, 'export_clause');
|
|
108
|
+
if (exportClause) {
|
|
109
|
+
const bindings = [];
|
|
110
|
+
for (let i = 0; i < exportClause.namedChildCount; i++) {
|
|
111
|
+
const specifier = exportClause.namedChild(i);
|
|
112
|
+
if (specifier?.type !== 'export_specifier')
|
|
113
|
+
continue;
|
|
114
|
+
const identifiers = [];
|
|
115
|
+
for (let j = 0; j < specifier.namedChildCount; j++) {
|
|
116
|
+
const child = specifier.namedChild(j);
|
|
117
|
+
if (child?.type === 'identifier')
|
|
118
|
+
identifiers.push(child.text);
|
|
119
|
+
}
|
|
120
|
+
if (identifiers.length === 1) {
|
|
121
|
+
// export { User } from './base' → re-exports User as User
|
|
122
|
+
bindings.push({ local: identifiers[0], exported: identifiers[0] });
|
|
123
|
+
}
|
|
124
|
+
else if (identifiers.length === 2) {
|
|
125
|
+
// export { Repo as Repository } from './models' → name=Repo, alias=Repository
|
|
126
|
+
// For re-exports, the first id is the source name, second is what's exported
|
|
127
|
+
// When another file imports { Repository }, they get Repo from the source
|
|
128
|
+
bindings.push({ local: identifiers[1], exported: identifiers[0] });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
export function extractPythonNamedBindings(importNode) {
|
|
136
|
+
// Only from import_from_statement, not plain import_statement
|
|
137
|
+
if (importNode.type !== 'import_from_statement')
|
|
138
|
+
return undefined;
|
|
139
|
+
const bindings = [];
|
|
140
|
+
for (let i = 0; i < importNode.namedChildCount; i++) {
|
|
141
|
+
const child = importNode.namedChild(i);
|
|
142
|
+
if (!child)
|
|
143
|
+
continue;
|
|
144
|
+
if (child.type === 'dotted_name') {
|
|
145
|
+
// Skip the module_name (first dotted_name is the source module)
|
|
146
|
+
const fieldName = importNode.childForFieldName?.('module_name');
|
|
147
|
+
if (fieldName && child.startIndex === fieldName.startIndex)
|
|
148
|
+
continue;
|
|
149
|
+
// This is an imported name: from x import User
|
|
150
|
+
const name = child.text;
|
|
151
|
+
if (name)
|
|
152
|
+
bindings.push({ local: name, exported: name });
|
|
153
|
+
}
|
|
154
|
+
if (child.type === 'aliased_import') {
|
|
155
|
+
// from x import Repo as R
|
|
156
|
+
const dottedName = findChild(child, 'dotted_name');
|
|
157
|
+
const aliasIdent = findChild(child, 'identifier');
|
|
158
|
+
if (dottedName && aliasIdent) {
|
|
159
|
+
bindings.push({ local: aliasIdent.text, exported: dottedName.text });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
164
|
+
}
|
|
165
|
+
export function extractKotlinNamedBindings(importNode) {
|
|
166
|
+
// import_header > identifier + import_alias > simple_identifier
|
|
167
|
+
if (importNode.type !== 'import_header')
|
|
168
|
+
return undefined;
|
|
169
|
+
const fullIdent = findChild(importNode, 'identifier');
|
|
170
|
+
if (!fullIdent)
|
|
171
|
+
return undefined;
|
|
172
|
+
const fullText = fullIdent.text;
|
|
173
|
+
const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
|
|
174
|
+
const importAlias = findChild(importNode, 'import_alias');
|
|
175
|
+
if (importAlias) {
|
|
176
|
+
// Aliased: import com.example.User as U
|
|
177
|
+
const aliasIdent = findChild(importAlias, 'simple_identifier');
|
|
178
|
+
if (!aliasIdent)
|
|
179
|
+
return undefined;
|
|
180
|
+
return [{ local: aliasIdent.text, exported: exportedName }];
|
|
181
|
+
}
|
|
182
|
+
// Non-aliased: import com.example.User → local="User", exported="User"
|
|
183
|
+
// Skip wildcard imports (ending in *)
|
|
184
|
+
if (fullText.endsWith('.*') || fullText.endsWith('*'))
|
|
185
|
+
return undefined;
|
|
186
|
+
// Skip lowercase last segments — those are member/function imports (e.g.,
|
|
187
|
+
// import util.OneArg.writeAudit), not class imports. Multiple member imports
|
|
188
|
+
// with the same function name would collide in NamedImportMap, breaking
|
|
189
|
+
// arity-based disambiguation.
|
|
190
|
+
if (exportedName[0] && exportedName[0] === exportedName[0].toLowerCase())
|
|
191
|
+
return undefined;
|
|
192
|
+
return [{ local: exportedName, exported: exportedName }];
|
|
193
|
+
}
|
|
194
|
+
export function extractRustNamedBindings(importNode) {
|
|
195
|
+
// use_declaration may contain use_as_clause at any depth
|
|
196
|
+
if (importNode.type !== 'use_declaration')
|
|
197
|
+
return undefined;
|
|
198
|
+
const bindings = [];
|
|
199
|
+
collectRustBindings(importNode, bindings);
|
|
200
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
201
|
+
}
|
|
202
|
+
function collectRustBindings(node, bindings) {
|
|
203
|
+
if (node.type === 'use_as_clause') {
|
|
204
|
+
// First identifier = exported name, second identifier = local alias
|
|
205
|
+
const idents = [];
|
|
206
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
207
|
+
const child = node.namedChild(i);
|
|
208
|
+
if (child?.type === 'identifier')
|
|
209
|
+
idents.push(child.text);
|
|
210
|
+
// For scoped_identifier, extract the last segment
|
|
211
|
+
if (child?.type === 'scoped_identifier') {
|
|
212
|
+
const nameNode = child.childForFieldName?.('name');
|
|
213
|
+
if (nameNode)
|
|
214
|
+
idents.push(nameNode.text);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (idents.length === 2) {
|
|
218
|
+
bindings.push({ local: idents[1], exported: idents[0] });
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Terminal identifier in a use_list: use crate::models::{User, Repo}
|
|
223
|
+
if (node.type === 'identifier' && node.parent?.type === 'use_list') {
|
|
224
|
+
bindings.push({ local: node.text, exported: node.text });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Skip scoped_identifier that serves as path prefix in scoped_use_list
|
|
228
|
+
// e.g. use crate::models::{User, Repo} — the path node "crate::models" is not an importable symbol
|
|
229
|
+
if (node.type === 'scoped_identifier' && node.parent?.type === 'scoped_use_list') {
|
|
230
|
+
return; // path prefix — the use_list sibling handles the actual symbols
|
|
231
|
+
}
|
|
232
|
+
// Terminal scoped_identifier: use crate::models::User;
|
|
233
|
+
// Only extract if this is a leaf (no deeper use_list/use_as_clause/scoped_use_list)
|
|
234
|
+
if (node.type === 'scoped_identifier') {
|
|
235
|
+
let hasDeeper = false;
|
|
236
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
237
|
+
const child = node.namedChild(i);
|
|
238
|
+
if (child?.type === 'use_list' || child?.type === 'use_as_clause' || child?.type === 'scoped_use_list') {
|
|
239
|
+
hasDeeper = true;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!hasDeeper) {
|
|
244
|
+
const nameNode = node.childForFieldName?.('name');
|
|
245
|
+
if (nameNode) {
|
|
246
|
+
bindings.push({ local: nameNode.text, exported: nameNode.text });
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Recurse into children
|
|
252
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
253
|
+
const child = node.namedChild(i);
|
|
254
|
+
if (child)
|
|
255
|
+
collectRustBindings(child, bindings);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export function extractPhpNamedBindings(importNode) {
|
|
259
|
+
// namespace_use_declaration > namespace_use_clause* (flat)
|
|
260
|
+
// namespace_use_declaration > namespace_use_group > namespace_use_clause* (grouped)
|
|
261
|
+
if (importNode.type !== 'namespace_use_declaration')
|
|
262
|
+
return undefined;
|
|
263
|
+
const bindings = [];
|
|
264
|
+
// Collect all clauses — from direct children AND from namespace_use_group
|
|
265
|
+
const clauses = [];
|
|
266
|
+
for (let i = 0; i < importNode.namedChildCount; i++) {
|
|
267
|
+
const child = importNode.namedChild(i);
|
|
268
|
+
if (child?.type === 'namespace_use_clause') {
|
|
269
|
+
clauses.push(child);
|
|
270
|
+
}
|
|
271
|
+
else if (child?.type === 'namespace_use_group') {
|
|
272
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
273
|
+
const groupChild = child.namedChild(j);
|
|
274
|
+
if (groupChild?.type === 'namespace_use_clause')
|
|
275
|
+
clauses.push(groupChild);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const clause of clauses) {
|
|
280
|
+
// Flat imports: qualified_name + name (alias)
|
|
281
|
+
let qualifiedName = null;
|
|
282
|
+
const names = [];
|
|
283
|
+
for (let j = 0; j < clause.namedChildCount; j++) {
|
|
284
|
+
const child = clause.namedChild(j);
|
|
285
|
+
if (child?.type === 'qualified_name')
|
|
286
|
+
qualifiedName = child;
|
|
287
|
+
else if (child?.type === 'name')
|
|
288
|
+
names.push(child);
|
|
289
|
+
}
|
|
290
|
+
if (qualifiedName && names.length > 0) {
|
|
291
|
+
// Flat aliased import: use App\Models\Repo as R;
|
|
292
|
+
const fullText = qualifiedName.text;
|
|
293
|
+
const exportedName = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
|
|
294
|
+
bindings.push({ local: names[0].text, exported: exportedName });
|
|
295
|
+
}
|
|
296
|
+
else if (qualifiedName && names.length === 0) {
|
|
297
|
+
// Flat non-aliased import: use App\Models\User;
|
|
298
|
+
const fullText = qualifiedName.text;
|
|
299
|
+
const lastSegment = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
|
|
300
|
+
bindings.push({ local: lastSegment, exported: lastSegment });
|
|
301
|
+
}
|
|
302
|
+
else if (!qualifiedName && names.length >= 2) {
|
|
303
|
+
// Grouped aliased import: {Repo as R} — first name = exported, second = alias
|
|
304
|
+
bindings.push({ local: names[1].text, exported: names[0].text });
|
|
305
|
+
}
|
|
306
|
+
else if (!qualifiedName && names.length === 1) {
|
|
307
|
+
// Grouped non-aliased import: {User} in use App\Models\{User, Repo as R}
|
|
308
|
+
bindings.push({ local: names[0].text, exported: names[0].text });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return bindings.length > 0 ? bindings : undefined;
|
|
312
|
+
}
|
|
313
|
+
export function extractCsharpNamedBindings(importNode) {
|
|
314
|
+
// using_directive with identifier (alias) + qualified_name (target)
|
|
315
|
+
if (importNode.type !== 'using_directive')
|
|
316
|
+
return undefined;
|
|
317
|
+
let aliasIdent = null;
|
|
318
|
+
let qualifiedName = null;
|
|
319
|
+
for (let i = 0; i < importNode.namedChildCount; i++) {
|
|
320
|
+
const child = importNode.namedChild(i);
|
|
321
|
+
if (child?.type === 'identifier' && !aliasIdent)
|
|
322
|
+
aliasIdent = child;
|
|
323
|
+
else if (child?.type === 'qualified_name')
|
|
324
|
+
qualifiedName = child;
|
|
325
|
+
}
|
|
326
|
+
if (!aliasIdent || !qualifiedName)
|
|
327
|
+
return undefined;
|
|
328
|
+
const fullText = qualifiedName.text;
|
|
329
|
+
const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
|
|
330
|
+
return [{ local: aliasIdent.text, exported: exportedName }];
|
|
331
|
+
}
|
|
332
|
+
export function extractJavaNamedBindings(importNode) {
|
|
333
|
+
// import_declaration > scoped_identifier "com.example.models.User"
|
|
334
|
+
// Wildcard imports (.*) don't produce named bindings
|
|
335
|
+
if (importNode.type !== 'import_declaration')
|
|
336
|
+
return undefined;
|
|
337
|
+
// Check for asterisk (wildcard import) — skip those
|
|
338
|
+
for (let i = 0; i < importNode.childCount; i++) {
|
|
339
|
+
const child = importNode.child(i);
|
|
340
|
+
if (child?.type === 'asterisk')
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const scopedId = findChild(importNode, 'scoped_identifier');
|
|
344
|
+
if (!scopedId)
|
|
345
|
+
return undefined;
|
|
346
|
+
const fullText = scopedId.text;
|
|
347
|
+
const lastDot = fullText.lastIndexOf('.');
|
|
348
|
+
if (lastDot === -1)
|
|
349
|
+
return undefined;
|
|
350
|
+
const className = fullText.slice(lastDot + 1);
|
|
351
|
+
// Skip lowercase names — those are package imports, not class imports
|
|
352
|
+
if (className[0] && className[0] === className[0].toLowerCase())
|
|
353
|
+
return undefined;
|
|
354
|
+
return [{ local: className, exported: className }];
|
|
355
|
+
}
|
|
356
|
+
function findChild(node, type) {
|
|
357
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
358
|
+
const child = node.namedChild(i);
|
|
359
|
+
if (child?.type === type)
|
|
360
|
+
return child;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
@@ -2,13 +2,16 @@ import { KnowledgeGraph } from '../graph/types.js';
|
|
|
2
2
|
import { SymbolTable } from './symbol-table.js';
|
|
3
3
|
import { ASTCache } from './ast-cache.js';
|
|
4
4
|
import { WorkerPool } from './workers/worker-pool.js';
|
|
5
|
-
import type { ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js';
|
|
5
|
+
import type { ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
|
6
6
|
export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
|
|
7
7
|
export interface WorkerExtractedData {
|
|
8
8
|
imports: ExtractedImport[];
|
|
9
9
|
calls: ExtractedCall[];
|
|
10
10
|
heritage: ExtractedHeritage[];
|
|
11
|
+
routes: ExtractedRoute[];
|
|
12
|
+
constructorBindings: FileConstructorBindings[];
|
|
11
13
|
}
|
|
14
|
+
export { isNodeExported } from './export-detection.js';
|
|
12
15
|
export declare const processParsing: (graph: KnowledgeGraph, files: {
|
|
13
16
|
path: string;
|
|
14
17
|
content: string;
|
|
@@ -1,103 +1,15 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
-
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
2
|
+
import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
|
3
3
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
4
4
|
import { generateId } from '../../lib/utils.js';
|
|
5
|
-
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* @param node - The AST node for the symbol name
|
|
14
|
-
* @param name - The symbol name
|
|
15
|
-
* @param language - The programming language
|
|
16
|
-
* @returns true if the symbol is exported/public
|
|
17
|
-
*/
|
|
18
|
-
const isNodeExported = (node, name, language) => {
|
|
19
|
-
let current = node;
|
|
20
|
-
switch (language) {
|
|
21
|
-
// JavaScript/TypeScript: Check for export keyword in ancestors
|
|
22
|
-
case 'javascript':
|
|
23
|
-
case 'typescript':
|
|
24
|
-
while (current) {
|
|
25
|
-
const type = current.type;
|
|
26
|
-
if (type === 'export_statement' ||
|
|
27
|
-
type === 'export_specifier' ||
|
|
28
|
-
type === 'lexical_declaration' && current.parent?.type === 'export_statement') {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
// Also check if text starts with 'export '
|
|
32
|
-
if (current.text?.startsWith('export ')) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
current = current.parent;
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
// Python: Public if no leading underscore (convention)
|
|
39
|
-
case 'python':
|
|
40
|
-
return !name.startsWith('_');
|
|
41
|
-
// Java: Check for 'public' modifier
|
|
42
|
-
// In tree-sitter Java, modifiers are siblings of the name node, not parents
|
|
43
|
-
case 'java':
|
|
44
|
-
while (current) {
|
|
45
|
-
// Check if this node or any sibling is a 'modifiers' node containing 'public'
|
|
46
|
-
if (current.parent) {
|
|
47
|
-
const parent = current.parent;
|
|
48
|
-
// Check all children of the parent for modifiers
|
|
49
|
-
for (let i = 0; i < parent.childCount; i++) {
|
|
50
|
-
const child = parent.child(i);
|
|
51
|
-
if (child?.type === 'modifiers' && child.text?.includes('public')) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
// Also check if the parent's text starts with 'public' (fallback)
|
|
56
|
-
if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') {
|
|
57
|
-
if (parent.text?.trimStart().startsWith('public')) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
current = current.parent;
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
// C#: Check for 'public' modifier in ancestors
|
|
66
|
-
case 'csharp':
|
|
67
|
-
while (current) {
|
|
68
|
-
if (current.type === 'modifier' || current.type === 'modifiers') {
|
|
69
|
-
if (current.text?.includes('public'))
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
current = current.parent;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
// Go: Uppercase first letter = exported
|
|
76
|
-
case 'go':
|
|
77
|
-
if (name.length === 0)
|
|
78
|
-
return false;
|
|
79
|
-
const first = name[0];
|
|
80
|
-
// Must be uppercase letter (not a number or symbol)
|
|
81
|
-
return first === first.toUpperCase() && first !== first.toLowerCase();
|
|
82
|
-
// Rust: Check for 'pub' visibility modifier
|
|
83
|
-
case 'rust':
|
|
84
|
-
while (current) {
|
|
85
|
-
if (current.type === 'visibility_modifier') {
|
|
86
|
-
if (current.text?.includes('pub'))
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
current = current.parent;
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
// C/C++: No native export concept at language level
|
|
93
|
-
// Entry points will be detected via name patterns (main, etc.)
|
|
94
|
-
case 'c':
|
|
95
|
-
case 'cpp':
|
|
96
|
-
return false;
|
|
97
|
-
default:
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
5
|
+
import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
|
|
6
|
+
import { isNodeExported } from './export-detection.js';
|
|
7
|
+
import { detectFrameworkFromAST } from './framework-detection.js';
|
|
8
|
+
import { typeConfigs } from './type-extractors/index.js';
|
|
9
|
+
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
|
|
10
|
+
// isNodeExported imported from ./export-detection.js (shared module)
|
|
11
|
+
// Re-export for backward compatibility with any external consumers
|
|
12
|
+
export { isNodeExported } from './export-detection.js';
|
|
101
13
|
// ============================================================================
|
|
102
14
|
// Worker-based parallel parsing
|
|
103
15
|
// ============================================================================
|
|
@@ -110,7 +22,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
110
22
|
parseableFiles.push({ path: file.path, content: file.content });
|
|
111
23
|
}
|
|
112
24
|
if (parseableFiles.length === 0)
|
|
113
|
-
return { imports: [], calls: [], heritage: [] };
|
|
25
|
+
return { imports: [], calls: [], heritage: [], routes: [], constructorBindings: [] };
|
|
114
26
|
const total = files.length;
|
|
115
27
|
// Dispatch to worker pool — pool handles splitting into chunks and sub-batching
|
|
116
28
|
const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
|
|
@@ -120,6 +32,8 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
120
32
|
const allImports = [];
|
|
121
33
|
const allCalls = [];
|
|
122
34
|
const allHeritage = [];
|
|
35
|
+
const allRoutes = [];
|
|
36
|
+
const allConstructorBindings = [];
|
|
123
37
|
for (const result of chunkResults) {
|
|
124
38
|
for (const node of result.nodes) {
|
|
125
39
|
graph.addNode({
|
|
@@ -132,15 +46,34 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
132
46
|
graph.addRelationship(rel);
|
|
133
47
|
}
|
|
134
48
|
for (const sym of result.symbols) {
|
|
135
|
-
symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type
|
|
49
|
+
symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
|
|
50
|
+
parameterCount: sym.parameterCount,
|
|
51
|
+
returnType: sym.returnType,
|
|
52
|
+
ownerId: sym.ownerId,
|
|
53
|
+
});
|
|
136
54
|
}
|
|
137
55
|
allImports.push(...result.imports);
|
|
138
56
|
allCalls.push(...result.calls);
|
|
139
57
|
allHeritage.push(...result.heritage);
|
|
58
|
+
allRoutes.push(...result.routes);
|
|
59
|
+
allConstructorBindings.push(...result.constructorBindings);
|
|
60
|
+
}
|
|
61
|
+
// Merge and log skipped languages from workers
|
|
62
|
+
const skippedLanguages = new Map();
|
|
63
|
+
for (const result of chunkResults) {
|
|
64
|
+
for (const [lang, count] of Object.entries(result.skippedLanguages)) {
|
|
65
|
+
skippedLanguages.set(lang, (skippedLanguages.get(lang) || 0) + count);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (skippedLanguages.size > 0) {
|
|
69
|
+
const summary = Array.from(skippedLanguages.entries())
|
|
70
|
+
.map(([lang, count]) => `${lang}: ${count}`)
|
|
71
|
+
.join(', ');
|
|
72
|
+
console.warn(` Skipped unsupported languages: ${summary}`);
|
|
140
73
|
}
|
|
141
74
|
// Final progress
|
|
142
75
|
onFileProgress?.(total, total, 'done');
|
|
143
|
-
return { imports: allImports, calls: allCalls, heritage: allHeritage };
|
|
76
|
+
return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes, constructorBindings: allConstructorBindings };
|
|
144
77
|
};
|
|
145
78
|
// ============================================================================
|
|
146
79
|
// Sequential fallback (original implementation)
|
|
@@ -148,6 +81,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
|
|
|
148
81
|
const processParsingSequential = async (graph, files, symbolTable, astCache, onFileProgress) => {
|
|
149
82
|
const parser = await loadParser();
|
|
150
83
|
const total = files.length;
|
|
84
|
+
const skippedLanguages = new Map();
|
|
151
85
|
for (let i = 0; i < files.length; i++) {
|
|
152
86
|
const file = files[i];
|
|
153
87
|
onFileProgress?.(i + 1, total, file.path);
|
|
@@ -156,13 +90,23 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
156
90
|
const language = getLanguageFromFilename(file.path);
|
|
157
91
|
if (!language)
|
|
158
92
|
continue;
|
|
159
|
-
// Skip
|
|
160
|
-
if (
|
|
93
|
+
// Skip unsupported languages (e.g. Swift when tree-sitter-swift not installed)
|
|
94
|
+
if (!isLanguageAvailable(language)) {
|
|
95
|
+
skippedLanguages.set(language, (skippedLanguages.get(language) || 0) + 1);
|
|
161
96
|
continue;
|
|
162
|
-
|
|
97
|
+
}
|
|
98
|
+
// Skip files larger than the max tree-sitter buffer (32 MB)
|
|
99
|
+
if (file.content.length > TREE_SITTER_MAX_BUFFER)
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
await loadLanguage(language, file.path);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue; // parser unavailable — safety net
|
|
106
|
+
}
|
|
163
107
|
let tree;
|
|
164
108
|
try {
|
|
165
|
-
tree = parser.parse(file.content, undefined, { bufferSize:
|
|
109
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
166
110
|
}
|
|
167
111
|
catch (parseError) {
|
|
168
112
|
console.warn(`Skipping unparseable file: ${file.path}`);
|
|
@@ -196,9 +140,10 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
196
140
|
return;
|
|
197
141
|
}
|
|
198
142
|
const nameNode = captureMap['name'];
|
|
199
|
-
|
|
143
|
+
// Synthesize name for constructors without explicit @name capture (e.g. Swift init)
|
|
144
|
+
if (!nameNode && !captureMap['definition.constructor'])
|
|
200
145
|
return;
|
|
201
|
-
const nodeName = nameNode.text;
|
|
146
|
+
const nodeName = nameNode ? nameNode.text : 'init';
|
|
202
147
|
let nodeLabel = 'CodeElement';
|
|
203
148
|
if (captureMap['definition.function'])
|
|
204
149
|
nodeLabel = 'Function';
|
|
@@ -244,7 +189,27 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
244
189
|
nodeLabel = 'Constructor';
|
|
245
190
|
else if (captureMap['definition.template'])
|
|
246
191
|
nodeLabel = 'Template';
|
|
192
|
+
const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap);
|
|
193
|
+
const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
|
|
247
194
|
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
|
|
195
|
+
const definitionNode = getDefinitionNodeFromCaptures(captureMap);
|
|
196
|
+
const frameworkHint = definitionNode
|
|
197
|
+
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
|
198
|
+
: null;
|
|
199
|
+
// Extract method signature for Method/Constructor nodes
|
|
200
|
+
const methodSig = (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
|
|
201
|
+
? extractMethodSignature(definitionNode)
|
|
202
|
+
: undefined;
|
|
203
|
+
// Language-specific return type fallback (e.g. Ruby YARD @return [Type])
|
|
204
|
+
// Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
|
|
205
|
+
if (methodSig && (!methodSig.returnType || methodSig.returnType === 'array' || methodSig.returnType === 'iterable') && definitionNode) {
|
|
206
|
+
const tc = typeConfigs[language];
|
|
207
|
+
if (tc?.extractReturnType) {
|
|
208
|
+
const docReturn = tc.extractReturnType(definitionNode);
|
|
209
|
+
if (docReturn)
|
|
210
|
+
methodSig.returnType = docReturn;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
248
213
|
const node = {
|
|
249
214
|
id: nodeId,
|
|
250
215
|
label: nodeLabel,
|
|
@@ -254,11 +219,27 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
254
219
|
startLine: nameNode.startPosition.row + 1,
|
|
255
220
|
endLine: nameNode.endPosition.row + 1,
|
|
256
221
|
language: language,
|
|
257
|
-
isExported: isNodeExported(nameNode, nodeName, language),
|
|
258
|
-
|
|
222
|
+
isExported: isNodeExported(nameNode || definitionNodeForRange, nodeName, language),
|
|
223
|
+
...(frameworkHint ? {
|
|
224
|
+
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
|
|
225
|
+
astFrameworkReason: frameworkHint.reason,
|
|
226
|
+
} : {}),
|
|
227
|
+
...(methodSig ? {
|
|
228
|
+
parameterCount: methodSig.parameterCount,
|
|
229
|
+
returnType: methodSig.returnType,
|
|
230
|
+
} : {}),
|
|
231
|
+
},
|
|
259
232
|
};
|
|
260
233
|
graph.addNode(node);
|
|
261
|
-
|
|
234
|
+
// Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
|
|
235
|
+
// Function is included because Kotlin/Rust/Python capture class methods as Function nodes
|
|
236
|
+
const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
|
|
237
|
+
const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
|
|
238
|
+
symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
|
|
239
|
+
parameterCount: methodSig?.parameterCount,
|
|
240
|
+
returnType: methodSig?.returnType,
|
|
241
|
+
ownerId: enclosingClassId ?? undefined,
|
|
242
|
+
});
|
|
262
243
|
const fileId = generateId('File', file.path);
|
|
263
244
|
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
264
245
|
const relationship = {
|
|
@@ -270,8 +251,25 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
|
|
|
270
251
|
reason: '',
|
|
271
252
|
};
|
|
272
253
|
graph.addRelationship(relationship);
|
|
254
|
+
// ── HAS_METHOD: link method/constructor/property to enclosing class ──
|
|
255
|
+
if (enclosingClassId) {
|
|
256
|
+
graph.addRelationship({
|
|
257
|
+
id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
|
|
258
|
+
sourceId: enclosingClassId,
|
|
259
|
+
targetId: nodeId,
|
|
260
|
+
type: 'HAS_METHOD',
|
|
261
|
+
confidence: 1.0,
|
|
262
|
+
reason: '',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
273
265
|
});
|
|
274
266
|
}
|
|
267
|
+
if (skippedLanguages.size > 0) {
|
|
268
|
+
const summary = Array.from(skippedLanguages.entries())
|
|
269
|
+
.map(([lang, count]) => `${lang}: ${count}`)
|
|
270
|
+
.join(', ');
|
|
271
|
+
console.warn(` Skipped unsupported languages: ${summary}`);
|
|
272
|
+
}
|
|
275
273
|
};
|
|
276
274
|
// ============================================================================
|
|
277
275
|
// Public API
|