clast-ai 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +11 -9
- package/README.md +468 -398
- package/bench/ab/onair-concept.json +22 -0
- package/bench/ab/onair-deadcode.json +7 -0
- package/bench/ab/onair-hard.json +32 -0
- package/bench/ab/onair-impact.json +17 -0
- package/bench/ab/onair-refactors.json +18 -0
- package/bench/ab/onair-survey.json +17 -0
- package/bench/ab/onair-tasks.json +20 -0
- package/bench/ab/onair.json +52 -0
- package/bench/ab/vscode-concept.json +22 -0
- package/bench/ab/vscode-hard.json +32 -0
- package/bench/ab/vscode-impact.json +17 -0
- package/bench/ab/vscode-refactors.json +18 -0
- package/bench/ab/vscode-tasks.json +20 -0
- package/bench/ab/vscode.json +52 -0
- package/bench/ab.mjs +213 -0
- package/bench/bench.mjs +98 -0
- package/bench/resolution.mjs +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +266 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +12 -2
- package/dist/config/defaults.js +38 -31
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +26 -14
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +73 -13
- package/dist/db/index.js +481 -39
- package/dist/db/index.js.map +1 -1
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +28 -0
- package/dist/engine.js.map +1 -0
- package/dist/indexer/imports.d.ts +6 -0
- package/dist/indexer/imports.js +254 -0
- package/dist/indexer/imports.js.map +1 -0
- package/dist/indexer/index.d.ts +63 -0
- package/dist/indexer/index.js +360 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/process.d.ts +22 -0
- package/dist/indexer/process.js +42 -0
- package/dist/indexer/process.js.map +1 -0
- package/dist/indexer/scan.d.ts +43 -0
- package/dist/indexer/scan.js +223 -0
- package/dist/indexer/scan.js.map +1 -0
- package/dist/indexer/worker.d.ts +1 -0
- package/dist/indexer/worker.js +23 -0
- package/dist/indexer/worker.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +17 -19
- package/dist/llm/comment-generator.js +46 -80
- package/dist/llm/comment-generator.js.map +1 -1
- package/dist/llm/index.d.ts +0 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/prompts.d.ts +0 -1
- package/dist/llm/prompts.js +16 -16
- package/dist/migrate.d.ts +35 -0
- package/dist/migrate.js +194 -0
- package/dist/migrate.js.map +1 -0
- package/dist/parser/extractor.d.ts +21 -16
- package/dist/parser/extractor.js +700 -228
- package/dist/parser/extractor.js.map +1 -1
- package/dist/parser/index.d.ts +17 -21
- package/dist/parser/index.js +115 -171
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/langs/c.d.ts +3 -0
- package/dist/parser/langs/c.js +879 -0
- package/dist/parser/langs/c.js.map +1 -0
- package/dist/parser/langs/csharp.d.ts +2 -0
- package/dist/parser/langs/csharp.js +548 -0
- package/dist/parser/langs/csharp.js.map +1 -0
- package/dist/parser/langs/dart.d.ts +2 -0
- package/dist/parser/langs/dart.js +893 -0
- package/dist/parser/langs/dart.js.map +1 -0
- package/dist/parser/langs/go.d.ts +2 -0
- package/dist/parser/langs/go.js +455 -0
- package/dist/parser/langs/go.js.map +1 -0
- package/dist/parser/langs/java.d.ts +2 -0
- package/dist/parser/langs/java.js +390 -0
- package/dist/parser/langs/java.js.map +1 -0
- package/dist/parser/langs/php.d.ts +2 -0
- package/dist/parser/langs/php.js +486 -0
- package/dist/parser/langs/php.js.map +1 -0
- package/dist/parser/langs/python.d.ts +2 -0
- package/dist/parser/langs/python.js +216 -0
- package/dist/parser/langs/python.js.map +1 -0
- package/dist/parser/langs/ruby.d.ts +2 -0
- package/dist/parser/langs/ruby.js +602 -0
- package/dist/parser/langs/ruby.js.map +1 -0
- package/dist/parser/langs/rust.d.ts +2 -0
- package/dist/parser/langs/rust.js +931 -0
- package/dist/parser/langs/rust.js.map +1 -0
- package/dist/parser/langs/swift.d.ts +2 -0
- package/dist/parser/langs/swift.js +616 -0
- package/dist/parser/langs/swift.js.map +1 -0
- package/dist/parser/langs/typescript.d.ts +4 -0
- package/dist/parser/langs/typescript.js +535 -0
- package/dist/parser/langs/typescript.js.map +1 -0
- package/dist/parser/registry.d.ts +16 -0
- package/dist/parser/registry.js +87 -0
- package/dist/parser/registry.js.map +1 -0
- package/dist/parser/returns.d.ts +9 -0
- package/dist/parser/returns.js +152 -0
- package/dist/parser/returns.js.map +1 -0
- package/dist/parser/spec.d.ts +165 -0
- package/dist/parser/spec.js +188 -0
- package/dist/parser/spec.js.map +1 -0
- package/dist/query/arity.d.ts +27 -0
- package/dist/query/arity.js +187 -0
- package/dist/query/arity.js.map +1 -0
- package/dist/query/callgraph.d.ts +113 -0
- package/dist/query/callgraph.js +332 -0
- package/dist/query/callgraph.js.map +1 -0
- package/dist/query/format.d.ts +36 -0
- package/dist/query/format.js +136 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/hierarchy.d.ts +44 -0
- package/dist/query/hierarchy.js +441 -0
- package/dist/query/hierarchy.js.map +1 -0
- package/dist/query/resolve.d.ts +265 -0
- package/dist/query/resolve.js +1957 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/search.d.ts +35 -0
- package/dist/query/search.js +379 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/words.d.ts +30 -0
- package/dist/query/words.js +201 -0
- package/dist/query/words.js.map +1 -0
- package/dist/server/index.d.ts +1 -3
- package/dist/server/index.js +313 -73
- package/dist/server/index.js.map +1 -1
- package/dist/tools/context.d.ts +35 -0
- package/dist/tools/context.js +79 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/explore.d.ts +51 -0
- package/dist/tools/explore.js +779 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/impact.d.ts +21 -0
- package/dist/tools/impact.js +743 -0
- package/dist/tools/impact.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.js +843 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/stats.d.ts +26 -0
- package/dist/tools/stats.js +604 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +95 -40
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher/index.d.ts +18 -16
- package/dist/watcher/index.js +94 -77
- package/dist/watcher/index.js.map +1 -1
- package/package.json +21 -24
- package/plugin/server.mjs +72 -0
- package/skills/audit/SKILL.md +37 -0
- package/skills/deadcode/SKILL.md +33 -0
- package/skills/explore/SKILL.md +54 -0
- package/skills/impact/SKILL.md +24 -0
- package/skills/status/SKILL.md +11 -0
- package/src/cli.ts +265 -0
- package/src/config/defaults.ts +50 -32
- package/src/config/index.ts +23 -14
- package/src/db/index.ts +540 -43
- package/src/engine.ts +30 -0
- package/src/indexer/imports.ts +241 -0
- package/src/indexer/index.ts +369 -0
- package/src/indexer/process.ts +63 -0
- package/src/indexer/scan.ts +226 -0
- package/src/indexer/worker.ts +24 -0
- package/src/llm/comment-generator.ts +52 -103
- package/src/llm/index.ts +80 -80
- package/src/llm/prompts.ts +95 -95
- package/src/migrate.ts +213 -0
- package/src/parser/extractor.ts +643 -256
- package/src/parser/index.ts +105 -205
- package/src/parser/langs/c.ts +860 -0
- package/src/parser/langs/csharp.ts +519 -0
- package/src/parser/langs/dart.ts +820 -0
- package/src/parser/langs/go.ts +437 -0
- package/src/parser/langs/java.ts +361 -0
- package/src/parser/langs/php.ts +450 -0
- package/src/parser/langs/python.ts +206 -0
- package/src/parser/langs/ruby.ts +554 -0
- package/src/parser/langs/rust.ts +904 -0
- package/src/parser/langs/swift.ts +590 -0
- package/src/parser/langs/typescript.ts +499 -0
- package/src/parser/registry.ts +88 -0
- package/src/parser/returns.ts +137 -0
- package/src/parser/spec.ts +309 -0
- package/src/query/arity.ts +173 -0
- package/src/query/callgraph.ts +473 -0
- package/src/query/format.ts +136 -0
- package/src/query/hierarchy.ts +447 -0
- package/src/query/resolve.ts +1972 -0
- package/src/query/search.ts +430 -0
- package/src/query/words.ts +183 -0
- package/src/server/index.ts +390 -79
- package/src/tools/context.ts +82 -0
- package/src/tools/explore.ts +860 -0
- package/src/tools/impact.ts +748 -0
- package/src/tools/search.ts +886 -0
- package/src/tools/stats.ts +633 -0
- package/src/types.ts +113 -45
- package/src/version.ts +2 -0
- package/src/watcher/index.ts +82 -88
- package/tsconfig.json +6 -3
- package/.mcp.json +0 -8
- package/commands/clast-status.md +0 -9
- package/dist/config/defaults.d.ts.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/queries.d.ts +0 -44
- package/dist/db/queries.d.ts.map +0 -1
- package/dist/db/queries.js +0 -251
- package/dist/db/queries.js.map +0 -1
- package/dist/db/schema.d.ts +0 -3
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/db/schema.js +0 -94
- package/dist/db/schema.js.map +0 -1
- package/dist/db/types.d.ts +0 -44
- package/dist/db/types.d.ts.map +0 -1
- package/dist/db/types.js +0 -2
- package/dist/db/types.js.map +0 -1
- package/dist/llm/comment-generator.d.ts.map +0 -1
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/prompts.d.ts.map +0 -1
- package/dist/parser/call-resolver.d.ts +0 -15
- package/dist/parser/call-resolver.d.ts.map +0 -1
- package/dist/parser/call-resolver.js +0 -16
- package/dist/parser/call-resolver.js.map +0 -1
- package/dist/parser/extractor.d.ts.map +0 -1
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/languages.d.ts +0 -11
- package/dist/parser/languages.d.ts.map +0 -1
- package/dist/parser/languages.js +0 -74
- package/dist/parser/languages.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/register-tools.d.ts +0 -7
- package/dist/server/register-tools.d.ts.map +0 -1
- package/dist/server/register-tools.js +0 -17
- package/dist/server/register-tools.js.map +0 -1
- package/dist/server/tools/clast-call-graph.d.ts +0 -4
- package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
- package/dist/server/tools/clast-call-graph.js +0 -70
- package/dist/server/tools/clast-call-graph.js.map +0 -1
- package/dist/server/tools/clast-comment.d.ts +0 -5
- package/dist/server/tools/clast-comment.d.ts.map +0 -1
- package/dist/server/tools/clast-comment.js +0 -64
- package/dist/server/tools/clast-comment.js.map +0 -1
- package/dist/server/tools/clast-file-summary.d.ts +0 -4
- package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
- package/dist/server/tools/clast-file-summary.js +0 -42
- package/dist/server/tools/clast-file-summary.js.map +0 -1
- package/dist/server/tools/clast-get-context.d.ts +0 -4
- package/dist/server/tools/clast-get-context.d.ts.map +0 -1
- package/dist/server/tools/clast-get-context.js +0 -80
- package/dist/server/tools/clast-get-context.js.map +0 -1
- package/dist/server/tools/clast-reindex.d.ts +0 -5
- package/dist/server/tools/clast-reindex.d.ts.map +0 -1
- package/dist/server/tools/clast-reindex.js +0 -36
- package/dist/server/tools/clast-reindex.js.map +0 -1
- package/dist/server/tools/clast-search.d.ts +0 -4
- package/dist/server/tools/clast-search.d.ts.map +0 -1
- package/dist/server/tools/clast-search.js +0 -29
- package/dist/server/tools/clast-search.js.map +0 -1
- package/dist/server/tools/clast-status.d.ts +0 -5
- package/dist/server/tools/clast-status.d.ts.map +0 -1
- package/dist/server/tools/clast-status.js +0 -23
- package/dist/server/tools/clast-status.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/watcher/gitignore.d.ts +0 -9
- package/dist/watcher/gitignore.d.ts.map +0 -1
- package/dist/watcher/gitignore.js +0 -38
- package/dist/watcher/gitignore.js.map +0 -1
- package/dist/watcher/index.d.ts.map +0 -1
- package/skills/clast-explore/SKILL.md +0 -41
- package/src/db/queries.ts +0 -332
- package/src/db/schema.ts +0 -106
- package/src/db/types.ts +0 -47
- package/src/parser/call-resolver.ts +0 -17
- package/src/parser/languages.ts +0 -86
- package/src/server/register-tools.ts +0 -30
- package/src/server/tools/clast-call-graph.ts +0 -82
- package/src/server/tools/clast-comment.ts +0 -76
- package/src/server/tools/clast-file-summary.ts +0 -59
- package/src/server/tools/clast-get-context.ts +0 -97
- package/src/server/tools/clast-reindex.ts +0 -52
- package/src/server/tools/clast-search.ts +0 -38
- package/src/server/tools/clast-status.ts +0 -37
- package/src/watcher/gitignore.ts +0 -42
package/src/parser/extractor.ts
CHANGED
|
@@ -1,303 +1,690 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'type_declaration', // Go
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
const INTERFACE_TYPES = new Set([
|
|
26
|
-
'interface_declaration',
|
|
27
|
-
'trait_item', // Rust
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const IMPORT_TYPES = new Set([
|
|
31
|
-
'import_statement',
|
|
32
|
-
'import_declaration',
|
|
33
|
-
'use_declaration', // Rust
|
|
34
|
-
'preproc_include', // C/C++ (#include)
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const EXPORT_TYPES = new Set([
|
|
38
|
-
'export_statement',
|
|
39
|
-
'export_declaration',
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
const ENUM_TYPES = new Set([
|
|
43
|
-
'enum_declaration',
|
|
44
|
-
'enum_item', // Rust
|
|
45
|
-
'enum_specifier', // C/C++
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
const CALL_TYPES = new Set([
|
|
49
|
-
'call_expression',
|
|
50
|
-
'new_expression',
|
|
51
|
-
'method_invocation', // Java
|
|
52
|
-
'invocation_expression', // C#
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
function classifyNodeType(treeSitterType: string): AstNodeType | null {
|
|
56
|
-
if (FUNCTION_TYPES.has(treeSitterType)) return 'function';
|
|
57
|
-
if (CLASS_TYPES.has(treeSitterType)) return 'class';
|
|
58
|
-
if (INTERFACE_TYPES.has(treeSitterType)) return 'interface';
|
|
59
|
-
if (IMPORT_TYPES.has(treeSitterType)) return 'import';
|
|
60
|
-
if (EXPORT_TYPES.has(treeSitterType)) return 'export';
|
|
61
|
-
if (ENUM_TYPES.has(treeSitterType)) return 'enum';
|
|
62
|
-
return null;
|
|
1
|
+
import type { Language, Node, Tree } from 'web-tree-sitter';
|
|
2
|
+
import { CONTAINER_KINDS, type ExCall, type ExHeritage, type ExImport, type ExSymbol, type Extraction, type SymbolKind } from '../types.js';
|
|
3
|
+
import { genericArity, typeName, type Ctx, type DefOpts, type LangSpec, type Visitor } from './spec.js';
|
|
4
|
+
import { returnTypeOf } from './returns.js';
|
|
5
|
+
|
|
6
|
+
const MAX_SIGNATURE = 240;
|
|
7
|
+
const MAX_DOC = 600;
|
|
8
|
+
const MAX_OCC_LINES = 40;
|
|
9
|
+
const MAX_NAME = 120;
|
|
10
|
+
|
|
11
|
+
/** A language spec bound to a loaded grammar (node types resolved to ids). */
|
|
12
|
+
export interface CompiledSpec {
|
|
13
|
+
spec: LangSpec;
|
|
14
|
+
language: Language;
|
|
15
|
+
visitors: Array<Visitor | undefined>;
|
|
16
|
+
isIdent: Uint8Array;
|
|
17
|
+
isComment: Uint8Array;
|
|
18
|
+
/** Node types skipped between a doc comment and its item (attributes). */
|
|
19
|
+
isDocSkip: Uint8Array;
|
|
20
|
+
/** Visitor / identifier node types that do not exist in the grammar. */
|
|
21
|
+
unknownTypes: string[];
|
|
63
22
|
}
|
|
64
23
|
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
24
|
+
export function compileSpec(spec: LangSpec, language: Language): CompiledSpec {
|
|
25
|
+
const count = language.nodeTypeCount;
|
|
26
|
+
const visitors: Array<Visitor | undefined> = new Array(count);
|
|
27
|
+
const isIdent = new Uint8Array(count);
|
|
28
|
+
const isComment = new Uint8Array(count);
|
|
29
|
+
|
|
30
|
+
const idsByName = new Map<string, number[]>();
|
|
31
|
+
for (let id = 0; id < count; id++) {
|
|
32
|
+
if (!language.nodeTypeIsNamed(id)) continue;
|
|
33
|
+
const name = language.nodeTypeForId(id);
|
|
34
|
+
if (!name) continue;
|
|
35
|
+
let list = idsByName.get(name);
|
|
36
|
+
if (!list) idsByName.set(name, (list = []));
|
|
37
|
+
list.push(id);
|
|
73
38
|
}
|
|
74
39
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
40
|
+
const unknownTypes: string[] = [];
|
|
41
|
+
for (const [type, visitor] of Object.entries(spec.visit)) {
|
|
42
|
+
const ids = idsByName.get(type);
|
|
43
|
+
if (!ids) { unknownTypes.push(type); continue; }
|
|
44
|
+
for (const id of ids) visitors[id] = visitor;
|
|
79
45
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (decl) return extractName(decl);
|
|
85
|
-
return '<export>';
|
|
46
|
+
for (const type of spec.identifiers) {
|
|
47
|
+
const ids = idsByName.get(type);
|
|
48
|
+
if (!ids) { unknownTypes.push(type); continue; }
|
|
49
|
+
for (const id of ids) isIdent[id] = 1;
|
|
86
50
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const source = node.childForFieldName('source') ?? node.childForFieldName('path');
|
|
91
|
-
if (source) return source.text.replace(/['"]/g, '');
|
|
92
|
-
return node.text.slice(0, 80);
|
|
51
|
+
const commentTypes = spec.comments ?? [...idsByName.keys()].filter(n => n.includes('comment'));
|
|
52
|
+
for (const type of commentTypes) {
|
|
53
|
+
for (const id of idsByName.get(type) ?? []) isComment[id] = 1;
|
|
93
54
|
}
|
|
94
55
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const braceIdx = text.indexOf('{');
|
|
101
|
-
const parenCloseIdx = text.indexOf(')');
|
|
102
|
-
|
|
103
|
-
let end = text.length;
|
|
104
|
-
if (braceIdx > 0) {
|
|
105
|
-
end = braceIdx;
|
|
106
|
-
} else if (parenCloseIdx > 0) {
|
|
107
|
-
end = parenCloseIdx + 1;
|
|
56
|
+
const isDocSkip = new Uint8Array(count);
|
|
57
|
+
for (const type of spec.docSkip ?? []) {
|
|
58
|
+
const ids = idsByName.get(type);
|
|
59
|
+
if (!ids) { unknownTypes.push(type); continue; }
|
|
60
|
+
for (const id of ids) isDocSkip[id] = 1;
|
|
108
61
|
}
|
|
109
62
|
|
|
110
|
-
|
|
111
|
-
return sig.length > maxLength ? sig.slice(0, maxLength) + '...' : sig;
|
|
63
|
+
return { spec, language, visitors, isIdent, isComment, isDocSkip, unknownTypes };
|
|
112
64
|
}
|
|
113
65
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let checkLine = nodeLine - 1;
|
|
123
|
-
|
|
124
|
-
while (checkLine >= 0) {
|
|
125
|
-
const line = lines[checkLine]?.trim() ?? '';
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
line.startsWith('//') ||
|
|
129
|
-
line.startsWith('#') ||
|
|
130
|
-
line.startsWith('*') ||
|
|
131
|
-
line.startsWith('/*') ||
|
|
132
|
-
line.startsWith('///') ||
|
|
133
|
-
line.startsWith('/**')
|
|
134
|
-
) {
|
|
135
|
-
commentLines.unshift(line);
|
|
136
|
-
checkLine--;
|
|
137
|
-
} else if (line === '' && commentLines.length === 0) {
|
|
138
|
-
checkLine--;
|
|
139
|
-
} else {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
66
|
+
interface Scope {
|
|
67
|
+
/** Symbol index, or -1 for naming-only scopes. */
|
|
68
|
+
sym: number;
|
|
69
|
+
name: string;
|
|
70
|
+
kind: SymbolKind | null;
|
|
71
|
+
/** Whether calls inside are attributed to this symbol. */
|
|
72
|
+
callScope: boolean;
|
|
73
|
+
}
|
|
143
74
|
|
|
144
|
-
|
|
75
|
+
export function cleanComment(raw: string): string {
|
|
76
|
+
let s = raw.trim();
|
|
77
|
+
// Block comments: /** ... */, /* ... */, """ ... """, =begin ... =end
|
|
78
|
+
s = s.replace(/^\/\*+!?/, '').replace(/\*+\/$/, '');
|
|
79
|
+
const lines = s.split(/\r?\n/).map(l =>
|
|
80
|
+
l
|
|
81
|
+
.replace(/^\s*(\/\/[\/!]?|#+!?|--+|\*+(?!\/)|;+)\s?/, '')
|
|
82
|
+
.replace(/\s+$/, '')
|
|
83
|
+
);
|
|
84
|
+
while (lines.length && !lines[0]!.trim()) lines.shift();
|
|
85
|
+
while (lines.length && !lines[lines.length - 1]!.trim()) lines.pop();
|
|
86
|
+
return lines.join('\n').trim();
|
|
87
|
+
}
|
|
145
88
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
89
|
+
const VALUE_WRAPPERS = new Set([
|
|
90
|
+
'await_expression', 'try_expression', 'parenthesized_expression', 'non_null_expression', 'as_expression',
|
|
91
|
+
'expression_list', 'equals_value_clause', 'unary_expression', 'reference_expression', 'type_cast_expression',
|
|
92
|
+
]);
|
|
93
|
+
const BINDING_NODES = new Set([
|
|
94
|
+
'variable_declarator', 'assignment_expression', 'assignment', 'short_var_declaration', 'let_declaration',
|
|
95
|
+
'init_declarator', 'var_spec', 'local_variable_declaration', 'const_spec', 'augmented_assignment',
|
|
96
|
+
'property_declaration', 'initialized_variable_definition', // Swift, Dart
|
|
97
|
+
]);
|
|
150
98
|
|
|
151
|
-
|
|
99
|
+
/**
|
|
100
|
+
* If `call` is the whole right-hand side of a simple variable binding
|
|
101
|
+
* (`x = f()`, `let x = f()?`, `x, err := f()`, `auto x = f()`), the name of
|
|
102
|
+
* that variable; otherwise null. Works across grammars by looking at the
|
|
103
|
+
* usual field names.
|
|
104
|
+
*/
|
|
105
|
+
function boundVariable(call: Node): string | null {
|
|
106
|
+
let node: Node = call;
|
|
107
|
+
let p = call.parent;
|
|
108
|
+
for (let i = 0; p && i < 3 && VALUE_WRAPPERS.has(p.type); i++) {
|
|
109
|
+
node = p;
|
|
110
|
+
p = p.parent;
|
|
111
|
+
}
|
|
112
|
+
if (!p || !BINDING_NODES.has(p.type)) return null;
|
|
113
|
+
const target =
|
|
114
|
+
p.childForFieldName('name') ?? p.childForFieldName('left') ?? p.childForFieldName('pattern') ??
|
|
115
|
+
p.childForFieldName('declarator');
|
|
116
|
+
if (!target || target.endIndex > node.startIndex) return null;
|
|
117
|
+
// Go: `x, err := f()` - the first name receives the first result.
|
|
118
|
+
const first = target.type === 'expression_list' ? target.namedChild(0) : target;
|
|
119
|
+
if (!first || first.endIndex - first.startIndex > 60) return null;
|
|
120
|
+
const text = first.text.replace(/^[$@*&]+/, '').replace(/^mut\s+/, '');
|
|
121
|
+
return /^[A-Za-z_]\w*$/.test(text) ? text : null;
|
|
152
122
|
}
|
|
153
123
|
|
|
154
|
-
|
|
155
|
-
const body = node.childForFieldName('body');
|
|
156
|
-
if (!body) return null;
|
|
124
|
+
const ARGUMENT_LISTS = new Set(['argument_list', 'arguments', 'value_arguments']);
|
|
157
125
|
|
|
158
|
-
|
|
159
|
-
|
|
126
|
+
/** Languages where a bare name inside a method can be a field of the enclosing class. */
|
|
127
|
+
const IMPLICIT_THIS_SPECS = new Set(['java', 'csharp', 'cpp', 'c', 'swift', 'dart', 'kotlin']);
|
|
160
128
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
|
|
129
|
+
/** Number of arguments of a call node, or -1 when it cannot be counted (spread, no argument list). */
|
|
130
|
+
function argCount(node: Node): number {
|
|
131
|
+
// Flat grammars (Dart) hand in synthetic call spans that are not real nodes.
|
|
132
|
+
if (typeof node.childForFieldName !== 'function' || !Array.isArray(node.namedChildren)) return -1;
|
|
133
|
+
let args = node.childForFieldName('arguments');
|
|
134
|
+
if (!args) {
|
|
135
|
+
for (const c of node.namedChildren) {
|
|
136
|
+
if (c && ARGUMENT_LISTS.has(c.type)) { args = c; break; }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!args || !ARGUMENT_LISTS.has(args.type)) return -1;
|
|
140
|
+
let n = 0;
|
|
141
|
+
for (const c of args.namedChildren) {
|
|
142
|
+
if (!c || c.type.includes('comment')) continue;
|
|
143
|
+
// f(*args), f(...xs), f(xs...): the count is not known statically.
|
|
144
|
+
if (/splat|spread|variadic/.test(c.type)) return -1;
|
|
145
|
+
n++;
|
|
167
146
|
}
|
|
147
|
+
return Math.min(n, 99);
|
|
148
|
+
}
|
|
168
149
|
|
|
169
|
-
|
|
150
|
+
function squash(s: string, max: number): string {
|
|
151
|
+
const t = s.replace(/\s+/g, ' ').trim();
|
|
152
|
+
return t.length > max ? t.slice(0, max - 3) + '...' : t;
|
|
170
153
|
}
|
|
171
154
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Walk a parsed tree and extract symbols, calls, imports, heritage and
|
|
157
|
+
* identifier occurrences according to the language spec.
|
|
158
|
+
*/
|
|
159
|
+
export function extract(tree: Tree, source: string, compiled: CompiledSpec): Extraction {
|
|
160
|
+
const { visitors, isIdent, isComment, isDocSkip } = compiled;
|
|
161
|
+
|
|
162
|
+
const symbols: ExSymbol[] = [];
|
|
163
|
+
const imports: ExImport[] = [];
|
|
164
|
+
const heritage: ExHeritage[] = [];
|
|
165
|
+
const callMap = new Map<string, ExCall>();
|
|
166
|
+
const occ = new Map<string, number[]>();
|
|
167
|
+
const scopes: Scope[] = [];
|
|
168
|
+
const containerDefs = new Map<string, number>();
|
|
169
|
+
/** Outer calls of a chain waiting for the call that produces their receiver, by start offset. */
|
|
170
|
+
const chainOuters = new Map<number, Array<{ call: ExCall; end: number }>>();
|
|
171
|
+
const viaRef = new Map<ExCall, ExCall>();
|
|
172
|
+
/** Variables initialised from a call: owner symbol index -> variable name -> that call. */
|
|
173
|
+
const callHints = new Map<number, Map<string, ExCall>>();
|
|
174
|
+
let defaultExport: string | null = null;
|
|
175
|
+
/** Declared types: owner symbol index (-1 = file) -> variable/field name -> type name. */
|
|
176
|
+
const hints = new Map<number, Map<string, string>>();
|
|
177
|
+
|
|
178
|
+
function addOcc(name: string, line: number): void {
|
|
179
|
+
const lines = occ.get(name);
|
|
180
|
+
if (!lines) occ.set(name, [line]);
|
|
181
|
+
else if (lines.length < MAX_OCC_LINES && lines[lines.length - 1] !== line) lines.push(line);
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
function currentCaller(): number {
|
|
185
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
186
|
+
const s = scopes[i]!;
|
|
187
|
+
if (s.sym >= 0 && s.callScope) return s.sym;
|
|
188
|
+
}
|
|
189
|
+
return -1;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const nodes: AstNode[] = [];
|
|
208
|
-
const edges: Array<{ callerNodeIndex: number; calleeName: string; line: number }> = [];
|
|
209
|
-
const comments: Array<{ nodeIndex: number; text: string; source: 'original' }> = [];
|
|
210
|
-
|
|
211
|
-
const nodeIdToIndex = new Map<number, number>();
|
|
212
|
-
|
|
213
|
-
function findEnclosingFunctionIndex(node: TreeSitter.SyntaxNode): number | null {
|
|
214
|
-
let current = node.parent;
|
|
215
|
-
while (current) {
|
|
216
|
-
const type = classifyNodeType(current.type);
|
|
217
|
-
if (type === 'function') {
|
|
218
|
-
return nodeIdToIndex.get(current.id) ?? null;
|
|
192
|
+
function leadingDoc(range: Node): { doc: string; line: number; returns: string | null } {
|
|
193
|
+
const parts: string[] = [];
|
|
194
|
+
let line = 0;
|
|
195
|
+
let nextRow = range.startPosition.row;
|
|
196
|
+
let prev = range.previousSibling;
|
|
197
|
+
if (!prev) {
|
|
198
|
+
// First node of a wrapper (e.g. Ruby body_statement): the comment sits before the wrapper.
|
|
199
|
+
const p = range.parent;
|
|
200
|
+
if (p && p.startIndex === range.startIndex) prev = p.previousSibling;
|
|
201
|
+
}
|
|
202
|
+
while (prev) {
|
|
203
|
+
if (isDocSkip[prev.typeId]) {
|
|
204
|
+
// Attributes / annotations between the doc comment and the item.
|
|
205
|
+
nextRow = prev.startPosition.row;
|
|
206
|
+
prev = prev.previousSibling;
|
|
207
|
+
continue;
|
|
219
208
|
}
|
|
220
|
-
|
|
209
|
+
if (!isComment[prev.typeId]) break;
|
|
210
|
+
if (prev.endPosition.row < nextRow - 1) break;
|
|
211
|
+
// A comment trailing a statement on the same line documents that statement.
|
|
212
|
+
// (Preprocessor nodes end at column 0 of the next line: not trailing.)
|
|
213
|
+
const before = prev.previousSibling;
|
|
214
|
+
if (
|
|
215
|
+
before && !isComment[before.typeId] &&
|
|
216
|
+
before.endPosition.row === prev.startPosition.row && before.endPosition.column > 0
|
|
217
|
+
) break;
|
|
218
|
+
parts.unshift(prev.text);
|
|
219
|
+
nextRow = prev.startPosition.row;
|
|
220
|
+
line = nextRow + 1;
|
|
221
|
+
if (parts.length >= 200) break;
|
|
222
|
+
prev = prev.previousSibling;
|
|
221
223
|
}
|
|
222
|
-
return null;
|
|
224
|
+
if (parts.length === 0) return { doc: '', line: 0, returns: null };
|
|
225
|
+
let doc = parts.map(cleanComment).join('\n').trim();
|
|
226
|
+
// Documented return type (PHPDoc / JSDoc): taken before the text is truncated.
|
|
227
|
+
const returns = /@returns?\s+\{?([^\s}|]+)/.exec(doc)?.[1] ?? null;
|
|
228
|
+
if (doc.length > MAX_DOC) doc = doc.slice(0, MAX_DOC - 3) + '...';
|
|
229
|
+
return { doc, line, returns };
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
const ctx: Ctx = {
|
|
233
|
+
src: source,
|
|
234
|
+
|
|
235
|
+
def(node: Node, o: DefOpts): number {
|
|
236
|
+
let name = o.name;
|
|
237
|
+
if (!name) name = '<anonymous>';
|
|
238
|
+
if (name.length > MAX_NAME) name = name.slice(0, MAX_NAME);
|
|
239
|
+
const range = o.range ?? node;
|
|
240
|
+
|
|
241
|
+
const prefix: string[] = [];
|
|
242
|
+
let parent = -1;
|
|
243
|
+
let depth = scopes.length;
|
|
244
|
+
if (o.inContainer) {
|
|
245
|
+
// Attach to the nearest enclosing class-like scope instead of the method we are in.
|
|
246
|
+
depth = 0;
|
|
247
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
248
|
+
const k = scopes[i]!.kind;
|
|
249
|
+
if (k !== null && CONTAINER_KINDS.has(k)) { depth = i + 1; break; }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (let i = 0; i < depth; i++) {
|
|
253
|
+
prefix.push(scopes[i]!.name);
|
|
254
|
+
}
|
|
255
|
+
for (let i = depth - 1; i >= 0; i--) {
|
|
256
|
+
if (scopes[i]!.sym >= 0) { parent = scopes[i]!.sym; break; }
|
|
257
|
+
}
|
|
258
|
+
if (o.qualifier) prefix.push(...o.qualifier.split(/::|\./).filter(Boolean));
|
|
259
|
+
const qname = prefix.length ? prefix.join('.') + '.' + name : name;
|
|
260
|
+
|
|
261
|
+
let signature: string;
|
|
262
|
+
if (o.sig !== undefined) {
|
|
263
|
+
signature = squash(o.sig, MAX_SIGNATURE).replace(/\s*\{$/, '');
|
|
264
|
+
} else {
|
|
265
|
+
const body = o.body === undefined ? node.childForFieldName('body') : o.body;
|
|
266
|
+
const start = range.startIndex;
|
|
267
|
+
let end: number;
|
|
268
|
+
if (body && body.startIndex > start) {
|
|
269
|
+
end = body.startIndex;
|
|
270
|
+
} else {
|
|
271
|
+
end = range.endIndex;
|
|
272
|
+
// Without a body: short declarations in full, long ones (object
|
|
273
|
+
// literals, tables) only up to the end of their first line.
|
|
274
|
+
if (end - start > 200) {
|
|
275
|
+
const nl = source.indexOf('\n', start);
|
|
276
|
+
end = nl !== -1 && nl < end ? nl : Math.min(end, start + 200);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
signature = squash(source.slice(start, end), MAX_SIGNATURE).replace(/\s*[{=]$/, '');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let doc = '';
|
|
283
|
+
let docLine = 0;
|
|
284
|
+
if (o.doc !== undefined && o.doc !== null) {
|
|
285
|
+
doc = o.doc.length > MAX_DOC ? o.doc.slice(0, MAX_DOC - 3) + '...' : o.doc;
|
|
286
|
+
}
|
|
287
|
+
const lead = leadingDoc(range);
|
|
288
|
+
if (lead.line) {
|
|
289
|
+
docLine = lead.line;
|
|
290
|
+
if (!doc) doc = lead.doc;
|
|
291
|
+
}
|
|
228
292
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
293
|
+
if (o.inContainer) {
|
|
294
|
+
// Instance attributes are assigned many times; keep the first.
|
|
295
|
+
const key = o.kind + ' ' + qname;
|
|
296
|
+
const seen = containerDefs.get(key);
|
|
297
|
+
if (seen !== undefined) return seen;
|
|
298
|
+
containerDefs.set(key, symbols.length);
|
|
299
|
+
}
|
|
235
300
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
signature,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
currentIndex = nodes.length;
|
|
251
|
-
nodes.push(astNode);
|
|
252
|
-
nodeIdToIndex.set(node.id, currentIndex);
|
|
253
|
-
|
|
254
|
-
const comment = language === 'python'
|
|
255
|
-
? (extractPythonDocstring(node) ?? extractPrecedingComment(node, source))
|
|
256
|
-
: extractPrecedingComment(node, source);
|
|
257
|
-
|
|
258
|
-
if (comment) {
|
|
259
|
-
comments.push({ nodeIndex: currentIndex, text: comment, source: 'original' });
|
|
301
|
+
let ret: string | null = null;
|
|
302
|
+
if (o.kind === 'function' || o.kind === 'method' || o.kind === 'declaration') {
|
|
303
|
+
// Enclosing class for Self / static / this return types: the last qualifier segment.
|
|
304
|
+
const selfType = prefix.length ? prefix[prefix.length - 1]!.split('.').pop()! : null;
|
|
305
|
+
ret = returnTypeOf(signature, name, compiled.spec.id, selfType, compiled.spec.lowercaseTypes === true);
|
|
306
|
+
if (!ret) {
|
|
307
|
+
// No declared type: fall back to the documented one (`@return $this`, `@returns {Foo}`).
|
|
308
|
+
const documented = lead.returns ?? /@returns?\s+\{?([^\s}|]+)/.exec(doc)?.[1] ?? null;
|
|
309
|
+
if (documented) {
|
|
310
|
+
ret = /^\\?(\$this|static|self|this)$/.test(documented)
|
|
311
|
+
? selfType
|
|
312
|
+
: typeName(documented, compiled.spec.lowercaseTypes === true);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
260
315
|
}
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
filePath,
|
|
316
|
+
|
|
317
|
+
const index = symbols.length;
|
|
318
|
+
symbols.push({
|
|
265
319
|
name,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
320
|
+
qname,
|
|
321
|
+
kind: o.kind,
|
|
322
|
+
ret,
|
|
323
|
+
signature,
|
|
324
|
+
doc,
|
|
325
|
+
startLine: range.startPosition.row + 1,
|
|
326
|
+
// Nodes that swallow the trailing newline (preprocessor) end at column 0 of the next row.
|
|
327
|
+
endLine:
|
|
328
|
+
range.endPosition.column === 0 && range.endPosition.row > range.startPosition.row
|
|
329
|
+
? range.endPosition.row
|
|
330
|
+
: range.endPosition.row + 1,
|
|
331
|
+
startCol: range.startPosition.column,
|
|
332
|
+
docLine,
|
|
333
|
+
parent,
|
|
334
|
+
exported: o.exported ?? false,
|
|
275
335
|
});
|
|
336
|
+
if (o.scope !== false) {
|
|
337
|
+
// The qualifier is part of the scope so nested definitions inherit it.
|
|
338
|
+
const scopeName = o.qualifier ? o.qualifier.split(/::|\./).filter(Boolean).concat(name).join('.') : name;
|
|
339
|
+
scopes.push({ sym: index, name: scopeName, kind: o.kind, callScope: true });
|
|
340
|
+
}
|
|
341
|
+
return index;
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
scope(name: string): void {
|
|
345
|
+
if (!name) return;
|
|
346
|
+
scopes.push({ sym: -1, name, kind: null, callScope: false });
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
exportDefault(name): void {
|
|
350
|
+
// '*' = "whatever the required module exports" (`module.exports = require('./lib/x')`).
|
|
351
|
+
if (name === '*') defaultExport ??= '*';
|
|
352
|
+
else if (name && name.length <= MAX_NAME && /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(name)) defaultExport = name;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
ref(name, node): void {
|
|
356
|
+
if (!name || name.length < 2 || name.length > MAX_NAME) return;
|
|
357
|
+
addOcc(name, node.startPosition.row + 1);
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
typeHint(name, type, onContainer = false): void {
|
|
361
|
+
if (!name || name.length > MAX_NAME) return;
|
|
362
|
+
const t = typeName(type, compiled.spec.lowercaseTypes === true, compiled.spec.packageTypes === true);
|
|
363
|
+
if (!t) return;
|
|
364
|
+
// Owner: nearest enclosing function/method, else nearest container, else the file.
|
|
365
|
+
let owner = -1;
|
|
366
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
367
|
+
const s = scopes[i]!;
|
|
368
|
+
if (s.sym < 0 || s.kind === null) continue;
|
|
369
|
+
const isContainer = CONTAINER_KINDS.has(s.kind);
|
|
370
|
+
if (isContainer || (!onContainer && (s.kind === 'function' || s.kind === 'method'))) { owner = s.sym; break; }
|
|
371
|
+
}
|
|
372
|
+
let map = hints.get(owner);
|
|
373
|
+
if (!map) hints.set(owner, (map = new Map()));
|
|
374
|
+
if (!map.has(name)) map.set(name, t);
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
typeOf(name): string | null {
|
|
378
|
+
if (!name) return null;
|
|
379
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
380
|
+
const s = scopes[i]!;
|
|
381
|
+
// Class-level hints are fields (reached through this / self), not variables in scope.
|
|
382
|
+
if (s.sym < 0 || (s.kind !== 'function' && s.kind !== 'method')) continue;
|
|
383
|
+
const hit = hints.get(s.sym)?.get(name);
|
|
384
|
+
if (hit) return hit;
|
|
385
|
+
}
|
|
386
|
+
return hints.get(-1)?.get(name) ?? null;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
call(name, node, receiver = null, receiverType = null): void {
|
|
390
|
+
if (!name || name.length > MAX_NAME) return;
|
|
391
|
+
if (!/^[\p{L}_$~][\p{L}\p{N}_$!?]*$/u.test(name)) return;
|
|
392
|
+
const caller = currentCaller();
|
|
393
|
+
const start = node.startIndex;
|
|
394
|
+
const end = node.endIndex;
|
|
395
|
+
const argc = argCount(node);
|
|
396
|
+
// Calls on expression receivers are kept apart per site until the chain is known (merged below).
|
|
397
|
+
const key = caller + '\0' + name + '\0' + (receiver === '?' ? '?' + start + ':' + end : receiver ?? '') + '\0' + argc;
|
|
398
|
+
let rec = callMap.get(key);
|
|
399
|
+
if (rec) {
|
|
400
|
+
rec.count++;
|
|
401
|
+
} else {
|
|
402
|
+
const rtype = receiverType
|
|
403
|
+
? typeName(receiverType, compiled.spec.lowercaseTypes === true, compiled.spec.packageTypes === true)
|
|
404
|
+
: null;
|
|
405
|
+
// A chained call spanning several lines starts at the head of the chain; report the
|
|
406
|
+
// line where this call's own argument list opens (`->then(`), which is where a reader looks.
|
|
407
|
+
let row = node.startPosition.row;
|
|
408
|
+
if (node.endPosition.row !== row) {
|
|
409
|
+
const args = node.childForFieldName('arguments');
|
|
410
|
+
if (args) row = args.startPosition.row;
|
|
411
|
+
}
|
|
412
|
+
rec = { caller, name, line: row + 1, receiver, rtype, argc, count: 1, via: -1 };
|
|
413
|
+
callMap.set(key, rec);
|
|
414
|
+
}
|
|
415
|
+
// Call chains: in `a.b().c()` the expression for c starts where the expression for b starts,
|
|
416
|
+
// and the walker reaches c first. Link each outer call to the call that produces its receiver.
|
|
417
|
+
const outers = chainOuters.get(start);
|
|
418
|
+
if (outers) {
|
|
419
|
+
for (const o of outers) {
|
|
420
|
+
if (o.end > end && !viaRef.has(o.call) && o.call !== rec) viaRef.set(o.call, rec);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (receiver === '?' && !rec.rtype) {
|
|
424
|
+
if (outers) outers.push({ call: rec, end });
|
|
425
|
+
else chainOuters.set(start, [{ call: rec, end }]);
|
|
426
|
+
}
|
|
427
|
+
// `x = make()`, `let x = Foo::new()?`, `x, err := load()`: x gets the call's return type.
|
|
428
|
+
const bound = boundVariable(node);
|
|
429
|
+
if (bound) {
|
|
430
|
+
let map = callHints.get(caller);
|
|
431
|
+
if (!map) callHints.set(caller, (map = new Map()));
|
|
432
|
+
if (!map.has(bound)) map.set(bound, rec);
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
imp(src, node, names = []): void {
|
|
437
|
+
if (!src) return;
|
|
438
|
+
imports.push({ source: src.slice(0, 300), names: names.slice(0, 50), line: node.startPosition.row + 1 });
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
inherit(sym, name, kind = 'extends', arity?: number): void {
|
|
442
|
+
if (!name || sym < 0) return;
|
|
443
|
+
const clean = name.trim().replace(/<.*$/s, '');
|
|
444
|
+
if (arity === undefined) arity = genericArity(name);
|
|
445
|
+
if (!clean || clean.length > MAX_NAME) return;
|
|
446
|
+
// `pkg.mod.Base` / `ns::Base`: index by the bare name, keep the qualifier for resolution.
|
|
447
|
+
const parts = clean.split(/\.|::|\\/).filter(Boolean);
|
|
448
|
+
const bare = parts.pop();
|
|
449
|
+
if (!bare) return;
|
|
450
|
+
const qualifier = parts.length ? parts[parts.length - 1]! : null;
|
|
451
|
+
// (`include Thor::Actions` and `include Rails::Generators::Actions` are two different supertypes.)
|
|
452
|
+
if (heritage.some(h => h.symbol === sym && h.name === bare && (h.qualifier ?? null) === (qualifier ?? null))) return;
|
|
453
|
+
heritage.push({ symbol: sym, name: bare, qualifier, kind, arity });
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
parentKind(): SymbolKind | null {
|
|
457
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
458
|
+
if (scopes[i]!.sym >= 0) return scopes[i]!.kind;
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
inCallable(): boolean {
|
|
464
|
+
for (const s of scopes) {
|
|
465
|
+
if (s.kind === 'function' || s.kind === 'method') return true;
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
depth(): number {
|
|
471
|
+
let d = 0;
|
|
472
|
+
for (const s of scopes) if (s.sym >= 0) d++;
|
|
473
|
+
return d;
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const cursor = tree.walk();
|
|
478
|
+
let hadError = tree.rootNode.hasError;
|
|
479
|
+
|
|
480
|
+
// Iterative pre-order walk (no recursion: generated files nest tens of thousands deep).
|
|
481
|
+
// `open` holds, for every node whose children are being visited, the scope depth to restore on leaving it.
|
|
482
|
+
const open: number[] = [];
|
|
483
|
+
try {
|
|
484
|
+
let entering = true;
|
|
485
|
+
for (;;) {
|
|
486
|
+
if (entering) {
|
|
487
|
+
const typeId = cursor.nodeTypeId;
|
|
488
|
+
if (isIdent[typeId]) {
|
|
489
|
+
const name = source.slice(cursor.startIndex, cursor.endIndex);
|
|
490
|
+
if (name.length >= 2 && name.length <= MAX_NAME) {
|
|
491
|
+
addOcc(name, cursor.startPosition.row + 1);
|
|
492
|
+
}
|
|
493
|
+
// Identifiers are leaves in every supported grammar, but a few grammars
|
|
494
|
+
// wrap them (e.g. PHP `name`); fall through to children only if any.
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const before = scopes.length;
|
|
498
|
+
const visitor = visitors[typeId];
|
|
499
|
+
let skip = false;
|
|
500
|
+
if (visitor) {
|
|
501
|
+
try {
|
|
502
|
+
if (visitor(cursor.currentNode, ctx) === 'skip') skip = true;
|
|
503
|
+
} catch {
|
|
504
|
+
// A visitor bug must never take down indexing of the whole file.
|
|
505
|
+
hadError = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!skip && cursor.gotoFirstChild()) {
|
|
510
|
+
open.push(before);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
scopes.length = before; // leaf (or skipped subtree): its scope closes right away
|
|
514
|
+
}
|
|
515
|
+
if (cursor.gotoNextSibling()) {
|
|
516
|
+
entering = true;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (open.length === 0) break;
|
|
520
|
+
cursor.gotoParent();
|
|
521
|
+
scopes.length = open.pop()!;
|
|
522
|
+
entering = false;
|
|
276
523
|
}
|
|
524
|
+
} finally {
|
|
525
|
+
cursor.delete();
|
|
526
|
+
}
|
|
277
527
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
528
|
+
// Resolve receivers to declared types: `x.foo()` with `x: Foo` in scope,
|
|
529
|
+
// `this.repo.find()` with a typed `repo` field on the enclosing class,
|
|
530
|
+
// `s.pool.Get()` where `s` is a typed receiver/parameter whose class is in this file.
|
|
531
|
+
if (hints.size) {
|
|
532
|
+
const SELF = new Set(['this', 'self', '$this', 'base', 'Self', 'super']);
|
|
533
|
+
let containerByName: Map<string, number> | null = null;
|
|
534
|
+
const containerNamed = (name: string): number | undefined => {
|
|
535
|
+
if (!containerByName) {
|
|
536
|
+
containerByName = new Map();
|
|
537
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
538
|
+
const s = symbols[i]!;
|
|
539
|
+
if (CONTAINER_KINDS.has(s.kind) && !containerByName.has(s.name)) containerByName.set(s.name, i);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return containerByName.get(name);
|
|
543
|
+
};
|
|
544
|
+
/** Declared type of `name` visible from symbol `owner` (walks enclosing symbols, then the file). */
|
|
545
|
+
// Only where members can be named without `this.` is a class field in scope for a bare name:
|
|
546
|
+
// in TypeScript or Python a local `targets` is not the field `this.targets`.
|
|
547
|
+
const implicitThis = IMPLICIT_THIS_SPECS.has(compiled.spec.id);
|
|
548
|
+
const lookup = (owner: number, name: string): string | undefined => {
|
|
549
|
+
for (let cur = owner, guard = 0; guard < 12; guard++) {
|
|
550
|
+
const skip = !implicitThis && cur >= 0 && CONTAINER_KINDS.has(symbols[cur]!.kind);
|
|
551
|
+
const hit = skip ? undefined : hints.get(cur)?.get(name);
|
|
552
|
+
if (hit) return hit;
|
|
553
|
+
if (cur < 0) return undefined;
|
|
554
|
+
cur = symbols[cur]!.parent;
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
};
|
|
558
|
+
/** Class-like symbol a method belongs to: lexical parent, or by qualified name (Go receivers, Rust impl, C++ out-of-line). */
|
|
559
|
+
const containerOf = (sym: number): number | undefined => {
|
|
560
|
+
for (let cur = sym; cur >= 0; cur = symbols[cur]!.parent) {
|
|
561
|
+
if (CONTAINER_KINDS.has(symbols[cur]!.kind)) return cur;
|
|
562
|
+
}
|
|
563
|
+
if (sym < 0) return undefined;
|
|
564
|
+
const parts = symbols[sym]!.qname.split('.');
|
|
565
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
566
|
+
const hit = containerNamed(parts[i]!);
|
|
567
|
+
if (hit !== undefined) return hit;
|
|
288
568
|
}
|
|
569
|
+
return undefined;
|
|
570
|
+
};
|
|
571
|
+
const fieldType = (container: number | undefined, name: string): string | undefined =>
|
|
572
|
+
container === undefined ? undefined : hints.get(container)?.get(name);
|
|
573
|
+
|
|
574
|
+
for (const call of callMap.values()) {
|
|
575
|
+
const r = call.receiver;
|
|
576
|
+
if (!r || r === '?' || call.rtype) continue;
|
|
577
|
+
const parts = r.replace(/->|::/g, '.').split('.');
|
|
578
|
+
if (parts.length > 2 || parts.some(p => !/^[$@]?[A-Za-z_]\w*$/.test(p))) continue;
|
|
579
|
+
let t: string | undefined;
|
|
580
|
+
if (parts.length === 1) {
|
|
581
|
+
const p = parts[0]!;
|
|
582
|
+
if (SELF.has(p)) continue;
|
|
583
|
+
// Ruby @ivar is a field of the enclosing class; anything else is a local / parameter.
|
|
584
|
+
t = p.startsWith('@')
|
|
585
|
+
? fieldType(containerOf(call.caller), p.slice(1))
|
|
586
|
+
: lookup(call.caller, p.replace(/^\$/, ''));
|
|
587
|
+
} else {
|
|
588
|
+
const [a, b] = parts as [string, string];
|
|
589
|
+
const field = b.replace(/^[$@]/, '');
|
|
590
|
+
if (SELF.has(a)) {
|
|
591
|
+
t = fieldType(containerOf(call.caller), field);
|
|
592
|
+
} else {
|
|
593
|
+
const ta = lookup(call.caller, a.replace(/^[$@]/, ''));
|
|
594
|
+
if (ta) {
|
|
595
|
+
t = fieldType(containerNamed(ta), field);
|
|
596
|
+
// `opts.logger.info()` with `opts: Options` declared in another file: the type of the
|
|
597
|
+
// field is not known here. Leave "Options#logger" for the resolver, which sees every file.
|
|
598
|
+
if (!t && containerNamed(ta) === undefined) t = ta + '#' + field;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (t) call.rtype = t;
|
|
289
603
|
}
|
|
604
|
+
}
|
|
605
|
+
// Receivers that are variables initialised from a call take that call's return type
|
|
606
|
+
// (resolved at query time through the same link as call chains).
|
|
607
|
+
if (callHints.size) {
|
|
608
|
+
for (const call of callMap.values()) {
|
|
609
|
+
const r = call.receiver;
|
|
610
|
+
if (!r || r === '?' || call.rtype || viaRef.has(call)) continue;
|
|
611
|
+
if (!/^[$@]?[A-Za-z_]\w*$/.test(r)) continue;
|
|
612
|
+
const name = r.replace(/^[$@]/, '');
|
|
613
|
+
for (let cur = call.caller, guard = 0; guard < 6; guard++) {
|
|
614
|
+
const hit = callHints.get(cur)?.get(name);
|
|
615
|
+
if (hit && hit !== call) {
|
|
616
|
+
viaRef.set(call, hit);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (cur < 0) break;
|
|
620
|
+
const s = symbols[cur]!;
|
|
621
|
+
// Stop at the function boundary: locals of an enclosing class / file are not visible by bare name.
|
|
622
|
+
if (s.kind !== 'function' && s.kind !== 'method') break;
|
|
623
|
+
cur = s.parent;
|
|
624
|
+
if (cur >= 0 && symbols[cur]!.kind !== 'function' && symbols[cur]!.kind !== 'method') break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
290
628
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
629
|
+
// Merge chained calls that are the same edge (`x.a().b()` twice in one function), then
|
|
630
|
+
// express receiver links as indexes into the final list.
|
|
631
|
+
const calls: ExCall[] = [];
|
|
632
|
+
{
|
|
633
|
+
const kept = new Map<string, ExCall>();
|
|
634
|
+
const redirect = new Map<ExCall, ExCall>();
|
|
635
|
+
for (const call of callMap.values()) {
|
|
636
|
+
if (call.receiver !== '?') {
|
|
637
|
+
calls.push(call);
|
|
638
|
+
continue;
|
|
296
639
|
}
|
|
640
|
+
const inner = viaRef.get(call);
|
|
641
|
+
const key =
|
|
642
|
+
call.caller + '\0' + call.name + '\0' + (inner ? inner.name + '\0' + (inner.receiver ?? '') : '') + '\0' +
|
|
643
|
+
(call.rtype ?? '') + '\0' + call.argc;
|
|
644
|
+
const first = kept.get(key);
|
|
645
|
+
if (first) {
|
|
646
|
+
first.count += call.count;
|
|
647
|
+
redirect.set(call, first);
|
|
648
|
+
} else {
|
|
649
|
+
kept.set(key, call);
|
|
650
|
+
calls.push(call);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const indexOf = new Map<ExCall, number>();
|
|
654
|
+
calls.forEach((c, i) => indexOf.set(c, i));
|
|
655
|
+
for (const [outer, innerRaw] of viaRef) {
|
|
656
|
+
if (redirect.has(outer)) continue;
|
|
657
|
+
const inner = redirect.get(innerRaw) ?? innerRaw;
|
|
658
|
+
const i = indexOf.get(inner);
|
|
659
|
+
if (i !== undefined) outer.via = i;
|
|
297
660
|
}
|
|
298
661
|
}
|
|
299
662
|
|
|
300
|
-
|
|
663
|
+
// Field types of class-like symbols are exported so the resolver can type
|
|
664
|
+
// `this.x.foo()` / `x_->foo()` when the class lives in another file
|
|
665
|
+
// (C++ headers, Go structs in the same package, partial classes).
|
|
666
|
+
const fieldTypes: Array<[string, string, string]> = [];
|
|
667
|
+
for (const [owner, map] of hints) {
|
|
668
|
+
if (owner < 0 || !CONTAINER_KINDS.has(symbols[owner]!.kind)) continue;
|
|
669
|
+
const container = symbols[owner]!.name;
|
|
670
|
+
for (const [field, type] of map) {
|
|
671
|
+
if (fieldTypes.length < 4000) fieldTypes.push([container, field, type]);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
301
674
|
|
|
302
|
-
|
|
675
|
+
let lineCount = 1;
|
|
676
|
+
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) lineCount++;
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
lang: compiled.spec.id,
|
|
680
|
+
lineCount,
|
|
681
|
+
symbols,
|
|
682
|
+
calls,
|
|
683
|
+
imports,
|
|
684
|
+
heritage,
|
|
685
|
+
fieldTypes,
|
|
686
|
+
defaultExport,
|
|
687
|
+
occ: [...occ.entries()].map(([name, lines]) => [name, lines.join(',')] as [string, string]),
|
|
688
|
+
hadError,
|
|
689
|
+
};
|
|
303
690
|
}
|