code-auditor-mcp 3.4.12 → 3.4.13
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/CHANGELOG.md +46 -1
- package/dist/analyzers/__verify_self_probe.d.ts +2 -0
- package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
- package/dist/analyzers/__verify_self_probe.js +5 -0
- package/dist/analyzers/__verify_self_probe.js.map +1 -0
- package/dist/analyzers/analyzerUtils.d.ts +34 -1
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +61 -22
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +259 -198
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +57 -50
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.js +10 -0
- package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.js +42 -44
- package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.js +30 -0
- package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +62 -2
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +443 -349
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +11 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +188 -125
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +110 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +1714 -124
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +54 -0
- package/dist/analyzers/ruleTiming.d.ts.map +1 -0
- package/dist/analyzers/ruleTiming.js +71 -0
- package/dist/analyzers/ruleTiming.js.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
- package/dist/analyzers/universal/schema/config.d.ts +31 -0
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/config.js +57 -0
- package/dist/analyzers/universal/schema/config.js.map +1 -0
- package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
- package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/discovery.js +616 -0
- package/dist/analyzers/universal/schema/discovery.js.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
- package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/migrations.js +238 -0
- package/dist/analyzers/universal/schema/migrations.js.map +1 -0
- package/dist/analyzers/universal/schema/types.d.ts +131 -0
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/types.js +10 -0
- package/dist/analyzers/universal/schema/types.js.map +1 -0
- package/dist/analyzers/universal/schema/violations.d.ts +44 -0
- package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/violations.js +54 -0
- package/dist/analyzers/universal/schema/violations.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +89 -13
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +315 -40
- package/dist/cli.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +9 -7
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +12 -3
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +15 -5
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts +75 -0
- package/dist/config/effectiveConfig.d.ts.map +1 -0
- package/dist/config/effectiveConfig.js +213 -0
- package/dist/config/effectiveConfig.js.map +1 -0
- package/dist/config/pathProfiles.d.ts +7 -4
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +8 -10
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/config/thresholdRationales.d.ts +45 -0
- package/dist/config/thresholdRationales.d.ts.map +1 -0
- package/dist/config/thresholdRationales.js +67 -0
- package/dist/config/thresholdRationales.js.map +1 -0
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +6 -2
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/enforcement/diffGate.d.ts +55 -0
- package/dist/enforcement/diffGate.d.ts.map +1 -0
- package/dist/enforcement/diffGate.js +136 -0
- package/dist/enforcement/diffGate.js.map +1 -0
- package/dist/enforcement/gate.d.ts +36 -0
- package/dist/enforcement/gate.d.ts.map +1 -0
- package/dist/enforcement/gate.js +72 -0
- package/dist/enforcement/gate.js.map +1 -0
- package/dist/enforcement/suppressions.d.ts +41 -0
- package/dist/enforcement/suppressions.d.ts.map +1 -0
- package/dist/enforcement/suppressions.js +170 -0
- package/dist/enforcement/suppressions.js.map +1 -0
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +4 -1
- package/dist/fingerprint.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +3 -4
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +2 -2
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +14 -0
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +93 -55
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts +8 -0
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/LanguageRegistry.js +8 -0
- package/dist/languages/LanguageRegistry.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +82 -60
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +404 -349
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +51 -6
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +127 -88
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +36 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -1
- package/dist/languages/adapterBridge.js +89 -35
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +66 -23
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +553 -474
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.d.ts +18 -0
- package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/converter.js +18 -0
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.d.ts +39 -2
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
- package/dist/languages/tree-sitter/parser.js +93 -12
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/types.d.ts +67 -2
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/pipeline.d.ts +13 -5
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +381 -200
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +3 -8
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +149 -94
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/presets/presets.d.ts +54 -0
- package/dist/presets/presets.d.ts.map +1 -0
- package/dist/presets/presets.js +181 -0
- package/dist/presets/presets.js.map +1 -0
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +0 -2
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/ruleAliases.d.ts +67 -0
- package/dist/ruleAliases.d.ts.map +1 -0
- package/dist/ruleAliases.js +79 -0
- package/dist/ruleAliases.js.map +1 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -1
- package/dist/styles/cssAstExtractor.js +78 -18
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.js +10 -1
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.d.ts +8 -6
- package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
- package/dist/styles/tailwindConfigLoader.js +61 -15
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +13 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +181 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/scripts/hook-audit.sh +25 -7
- package/plugin/skills/code-auditor/SKILL.md +5 -5
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses web-tree-sitter WASM parsers behind the adapter seam — no TypeScript compiler API.
|
|
5
5
|
* Supports .ts, .tsx, .js, and .jsx files.
|
|
6
|
+
*
|
|
7
|
+
* The adapter is split across a leaf-first inheritance chain (Spec-33
|
|
8
|
+
* interface-segregation): each link contributes a cohesive slice of the
|
|
9
|
+
* adapter so no single class exceeds the SOLID class-size threshold.
|
|
10
|
+
* Ancestors hold the tree-traversal, extraction, and scope-resolution
|
|
11
|
+
* primitives; descendants compose them into the public LanguageAdapter
|
|
12
|
+
* surface. The mutually-recursive cross-function safety cluster is
|
|
13
|
+
* co-located in TsSafetyAnalysis (TypeScript forbids a base class calling a
|
|
14
|
+
* subclass method, so those must live in one class).
|
|
6
15
|
*/
|
|
7
|
-
import { getParser } from '../tree-sitter/parser.js';
|
|
16
|
+
import { getParser, parseWithRecovery } from '../tree-sitter/parser.js';
|
|
8
17
|
import { toASTNode, toSourceLocation } from '../tree-sitter/converter.js';
|
|
9
18
|
// ---------------------------------------------------------------------------
|
|
10
19
|
// Source code storage
|
|
@@ -14,316 +23,28 @@ import { toASTNode, toSourceLocation } from '../tree-sitter/converter.js';
|
|
|
14
23
|
* can recover the original source text.
|
|
15
24
|
*/
|
|
16
25
|
const sourceCodeMap = new WeakMap();
|
|
26
|
+
/**
|
|
27
|
+
* A sentinel ASTNode standing in for a provably-constant string. Used when
|
|
28
|
+
* binding `.map()` callback element parameters (which iterate a static array)
|
|
29
|
+
* during cross-function safety analysis: the element is unknown but its value
|
|
30
|
+
* is constrained to the array's compile-time elements, so it is safe to embed
|
|
31
|
+
* in a SQL fragment the same way a literal is.
|
|
32
|
+
*/
|
|
33
|
+
const SAFE_STRING_NODE = {
|
|
34
|
+
type: 'string',
|
|
35
|
+
range: [-1, -1],
|
|
36
|
+
location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
|
|
37
|
+
raw: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
text: "''",
|
|
40
|
+
startIndex: -1,
|
|
41
|
+
endIndex: -1,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
17
44
|
// ---------------------------------------------------------------------------
|
|
18
|
-
//
|
|
45
|
+
// Traversal + primitive helpers
|
|
19
46
|
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
name = 'typescript';
|
|
22
|
-
fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
23
|
-
// -- File detection -------------------------------------------------------
|
|
24
|
-
supportsFile(filePath) {
|
|
25
|
-
return this.fileExtensions.some((ext) => filePath.endsWith(ext));
|
|
26
|
-
}
|
|
27
|
-
// -- Parsing --------------------------------------------------------------
|
|
28
|
-
async parse(filePath, content) {
|
|
29
|
-
const isTsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
|
|
30
|
-
const lang = isTsx ? 'tsx' : filePath.endsWith('.go') ? 'go' : 'typescript';
|
|
31
|
-
const parser = getParser(lang, isTsx);
|
|
32
|
-
const tree = parser.parse(content);
|
|
33
|
-
if (!tree)
|
|
34
|
-
throw new Error(`Failed to parse file: ${filePath}`);
|
|
35
|
-
const errors = [];
|
|
36
|
-
// Collect ERROR nodes as parse errors
|
|
37
|
-
this.collectErrors(tree.rootNode, errors);
|
|
38
|
-
const root = toASTNode(tree.rootNode, undefined, lang);
|
|
39
|
-
const ast = {
|
|
40
|
-
root,
|
|
41
|
-
language: lang,
|
|
42
|
-
filePath,
|
|
43
|
-
errors,
|
|
44
|
-
};
|
|
45
|
-
sourceCodeMap.set(ast, content);
|
|
46
|
-
return ast;
|
|
47
|
-
}
|
|
48
|
-
// -- AST Navigation -------------------------------------------------------
|
|
49
|
-
findNodes(ast, pattern) {
|
|
50
|
-
const results = [];
|
|
51
|
-
this.walk(ast.root, (node) => {
|
|
52
|
-
if (this.matchesPattern(node, pattern)) {
|
|
53
|
-
results.push(node);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return results;
|
|
57
|
-
}
|
|
58
|
-
getParent(node) {
|
|
59
|
-
return node.parent ?? null;
|
|
60
|
-
}
|
|
61
|
-
getChildren(node) {
|
|
62
|
-
return node.children ?? [];
|
|
63
|
-
}
|
|
64
|
-
getSiblings(node) {
|
|
65
|
-
if (!node.parent?.children)
|
|
66
|
-
return [];
|
|
67
|
-
return node.parent.children.filter((c) => c !== node);
|
|
68
|
-
}
|
|
69
|
-
// -- Node Information -----------------------------------------------------
|
|
70
|
-
getNodeType(node) {
|
|
71
|
-
return node.type;
|
|
72
|
-
}
|
|
73
|
-
getNodeText(node, sourceCode) {
|
|
74
|
-
return sourceCode.slice(node.range[0], node.range[1]);
|
|
75
|
-
}
|
|
76
|
-
getNodeName(node) {
|
|
77
|
-
const syntaxNode = node.raw;
|
|
78
|
-
return this.extractName(syntaxNode);
|
|
79
|
-
}
|
|
80
|
-
getNodeLocation(node) {
|
|
81
|
-
return node.location;
|
|
82
|
-
}
|
|
83
|
-
// -- Language-Specific Extraction -----------------------------------------
|
|
84
|
-
extractFunctions(ast) {
|
|
85
|
-
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
86
|
-
const functions = [];
|
|
87
|
-
this.walk(ast.root, (node) => {
|
|
88
|
-
const syntaxNode = node.raw;
|
|
89
|
-
const type = syntaxNode.type;
|
|
90
|
-
if (type === 'function_declaration' ||
|
|
91
|
-
type === 'generator_function_declaration' ||
|
|
92
|
-
type === 'function_expression' ||
|
|
93
|
-
type === 'arrow_function' ||
|
|
94
|
-
type === 'method_definition') {
|
|
95
|
-
const fn = this.buildFunctionInfo(syntaxNode, sourceCode);
|
|
96
|
-
if (fn)
|
|
97
|
-
functions.push(fn);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
return functions;
|
|
101
|
-
}
|
|
102
|
-
extractClasses(ast) {
|
|
103
|
-
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
104
|
-
const classes = [];
|
|
105
|
-
this.walk(ast.root, (node) => {
|
|
106
|
-
const syntaxNode = node.raw;
|
|
107
|
-
if (syntaxNode.type === 'class_declaration') {
|
|
108
|
-
const cls = this.buildClassInfo(syntaxNode, sourceCode);
|
|
109
|
-
if (cls)
|
|
110
|
-
classes.push(cls);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
return classes;
|
|
114
|
-
}
|
|
115
|
-
extractImports(ast) {
|
|
116
|
-
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
117
|
-
const imports = [];
|
|
118
|
-
this.walk(ast.root, (node) => {
|
|
119
|
-
const syntaxNode = node.raw;
|
|
120
|
-
if (syntaxNode.type === 'import_statement') {
|
|
121
|
-
const imp = this.buildImportInfo(syntaxNode, sourceCode);
|
|
122
|
-
if (imp)
|
|
123
|
-
imports.push(imp);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
return imports;
|
|
127
|
-
}
|
|
128
|
-
extractExports(ast) {
|
|
129
|
-
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
130
|
-
const exports = [];
|
|
131
|
-
this.walk(ast.root, (node) => {
|
|
132
|
-
const syntaxNode = node.raw;
|
|
133
|
-
if (syntaxNode.type === 'export_statement') {
|
|
134
|
-
const ex = this.buildExportInfo(syntaxNode, sourceCode);
|
|
135
|
-
if (ex)
|
|
136
|
-
exports.push(ex);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
return exports;
|
|
140
|
-
}
|
|
141
|
-
// -- Pattern Matching Helpers ---------------------------------------------
|
|
142
|
-
isClass(node) {
|
|
143
|
-
const type = node.raw.type;
|
|
144
|
-
return type === 'class_declaration' || type === 'class_expression';
|
|
145
|
-
}
|
|
146
|
-
isFunction(node) {
|
|
147
|
-
const type = node.raw.type;
|
|
148
|
-
return (type === 'function_declaration' ||
|
|
149
|
-
type === 'function_expression' ||
|
|
150
|
-
type === 'arrow_function' ||
|
|
151
|
-
type === 'generator_function_declaration' ||
|
|
152
|
-
type === 'generator_function_expression');
|
|
153
|
-
}
|
|
154
|
-
isMethod(node) {
|
|
155
|
-
return node.raw.type === 'method_definition';
|
|
156
|
-
}
|
|
157
|
-
isInterface(node) {
|
|
158
|
-
return node.raw.type === 'interface_declaration';
|
|
159
|
-
}
|
|
160
|
-
isImport(node) {
|
|
161
|
-
return node.raw.type === 'import_statement';
|
|
162
|
-
}
|
|
163
|
-
isExport(node) {
|
|
164
|
-
return node.raw.type === 'export_statement';
|
|
165
|
-
}
|
|
166
|
-
isLoop(node) {
|
|
167
|
-
const type = node.raw.type;
|
|
168
|
-
return (type === 'for_statement' ||
|
|
169
|
-
type === 'for_in_statement' ||
|
|
170
|
-
type === 'while_statement' ||
|
|
171
|
-
type === 'do_statement');
|
|
172
|
-
}
|
|
173
|
-
isConditional(node) {
|
|
174
|
-
const type = node.raw.type;
|
|
175
|
-
return (type === 'if_statement' ||
|
|
176
|
-
type === 'switch_statement' ||
|
|
177
|
-
type === 'ternary_expression' ||
|
|
178
|
-
type === 'switch_case');
|
|
179
|
-
}
|
|
180
|
-
isVariableDeclaration(node) {
|
|
181
|
-
const type = node.raw.type;
|
|
182
|
-
return (type === 'variable_declaration' ||
|
|
183
|
-
type === 'lexical_declaration' ||
|
|
184
|
-
type === 'variable_declarator');
|
|
185
|
-
}
|
|
186
|
-
// -- Advanced Features ----------------------------------------------------
|
|
187
|
-
getTypeInfo(node) {
|
|
188
|
-
const syntaxNode = node.raw;
|
|
189
|
-
return this.extractTypeAnnotation(syntaxNode);
|
|
190
|
-
}
|
|
191
|
-
getDocumentation(node) {
|
|
192
|
-
const syntaxNode = node.raw;
|
|
193
|
-
return this.extractDocumentation(syntaxNode);
|
|
194
|
-
}
|
|
195
|
-
getComplexity(node) {
|
|
196
|
-
const syntaxNode = node.raw;
|
|
197
|
-
return this.calculateCyclomaticComplexity(syntaxNode);
|
|
198
|
-
}
|
|
199
|
-
// -- Optional: Interfaces -------------------------------------------------
|
|
200
|
-
extractInterfaces(ast) {
|
|
201
|
-
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
202
|
-
const interfaces = [];
|
|
203
|
-
this.walk(ast.root, (node) => {
|
|
204
|
-
const syntaxNode = node.raw;
|
|
205
|
-
if (syntaxNode.type === 'interface_declaration') {
|
|
206
|
-
const iface = this.buildInterfaceInfo(syntaxNode, sourceCode);
|
|
207
|
-
if (iface)
|
|
208
|
-
interfaces.push(iface);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
return interfaces;
|
|
212
|
-
}
|
|
213
|
-
// -- Optional: Raw imports (static, dynamic, require) ----------------------
|
|
214
|
-
extractRawImports(_filePath, content) {
|
|
215
|
-
const results = [];
|
|
216
|
-
const parser = getParser('typescript');
|
|
217
|
-
const tree = parser.parse(content);
|
|
218
|
-
if (!tree)
|
|
219
|
-
return results;
|
|
220
|
-
// Static imports
|
|
221
|
-
this.walkRaw(tree.rootNode, (node) => {
|
|
222
|
-
if (node.type === 'import_statement') {
|
|
223
|
-
const source = this.getChildByType(node, 'string');
|
|
224
|
-
if (source) {
|
|
225
|
-
const specifier = source.text.slice(1, -1); // strip quotes
|
|
226
|
-
results.push({
|
|
227
|
-
moduleSpecifier: specifier,
|
|
228
|
-
isStatic: true,
|
|
229
|
-
isDynamic: false,
|
|
230
|
-
isRequire: false,
|
|
231
|
-
line: source.startPosition.row,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// Dynamic import(): import('...')
|
|
236
|
-
if (node.type === 'call_expression') {
|
|
237
|
-
const fn = node.firstChild;
|
|
238
|
-
if (fn?.type === 'import') {
|
|
239
|
-
const args = this.getChildByType(node, 'arguments');
|
|
240
|
-
if (args) {
|
|
241
|
-
const strNode = this.findFirstNamedChild(args, 'string');
|
|
242
|
-
if (strNode) {
|
|
243
|
-
const specifier = strNode.text.slice(1, -1);
|
|
244
|
-
results.push({
|
|
245
|
-
moduleSpecifier: specifier,
|
|
246
|
-
isStatic: false,
|
|
247
|
-
isDynamic: true,
|
|
248
|
-
isRequire: false,
|
|
249
|
-
line: strNode.startPosition.row,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// require('...')
|
|
255
|
-
if (fn?.type === 'identifier' && fn.text === 'require') {
|
|
256
|
-
const args = this.getChildByType(node, 'arguments');
|
|
257
|
-
if (args) {
|
|
258
|
-
const strNode = this.findFirstNamedChild(args, 'string');
|
|
259
|
-
if (strNode) {
|
|
260
|
-
const specifier = strNode.text.slice(1, -1);
|
|
261
|
-
results.push({
|
|
262
|
-
moduleSpecifier: specifier,
|
|
263
|
-
isStatic: false,
|
|
264
|
-
isDynamic: false,
|
|
265
|
-
isRequire: true,
|
|
266
|
-
line: strNode.startPosition.row,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
return results;
|
|
274
|
-
}
|
|
275
|
-
// -- Optional: Exported symbol names --------------------------------------
|
|
276
|
-
extractExportedSymbols(ast) {
|
|
277
|
-
const symbols = [];
|
|
278
|
-
this.walk(ast.root, (astNode) => {
|
|
279
|
-
const node = astNode.raw;
|
|
280
|
-
if (node.type === 'export_statement') {
|
|
281
|
-
// export function foo / export class Foo / export const x
|
|
282
|
-
const declaration = this.findFirstNamedChild(node, [
|
|
283
|
-
'function_declaration',
|
|
284
|
-
'class_declaration',
|
|
285
|
-
'lexical_declaration',
|
|
286
|
-
'variable_declaration',
|
|
287
|
-
'interface_declaration',
|
|
288
|
-
'type_alias_declaration',
|
|
289
|
-
'enum_declaration',
|
|
290
|
-
]);
|
|
291
|
-
if (declaration) {
|
|
292
|
-
const name = this.extractName(declaration);
|
|
293
|
-
if (name) {
|
|
294
|
-
symbols.push({ name, line: declaration.startPosition.row });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// export { foo, bar } or export { default }
|
|
299
|
-
const clause = this.getChildByType(node, 'export_clause');
|
|
300
|
-
if (clause) {
|
|
301
|
-
for (const child of clause.namedChildren) {
|
|
302
|
-
if (child.type === 'export_specifier') {
|
|
303
|
-
const nameNode = this.getChildByType(child, 'identifier');
|
|
304
|
-
if (nameNode) {
|
|
305
|
-
symbols.push({
|
|
306
|
-
name: nameNode.text,
|
|
307
|
-
line: nameNode.startPosition.row,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// export default <expression>
|
|
314
|
-
if (node.childForFieldName?.('value')) {
|
|
315
|
-
symbols.push({
|
|
316
|
-
name: 'default',
|
|
317
|
-
line: node.startPosition.row,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
return symbols;
|
|
323
|
-
}
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
// Internal helpers
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
47
|
+
class TsTraversalHelpers {
|
|
327
48
|
/** Walk all ASTNodes in a tree (depth-first). */
|
|
328
49
|
walk(node, visitor) {
|
|
329
50
|
visitor(node);
|
|
@@ -353,93 +74,164 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
353
74
|
this.collectErrors(child, errors);
|
|
354
75
|
}
|
|
355
76
|
}
|
|
356
|
-
/**
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const types = Array.isArray(pattern.type) ? pattern.type : [pattern.type];
|
|
362
|
-
if (!types.includes(syntaxNode.type))
|
|
363
|
-
return false;
|
|
77
|
+
/** Find first child of node matching one of the given types. */
|
|
78
|
+
getChildByType(node, type) {
|
|
79
|
+
for (const child of node.children) {
|
|
80
|
+
if (child.type === type)
|
|
81
|
+
return child;
|
|
364
82
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
else if (pattern.name instanceof RegExp) {
|
|
375
|
-
if (!pattern.name.test(nodeName))
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/** Find the first named child matching one of the given types. */
|
|
86
|
+
findFirstNamedChild(node, types) {
|
|
87
|
+
const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
|
|
88
|
+
for (const child of node.namedChildren) {
|
|
89
|
+
if (typeSet.has(child.type))
|
|
90
|
+
return child;
|
|
378
91
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/** Check if node has a direct child (named or anonymous) with the given text. */
|
|
95
|
+
hasChild(node, text) {
|
|
96
|
+
for (const child of node.children) {
|
|
97
|
+
if (child.text === text || child.type === text)
|
|
98
|
+
return true;
|
|
385
99
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/** Get the operator text from a binary expression node. */
|
|
103
|
+
getOperator(node) {
|
|
104
|
+
for (const child of node.children) {
|
|
105
|
+
if (!child.isNamed &&
|
|
106
|
+
['&&', '||', '+', '-', '*', '/', '%', '===', '!==', '==', '!='].includes(child.type)) {
|
|
107
|
+
return child.type;
|
|
108
|
+
}
|
|
392
109
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/** Check if a node has an `async` modifier keyword. */
|
|
113
|
+
hasModifier(node, modifier) {
|
|
114
|
+
for (const child of node.children) {
|
|
115
|
+
if (!child.isNamed && child.type === modifier)
|
|
116
|
+
return true;
|
|
117
|
+
// tree-sitter may represent modifiers as named decorator/modifier nodes
|
|
118
|
+
if (child.isNamed &&
|
|
119
|
+
(child.type === modifier ||
|
|
120
|
+
child.type === 'accessibility_modifier' ||
|
|
121
|
+
child.type === 'override_modifier' ||
|
|
122
|
+
child.type === 'readonly' ||
|
|
123
|
+
child.type === 'static' ||
|
|
124
|
+
child.type === 'abstract')) {
|
|
125
|
+
if (child.text === modifier)
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
397
128
|
}
|
|
398
|
-
return
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/** Check if a node is under an export_statement. */
|
|
132
|
+
isNodeExported(node) {
|
|
133
|
+
const parent = node.parent;
|
|
134
|
+
if (parent?.type === 'export_statement')
|
|
135
|
+
return true;
|
|
136
|
+
// Also check for `export` keyword on the node itself (TS module syntax)
|
|
137
|
+
for (const child of node.children) {
|
|
138
|
+
if (!child.isNamed && child.type === 'export')
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
extractParameters(node, sourceCode) {
|
|
144
|
+
const params = [];
|
|
145
|
+
const formalParams = node.childForFieldName?.('parameters');
|
|
146
|
+
if (!formalParams)
|
|
147
|
+
return params;
|
|
148
|
+
for (const child of formalParams.namedChildren) {
|
|
149
|
+
if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
|
|
150
|
+
const paramNode = child.childForFieldName?.('pattern');
|
|
151
|
+
const name = paramNode?.text ?? child.firstNamedChild?.text;
|
|
152
|
+
const isOptional = child.type === 'optional_parameter';
|
|
153
|
+
const typeNode = child.childForFieldName?.('type');
|
|
154
|
+
let defaultValue;
|
|
155
|
+
// Check for a default value
|
|
156
|
+
const valueNode = child.childForFieldName?.('value');
|
|
157
|
+
if (valueNode) {
|
|
158
|
+
defaultValue = valueNode.text;
|
|
159
|
+
}
|
|
160
|
+
if (name) {
|
|
161
|
+
params.push({
|
|
162
|
+
name,
|
|
163
|
+
type: typeNode?.text.replace(/^:\s*/, ''),
|
|
164
|
+
optional: isOptional,
|
|
165
|
+
defaultValue,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return params;
|
|
171
|
+
}
|
|
172
|
+
/** Extract ordered parameter names from a function node. */
|
|
173
|
+
getParamNames(fnNode) {
|
|
174
|
+
const raw = fnNode.raw;
|
|
175
|
+
const paramsNode = raw.childForFieldName?.('parameters');
|
|
176
|
+
if (!paramsNode)
|
|
177
|
+
return [];
|
|
178
|
+
const names = [];
|
|
179
|
+
for (const child of paramsNode.namedChildren) {
|
|
180
|
+
if (child.type === 'identifier') {
|
|
181
|
+
names.push(child.text);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const pattern = child.childForFieldName?.('pattern');
|
|
185
|
+
if (pattern && pattern.type === 'identifier') {
|
|
186
|
+
names.push(pattern.text);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
for (const c of child.namedChildren) {
|
|
190
|
+
if (c.type === 'identifier') {
|
|
191
|
+
names.push(c.text);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return names;
|
|
197
|
+
}
|
|
198
|
+
/** Wrap a raw tree-sitter node into a detached ASTNode for local analysis. */
|
|
199
|
+
wrapRaw(raw) {
|
|
200
|
+
if (!raw)
|
|
201
|
+
return null;
|
|
202
|
+
return toASTNode(raw, undefined, 'typescript');
|
|
399
203
|
}
|
|
204
|
+
/** Return the ASTNodes for a call expression's arguments. */
|
|
205
|
+
getCallArgASTNodes(callNode) {
|
|
206
|
+
const raw = callNode.raw;
|
|
207
|
+
const argsNode = raw.childForFieldName?.('arguments');
|
|
208
|
+
if (!argsNode)
|
|
209
|
+
return [];
|
|
210
|
+
return argsNode.namedChildren.map((c) => this.wrapRaw(c));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Name / type / documentation extraction
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
class TsNameDocumentation extends TsTraversalHelpers {
|
|
400
217
|
/** Extract the name/identifier from a tree-sitter TreeSitterNode. */
|
|
401
218
|
extractName(node) {
|
|
402
219
|
switch (node.type) {
|
|
403
220
|
case 'function_declaration':
|
|
404
221
|
case 'generator_function_declaration':
|
|
405
222
|
case 'class_declaration':
|
|
223
|
+
case 'abstract_class_declaration':
|
|
406
224
|
case 'interface_declaration':
|
|
407
225
|
case 'enum_declaration':
|
|
408
|
-
case 'type_alias_declaration':
|
|
409
|
-
|
|
410
|
-
if (nameNode)
|
|
411
|
-
return nameNode.text;
|
|
412
|
-
// Fallback: find first identifier child
|
|
413
|
-
for (const child of node.namedChildren) {
|
|
414
|
-
if (child.type === 'identifier' || child.type === 'type_identifier') {
|
|
415
|
-
return child.text;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
226
|
+
case 'type_alias_declaration':
|
|
227
|
+
return this.resolveNameField(node, ['identifier', 'type_identifier']);
|
|
420
228
|
case 'method_definition':
|
|
421
|
-
case 'public_field_definition':
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
// property_identifier or string
|
|
426
|
-
for (const child of node.namedChildren) {
|
|
427
|
-
if (child.type === 'property_identifier' ||
|
|
428
|
-
child.type === 'string') {
|
|
429
|
-
return child.text.replace(/^["']|["']$/g, '');
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
case 'variable_declarator': {
|
|
435
|
-
const nameNode = node.childForFieldName?.('name');
|
|
436
|
-
if (nameNode)
|
|
437
|
-
return nameNode.text;
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
229
|
+
case 'public_field_definition':
|
|
230
|
+
return this.resolveNameField(node, ['property_identifier', 'string']);
|
|
231
|
+
case 'variable_declarator':
|
|
232
|
+
return this.resolveNameField(node, []);
|
|
440
233
|
case 'lexical_declaration':
|
|
441
234
|
case 'variable_declaration': {
|
|
442
|
-
// Find the declarator child and extract its name
|
|
443
235
|
for (const child of node.namedChildren) {
|
|
444
236
|
if (child.type === 'variable_declarator') {
|
|
445
237
|
return this.extractName(child);
|
|
@@ -448,24 +240,15 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
448
240
|
return null;
|
|
449
241
|
}
|
|
450
242
|
case 'arrow_function': {
|
|
451
|
-
// Check if assigned to a variable: const foo = () => {}
|
|
452
243
|
const parent = node.parent;
|
|
453
|
-
|
|
454
|
-
return this.extractName(parent);
|
|
455
|
-
}
|
|
456
|
-
return null;
|
|
244
|
+
return parent?.type === 'variable_declarator' ? this.extractName(parent) : null;
|
|
457
245
|
}
|
|
458
246
|
case 'function_expression': {
|
|
459
|
-
// Might have a name: const foo = function bar() {}
|
|
460
247
|
const nameNode = node.childForFieldName?.('name');
|
|
461
248
|
if (nameNode)
|
|
462
249
|
return nameNode.text;
|
|
463
|
-
// Check if assigned to a variable
|
|
464
250
|
const parent = node.parent;
|
|
465
|
-
|
|
466
|
-
return this.extractName(parent);
|
|
467
|
-
}
|
|
468
|
-
return null;
|
|
251
|
+
return parent?.type === 'variable_declarator' ? this.extractName(parent) : null;
|
|
469
252
|
}
|
|
470
253
|
case 'property_identifier':
|
|
471
254
|
return node.text;
|
|
@@ -475,6 +258,21 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
475
258
|
return null;
|
|
476
259
|
}
|
|
477
260
|
}
|
|
261
|
+
/** Return `node`'s `name` field text, else the first named child whose type
|
|
262
|
+
* is in `fallbackTypes` (stripping quotes from `string` children). */
|
|
263
|
+
resolveNameField(node, fallbackTypes) {
|
|
264
|
+
const nameNode = node.childForFieldName?.('name');
|
|
265
|
+
if (nameNode)
|
|
266
|
+
return nameNode.text;
|
|
267
|
+
for (const child of node.namedChildren) {
|
|
268
|
+
if (fallbackTypes.includes(child.type)) {
|
|
269
|
+
return child.type === 'string'
|
|
270
|
+
? child.text.replace(/^["']|["']$/g, '')
|
|
271
|
+
: child.text;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
478
276
|
/** Extract a type annotation string from a node. */
|
|
479
277
|
extractTypeAnnotation(node) {
|
|
480
278
|
// Function return type
|
|
@@ -538,37 +336,36 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
538
336
|
const text = comments.join('\n').trim();
|
|
539
337
|
return this.cleanCommentText(text);
|
|
540
338
|
}
|
|
541
|
-
// When the
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
339
|
+
// When the parent is `export_statement`, the JSDoc lives at the grandparent
|
|
340
|
+
// (program) level as a sibling of the wrapper — ascend and look for it.
|
|
341
|
+
return this.findExportStatementDoc(parent);
|
|
342
|
+
}
|
|
343
|
+
/** When `parent` is an `export_statement`, find an adjacent /** JSDoc comment
|
|
344
|
+
* at the grandparent level. Adjacency guard (comment ends on line L, wrapper
|
|
345
|
+
* starts on L+1) prevents misattributing file-level comments. */
|
|
346
|
+
findExportStatementDoc(parent) {
|
|
347
|
+
if (parent.type !== 'export_statement' || !parent.parent)
|
|
348
|
+
return null;
|
|
349
|
+
const gpSiblings = parent.parent.children;
|
|
350
|
+
let parentIndex = -1;
|
|
351
|
+
for (let i = 0; i < gpSiblings.length; i++) {
|
|
352
|
+
if (gpSiblings[i].equals(parent)) {
|
|
353
|
+
parentIndex = i;
|
|
354
|
+
break;
|
|
555
355
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return this.cleanCommentText(sibling.text);
|
|
565
|
-
}
|
|
566
|
-
break; // Found a comment but not adjacent — stop looking
|
|
567
|
-
}
|
|
568
|
-
else if (sibling.isNamed) {
|
|
569
|
-
break; // Stop at named siblings
|
|
570
|
-
}
|
|
356
|
+
}
|
|
357
|
+
if (parentIndex <= 0)
|
|
358
|
+
return null;
|
|
359
|
+
for (let i = parentIndex - 1; i >= 0; i--) {
|
|
360
|
+
const sibling = gpSiblings[i];
|
|
361
|
+
if (sibling.type === 'comment' && sibling.text.trimStart().startsWith('/**')) {
|
|
362
|
+
if (sibling.endPosition.row + 1 === parent.startPosition.row) {
|
|
363
|
+
return this.cleanCommentText(sibling.text);
|
|
571
364
|
}
|
|
365
|
+
break; // Found a comment but not adjacent — stop looking
|
|
366
|
+
}
|
|
367
|
+
else if (sibling.isNamed) {
|
|
368
|
+
break; // Stop at named siblings
|
|
572
369
|
}
|
|
573
370
|
}
|
|
574
371
|
return null;
|
|
@@ -623,17 +420,55 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
623
420
|
});
|
|
624
421
|
return complexity;
|
|
625
422
|
}
|
|
626
|
-
/**
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
423
|
+
/** Match an ASTNode against a NodePattern. */
|
|
424
|
+
matchesPattern(node, pattern) {
|
|
425
|
+
const syntaxNode = node.raw;
|
|
426
|
+
// type matching
|
|
427
|
+
if (pattern.type !== undefined) {
|
|
428
|
+
const types = Array.isArray(pattern.type) ? pattern.type : [pattern.type];
|
|
429
|
+
if (!types.includes(syntaxNode.type))
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
// name matching
|
|
433
|
+
if (pattern.name !== undefined) {
|
|
434
|
+
const nodeName = this.extractName(syntaxNode);
|
|
435
|
+
if (nodeName === null)
|
|
436
|
+
return false;
|
|
437
|
+
if (typeof pattern.name === 'string') {
|
|
438
|
+
if (nodeName !== pattern.name)
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
else if (pattern.name instanceof RegExp) {
|
|
442
|
+
if (!pattern.name.test(nodeName))
|
|
443
|
+
return false;
|
|
632
444
|
}
|
|
633
445
|
}
|
|
634
|
-
|
|
446
|
+
// hasChild matching
|
|
447
|
+
if (pattern.hasChild !== undefined) {
|
|
448
|
+
const childNodes = node.children ?? [];
|
|
449
|
+
const hasMatch = childNodes.some((c) => this.matchesPattern(c, pattern.hasChild));
|
|
450
|
+
if (!hasMatch)
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
// hasParent matching
|
|
454
|
+
if (pattern.hasParent !== undefined) {
|
|
455
|
+
if (!node.parent)
|
|
456
|
+
return false;
|
|
457
|
+
if (!this.matchesPattern(node.parent, pattern.hasParent))
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
// custom predicate
|
|
461
|
+
if (pattern.custom !== undefined) {
|
|
462
|
+
if (!pattern.custom(node))
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
635
466
|
}
|
|
636
|
-
|
|
467
|
+
}
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Extraction builders (functions, classes, imports, exports, interfaces)
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
class TsExtraction extends TsNameDocumentation {
|
|
637
472
|
buildFunctionInfo(node, sourceCode) {
|
|
638
473
|
const name = this.extractName(node);
|
|
639
474
|
if (name === null && node.type !== 'arrow_function')
|
|
@@ -679,28 +514,10 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
679
514
|
if (implementsClause) {
|
|
680
515
|
implementsList = implementsClause.namedChildren.map((c) => c.text);
|
|
681
516
|
}
|
|
682
|
-
// Extract methods
|
|
683
517
|
const classBody = node.childForFieldName?.('body');
|
|
684
|
-
const methods =
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
for (const member of classBody.namedChildren) {
|
|
688
|
-
if (member.type === 'method_definition') {
|
|
689
|
-
const fnInfo = this.buildFunctionInfo(member, sourceCode);
|
|
690
|
-
if (fnInfo) {
|
|
691
|
-
fnInfo.className = name;
|
|
692
|
-
fnInfo.isMethod = true;
|
|
693
|
-
methods.push(fnInfo);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
else if (member.type === 'public_field_definition' ||
|
|
697
|
-
member.type === 'field_definition') {
|
|
698
|
-
const propInfo = this.buildPropertyInfo(member);
|
|
699
|
-
if (propInfo)
|
|
700
|
-
properties.push(propInfo);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
518
|
+
const { methods, properties } = classBody
|
|
519
|
+
? this.buildClassMembers(classBody, sourceCode, name)
|
|
520
|
+
: { methods: [], properties: [] };
|
|
704
521
|
return {
|
|
705
522
|
name,
|
|
706
523
|
location: toSourceLocation(node),
|
|
@@ -713,6 +530,28 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
713
530
|
jsDoc: jsDoc ?? undefined,
|
|
714
531
|
};
|
|
715
532
|
}
|
|
533
|
+
/** Build method/property lists from a class body's members. */
|
|
534
|
+
buildClassMembers(classBody, sourceCode, className) {
|
|
535
|
+
const methods = [];
|
|
536
|
+
const properties = [];
|
|
537
|
+
for (const member of classBody.namedChildren) {
|
|
538
|
+
if (member.type === 'method_definition') {
|
|
539
|
+
const fnInfo = this.buildFunctionInfo(member, sourceCode);
|
|
540
|
+
if (fnInfo) {
|
|
541
|
+
fnInfo.className = className;
|
|
542
|
+
fnInfo.isMethod = true;
|
|
543
|
+
methods.push(fnInfo);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else if (member.type === 'public_field_definition' ||
|
|
547
|
+
member.type === 'field_definition') {
|
|
548
|
+
const propInfo = this.buildPropertyInfo(member);
|
|
549
|
+
if (propInfo)
|
|
550
|
+
properties.push(propInfo);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { methods, properties };
|
|
554
|
+
}
|
|
716
555
|
buildPropertyInfo(node) {
|
|
717
556
|
const name = this.extractName(node);
|
|
718
557
|
if (!name)
|
|
@@ -742,90 +581,77 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
742
581
|
return null;
|
|
743
582
|
const source = sourceNode.text.slice(1, -1); // strip quotes
|
|
744
583
|
const specifiers = [];
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
584
|
+
for (const child of node.namedChildren) {
|
|
585
|
+
this.collectImportSpecifiers(child, specifiers);
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
source,
|
|
589
|
+
specifiers,
|
|
590
|
+
location: toSourceLocation(node),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** Recursively collect import specifiers (default / named / namespace).
|
|
594
|
+
* Tree-sitter nests named imports inside import_clause -> named_imports. */
|
|
595
|
+
collectImportSpecifiers(child, specifiers) {
|
|
596
|
+
if (child.type === 'import_specifier') {
|
|
597
|
+
const nameNode = child.childForFieldName?.('name');
|
|
598
|
+
const aliasNode = child.childForFieldName?.('alias');
|
|
599
|
+
if (nameNode) {
|
|
600
|
+
specifiers.push({
|
|
601
|
+
name: nameNode.text,
|
|
602
|
+
alias: aliasNode?.text,
|
|
603
|
+
isDefault: false,
|
|
604
|
+
isNamespace: false,
|
|
605
|
+
});
|
|
763
606
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
607
|
+
}
|
|
608
|
+
else if (child.type === 'namespace_import') {
|
|
609
|
+
const nameNode = child.childForFieldName?.('name');
|
|
610
|
+
if (nameNode) {
|
|
611
|
+
specifiers.push({
|
|
612
|
+
name: nameNode.text,
|
|
613
|
+
isDefault: false,
|
|
614
|
+
isNamespace: true,
|
|
615
|
+
});
|
|
773
616
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
617
|
+
}
|
|
618
|
+
else if (child.type === 'import_clause') {
|
|
619
|
+
// Default import: `import foo from '...'` — import_clause children include
|
|
620
|
+
// an identifier for the default binding and optionally named_imports for
|
|
621
|
+
// mixed imports (`import foo, { bar }`).
|
|
622
|
+
for (const grandchild of child.namedChildren) {
|
|
623
|
+
if (grandchild.type === 'identifier') {
|
|
778
624
|
specifiers.push({
|
|
779
|
-
name:
|
|
625
|
+
name: grandchild.text,
|
|
780
626
|
isDefault: true,
|
|
781
627
|
isNamespace: false,
|
|
782
628
|
});
|
|
783
629
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
// Recurse into containers: import_clause, named_imports
|
|
787
|
-
for (const grandchild of child.namedChildren) {
|
|
788
|
-
collectSpecifiers(grandchild);
|
|
630
|
+
else {
|
|
631
|
+
this.collectImportSpecifiers(grandchild, specifiers);
|
|
789
632
|
}
|
|
790
633
|
}
|
|
791
|
-
};
|
|
792
|
-
for (const child of node.namedChildren) {
|
|
793
|
-
collectSpecifiers(child);
|
|
794
634
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
635
|
+
else {
|
|
636
|
+
// Recurse into containers: import_clause, named_imports
|
|
637
|
+
for (const grandchild of child.namedChildren) {
|
|
638
|
+
this.collectImportSpecifiers(grandchild, specifiers);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
800
641
|
}
|
|
801
642
|
buildExportInfo(node, _sourceCode) {
|
|
802
643
|
// export default <expression>
|
|
803
644
|
const isDefault = this.hasChild(node, 'default');
|
|
804
|
-
// export { foo, bar } [from '...']
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
for (const spec of clause.namedChildren) {
|
|
810
|
-
if (spec.type === 'export_specifier') {
|
|
811
|
-
const nameNode = spec.childForFieldName?.('name');
|
|
812
|
-
if (nameNode) {
|
|
813
|
-
exports.push({
|
|
814
|
-
name: nameNode.text,
|
|
815
|
-
location: toSourceLocation(nameNode),
|
|
816
|
-
isDefault: false,
|
|
817
|
-
source: sourceNode?.text.slice(1, -1),
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
// Return first (handled by caller iterating export_statement nodes)
|
|
823
|
-
return exports[0] ?? null;
|
|
824
|
-
}
|
|
645
|
+
// export { foo, bar } [from '...'] — return the first named export
|
|
646
|
+
// (caller iterates export_statement nodes).
|
|
647
|
+
const named = this.buildNamedExport(node);
|
|
648
|
+
if (named)
|
|
649
|
+
return named;
|
|
825
650
|
// export function/class/const/let/var name
|
|
826
651
|
const declaration = this.findFirstNamedChild(node, [
|
|
827
652
|
'function_declaration',
|
|
828
653
|
'class_declaration',
|
|
654
|
+
'abstract_class_declaration',
|
|
829
655
|
'lexical_declaration',
|
|
830
656
|
'variable_declaration',
|
|
831
657
|
]);
|
|
@@ -853,6 +679,27 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
853
679
|
}
|
|
854
680
|
return null;
|
|
855
681
|
}
|
|
682
|
+
/** Build the ExportInfo for an `export { a, b } [from '...']` statement. */
|
|
683
|
+
buildNamedExport(node) {
|
|
684
|
+
const clause = node.childForFieldName?.('clause');
|
|
685
|
+
if (clause?.type !== 'export_clause')
|
|
686
|
+
return null;
|
|
687
|
+
const sourceNode = node.childForFieldName?.('source');
|
|
688
|
+
for (const spec of clause.namedChildren) {
|
|
689
|
+
if (spec.type === 'export_specifier') {
|
|
690
|
+
const nameNode = spec.childForFieldName?.('name');
|
|
691
|
+
if (nameNode) {
|
|
692
|
+
return {
|
|
693
|
+
name: nameNode.text,
|
|
694
|
+
location: toSourceLocation(nameNode),
|
|
695
|
+
isDefault: false,
|
|
696
|
+
source: sourceNode?.text.slice(1, -1),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
856
703
|
buildInterfaceInfo(node, _sourceCode) {
|
|
857
704
|
const name = this.extractName(node);
|
|
858
705
|
if (!name)
|
|
@@ -866,31 +713,9 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
866
713
|
}
|
|
867
714
|
// Members
|
|
868
715
|
const body = node.childForFieldName?.('body');
|
|
869
|
-
const members =
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (member.type === 'method_signature') {
|
|
873
|
-
const memberName = member.childForFieldName?.('name')?.text;
|
|
874
|
-
if (memberName) {
|
|
875
|
-
members.push({
|
|
876
|
-
name: memberName,
|
|
877
|
-
type: 'method',
|
|
878
|
-
location: toSourceLocation(member),
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
else if (member.type === 'property_signature') {
|
|
883
|
-
const memberName = member.childForFieldName?.('name')?.text;
|
|
884
|
-
if (memberName) {
|
|
885
|
-
members.push({
|
|
886
|
-
name: memberName,
|
|
887
|
-
type: 'property',
|
|
888
|
-
location: toSourceLocation(member),
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
716
|
+
const members = body
|
|
717
|
+
? this.buildInterfaceMembers(body)
|
|
718
|
+
: [];
|
|
894
719
|
return {
|
|
895
720
|
name,
|
|
896
721
|
location: toSourceLocation(node),
|
|
@@ -899,414 +724,350 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
899
724
|
isExported,
|
|
900
725
|
};
|
|
901
726
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const typeNode = child.childForFieldName?.('type');
|
|
914
|
-
let defaultValue;
|
|
915
|
-
// Check for a default value
|
|
916
|
-
const valueNode = child.childForFieldName?.('value');
|
|
917
|
-
if (valueNode) {
|
|
918
|
-
defaultValue = valueNode.text;
|
|
919
|
-
}
|
|
920
|
-
if (name) {
|
|
921
|
-
params.push({
|
|
922
|
-
name,
|
|
923
|
-
type: typeNode?.text.replace(/^:\s*/, ''),
|
|
924
|
-
optional: isOptional,
|
|
925
|
-
defaultValue,
|
|
727
|
+
/** Build method/property member descriptors from an interface body. */
|
|
728
|
+
buildInterfaceMembers(body) {
|
|
729
|
+
const members = [];
|
|
730
|
+
for (const member of body.namedChildren) {
|
|
731
|
+
if (member.type === 'method_signature' || member.type === 'property_signature') {
|
|
732
|
+
const memberName = member.childForFieldName?.('name')?.text;
|
|
733
|
+
if (memberName) {
|
|
734
|
+
members.push({
|
|
735
|
+
name: memberName,
|
|
736
|
+
type: member.type === 'method_signature' ? 'method' : 'property',
|
|
737
|
+
location: toSourceLocation(member),
|
|
926
738
|
});
|
|
927
739
|
}
|
|
928
740
|
}
|
|
929
741
|
}
|
|
930
|
-
return
|
|
931
|
-
}
|
|
932
|
-
// -- Context helpers -------------------------------------------------------
|
|
933
|
-
/** Check if a node has an `async` modifier keyword. */
|
|
934
|
-
hasModifier(node, modifier) {
|
|
935
|
-
for (const child of node.children) {
|
|
936
|
-
if (!child.isNamed && child.type === modifier)
|
|
937
|
-
return true;
|
|
938
|
-
// tree-sitter may represent modifiers as named decorator/modifier nodes
|
|
939
|
-
if (child.isNamed &&
|
|
940
|
-
(child.type === modifier ||
|
|
941
|
-
child.type === 'accessibility_modifier' ||
|
|
942
|
-
child.type === 'override_modifier' ||
|
|
943
|
-
child.type === 'readonly' ||
|
|
944
|
-
child.type === 'static' ||
|
|
945
|
-
child.type === 'abstract')) {
|
|
946
|
-
if (child.text === modifier)
|
|
947
|
-
return true;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return false;
|
|
951
|
-
}
|
|
952
|
-
/** Check if a node is under an export_statement. */
|
|
953
|
-
isNodeExported(node) {
|
|
954
|
-
const parent = node.parent;
|
|
955
|
-
if (parent?.type === 'export_statement')
|
|
956
|
-
return true;
|
|
957
|
-
// Also check for `export` keyword on the node itself (TS module syntax)
|
|
958
|
-
for (const child of node.children) {
|
|
959
|
-
if (!child.isNamed && child.type === 'export')
|
|
960
|
-
return true;
|
|
961
|
-
}
|
|
962
|
-
return false;
|
|
742
|
+
return members;
|
|
963
743
|
}
|
|
964
744
|
/** Get the enclosing class name for a method. */
|
|
965
745
|
getEnclosingClassName(node) {
|
|
966
746
|
let current = node.parent;
|
|
967
747
|
while (current) {
|
|
968
|
-
if (current.type === 'class_declaration') {
|
|
748
|
+
if (current.type === 'class_declaration' || current.type === 'abstract_class_declaration') {
|
|
969
749
|
return this.extractName(current) ?? undefined;
|
|
970
750
|
}
|
|
971
751
|
current = current.parent ?? null;
|
|
972
752
|
}
|
|
973
753
|
return undefined;
|
|
974
754
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
for (const child of node.namedChildren) {
|
|
988
|
-
if (typeSet.has(child.type))
|
|
989
|
-
return child;
|
|
990
|
-
}
|
|
991
|
-
return null;
|
|
992
|
-
}
|
|
993
|
-
/** Check if node has a direct child (named or anonymous) with the given text. */
|
|
994
|
-
hasChild(node, text) {
|
|
995
|
-
for (const child of node.children) {
|
|
996
|
-
if (child.text === text || child.type === text)
|
|
997
|
-
return true;
|
|
998
|
-
}
|
|
999
|
-
return false;
|
|
1000
|
-
}
|
|
1001
|
-
// -- String Construction Capabilities (v3.4.7) ----------------------------
|
|
1002
|
-
/**
|
|
1003
|
-
* Returns true when the node is a dynamically-constructed string in TS/JS:
|
|
1004
|
-
* - template_string with template_substitution children
|
|
1005
|
-
* - binary_expression with + operator (string concatenation)
|
|
1006
|
-
* - call_expression with .concat() method
|
|
1007
|
-
* String-like identifiers and plain string literals are never dynamic.
|
|
1008
|
-
*/
|
|
1009
|
-
isDynamicStringConstruction(node) {
|
|
1010
|
-
const type = node.raw.type;
|
|
1011
|
-
if (type === 'template_string') {
|
|
1012
|
-
// template_string is dynamic only if it has template_substitution children
|
|
1013
|
-
for (const child of node.children ?? []) {
|
|
1014
|
-
if (child.raw.type === 'template_substitution') {
|
|
1015
|
-
return true;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
return false;
|
|
1019
|
-
}
|
|
1020
|
-
if (type === 'binary_expression') {
|
|
1021
|
-
// Check for string concatenation: operands include a string literal
|
|
1022
|
-
const children = node.children ?? [];
|
|
1023
|
-
const hasStringLiteral = children.some(c => c.raw.type === 'string');
|
|
1024
|
-
return hasStringLiteral;
|
|
1025
|
-
}
|
|
1026
|
-
if (type === 'call_expression') {
|
|
1027
|
-
// Check for .concat() calls
|
|
1028
|
-
const text = node.raw.text;
|
|
1029
|
-
if (text.includes('.concat(') || text.includes('?.concat('))
|
|
1030
|
-
return true;
|
|
1031
|
-
// Recurse into arguments: query(binaryExpression) where the argument
|
|
1032
|
-
// itself is a dynamic string construction.
|
|
1033
|
-
for (const child of node.children ?? []) {
|
|
1034
|
-
if (child.raw.type === 'arguments') {
|
|
1035
|
-
for (const arg of child.children ?? []) {
|
|
1036
|
-
const argType = arg.raw.type;
|
|
1037
|
-
if (argType === '(' || argType === ')' || argType === ',')
|
|
1038
|
-
continue;
|
|
1039
|
-
if (this.isDynamicStringConstruction(arg))
|
|
1040
|
-
return true;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
return false;
|
|
1045
|
-
}
|
|
1046
|
-
return false;
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Extracts dynamic sub-parts from a string construction node.
|
|
1050
|
-
* Returns template_substitution text for template literals,
|
|
1051
|
-
* non-string-literal operands for binary expressions,
|
|
1052
|
-
* and non-literal arguments for concat calls.
|
|
1053
|
-
*/
|
|
1054
|
-
getDynamicParts(node, sourceCode) {
|
|
1055
|
-
const type = node.raw.type;
|
|
1056
|
-
const parts = [];
|
|
1057
|
-
// For call_expressions (other than .concat), walk into the first
|
|
1058
|
-
// argument that is itself a dynamic string construction.
|
|
1059
|
-
if (type === 'call_expression') {
|
|
1060
|
-
const text = node.raw.text;
|
|
1061
|
-
// .concat() handled below
|
|
1062
|
-
if (!text.includes('.concat(') && !text.includes('?.concat(')) {
|
|
1063
|
-
for (const child of node.children ?? []) {
|
|
1064
|
-
if (child.raw.type === 'arguments') {
|
|
1065
|
-
for (const arg of child.children ?? []) {
|
|
1066
|
-
const argType = arg.raw.type;
|
|
1067
|
-
if (argType === '(' || argType === ')' || argType === ',')
|
|
1068
|
-
continue;
|
|
1069
|
-
if (this.isDynamicStringConstruction(arg)) {
|
|
1070
|
-
return this.getDynamicParts(arg, sourceCode);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
755
|
+
/** Collect static imports, dynamic `import()`, and `require()` calls. */
|
|
756
|
+
collectRawImport(node, results) {
|
|
757
|
+
if (node.type === 'import_statement') {
|
|
758
|
+
const source = this.getChildByType(node, 'string');
|
|
759
|
+
if (source) {
|
|
760
|
+
results.push({
|
|
761
|
+
moduleSpecifier: source.text.slice(1, -1), // strip quotes
|
|
762
|
+
isStatic: true,
|
|
763
|
+
isDynamic: false,
|
|
764
|
+
isRequire: false,
|
|
765
|
+
line: source.startPosition.row,
|
|
766
|
+
});
|
|
1075
767
|
}
|
|
1076
768
|
}
|
|
1077
|
-
if (type === '
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
// Strip the ${ } wrapper to get the inner identifier/expression
|
|
1082
|
-
// tree-sitter: template_substitution text includes ${ and }
|
|
1083
|
-
const inner = text.startsWith('${') ? text.slice(2, -1).trim() : text;
|
|
1084
|
-
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(inner);
|
|
1085
|
-
// Extract the actual identifier node from inside the template_substitution
|
|
1086
|
-
// (tree-sitter nests it as: ${ <identifier> }), so resolveLocalConstant
|
|
1087
|
-
// can use its range to extract the raw name.
|
|
1088
|
-
let idNode;
|
|
1089
|
-
if (isId) {
|
|
1090
|
-
for (const subChild of child.children ?? []) {
|
|
1091
|
-
const subType = subChild.raw.type;
|
|
1092
|
-
if (subType === 'identifier') {
|
|
1093
|
-
idNode = subChild;
|
|
1094
|
-
break;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
parts.push({ text: inner, isIdentifier: isId, node: idNode ?? (isId ? child : undefined) });
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
return parts;
|
|
1102
|
-
}
|
|
1103
|
-
if (type === 'binary_expression') {
|
|
1104
|
-
for (const child of node.children ?? []) {
|
|
1105
|
-
const childType = child.raw.type;
|
|
1106
|
-
if (childType !== 'string' && childType !== '+' && childType !== 'template_string') {
|
|
1107
|
-
const text = sourceCode.slice(child.range[0], child.range[1]);
|
|
1108
|
-
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text.trim());
|
|
1109
|
-
parts.push({ text: text.trim(), isIdentifier: isId, node: isId ? child : undefined });
|
|
1110
|
-
}
|
|
769
|
+
if (node.type === 'call_expression') {
|
|
770
|
+
const fn = node.firstChild;
|
|
771
|
+
if (fn?.type === 'import') {
|
|
772
|
+
this.pushCallImport(node, results, /* isRequire */ false);
|
|
1111
773
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (type === 'call_expression') {
|
|
1115
|
-
// .concat() — arguments after the first are dynamic
|
|
1116
|
-
for (const child of node.children ?? []) {
|
|
1117
|
-
if (child.raw.type === 'arguments') {
|
|
1118
|
-
for (const arg of child.children ?? []) {
|
|
1119
|
-
const argType = arg.raw.type;
|
|
1120
|
-
if (argType !== '(' && argType !== ')' && argType !== ',') {
|
|
1121
|
-
const text = arg.raw.text.trim();
|
|
1122
|
-
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text);
|
|
1123
|
-
parts.push({ text, isIdentifier: isId, node: isId ? arg : undefined });
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
774
|
+
else if (fn?.type === 'identifier' && fn.text === 'require') {
|
|
775
|
+
this.pushCallImport(node, results, /* isRequire */ true);
|
|
1127
776
|
}
|
|
1128
|
-
return parts;
|
|
1129
777
|
}
|
|
1130
|
-
return parts;
|
|
1131
778
|
}
|
|
1132
|
-
/**
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
// child — NOT wrapped in `variable_declarator` / `lexical_declaration`.
|
|
1165
|
-
// Walk up from the identifier to a `for_in_statement` and trace
|
|
1166
|
-
// through the iterable to determine static-ness.
|
|
1167
|
-
// const TABLES = ["a", "b"];
|
|
1168
|
-
// for (const table of TABLES) { … `${table}` … }
|
|
1169
|
-
const idRaw = identifierNode.raw;
|
|
1170
|
-
let tsCurrent = idRaw.parent;
|
|
1171
|
-
while (tsCurrent) {
|
|
1172
|
-
if (tsCurrent.type === 'for_in_statement') {
|
|
1173
|
-
const left = tsCurrent.childForFieldName?.('left');
|
|
1174
|
-
if (left && left.text === idName) {
|
|
1175
|
-
const right = tsCurrent.childForFieldName?.('right');
|
|
1176
|
-
if (right && right.type === 'identifier') {
|
|
1177
|
-
const iterName = right.text;
|
|
1178
|
-
let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
|
|
1179
|
-
if (!iterDecl && enclosing && enclosing !== ast.root) {
|
|
1180
|
-
iterDecl = this.findDeclarationInScope(ast.root, iterName);
|
|
1181
|
-
}
|
|
1182
|
-
if (iterDecl) {
|
|
1183
|
-
const iterRaw = iterDecl.raw;
|
|
1184
|
-
const iterValue = iterRaw.childForFieldName?.('value');
|
|
1185
|
-
const iterLine = iterDecl.location.start.line;
|
|
1186
|
-
const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
|
|
1187
|
-
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
|
|
1188
|
-
if (!iterReassigned && this.isStaticValueNode(iterValue)) {
|
|
1189
|
-
const initText = iterValue ? sourceCode.slice(iterValue.startIndex, iterValue.endIndex).trim() : '';
|
|
1190
|
-
return { initText, isStatic: true, declLine: iterDecl.location.start.line };
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
break;
|
|
1196
|
-
}
|
|
1197
|
-
tsCurrent = tsCurrent.parent;
|
|
1198
|
-
}
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
// Extract value from AST (handles multiline declarations that the old
|
|
1202
|
-
// regex missed — `.` doesn't match `\n` so `.+?` truncated at newlines).
|
|
1203
|
-
const raw = declNode.raw;
|
|
1204
|
-
const valueNode = raw.childForFieldName?.('value');
|
|
1205
|
-
const declLine = declNode.location.start.line;
|
|
1206
|
-
// Check for reassignment after declaration
|
|
1207
|
-
const reassigned = this.hasReassignment(scopeRoot, idName, declLine);
|
|
1208
|
-
// Determine if static by inspecting the value node's AST type. String
|
|
1209
|
-
// literals and substitution-free template strings are compile-time constants
|
|
1210
|
-
// regardless of whether they contain `?` placeholders — SQL fragment
|
|
1211
|
-
// constants (WHERE clauses, column lists) are just as static as
|
|
1212
|
-
// parameterised query strings.
|
|
1213
|
-
let isStatic = !reassigned && this.isStaticValueNode(valueNode);
|
|
1214
|
-
// For-of / for-in loop variables: the declarator has no `value` field
|
|
1215
|
-
// (the iterable is on the for-statement's `right` child). Trace the
|
|
1216
|
-
// iterable to see if all possible loop values are known constants.
|
|
1217
|
-
// const TABLES = ["a", "b"];
|
|
1218
|
-
// for (const table of TABLES) { … `${table}` … }
|
|
1219
|
-
if (!isStatic && !valueNode) {
|
|
1220
|
-
const forParent = this.findEnclosingForStatement(declNode);
|
|
1221
|
-
if (forParent) {
|
|
1222
|
-
const forRaw = forParent.raw;
|
|
1223
|
-
const iterable = forRaw.childForFieldName?.('right');
|
|
1224
|
-
if (iterable && iterable.type === 'identifier') {
|
|
1225
|
-
const iterName = iterable.text;
|
|
1226
|
-
let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
|
|
1227
|
-
if (!iterDecl && enclosing && enclosing !== ast.root) {
|
|
1228
|
-
iterDecl = this.findDeclarationInScope(ast.root, iterName);
|
|
1229
|
-
}
|
|
1230
|
-
if (iterDecl) {
|
|
1231
|
-
const iterRaw = iterDecl.raw;
|
|
1232
|
-
const iterValue = iterRaw.childForFieldName?.('value');
|
|
1233
|
-
const iterLine = iterDecl.location.start.line;
|
|
1234
|
-
const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
|
|
1235
|
-
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
|
|
1236
|
-
if (!iterReassigned && this.isStaticValueNode(iterValue)) {
|
|
1237
|
-
isStatic = true;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
779
|
+
/** Push a `import('...')` or `require('...')` record for a call expression. */
|
|
780
|
+
pushCallImport(node, results, isRequire) {
|
|
781
|
+
const args = this.getChildByType(node, 'arguments');
|
|
782
|
+
const strNode = args ? this.findFirstNamedChild(args, 'string') : null;
|
|
783
|
+
if (!strNode)
|
|
784
|
+
return;
|
|
785
|
+
results.push({
|
|
786
|
+
moduleSpecifier: strNode.text.slice(1, -1),
|
|
787
|
+
isStatic: false,
|
|
788
|
+
isDynamic: !isRequire,
|
|
789
|
+
isRequire,
|
|
790
|
+
line: strNode.startPosition.row,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
/** Collect the exported symbol(s) declared by a single `export_statement`. */
|
|
794
|
+
collectExportedSymbol(node, symbols) {
|
|
795
|
+
// export function foo / export class Foo / export const x
|
|
796
|
+
const declaration = this.findFirstNamedChild(node, [
|
|
797
|
+
'function_declaration',
|
|
798
|
+
'class_declaration',
|
|
799
|
+
'abstract_class_declaration',
|
|
800
|
+
'lexical_declaration',
|
|
801
|
+
'variable_declaration',
|
|
802
|
+
'interface_declaration',
|
|
803
|
+
'type_alias_declaration',
|
|
804
|
+
'enum_declaration',
|
|
805
|
+
]);
|
|
806
|
+
if (declaration) {
|
|
807
|
+
const name = this.extractName(declaration);
|
|
808
|
+
if (name) {
|
|
809
|
+
symbols.push({ name, line: declaration.startPosition.row });
|
|
810
|
+
return;
|
|
1241
811
|
}
|
|
1242
812
|
}
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (linkedName && linkedName !== idName) {
|
|
1252
|
-
let linkedDecl = this.findDeclarationInScope(scopeRoot, linkedName);
|
|
1253
|
-
if (!linkedDecl && enclosing && enclosing !== ast.root) {
|
|
1254
|
-
linkedDecl = this.findDeclarationInScope(ast.root, linkedName);
|
|
1255
|
-
}
|
|
1256
|
-
if (linkedDecl) {
|
|
1257
|
-
const linkedRaw = linkedDecl.raw;
|
|
1258
|
-
const linkedValue = linkedRaw.childForFieldName?.('value');
|
|
1259
|
-
const linkedLine = linkedDecl.location.start.line;
|
|
1260
|
-
const linkedReassigned = this.hasReassignment(scopeRoot, linkedName, linkedLine)
|
|
1261
|
-
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, linkedName, linkedLine) : false);
|
|
1262
|
-
if (!linkedReassigned && this.isStaticValueNode(linkedValue)) {
|
|
1263
|
-
isStatic = true;
|
|
813
|
+
// export { foo, bar } or export { default }
|
|
814
|
+
const clause = this.getChildByType(node, 'export_clause');
|
|
815
|
+
if (clause) {
|
|
816
|
+
for (const child of clause.namedChildren) {
|
|
817
|
+
if (child.type === 'export_specifier') {
|
|
818
|
+
const nameNode = this.getChildByType(child, 'identifier');
|
|
819
|
+
if (nameNode) {
|
|
820
|
+
symbols.push({ name: nameNode.text, line: nameNode.startPosition.row });
|
|
1264
821
|
}
|
|
1265
822
|
}
|
|
1266
823
|
}
|
|
1267
824
|
}
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
initText = sourceCode.slice(valueNode.startIndex, valueNode.endIndex).trim();
|
|
1272
|
-
}
|
|
1273
|
-
else {
|
|
1274
|
-
// No initializer node — try regex as a last resort.
|
|
1275
|
-
const declText = sourceCode.slice(declNode.range[0], declNode.range[1]);
|
|
1276
|
-
const declMatch = declText.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$|^(\w+)\s*=\s*(.+?);?\s*$/s);
|
|
1277
|
-
initText = declMatch
|
|
1278
|
-
? (declMatch[2] ?? declMatch[4]).replace(/;\s*$/, '').trim()
|
|
1279
|
-
: '';
|
|
825
|
+
// export default <expression>
|
|
826
|
+
if (node.childForFieldName?.('value')) {
|
|
827
|
+
symbols.push({ name: 'default', line: node.startPosition.row });
|
|
1280
828
|
}
|
|
1281
|
-
return { initText, isStatic, declLine };
|
|
1282
829
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
if (
|
|
1298
|
-
|
|
830
|
+
}
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// Public LanguageAdapter surface: parsing, navigation, extraction
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
class TsPublicApi extends TsExtraction {
|
|
835
|
+
name = 'typescript';
|
|
836
|
+
fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
837
|
+
supportsFile(filePath) {
|
|
838
|
+
return this.fileExtensions.some((ext) => filePath.endsWith(ext));
|
|
839
|
+
}
|
|
840
|
+
async parse(filePath, content) {
|
|
841
|
+
const isTsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
|
|
842
|
+
const lang = isTsx ? 'tsx' : filePath.endsWith('.go') ? 'go' : 'typescript';
|
|
843
|
+
const tree = await parseWithRecovery(lang, isTsx, content);
|
|
844
|
+
if (!tree)
|
|
845
|
+
throw new Error(`Failed to parse file: ${filePath}`);
|
|
846
|
+
const errors = [];
|
|
847
|
+
// Collect ERROR nodes as parse errors
|
|
848
|
+
this.collectErrors(tree.rootNode, errors);
|
|
849
|
+
const root = toASTNode(tree.rootNode, undefined, lang);
|
|
850
|
+
const ast = {
|
|
851
|
+
root,
|
|
852
|
+
language: lang,
|
|
853
|
+
filePath,
|
|
854
|
+
errors,
|
|
855
|
+
dispose: () => tree.delete(),
|
|
856
|
+
};
|
|
857
|
+
sourceCodeMap.set(ast, content);
|
|
858
|
+
return ast;
|
|
859
|
+
}
|
|
860
|
+
findNodes(ast, pattern) {
|
|
861
|
+
const results = [];
|
|
862
|
+
this.walk(ast.root, (node) => {
|
|
863
|
+
if (this.matchesPattern(node, pattern)) {
|
|
864
|
+
results.push(node);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
return results;
|
|
868
|
+
}
|
|
869
|
+
getParent(node) {
|
|
870
|
+
return node.parent ?? null;
|
|
871
|
+
}
|
|
872
|
+
getChildren(node) {
|
|
873
|
+
return node.children ?? [];
|
|
874
|
+
}
|
|
875
|
+
getSiblings(node) {
|
|
876
|
+
if (!node.parent?.children)
|
|
877
|
+
return [];
|
|
878
|
+
return node.parent.children.filter((c) => c !== node);
|
|
879
|
+
}
|
|
880
|
+
getNodeType(node) {
|
|
881
|
+
return node.type;
|
|
882
|
+
}
|
|
883
|
+
getNodeText(node, sourceCode) {
|
|
884
|
+
return sourceCode.slice(node.range[0], node.range[1]);
|
|
885
|
+
}
|
|
886
|
+
getNodeName(node) {
|
|
887
|
+
const syntaxNode = node.raw;
|
|
888
|
+
return this.extractName(syntaxNode);
|
|
889
|
+
}
|
|
890
|
+
getNodeLocation(node) {
|
|
891
|
+
return node.location;
|
|
892
|
+
}
|
|
893
|
+
extractFunctions(ast) {
|
|
894
|
+
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
895
|
+
const functions = [];
|
|
896
|
+
this.walk(ast.root, (node) => {
|
|
897
|
+
const syntaxNode = node.raw;
|
|
898
|
+
const type = syntaxNode.type;
|
|
899
|
+
if (type === 'function_declaration' ||
|
|
900
|
+
type === 'generator_function_declaration' ||
|
|
901
|
+
type === 'function_expression' ||
|
|
902
|
+
type === 'arrow_function' ||
|
|
903
|
+
type === 'method_definition') {
|
|
904
|
+
const fn = this.buildFunctionInfo(syntaxNode, sourceCode);
|
|
905
|
+
if (fn)
|
|
906
|
+
functions.push(fn);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
return functions;
|
|
910
|
+
}
|
|
911
|
+
extractClasses(ast) {
|
|
912
|
+
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
913
|
+
const classes = [];
|
|
914
|
+
this.walk(ast.root, (node) => {
|
|
915
|
+
const syntaxNode = node.raw;
|
|
916
|
+
if (syntaxNode.type === 'class_declaration' || syntaxNode.type === 'abstract_class_declaration') {
|
|
917
|
+
const cls = this.buildClassInfo(syntaxNode, sourceCode);
|
|
918
|
+
if (cls)
|
|
919
|
+
classes.push(cls);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
return classes;
|
|
923
|
+
}
|
|
924
|
+
extractImports(ast) {
|
|
925
|
+
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
926
|
+
const imports = [];
|
|
927
|
+
this.walk(ast.root, (node) => {
|
|
928
|
+
const syntaxNode = node.raw;
|
|
929
|
+
if (syntaxNode.type === 'import_statement') {
|
|
930
|
+
const imp = this.buildImportInfo(syntaxNode, sourceCode);
|
|
931
|
+
if (imp)
|
|
932
|
+
imports.push(imp);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
return imports;
|
|
936
|
+
}
|
|
937
|
+
extractExports(ast) {
|
|
938
|
+
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
939
|
+
const exports = [];
|
|
940
|
+
this.walk(ast.root, (node) => {
|
|
941
|
+
const syntaxNode = node.raw;
|
|
942
|
+
if (syntaxNode.type === 'export_statement') {
|
|
943
|
+
const ex = this.buildExportInfo(syntaxNode, sourceCode);
|
|
944
|
+
if (ex)
|
|
945
|
+
exports.push(ex);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
return exports;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// ---------------------------------------------------------------------------
|
|
952
|
+
// Node predicates + advanced + optional capabilities
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
class TsPredicatesOptional extends TsPublicApi {
|
|
955
|
+
isClass(node) {
|
|
956
|
+
const type = node.raw.type;
|
|
957
|
+
return type === 'class_declaration' || type === 'abstract_class_declaration' || type === 'class_expression';
|
|
958
|
+
}
|
|
959
|
+
isFunction(node) {
|
|
960
|
+
const type = node.raw.type;
|
|
961
|
+
return (type === 'function_declaration' ||
|
|
962
|
+
type === 'function_expression' ||
|
|
963
|
+
type === 'arrow_function' ||
|
|
964
|
+
type === 'generator_function_declaration' ||
|
|
965
|
+
type === 'generator_function_expression');
|
|
966
|
+
}
|
|
967
|
+
isMethod(node) {
|
|
968
|
+
return node.raw.type === 'method_definition';
|
|
969
|
+
}
|
|
970
|
+
isInterface(node) {
|
|
971
|
+
return node.raw.type === 'interface_declaration';
|
|
972
|
+
}
|
|
973
|
+
isImport(node) {
|
|
974
|
+
return node.raw.type === 'import_statement';
|
|
975
|
+
}
|
|
976
|
+
isExport(node) {
|
|
977
|
+
return node.raw.type === 'export_statement';
|
|
978
|
+
}
|
|
979
|
+
isLoop(node) {
|
|
980
|
+
const type = node.raw.type;
|
|
981
|
+
return (type === 'for_statement' ||
|
|
982
|
+
type === 'for_in_statement' ||
|
|
983
|
+
type === 'while_statement' ||
|
|
984
|
+
type === 'do_statement');
|
|
985
|
+
}
|
|
986
|
+
isConditional(node) {
|
|
987
|
+
const type = node.raw.type;
|
|
988
|
+
return (type === 'if_statement' ||
|
|
989
|
+
type === 'switch_statement' ||
|
|
990
|
+
type === 'ternary_expression' ||
|
|
991
|
+
type === 'switch_case');
|
|
992
|
+
}
|
|
993
|
+
isVariableDeclaration(node) {
|
|
994
|
+
const type = node.raw.type;
|
|
995
|
+
return (type === 'variable_declaration' ||
|
|
996
|
+
type === 'lexical_declaration' ||
|
|
997
|
+
type === 'variable_declarator');
|
|
998
|
+
}
|
|
999
|
+
getTypeInfo(node) {
|
|
1000
|
+
const syntaxNode = node.raw;
|
|
1001
|
+
return this.extractTypeAnnotation(syntaxNode);
|
|
1002
|
+
}
|
|
1003
|
+
getDocumentation(node) {
|
|
1004
|
+
const syntaxNode = node.raw;
|
|
1005
|
+
return this.extractDocumentation(syntaxNode);
|
|
1006
|
+
}
|
|
1007
|
+
getComplexity(node) {
|
|
1008
|
+
const syntaxNode = node.raw;
|
|
1009
|
+
return this.calculateCyclomaticComplexity(syntaxNode);
|
|
1010
|
+
}
|
|
1011
|
+
extractInterfaces(ast) {
|
|
1012
|
+
const sourceCode = sourceCodeMap.get(ast) ?? '';
|
|
1013
|
+
const interfaces = [];
|
|
1014
|
+
this.walk(ast.root, (node) => {
|
|
1015
|
+
const syntaxNode = node.raw;
|
|
1016
|
+
if (syntaxNode.type === 'interface_declaration') {
|
|
1017
|
+
const iface = this.buildInterfaceInfo(syntaxNode, sourceCode);
|
|
1018
|
+
if (iface)
|
|
1019
|
+
interfaces.push(iface);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
return interfaces;
|
|
1023
|
+
}
|
|
1024
|
+
extractRawImports(_filePath, content) {
|
|
1025
|
+
const results = [];
|
|
1026
|
+
const parser = getParser('typescript');
|
|
1027
|
+
const tree = parser.parse(content);
|
|
1028
|
+
if (!tree)
|
|
1029
|
+
return results;
|
|
1030
|
+
this.walkRaw(tree.rootNode, (node) => this.collectRawImport(node, results));
|
|
1031
|
+
return results;
|
|
1032
|
+
}
|
|
1033
|
+
extractExportedSymbols(ast) {
|
|
1034
|
+
const symbols = [];
|
|
1035
|
+
this.walk(ast.root, (astNode) => {
|
|
1036
|
+
const node = astNode.raw;
|
|
1037
|
+
if (node.type === 'export_statement')
|
|
1038
|
+
this.collectExportedSymbol(node, symbols);
|
|
1039
|
+
});
|
|
1040
|
+
return symbols;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
1044
|
+
// Scope + static-value resolution
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
class TsScopeStatic extends TsPredicatesOptional {
|
|
1047
|
+
/** Determine whether a tree-sitter value node represents a static (compile-time
|
|
1048
|
+
* constant) expression — no variables, function calls, or runtime evaluation. */
|
|
1049
|
+
isStaticValueNode(node) {
|
|
1050
|
+
if (!node)
|
|
1051
|
+
return false;
|
|
1052
|
+
const type = node.type;
|
|
1053
|
+
// Literals that are trivially compile-time constants.
|
|
1054
|
+
if (type === 'string')
|
|
1055
|
+
return true;
|
|
1056
|
+
if (type === 'number')
|
|
1057
|
+
return true;
|
|
1058
|
+
if (type === 'true' || type === 'false' || type === 'null' || type === 'undefined')
|
|
1059
|
+
return true;
|
|
1060
|
+
// Template strings: static only when they contain no ${…} substitutions.
|
|
1061
|
+
if (type === 'template_string') {
|
|
1062
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1299
1063
|
const child = node.child(i);
|
|
1300
1064
|
if (child && child.type === 'template_substitution')
|
|
1301
1065
|
return false;
|
|
1302
1066
|
}
|
|
1303
1067
|
return true;
|
|
1304
1068
|
}
|
|
1305
|
-
// Array literals
|
|
1306
|
-
//
|
|
1307
|
-
// const TABLES = ["a", "b"];
|
|
1308
|
-
// for (const t of TABLES) { … `${t}` … }
|
|
1309
|
-
// where every possible value of `t` is known at compile time.
|
|
1069
|
+
// Array literals are static when every element is a compile-time constant
|
|
1070
|
+
// (enables tracing `for (const t of TABLES)` over a static column list).
|
|
1310
1071
|
if (type === 'array') {
|
|
1311
1072
|
for (let i = 0; i < node.childCount; i++) {
|
|
1312
1073
|
const child = node.child(i);
|
|
@@ -1318,6 +1079,14 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
1318
1079
|
}
|
|
1319
1080
|
return true;
|
|
1320
1081
|
}
|
|
1082
|
+
// Type assertions (`[...] as const`, `x as T`, `x satisfies T`) wrap an inner
|
|
1083
|
+
// expression without changing its runtime value — unwrap and judge that.
|
|
1084
|
+
if (type === 'as_expression' ||
|
|
1085
|
+
type === 'type_assertion' ||
|
|
1086
|
+
type === 'satisfies_expression' ||
|
|
1087
|
+
type === 'non_null_expression') {
|
|
1088
|
+
return this.isStaticValueNode(node.namedChild(0));
|
|
1089
|
+
}
|
|
1321
1090
|
// Everything else (identifiers, call expressions, binary expressions,
|
|
1322
1091
|
// object literals, regexes, etc.) is conservatively treated as non-static.
|
|
1323
1092
|
return false;
|
|
@@ -1421,14 +1190,18 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
1421
1190
|
declLine: r.declLine,
|
|
1422
1191
|
};
|
|
1423
1192
|
}
|
|
1424
|
-
/** Check if identifier targetName is reassigned in scope after declLine.
|
|
1193
|
+
/** Check if identifier targetName is reassigned in scope after declLine.
|
|
1194
|
+
* Handles both plain assignment (`x = …`) and augmented assignment
|
|
1195
|
+
* (`x += …`, `x -= …`, etc.) — the latter accumulates into a string the
|
|
1196
|
+
* same way `x = x + …` does, so a variable built up with `+=` is just as
|
|
1197
|
+
* runtime-dependent as one reassigned with `=`. */
|
|
1425
1198
|
hasReassignment(scopeRoot, targetName, declLine) {
|
|
1426
1199
|
let found = false;
|
|
1427
1200
|
this.walk(scopeRoot, (astNode) => {
|
|
1428
1201
|
if (found)
|
|
1429
1202
|
return;
|
|
1430
1203
|
const t = astNode.raw.type;
|
|
1431
|
-
if (t === 'assignment_expression') {
|
|
1204
|
+
if (t === 'assignment_expression' || t === 'augmented_assignment_expression') {
|
|
1432
1205
|
if (astNode.location.start.line < declLine)
|
|
1433
1206
|
return;
|
|
1434
1207
|
const raw = astNode.raw;
|
|
@@ -1440,20 +1213,743 @@ export class TreeSitterTypeScriptAdapter {
|
|
|
1440
1213
|
});
|
|
1441
1214
|
return found;
|
|
1442
1215
|
}
|
|
1443
|
-
/**
|
|
1444
|
-
|
|
1445
|
-
|
|
1216
|
+
/** Find a function declaration (named or arrow-assigned) by name in the file. */
|
|
1217
|
+
findFunctionDeclaration(name, ast) {
|
|
1218
|
+
let result = null;
|
|
1219
|
+
this.walk(ast.root, (node) => {
|
|
1220
|
+
if (result)
|
|
1221
|
+
return;
|
|
1222
|
+
const raw = node.raw;
|
|
1223
|
+
const t = raw.type;
|
|
1224
|
+
if (t === 'function_declaration' || t === 'generator_function_declaration') {
|
|
1225
|
+
const nameNode = raw.childForFieldName?.('name');
|
|
1226
|
+
if (nameNode && nameNode.text === name)
|
|
1227
|
+
result = node;
|
|
1228
|
+
}
|
|
1229
|
+
else if (t === 'variable_declarator') {
|
|
1230
|
+
const nameNode = raw.childForFieldName?.('name');
|
|
1231
|
+
const valueNode = raw.childForFieldName?.('value');
|
|
1232
|
+
if (nameNode && nameNode.type === 'identifier' && nameNode.text === name
|
|
1233
|
+
&& valueNode && ['arrow_function', 'function_expression', 'function'].includes(valueNode.type)) {
|
|
1234
|
+
result = this.wrapRaw(valueNode);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
return result;
|
|
1239
|
+
}
|
|
1240
|
+
/** Extract a function's name (named functions and arrow-function assignments). */
|
|
1241
|
+
getFunctionName(node) {
|
|
1242
|
+
const raw = node.raw;
|
|
1243
|
+
const t = raw.type;
|
|
1244
|
+
if (t === 'function_declaration' || t === 'generator_function_declaration'
|
|
1245
|
+
|| t === 'function_expression' || t === 'method_definition') {
|
|
1246
|
+
const nameNode = raw.childForFieldName?.('name');
|
|
1247
|
+
if (nameNode && nameNode.text)
|
|
1248
|
+
return nameNode.text;
|
|
1249
|
+
}
|
|
1250
|
+
if (t === 'arrow_function') {
|
|
1251
|
+
const parent = node.parent;
|
|
1252
|
+
if (parent) {
|
|
1253
|
+
const pRaw = parent.raw;
|
|
1254
|
+
if (pRaw.type === 'variable_declarator') {
|
|
1255
|
+
const nameNode = pRaw.childForFieldName?.('name');
|
|
1256
|
+
if (nameNode && nameNode.type === 'identifier')
|
|
1257
|
+
return nameNode.text;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
/** Find all call sites of a named function in the file. */
|
|
1264
|
+
findCallSites(fnName, ast) {
|
|
1265
|
+
const sites = [];
|
|
1266
|
+
this.walk(ast.root, (node) => {
|
|
1267
|
+
const raw = node.raw;
|
|
1268
|
+
if (raw.type !== 'call_expression')
|
|
1269
|
+
return;
|
|
1270
|
+
const fn = raw.childForFieldName?.('function');
|
|
1271
|
+
if (fn && fn.type === 'identifier' && fn.text === fnName)
|
|
1272
|
+
sites.push(node);
|
|
1273
|
+
});
|
|
1274
|
+
return sites;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
// ---------------------------------------------------------------------------
|
|
1278
|
+
// Dynamic string construction + local-constant resolution
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
class TsDynamicStringConstruction extends TsScopeStatic {
|
|
1281
|
+
/**
|
|
1282
|
+
* Returns true when the node is a dynamically-constructed string in TS/JS:
|
|
1283
|
+
* - template_string with template_substitution children
|
|
1284
|
+
* - binary_expression with + operator (string concatenation)
|
|
1285
|
+
* - call_expression with .concat() method
|
|
1286
|
+
* String-like identifiers and plain string literals are never dynamic.
|
|
1287
|
+
*/
|
|
1288
|
+
isDynamicStringConstruction(node) {
|
|
1289
|
+
const type = node.raw.type;
|
|
1290
|
+
if (type === 'template_string') {
|
|
1291
|
+
// template_string is dynamic only if it has template_substitution children
|
|
1292
|
+
for (const child of node.children ?? []) {
|
|
1293
|
+
if (child.raw.type === 'template_substitution') {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
if (type === 'binary_expression') {
|
|
1300
|
+
// Check for string concatenation: operands include a string literal
|
|
1301
|
+
const children = node.children ?? [];
|
|
1302
|
+
const hasStringLiteral = children.some(c => c.raw.type === 'string');
|
|
1303
|
+
return hasStringLiteral;
|
|
1304
|
+
}
|
|
1305
|
+
if (type === 'call_expression') {
|
|
1306
|
+
// Check for .concat() calls
|
|
1307
|
+
const text = node.raw.text;
|
|
1308
|
+
if (text.includes('.concat(') || text.includes('?.concat('))
|
|
1309
|
+
return true;
|
|
1310
|
+
// Recurse into arguments: query(binaryExpression) where the argument
|
|
1311
|
+
// itself is a dynamic string construction.
|
|
1312
|
+
for (const child of node.children ?? []) {
|
|
1313
|
+
if (child.raw.type === 'arguments') {
|
|
1314
|
+
for (const arg of child.children ?? []) {
|
|
1315
|
+
const argType = arg.raw.type;
|
|
1316
|
+
if (argType === '(' || argType === ')' || argType === ',')
|
|
1317
|
+
continue;
|
|
1318
|
+
if (this.isDynamicStringConstruction(arg))
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Extracts dynamic sub-parts from a string construction node.
|
|
1329
|
+
* Returns template_substitution text for template literals,
|
|
1330
|
+
* non-string-literal operands for binary expressions,
|
|
1331
|
+
* and non-literal arguments for concat calls.
|
|
1332
|
+
*/
|
|
1333
|
+
getDynamicParts(node, sourceCode) {
|
|
1334
|
+
const type = node.raw.type;
|
|
1335
|
+
if (type === 'call_expression') {
|
|
1336
|
+
const nested = this.getNestedDynamicCallParts(node, sourceCode);
|
|
1337
|
+
if (nested)
|
|
1338
|
+
return nested;
|
|
1339
|
+
return this.getCallArgParts(node);
|
|
1340
|
+
}
|
|
1341
|
+
if (type === 'template_string')
|
|
1342
|
+
return this.getTemplateStringParts(node, sourceCode);
|
|
1343
|
+
if (type === 'binary_expression')
|
|
1344
|
+
return this.getBinaryExpressionParts(node, sourceCode);
|
|
1345
|
+
return [];
|
|
1346
|
+
}
|
|
1347
|
+
/** For a non-`.concat()` call, recurse into the first argument that is itself
|
|
1348
|
+
* a dynamic string construction; null when there is none (or it's .concat). */
|
|
1349
|
+
getNestedDynamicCallParts(node, sourceCode) {
|
|
1350
|
+
const text = node.raw.text;
|
|
1351
|
+
if (text.includes('.concat(') || text.includes('?.concat('))
|
|
1352
|
+
return null;
|
|
1446
1353
|
for (const child of node.children ?? []) {
|
|
1447
|
-
if (child.raw.type
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1354
|
+
if (child.raw.type !== 'arguments')
|
|
1355
|
+
continue;
|
|
1356
|
+
for (const arg of child.children ?? []) {
|
|
1357
|
+
const argType = arg.raw.type;
|
|
1358
|
+
if (argType === '(' || argType === ')' || argType === ',')
|
|
1359
|
+
continue;
|
|
1360
|
+
if (this.isDynamicStringConstruction(arg)) {
|
|
1361
|
+
return this.getDynamicParts(arg, sourceCode);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
/** `.concat()` (or a call with no nested dynamic arg): every argument is dynamic. */
|
|
1368
|
+
getCallArgParts(node) {
|
|
1369
|
+
const parts = [];
|
|
1370
|
+
for (const child of node.children ?? []) {
|
|
1371
|
+
if (child.raw.type !== 'arguments')
|
|
1372
|
+
continue;
|
|
1373
|
+
for (const arg of child.children ?? []) {
|
|
1374
|
+
const argType = arg.raw.type;
|
|
1375
|
+
if (argType === '(' || argType === ')' || argType === ',')
|
|
1376
|
+
continue;
|
|
1377
|
+
const text = arg.raw.text.trim();
|
|
1378
|
+
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text);
|
|
1379
|
+
parts.push({ text, isIdentifier: isId, node: arg });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return parts;
|
|
1383
|
+
}
|
|
1384
|
+
/** Dynamic parts from a template string's `${…}` substitutions. */
|
|
1385
|
+
getTemplateStringParts(node, sourceCode) {
|
|
1386
|
+
const parts = [];
|
|
1387
|
+
for (const child of node.children ?? []) {
|
|
1388
|
+
if (child.raw.type !== 'template_substitution')
|
|
1389
|
+
continue;
|
|
1390
|
+
const text = sourceCode.slice(child.range[0], child.range[1]);
|
|
1391
|
+
// Strip the ${ } wrapper to get the inner identifier/expression.
|
|
1392
|
+
const inner = text.startsWith('${') ? text.slice(2, -1).trim() : text;
|
|
1393
|
+
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(inner);
|
|
1394
|
+
// The expression inside ${…} is the single named child; attach it for
|
|
1395
|
+
// isSafeInterpolation() cross-function safety checks.
|
|
1396
|
+
let exprNode;
|
|
1397
|
+
for (const subChild of child.children ?? []) {
|
|
1398
|
+
if (subChild.raw.type !== 'template_substitution') {
|
|
1399
|
+
exprNode = subChild;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
parts.push({ text: inner, isIdentifier: isId, node: exprNode ?? child });
|
|
1404
|
+
}
|
|
1405
|
+
return parts;
|
|
1406
|
+
}
|
|
1407
|
+
/** Dynamic parts from a `+`-concatenation of strings and expressions. */
|
|
1408
|
+
getBinaryExpressionParts(node, sourceCode) {
|
|
1409
|
+
const parts = [];
|
|
1410
|
+
for (const child of node.children ?? []) {
|
|
1411
|
+
const childType = child.raw.type;
|
|
1412
|
+
if (childType === 'string' || childType === '+' || childType === 'template_string')
|
|
1413
|
+
continue;
|
|
1414
|
+
const text = sourceCode.slice(child.range[0], child.range[1]);
|
|
1415
|
+
const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text.trim());
|
|
1416
|
+
parts.push({ text: text.trim(), isIdentifier: isId, node: child });
|
|
1417
|
+
}
|
|
1418
|
+
return parts;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
class TsConstantResolution extends TsDynamicStringConstruction {
|
|
1422
|
+
/**
|
|
1423
|
+
* Resolves a local const/let/var declaration for an identifier node.
|
|
1424
|
+
* Searches within the enclosing function scope for the declaration site.
|
|
1425
|
+
* Returns null when the identifier is a parameter, complex expression,
|
|
1426
|
+
* or cannot be statically resolved.
|
|
1427
|
+
*/
|
|
1428
|
+
resolveLocalConstant(identifierNode, ast, sourceCode) {
|
|
1429
|
+
const idName = sourceCode.slice(identifierNode.range[0], identifierNode.range[1]).trim();
|
|
1430
|
+
if (!idName)
|
|
1431
|
+
return null;
|
|
1432
|
+
// Enclosing function/file scope, falling back to the module scope for
|
|
1433
|
+
// module-level constants.
|
|
1434
|
+
const enclosing = this.findEnclosingScope(identifierNode, ast);
|
|
1435
|
+
const scopeRoot = enclosing ?? ast.root;
|
|
1436
|
+
const scope = { scopeRoot, enclosing, ast };
|
|
1437
|
+
const resolved = this.resolveDeclarationOrConstant(identifierNode, idName, sourceCode, scope);
|
|
1438
|
+
if (resolved.constant)
|
|
1439
|
+
return resolved.constant;
|
|
1440
|
+
const declNode = resolved.declNode;
|
|
1441
|
+
if (!declNode)
|
|
1442
|
+
return null;
|
|
1443
|
+
return this.resolveFromDeclaration(declNode, idName, sourceCode, scope);
|
|
1444
|
+
}
|
|
1445
|
+
/** Determine static-ness and init text from a resolved declaration node.
|
|
1446
|
+
*
|
|
1447
|
+
* "Static" means the value is a compile-time constant that is never
|
|
1448
|
+
* reassigned. String literals and substitution-free template strings are
|
|
1449
|
+
* constants regardless of `?` placeholders — SQL fragment constants (WHERE
|
|
1450
|
+
* clauses, column lists) are as static as parameterised query strings.
|
|
1451
|
+
*
|
|
1452
|
+
* The value is extracted from the AST rather than regex so that multiline
|
|
1453
|
+
* declarations survive (`.` doesn't match `\n`). */
|
|
1454
|
+
resolveFromDeclaration(declNode, idName, sourceCode, scope) {
|
|
1455
|
+
const { scopeRoot, enclosing, ast } = scope;
|
|
1456
|
+
const raw = declNode.raw;
|
|
1457
|
+
const valueNode = raw.childForFieldName?.('value');
|
|
1458
|
+
const declLine = declNode.location.start.line;
|
|
1459
|
+
const reassigned = this.hasReassignment(scopeRoot, idName, declLine);
|
|
1460
|
+
let isStatic = !reassigned && this.isStaticValueNode(valueNode);
|
|
1461
|
+
// For-of / for-in loop variables: the declarator has no `value` field
|
|
1462
|
+
// (the iterable is on the for-statement's `right` child). Trace the
|
|
1463
|
+
// iterable to see if all possible loop values are known constants.
|
|
1464
|
+
if (!isStatic && !valueNode && this.traceForOfLoopVariable(declNode, scopeRoot, enclosing, ast)) {
|
|
1465
|
+
isStatic = true;
|
|
1466
|
+
}
|
|
1467
|
+
// Value is a simple identifier (e.g. `table` ← `tables`): trace through to
|
|
1468
|
+
// the linked declaration (handles `for (const table of TABLES)` where the
|
|
1469
|
+
// iterable is a compile-time constant array).
|
|
1470
|
+
if (!isStatic && valueNode?.type === 'identifier' && this.traceLinkedIdentifier(valueNode, idName, scope)) {
|
|
1471
|
+
isStatic = true;
|
|
1472
|
+
}
|
|
1473
|
+
const initText = this.extractInitText(valueNode, declNode, sourceCode);
|
|
1474
|
+
return { initText, isStatic, declLine };
|
|
1475
|
+
}
|
|
1476
|
+
/** Resolve `idName` to a declaration in the enclosing scope (falling back to
|
|
1477
|
+
* the program-level scope), or — when no local declaration exists — to an
|
|
1478
|
+
* imported constant or a `for…in`/`for…of` loop variable. Returns either a
|
|
1479
|
+
* declaration node or a fully resolved constant. */
|
|
1480
|
+
resolveDeclarationOrConstant(identifierNode, idName, sourceCode, scope) {
|
|
1481
|
+
const { scopeRoot, enclosing, ast } = scope;
|
|
1482
|
+
let declNode = this.findDeclarationInScope(scopeRoot, idName);
|
|
1483
|
+
// If the enclosing scope is a function (not the program) and we didn't
|
|
1484
|
+
// find the declaration there, also search the program-level scope.
|
|
1485
|
+
if (!declNode && enclosing && enclosing !== ast.root) {
|
|
1486
|
+
declNode = this.findDeclarationInScope(ast.root, idName);
|
|
1487
|
+
}
|
|
1488
|
+
if (declNode)
|
|
1489
|
+
return { declNode, constant: null };
|
|
1490
|
+
// Not in local declarations — imported symbols are compile-time constants
|
|
1491
|
+
// resolved at link time, not runtime, so user input cannot reach them via
|
|
1492
|
+
// import bindings.
|
|
1493
|
+
const importResult = this.resolveImportConstant(idName, ast);
|
|
1494
|
+
if (importResult)
|
|
1495
|
+
return { declNode: null, constant: importResult };
|
|
1496
|
+
// tree-sitter-typescript (v0.x) parses both "for…in" and "for…of" as
|
|
1497
|
+
// `for_in_statement`; the loop variable is a bare identifier child.
|
|
1498
|
+
const forInResult = this.traceForInLoopVariable(identifierNode, idName, sourceCode, scope);
|
|
1499
|
+
if (forInResult)
|
|
1500
|
+
return { declNode: null, constant: forInResult };
|
|
1501
|
+
return { declNode: null, constant: null };
|
|
1502
|
+
}
|
|
1503
|
+
/** Trace a `for…in`/`for…of` loop variable whose identifier appears directly
|
|
1504
|
+
* under a `for_in_statement` `left` child. tree-sitter-typescript (v0.x)
|
|
1505
|
+
* parses both loop forms as `for_in_statement`; the loop variable is a bare
|
|
1506
|
+
* `identifier` child — NOT wrapped in `variable_declarator` /
|
|
1507
|
+
* `lexical_declaration`. Walk up from the identifier to a `for_in_statement`
|
|
1508
|
+
* and trace through the iterable to determine static-ness.
|
|
1509
|
+
* const TABLES = ["a", "b"];
|
|
1510
|
+
* for (const table of TABLES) { … `${table}` … }
|
|
1511
|
+
*/
|
|
1512
|
+
traceForInLoopVariable(identifierNode, idName, sourceCode, scope) {
|
|
1513
|
+
const { scopeRoot, enclosing, ast } = scope;
|
|
1514
|
+
const idRaw = identifierNode.raw;
|
|
1515
|
+
let tsCurrent = idRaw.parent;
|
|
1516
|
+
while (tsCurrent) {
|
|
1517
|
+
if (tsCurrent.type === 'for_in_statement') {
|
|
1518
|
+
const left = tsCurrent.childForFieldName?.('left');
|
|
1519
|
+
if (left && left.text === idName) {
|
|
1520
|
+
const right = tsCurrent.childForFieldName?.('right');
|
|
1521
|
+
if (right && right.type === 'identifier') {
|
|
1522
|
+
const iterName = right.text;
|
|
1523
|
+
let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
|
|
1524
|
+
if (!iterDecl && enclosing && enclosing !== ast.root) {
|
|
1525
|
+
iterDecl = this.findDeclarationInScope(ast.root, iterName);
|
|
1526
|
+
}
|
|
1527
|
+
if (iterDecl) {
|
|
1528
|
+
const iterRaw = iterDecl.raw;
|
|
1529
|
+
const iterValue = iterRaw.childForFieldName?.('value');
|
|
1530
|
+
const iterLine = iterDecl.location.start.line;
|
|
1531
|
+
const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
|
|
1532
|
+
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
|
|
1533
|
+
if (!iterReassigned && this.isStaticValueNode(iterValue)) {
|
|
1534
|
+
const initText = iterValue ? sourceCode.slice(iterValue.startIndex, iterValue.endIndex).trim() : '';
|
|
1535
|
+
return { initText, isStatic: true, declLine: iterDecl.location.start.line };
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1452
1538
|
}
|
|
1453
1539
|
}
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1542
|
+
tsCurrent = tsCurrent.parent;
|
|
1543
|
+
}
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
/** Trace a for-of / for-in loop variable whose declarator has no `value` field
|
|
1547
|
+
* (the iterable is on the for-statement's `right` child). Returns true when
|
|
1548
|
+
* all possible loop values are known compile-time constants. */
|
|
1549
|
+
traceForOfLoopVariable(declNode, scopeRoot, enclosing, ast) {
|
|
1550
|
+
const forParent = this.findEnclosingForStatement(declNode);
|
|
1551
|
+
if (!forParent)
|
|
1552
|
+
return false;
|
|
1553
|
+
const forRaw = forParent.raw;
|
|
1554
|
+
const iterable = forRaw.childForFieldName?.('right');
|
|
1555
|
+
if (!iterable || iterable.type !== 'identifier')
|
|
1556
|
+
return false;
|
|
1557
|
+
const iterName = iterable.text;
|
|
1558
|
+
let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
|
|
1559
|
+
if (!iterDecl && enclosing && enclosing !== ast.root) {
|
|
1560
|
+
iterDecl = this.findDeclarationInScope(ast.root, iterName);
|
|
1561
|
+
}
|
|
1562
|
+
if (!iterDecl)
|
|
1563
|
+
return false;
|
|
1564
|
+
const iterRaw = iterDecl.raw;
|
|
1565
|
+
const iterValue = iterRaw.childForFieldName?.('value');
|
|
1566
|
+
const iterLine = iterDecl.location.start.line;
|
|
1567
|
+
const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
|
|
1568
|
+
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
|
|
1569
|
+
return !iterReassigned && this.isStaticValueNode(iterValue);
|
|
1570
|
+
}
|
|
1571
|
+
/** Trace an identifier value (e.g. `table` ← `tables`) to its linked
|
|
1572
|
+
* declaration. Returns true when the linked initializer is a static
|
|
1573
|
+
* constant and is never reassigned. */
|
|
1574
|
+
traceLinkedIdentifier(valueNode, idName, scope) {
|
|
1575
|
+
const { scopeRoot, enclosing, ast } = scope;
|
|
1576
|
+
const linkedName = valueNode.text;
|
|
1577
|
+
if (!linkedName || linkedName === idName)
|
|
1578
|
+
return false;
|
|
1579
|
+
let linkedDecl = this.findDeclarationInScope(scopeRoot, linkedName);
|
|
1580
|
+
if (!linkedDecl && enclosing && enclosing !== ast.root) {
|
|
1581
|
+
linkedDecl = this.findDeclarationInScope(ast.root, linkedName);
|
|
1582
|
+
}
|
|
1583
|
+
if (!linkedDecl)
|
|
1584
|
+
return false;
|
|
1585
|
+
const linkedRaw = linkedDecl.raw;
|
|
1586
|
+
const linkedValue = linkedRaw.childForFieldName?.('value');
|
|
1587
|
+
const linkedLine = linkedDecl.location.start.line;
|
|
1588
|
+
const linkedReassigned = this.hasReassignment(scopeRoot, linkedName, linkedLine)
|
|
1589
|
+
|| (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, linkedName, linkedLine) : false);
|
|
1590
|
+
return !linkedReassigned && this.isStaticValueNode(linkedValue);
|
|
1591
|
+
}
|
|
1592
|
+
/** Extract the initializer text from a declaration: the value node when
|
|
1593
|
+
* present, otherwise a regex fallback over the full declaration text. */
|
|
1594
|
+
extractInitText(valueNode, declNode, sourceCode) {
|
|
1595
|
+
if (valueNode) {
|
|
1596
|
+
return sourceCode.slice(valueNode.startIndex, valueNode.endIndex).trim();
|
|
1597
|
+
}
|
|
1598
|
+
// No initializer node — try regex as a last resort.
|
|
1599
|
+
const declText = sourceCode.slice(declNode.range[0], declNode.range[1]);
|
|
1600
|
+
const declMatch = declText.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$|^(\w+)\s*=\s*(.+?);?\s*$/s);
|
|
1601
|
+
return declMatch
|
|
1602
|
+
? (declMatch[2] ?? declMatch[4]).replace(/;\s*$/, '').trim()
|
|
1603
|
+
: '';
|
|
1604
|
+
}
|
|
1605
|
+
/** True when the identifier names the enclosing function's parameter AND a
|
|
1606
|
+
* guard call (assert/validate/check/ensure/guard-prefixed) is invoked on it
|
|
1607
|
+
* before its use. Guards throw on invalid input, so a parameter that has
|
|
1608
|
+
* passed one cannot carry unvalidated attacker data past the check. */
|
|
1609
|
+
isGuardValidatedParameter(identifierNode, ast) {
|
|
1610
|
+
const idRaw = identifierNode.raw;
|
|
1611
|
+
const paramName = idRaw.text;
|
|
1612
|
+
const enclosing = this.findEnclosingScope(identifierNode, ast);
|
|
1613
|
+
if (!enclosing || enclosing === ast.root)
|
|
1614
|
+
return false;
|
|
1615
|
+
if (!this.getParamNames(enclosing).includes(paramName))
|
|
1616
|
+
return false;
|
|
1617
|
+
let guarded = false;
|
|
1618
|
+
this.walk(enclosing, (node) => {
|
|
1619
|
+
if (guarded)
|
|
1620
|
+
return;
|
|
1621
|
+
const raw = node.raw;
|
|
1622
|
+
if (raw.type !== 'call_expression')
|
|
1623
|
+
return;
|
|
1624
|
+
const fn = raw.childForFieldName?.('function');
|
|
1625
|
+
if (!fn)
|
|
1626
|
+
return;
|
|
1627
|
+
let calleeName = null;
|
|
1628
|
+
if (fn.type === 'identifier')
|
|
1629
|
+
calleeName = fn.text;
|
|
1630
|
+
else if (fn.type === 'member_expression') {
|
|
1631
|
+
calleeName = fn.childForFieldName?.('property')?.text ?? null;
|
|
1632
|
+
}
|
|
1633
|
+
if (!calleeName)
|
|
1634
|
+
return;
|
|
1635
|
+
if (!/^(assert|validate|check|ensure|guard)([A-Z_]|$)/i.test(calleeName))
|
|
1636
|
+
return;
|
|
1637
|
+
const argsNode = raw.childForFieldName?.('arguments');
|
|
1638
|
+
const first = argsNode?.namedChildren[0] ?? null;
|
|
1639
|
+
if (first && first.type === 'identifier' && first.text === paramName
|
|
1640
|
+
&& raw.startIndex < idRaw.startIndex) {
|
|
1641
|
+
guarded = true;
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
return guarded;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
class TsSafetyAnalysis extends TsConstantResolution {
|
|
1648
|
+
/** Public entry point (LanguageAdapter.isSafeInterpolation). */
|
|
1649
|
+
isSafeInterpolation(node, ast, sourceCode) {
|
|
1650
|
+
return this.isSafeExpression(node, {
|
|
1651
|
+
ast,
|
|
1652
|
+
sourceCode,
|
|
1653
|
+
seen: new Set(),
|
|
1654
|
+
seenFns: new Set(),
|
|
1655
|
+
paramMap: new Map(),
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
/** Recursive safety check against the traversal context in `ctx`. */
|
|
1659
|
+
isSafeExpression(node, ctx) {
|
|
1660
|
+
if (!node)
|
|
1661
|
+
return false;
|
|
1662
|
+
if (node === SAFE_STRING_NODE)
|
|
1663
|
+
return true;
|
|
1664
|
+
const raw = node.raw;
|
|
1665
|
+
// `seen` is path-based (added on entry, removed on exit) and keyed by the
|
|
1666
|
+
// node's unique `id` — NOT `startIndex`, which collides between an
|
|
1667
|
+
// expression and its leading operand (a `binary_expression` and its left
|
|
1668
|
+
// string share a start offset). Path-based semantics let a shared value
|
|
1669
|
+
// (one literal bound to two parameters) be re-checked via sibling branches
|
|
1670
|
+
// while still breaking true cycles (a node in its own ancestor chain).
|
|
1671
|
+
const key = raw.id;
|
|
1672
|
+
if (ctx.seen.has(key))
|
|
1673
|
+
return false;
|
|
1674
|
+
ctx.seen.add(key);
|
|
1675
|
+
const result = this.evaluateSafeExpression(node, raw, ctx);
|
|
1676
|
+
ctx.seen.delete(key);
|
|
1677
|
+
return result;
|
|
1678
|
+
}
|
|
1679
|
+
/** Evaluate a single expression node's safety, dispatching on its AST type. */
|
|
1680
|
+
evaluateSafeExpression(node, raw, ctx) {
|
|
1681
|
+
// Structural nodes (wrappers, binary, ternary) recurse into children.
|
|
1682
|
+
const structural = this.evaluateStructuralExpression(node, raw, ctx);
|
|
1683
|
+
if (structural !== null)
|
|
1684
|
+
return structural;
|
|
1685
|
+
switch (raw.type) {
|
|
1686
|
+
case 'string':
|
|
1687
|
+
case 'number':
|
|
1688
|
+
case 'true':
|
|
1689
|
+
case 'false':
|
|
1690
|
+
case 'null':
|
|
1691
|
+
case 'undefined':
|
|
1692
|
+
case 'regex':
|
|
1693
|
+
return true;
|
|
1694
|
+
case 'identifier':
|
|
1695
|
+
return this.isSafeIdentifier(node, ctx);
|
|
1696
|
+
case 'template_string':
|
|
1697
|
+
return this.isSafeTemplateString(node, ctx);
|
|
1698
|
+
case 'array':
|
|
1699
|
+
return this.isSafeArray(node, ctx);
|
|
1700
|
+
case 'call_expression':
|
|
1701
|
+
return this.isSafeCallExpression(node, ctx);
|
|
1702
|
+
// member_expression, object/class literals, await/async, etc. — a value we
|
|
1703
|
+
// cannot prove safe. Conservative: stay flagged.
|
|
1704
|
+
default:
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
/** Handle structural nodes that recurse into child expressions (parenthesized
|
|
1709
|
+
* wrappers, type casts/assertions, ternaries, binary ops). Returns `null`
|
|
1710
|
+
* when the node is not structural, so the caller can fall through to the
|
|
1711
|
+
* leaf switch. */
|
|
1712
|
+
evaluateStructuralExpression(node, raw, ctx) {
|
|
1713
|
+
switch (raw.type) {
|
|
1714
|
+
case 'parenthesized_expression': {
|
|
1715
|
+
const inner = (node.children ?? []).find((c) => !['(', ')'].includes(c.raw.type));
|
|
1716
|
+
return this.isSafeExpression(inner ?? null, ctx);
|
|
1717
|
+
}
|
|
1718
|
+
case 'as_expression':
|
|
1719
|
+
case 'type_assertion':
|
|
1720
|
+
case 'satisfies_expression':
|
|
1721
|
+
case 'non_null_expression': {
|
|
1722
|
+
const inner = raw.namedChild?.(0);
|
|
1723
|
+
return this.isSafeExpression(this.wrapRaw(inner), ctx);
|
|
1724
|
+
}
|
|
1725
|
+
case 'ternary_expression': {
|
|
1726
|
+
const consequence = raw.childForFieldName?.('consequence');
|
|
1727
|
+
const alternative = raw.childForFieldName?.('alternative');
|
|
1728
|
+
return this.isSafeExpression(this.wrapRaw(consequence), ctx)
|
|
1729
|
+
&& this.isSafeExpression(this.wrapRaw(alternative), ctx);
|
|
1730
|
+
}
|
|
1731
|
+
case 'binary_expression': {
|
|
1732
|
+
const left = raw.childForFieldName?.('left');
|
|
1733
|
+
const right = raw.childForFieldName?.('right');
|
|
1734
|
+
return this.isSafeExpression(this.wrapRaw(left), ctx)
|
|
1735
|
+
&& this.isSafeExpression(this.wrapRaw(right), ctx);
|
|
1736
|
+
}
|
|
1737
|
+
default:
|
|
1738
|
+
return null;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
/** Identifier safety: a bound parameter, compile-time constant, or validated
|
|
1742
|
+
* parameter at all call sites. */
|
|
1743
|
+
isSafeIdentifier(node, ctx) {
|
|
1744
|
+
const name = node.raw.text;
|
|
1745
|
+
// A parameter bound at the current call site — recurse into its value.
|
|
1746
|
+
if (ctx.paramMap.has(name)) {
|
|
1747
|
+
return this.isSafeExpression(ctx.paramMap.get(name) ?? null, ctx);
|
|
1748
|
+
}
|
|
1749
|
+
// Compile-time constant (string/number/static array/imported symbol).
|
|
1750
|
+
const resolved = this.resolveLocalConstant(node, ctx.ast, ctx.sourceCode);
|
|
1751
|
+
if (resolved && resolved.isStatic) {
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1754
|
+
if (this.isDeclarationValueSafe(node, ctx)) {
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1757
|
+
if (this.isGuardValidatedParameter(node, ctx.ast)) {
|
|
1758
|
+
return true;
|
|
1759
|
+
}
|
|
1760
|
+
if (this.isParamSafeAtAllCallSites(node, ctx)) {
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
/** A template string is safe only if every `${…}` substitution is safe. */
|
|
1766
|
+
isSafeTemplateString(node, ctx) {
|
|
1767
|
+
for (const child of node.children ?? []) {
|
|
1768
|
+
const ct = child.raw.type;
|
|
1769
|
+
if (ct !== 'template_substitution')
|
|
1770
|
+
continue;
|
|
1771
|
+
const inner = (child.children ?? []).find((c) => c.raw.type !== 'template_substitution') ?? child;
|
|
1772
|
+
if (!this.isSafeExpression(inner, ctx)) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
/** An array literal is safe only if every element is safe. */
|
|
1779
|
+
isSafeArray(node, ctx) {
|
|
1780
|
+
for (const child of node.children ?? []) {
|
|
1781
|
+
const ct = child.raw.type;
|
|
1782
|
+
if (ct === ',' || ct === '[' || ct === ']')
|
|
1783
|
+
continue;
|
|
1784
|
+
if (!this.isSafeExpression(child, ctx)) {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
/** Decide whether a call expression is provably safe to embed in SQL. */
|
|
1791
|
+
isSafeCallExpression(node, ctx) {
|
|
1792
|
+
const raw = node.raw;
|
|
1793
|
+
const fnNode = raw.childForFieldName?.('function');
|
|
1794
|
+
if (!fnNode)
|
|
1795
|
+
return false;
|
|
1796
|
+
// NOTE: manual quote-escaping (`x.replace(/'/g, "''")`) is deliberately NOT
|
|
1797
|
+
// treated as a sanitizer here — see the module-level comment above. It
|
|
1798
|
+
// covers only the single-quote case and would otherwise bless a real
|
|
1799
|
+
// vulnerability.
|
|
1800
|
+
// 1. Static-array `.map().join()` chain (e.g. `FLAGS.map((c) => \`a.${c}\`).join(", ")`).
|
|
1801
|
+
if (this.isSafeMapJoin(node, ctx))
|
|
1802
|
+
return true;
|
|
1803
|
+
// 2. Local function call with a safe body and safe call sites.
|
|
1804
|
+
if (this.isLocalFunctionCallSafe(node, ctx))
|
|
1805
|
+
return true;
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
/** True when the node is `.join(...)` over `.map(...)` of a static array whose
|
|
1809
|
+
* callback body is safe for every element. */
|
|
1810
|
+
isSafeMapJoin(node, ctx) {
|
|
1811
|
+
const raw = node.raw;
|
|
1812
|
+
const fnNode = raw.childForFieldName?.('function');
|
|
1813
|
+
if (!fnNode || fnNode.type !== 'member_expression')
|
|
1814
|
+
return false;
|
|
1815
|
+
const joinProp = fnNode.childForFieldName?.('property');
|
|
1816
|
+
const joinObj = fnNode.childForFieldName?.('object');
|
|
1817
|
+
if (!joinProp || !joinObj || joinProp.text !== 'join')
|
|
1818
|
+
return false;
|
|
1819
|
+
if (joinObj.type !== 'call_expression')
|
|
1820
|
+
return false;
|
|
1821
|
+
const mapFn = joinObj.childForFieldName?.('function');
|
|
1822
|
+
const mapArgs = joinObj.childForFieldName?.('arguments');
|
|
1823
|
+
if (!mapFn || mapFn.type !== 'member_expression')
|
|
1824
|
+
return false;
|
|
1825
|
+
const mapProp = mapFn.childForFieldName?.('property');
|
|
1826
|
+
const mapObj = mapFn.childForFieldName?.('object');
|
|
1827
|
+
if (!mapProp || !mapObj || mapProp.text !== 'map')
|
|
1828
|
+
return false;
|
|
1829
|
+
// The array being mapped must itself be provably safe (static array/const).
|
|
1830
|
+
if (!this.isSafeExpression(this.wrapRaw(mapObj), ctx))
|
|
1831
|
+
return false;
|
|
1832
|
+
// The callback must be safe for any element — bind its first parameter to a
|
|
1833
|
+
// compile-time string sentinel and check the callback body.
|
|
1834
|
+
const callback = mapArgs?.namedChildren[0] ?? null;
|
|
1835
|
+
if (!callback || !['arrow_function', 'function_expression', 'function'].includes(callback.type))
|
|
1836
|
+
return false;
|
|
1837
|
+
const cbParams = this.getParamNames(this.wrapRaw(callback));
|
|
1838
|
+
if (cbParams.length === 0)
|
|
1839
|
+
return false;
|
|
1840
|
+
const bound = new Map(ctx.paramMap);
|
|
1841
|
+
bound.set(cbParams[0], SAFE_STRING_NODE);
|
|
1842
|
+
return this.isBodySafeUnderParams(this.wrapRaw(callback), { ...ctx, paramMap: bound });
|
|
1843
|
+
}
|
|
1844
|
+
/** True when the node is a call to a local (in-file) function whose body is
|
|
1845
|
+
* safe under the values passed at this call site. */
|
|
1846
|
+
isLocalFunctionCallSafe(node, ctx) {
|
|
1847
|
+
const raw = node.raw;
|
|
1848
|
+
const fnNode = raw.childForFieldName?.('function');
|
|
1849
|
+
if (!fnNode || fnNode.type !== 'identifier')
|
|
1850
|
+
return false;
|
|
1851
|
+
const calleeName = fnNode.text;
|
|
1852
|
+
if (!calleeName || ctx.seenFns.has(calleeName))
|
|
1853
|
+
return false;
|
|
1854
|
+
ctx.seenFns.add(calleeName);
|
|
1855
|
+
const decl = this.findFunctionDeclaration(calleeName, ctx.ast);
|
|
1856
|
+
if (!decl)
|
|
1857
|
+
return false;
|
|
1858
|
+
const paramNames = this.getParamNames(decl);
|
|
1859
|
+
const argNodes = this.getCallArgASTNodes(node);
|
|
1860
|
+
const bound = new Map(ctx.paramMap);
|
|
1861
|
+
paramNames.forEach((p, i) => bound.set(p, argNodes[i] ?? null));
|
|
1862
|
+
return this.isBodySafeUnderParams(decl, { ...ctx, paramMap: bound });
|
|
1863
|
+
}
|
|
1864
|
+
/** Check a function's body returns only safe values, under the `paramMap`
|
|
1865
|
+
* already bound in `ctx` to the values passed at the call site. */
|
|
1866
|
+
isBodySafeUnderParams(fnNode, ctx) {
|
|
1867
|
+
const raw = fnNode.raw;
|
|
1868
|
+
const body = raw.childForFieldName?.('body');
|
|
1869
|
+
if (!body)
|
|
1870
|
+
return false;
|
|
1871
|
+
if (body.type === 'statement_block') {
|
|
1872
|
+
const returns = [];
|
|
1873
|
+
const collect = (n) => {
|
|
1874
|
+
if (n.type === 'return_statement')
|
|
1875
|
+
returns.push(n);
|
|
1876
|
+
for (const c of n.namedChildren)
|
|
1877
|
+
collect(c);
|
|
1878
|
+
};
|
|
1879
|
+
collect(body);
|
|
1880
|
+
if (returns.length === 0)
|
|
1881
|
+
return false;
|
|
1882
|
+
for (const r of returns) {
|
|
1883
|
+
const expr = r.namedChildren.find((c) => c.type !== 'return_statement') ?? null;
|
|
1884
|
+
if (!expr)
|
|
1885
|
+
return false;
|
|
1886
|
+
if (!this.isSafeExpression(this.wrapRaw(expr), ctx))
|
|
1887
|
+
return false;
|
|
1454
1888
|
}
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
// Arrow-function expression body (no braces).
|
|
1892
|
+
return this.isSafeExpression(this.wrapRaw(body), ctx);
|
|
1893
|
+
}
|
|
1894
|
+
/** True when the identifier names the enclosing function's parameter AND every
|
|
1895
|
+
* in-file call site of that function passes a provably-safe value for that
|
|
1896
|
+
* parameter position. */
|
|
1897
|
+
isParamSafeAtAllCallSites(identifierNode, ctx) {
|
|
1898
|
+
const idRaw = identifierNode.raw;
|
|
1899
|
+
const paramName = idRaw.text;
|
|
1900
|
+
const enclosing = this.findEnclosingScope(identifierNode, ctx.ast);
|
|
1901
|
+
if (!enclosing || enclosing === ctx.ast.root)
|
|
1902
|
+
return false;
|
|
1903
|
+
const paramNames = this.getParamNames(enclosing);
|
|
1904
|
+
const paramIndex = paramNames.indexOf(paramName);
|
|
1905
|
+
if (paramIndex < 0)
|
|
1906
|
+
return false;
|
|
1907
|
+
const fnName = this.getFunctionName(enclosing);
|
|
1908
|
+
if (!fnName)
|
|
1909
|
+
return false;
|
|
1910
|
+
const callSites = this.findCallSites(fnName, ctx.ast);
|
|
1911
|
+
if (callSites.length === 0)
|
|
1912
|
+
return false;
|
|
1913
|
+
for (const site of callSites) {
|
|
1914
|
+
const arg = this.getCallArgASTNodes(site)[paramIndex];
|
|
1915
|
+
if (!arg)
|
|
1916
|
+
return false;
|
|
1917
|
+
if (!this.isSafeExpression(arg, { ...ctx, paramMap: new Map() }))
|
|
1918
|
+
return false;
|
|
1455
1919
|
}
|
|
1456
|
-
return
|
|
1920
|
+
return true;
|
|
1457
1921
|
}
|
|
1922
|
+
/** True when the identifier names a local variable whose initializer is a
|
|
1923
|
+
* provably-safe expression and which is never reassigned. */
|
|
1924
|
+
isDeclarationValueSafe(identifierNode, ctx) {
|
|
1925
|
+
const idRaw = identifierNode.raw;
|
|
1926
|
+
const name = idRaw.text;
|
|
1927
|
+
const enclosing = this.findEnclosingScope(identifierNode, ctx.ast);
|
|
1928
|
+
const scopeRoot = enclosing ?? ctx.ast.root;
|
|
1929
|
+
let declNode = this.findDeclarationInScope(scopeRoot, name);
|
|
1930
|
+
if (!declNode && enclosing && enclosing !== ctx.ast.root) {
|
|
1931
|
+
declNode = this.findDeclarationInScope(ctx.ast.root, name);
|
|
1932
|
+
}
|
|
1933
|
+
if (!declNode)
|
|
1934
|
+
return false;
|
|
1935
|
+
const declLine = declNode.location.start.line;
|
|
1936
|
+
const reassigned = this.hasReassignment(scopeRoot, name, declLine)
|
|
1937
|
+
|| (enclosing && enclosing !== ctx.ast.root ? this.hasReassignment(ctx.ast.root, name, declLine) : false);
|
|
1938
|
+
if (reassigned)
|
|
1939
|
+
return false;
|
|
1940
|
+
const declRaw = declNode.raw;
|
|
1941
|
+
const valueRaw = declRaw.childForFieldName?.('value');
|
|
1942
|
+
if (!valueRaw)
|
|
1943
|
+
return false;
|
|
1944
|
+
return this.isSafeExpression(this.wrapRaw(valueRaw), ctx);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
// ---------------------------------------------------------------------------
|
|
1948
|
+
// Adapter (composes the slices above)
|
|
1949
|
+
// ---------------------------------------------------------------------------
|
|
1950
|
+
/**
|
|
1951
|
+
* Tree sitter type script adapter.
|
|
1952
|
+
*/
|
|
1953
|
+
export class TreeSitterTypeScriptAdapter extends TsSafetyAnalysis {
|
|
1458
1954
|
}
|
|
1459
1955
|
//# sourceMappingURL=TreeSitterTypeScriptAdapter.js.map
|