lattice-graph 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -25
- package/package.json +8 -9
- package/scripts/postinstall.ts +69 -0
- package/src/commands/build.ts +44 -173
- package/src/commands/init.ts +39 -17
- package/src/commands/lint.ts +145 -53
- package/src/commands/populate.ts +14 -3
- package/src/commands/update.ts +75 -137
- package/src/config.ts +0 -2
- package/src/extract/tag-scanner.ts +94 -0
- package/src/files.ts +56 -0
- package/src/graph/database.ts +6 -8
- package/src/graph/writer.ts +17 -21
- package/src/lsp/builder.ts +248 -0
- package/src/lsp/calls.ts +84 -0
- package/src/lsp/client.ts +211 -0
- package/src/lsp/symbols.ts +146 -0
- package/src/lsp/types.ts +73 -0
- package/src/main.ts +2 -18
- package/src/types/config.ts +0 -2
- package/src/types/graph.ts +6 -34
- package/src/types/lint.ts +0 -1
- package/src/extract/extractor.ts +0 -13
- package/src/extract/parser.ts +0 -117
- package/src/extract/python/calls.ts +0 -121
- package/src/extract/python/extractor.ts +0 -171
- package/src/extract/python/frameworks.ts +0 -142
- package/src/extract/python/imports.ts +0 -115
- package/src/extract/python/symbols.ts +0 -121
- package/src/extract/tags.ts +0 -77
- package/src/extract/typescript/calls.ts +0 -110
- package/src/extract/typescript/extractor.ts +0 -130
- package/src/extract/typescript/imports.ts +0 -71
- package/src/extract/typescript/symbols.ts +0 -252
package/src/extract/tags.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { TagKind } from "../types/graph.ts";
|
|
2
|
-
import { err, ok, type Result } from "../types/result.ts";
|
|
3
|
-
|
|
4
|
-
/** A parsed tag before it's associated with a specific node ID. */
|
|
5
|
-
type ParsedTag = {
|
|
6
|
-
readonly kind: TagKind;
|
|
7
|
-
readonly value: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const VALID_TAG_KINDS = new Set<string>(["flow", "boundary", "emits", "handles"]);
|
|
11
|
-
|
|
12
|
-
/** Matches @lattice:<kind> <value> at the START of a stripped comment line. */
|
|
13
|
-
const TAG_PATTERN = /^@lattice:(\w+)\s+(.+)/;
|
|
14
|
-
|
|
15
|
-
/** Valid tag name: lowercase letters, numbers, hyphens, dots. Must start with a letter or number. */
|
|
16
|
-
const NAME_PATTERN = /^[a-z0-9][a-z0-9\-.]*$/;
|
|
17
|
-
|
|
18
|
-
/** Strips common comment prefixes from a line. */
|
|
19
|
-
function stripCommentPrefix(line: string): string {
|
|
20
|
-
const trimmed = line.trim();
|
|
21
|
-
// Try each prefix in order
|
|
22
|
-
if (trimmed.startsWith("//")) return trimmed.slice(2).trim();
|
|
23
|
-
if (trimmed.startsWith("#")) return trimmed.slice(1).trim();
|
|
24
|
-
if (trimmed.startsWith("--")) return trimmed.slice(2).trim();
|
|
25
|
-
if (trimmed.startsWith("/*"))
|
|
26
|
-
return trimmed
|
|
27
|
-
.slice(2)
|
|
28
|
-
.replace(/\*\/\s*$/, "")
|
|
29
|
-
.trim();
|
|
30
|
-
return trimmed;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parses lattice tags from a comment block.
|
|
35
|
-
* Recognizes any comment style: #, //, /∗ ∗/, --.
|
|
36
|
-
* Returns parsed tags or an error for invalid tag name syntax.
|
|
37
|
-
*
|
|
38
|
-
* @param commentBlock - Raw comment text, possibly multiline
|
|
39
|
-
* @returns Parsed tags or an error message
|
|
40
|
-
*/
|
|
41
|
-
function parseTags(commentBlock: string): Result<readonly ParsedTag[], string> {
|
|
42
|
-
if (!commentBlock.trim()) return ok([]);
|
|
43
|
-
|
|
44
|
-
const tags: ParsedTag[] = [];
|
|
45
|
-
const lines = commentBlock.split("\n");
|
|
46
|
-
|
|
47
|
-
for (const line of lines) {
|
|
48
|
-
const stripped = stripCommentPrefix(line);
|
|
49
|
-
const match = TAG_PATTERN.exec(stripped);
|
|
50
|
-
if (!match) continue;
|
|
51
|
-
|
|
52
|
-
const kindStr = match[1];
|
|
53
|
-
const valuesStr = match[2];
|
|
54
|
-
if (!kindStr || !valuesStr) continue;
|
|
55
|
-
|
|
56
|
-
if (!VALID_TAG_KINDS.has(kindStr)) continue;
|
|
57
|
-
|
|
58
|
-
const kind = kindStr as TagKind;
|
|
59
|
-
const rawValues = valuesStr.split(",").map((v) => v.trim());
|
|
60
|
-
|
|
61
|
-
for (const value of rawValues) {
|
|
62
|
-
if (!value) continue;
|
|
63
|
-
|
|
64
|
-
if (!NAME_PATTERN.test(value)) {
|
|
65
|
-
return err(
|
|
66
|
-
`Invalid tag name "${value}": must be kebab-case (lowercase letters, numbers, hyphens, dots)`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
tags.push({ kind, value });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return ok(tags);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export { type ParsedTag, parseTags };
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import type { TreeSitterNode, TreeSitterTree } from "../parser.ts";
|
|
2
|
-
|
|
3
|
-
/** A raw call detected in the TypeScript AST. */
|
|
4
|
-
type RawCall = {
|
|
5
|
-
readonly sourceId: string;
|
|
6
|
-
readonly callee: string;
|
|
7
|
-
readonly line: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Extracts function calls from a TypeScript AST.
|
|
12
|
-
* Each call is scoped to its enclosing function, method, or arrow function.
|
|
13
|
-
*
|
|
14
|
-
* @param tree - Parsed tree-sitter tree
|
|
15
|
-
* @param filePath - Relative file path for source ID construction
|
|
16
|
-
* @returns Raw calls with caller ID and callee expression
|
|
17
|
-
*/
|
|
18
|
-
function extractTypeScriptCalls(tree: TreeSitterTree, filePath: string): readonly RawCall[] {
|
|
19
|
-
const calls: RawCall[] = [];
|
|
20
|
-
visitForCalls(tree.rootNode, filePath, [], calls);
|
|
21
|
-
return calls;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type ScopeEntry = { readonly name: string };
|
|
25
|
-
|
|
26
|
-
/** Recursively walks the AST finding call_expression nodes inside functions. */
|
|
27
|
-
function visitForCalls(
|
|
28
|
-
node: TreeSitterNode,
|
|
29
|
-
filePath: string,
|
|
30
|
-
scopeStack: readonly ScopeEntry[],
|
|
31
|
-
results: RawCall[],
|
|
32
|
-
): void {
|
|
33
|
-
// Enter new scope for function/method/class declarations
|
|
34
|
-
if (
|
|
35
|
-
node.type === "function_declaration" ||
|
|
36
|
-
node.type === "method_definition" ||
|
|
37
|
-
node.type === "class_declaration"
|
|
38
|
-
) {
|
|
39
|
-
const nameNode = node.childForFieldName("name");
|
|
40
|
-
if (nameNode) {
|
|
41
|
-
const newScope = [...scopeStack, { name: nameNode.text }];
|
|
42
|
-
for (const child of node.children) {
|
|
43
|
-
visitForCalls(child, filePath, newScope, results);
|
|
44
|
-
}
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Arrow function assigned to const
|
|
50
|
-
if (node.type === "variable_declarator") {
|
|
51
|
-
const nameNode = node.childForFieldName("name");
|
|
52
|
-
const valueNode = node.childForFieldName("value");
|
|
53
|
-
if (nameNode && valueNode?.type === "arrow_function") {
|
|
54
|
-
const newScope = [...scopeStack, { name: nameNode.text }];
|
|
55
|
-
for (const child of valueNode.children) {
|
|
56
|
-
visitForCalls(child, filePath, newScope, results);
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Export statement — unwrap
|
|
63
|
-
if (node.type === "export_statement") {
|
|
64
|
-
for (const child of node.children) {
|
|
65
|
-
visitForCalls(child, filePath, scopeStack, results);
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Detect call expressions inside a function scope
|
|
71
|
-
if (node.type === "call_expression" && scopeStack.length > 0) {
|
|
72
|
-
const callee = extractCalleeName(node);
|
|
73
|
-
if (callee) {
|
|
74
|
-
const sourceId = `${filePath}::${scopeStack.map((s) => s.name).join(".")}`;
|
|
75
|
-
results.push({ sourceId, callee, line: node.startPosition.row + 1 });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const child of node.children) {
|
|
80
|
-
visitForCalls(child, filePath, scopeStack, results);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Extracts the callee name from a call_expression node. */
|
|
85
|
-
function extractCalleeName(callNode: TreeSitterNode): string | undefined {
|
|
86
|
-
const funcNode = callNode.children[0];
|
|
87
|
-
if (!funcNode) return undefined;
|
|
88
|
-
if (funcNode.type === "identifier") return funcNode.text;
|
|
89
|
-
if (funcNode.type === "member_expression") return flattenMemberExpression(funcNode);
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Flattens a.b.c member expression into "a.b.c". */
|
|
94
|
-
function flattenMemberExpression(node: TreeSitterNode): string {
|
|
95
|
-
const parts: string[] = [];
|
|
96
|
-
let current: TreeSitterNode | null = node;
|
|
97
|
-
|
|
98
|
-
while (current?.type === "member_expression") {
|
|
99
|
-
const prop = current.children.find((c) => c.type === "property_identifier");
|
|
100
|
-
if (prop) parts.unshift(prop.text);
|
|
101
|
-
current = current.children[0] ?? null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (current?.type === "identifier") parts.unshift(current.text);
|
|
105
|
-
if (current?.type === "this") parts.unshift("this");
|
|
106
|
-
|
|
107
|
-
return parts.join(".");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export { extractTypeScriptCalls, type RawCall };
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { Edge, ExtractionResult, Tag } from "../../types/graph.ts";
|
|
2
|
-
import { isOk, unwrap } from "../../types/result.ts";
|
|
3
|
-
import type { Extractor } from "../extractor.ts";
|
|
4
|
-
import { createParser, type TreeSitterParser } from "../parser.ts";
|
|
5
|
-
import { parseTags } from "../tags.ts";
|
|
6
|
-
import { extractTypeScriptCalls } from "./calls.ts";
|
|
7
|
-
import { extractTypeScriptSymbols } from "./symbols.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Creates a TypeScript extractor with an initialized tree-sitter parser.
|
|
11
|
-
* Must be called after initTreeSitter().
|
|
12
|
-
*
|
|
13
|
-
* @returns An Extractor configured for TypeScript source files
|
|
14
|
-
*/
|
|
15
|
-
async function createTypeScriptExtractor(): Promise<Extractor> {
|
|
16
|
-
const parser = await createParser("typescript");
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
language: "typescript",
|
|
20
|
-
fileExtensions: [".ts", ".tsx"],
|
|
21
|
-
extract: (filePath: string, source: string): Promise<ExtractionResult> =>
|
|
22
|
-
extractTypeScript(parser, filePath, source),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extracts symbols, calls, and tags from a TypeScript file.
|
|
28
|
-
*
|
|
29
|
-
* @param parser - Initialized tree-sitter parser for TypeScript
|
|
30
|
-
* @param filePath - Relative file path
|
|
31
|
-
* @param source - Raw source code
|
|
32
|
-
* @returns Complete extraction result
|
|
33
|
-
*/
|
|
34
|
-
async function extractTypeScript(
|
|
35
|
-
parser: TreeSitterParser,
|
|
36
|
-
filePath: string,
|
|
37
|
-
source: string,
|
|
38
|
-
): Promise<ExtractionResult> {
|
|
39
|
-
if (!source.trim()) {
|
|
40
|
-
return { nodes: [], edges: [], tags: [], unresolved: [] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const tree = parser.parse(source);
|
|
44
|
-
|
|
45
|
-
// 1. Extract symbols
|
|
46
|
-
const nodes = [...extractTypeScriptSymbols(tree, filePath, source)];
|
|
47
|
-
|
|
48
|
-
// 2. Extract calls and convert to edges
|
|
49
|
-
const rawCalls = extractTypeScriptCalls(tree, filePath);
|
|
50
|
-
const edges: Edge[] = [];
|
|
51
|
-
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
52
|
-
|
|
53
|
-
for (const call of rawCalls) {
|
|
54
|
-
const targetId = resolveCalleeInFile(call.callee, filePath, nodeIds);
|
|
55
|
-
if (targetId) {
|
|
56
|
-
edges.push({ sourceId: call.sourceId, targetId, kind: "calls", certainty: "certain" });
|
|
57
|
-
} else {
|
|
58
|
-
edges.push({
|
|
59
|
-
sourceId: call.sourceId,
|
|
60
|
-
targetId: call.callee,
|
|
61
|
-
kind: "calls",
|
|
62
|
-
certainty: "uncertain",
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 3. Parse lattice tags from comments above functions
|
|
68
|
-
const tags = extractTagsFromSource(source, nodes);
|
|
69
|
-
|
|
70
|
-
return { nodes, edges, tags, unresolved: [] };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Resolves a callee name to a node ID within the same file. */
|
|
74
|
-
function resolveCalleeInFile(
|
|
75
|
-
callee: string,
|
|
76
|
-
filePath: string,
|
|
77
|
-
nodeIds: Set<string>,
|
|
78
|
-
): string | undefined {
|
|
79
|
-
const directId = `${filePath}::${callee}`;
|
|
80
|
-
if (nodeIds.has(directId)) return directId;
|
|
81
|
-
|
|
82
|
-
// this.method → try ClassName.method
|
|
83
|
-
if (callee.startsWith("this.")) {
|
|
84
|
-
const methodName = callee.slice(5);
|
|
85
|
-
for (const id of nodeIds) {
|
|
86
|
-
if (id.endsWith(`.${methodName}`) && id.startsWith(`${filePath}::`)) return id;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Extracts lattice tags from comment blocks above functions. */
|
|
94
|
-
function extractTagsFromSource(
|
|
95
|
-
source: string,
|
|
96
|
-
nodes: readonly { readonly id: string; readonly lineStart: number }[],
|
|
97
|
-
): readonly Tag[] {
|
|
98
|
-
const lines = source.split("\n");
|
|
99
|
-
const tags: Tag[] = [];
|
|
100
|
-
|
|
101
|
-
for (const node of nodes) {
|
|
102
|
-
const commentLines: string[] = [];
|
|
103
|
-
let lineIdx = node.lineStart - 2; // 1-based to 0-based
|
|
104
|
-
while (lineIdx >= 0) {
|
|
105
|
-
const line = lines[lineIdx]?.trim();
|
|
106
|
-
if (!line) break;
|
|
107
|
-
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
|
|
108
|
-
commentLines.unshift(line);
|
|
109
|
-
lineIdx--;
|
|
110
|
-
} else if (line.startsWith("@")) {
|
|
111
|
-
lineIdx--;
|
|
112
|
-
} else {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (commentLines.length === 0) continue;
|
|
118
|
-
|
|
119
|
-
const parseResult = parseTags(commentLines.join("\n"));
|
|
120
|
-
if (isOk(parseResult)) {
|
|
121
|
-
for (const parsed of unwrap(parseResult)) {
|
|
122
|
-
tags.push({ nodeId: node.id, kind: parsed.kind, value: parsed.value });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return tags;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export { createTypeScriptExtractor };
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { TreeSitterNode, TreeSitterTree } from "../parser.ts";
|
|
2
|
-
|
|
3
|
-
/** A parsed import statement from TypeScript source. */
|
|
4
|
-
type TypeScriptImport = {
|
|
5
|
-
readonly module: string;
|
|
6
|
-
readonly names: readonly string[];
|
|
7
|
-
readonly defaultImport: string | undefined;
|
|
8
|
-
readonly isRelative: boolean;
|
|
9
|
-
readonly line: number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extracts import statements from a TypeScript AST.
|
|
14
|
-
*
|
|
15
|
-
* @param tree - Parsed tree-sitter tree
|
|
16
|
-
* @param _filePath - Relative file path (unused, kept for interface consistency)
|
|
17
|
-
* @returns Parsed imports with module paths and imported names
|
|
18
|
-
*/
|
|
19
|
-
function extractTypeScriptImports(
|
|
20
|
-
tree: TreeSitterTree,
|
|
21
|
-
_filePath: string,
|
|
22
|
-
): readonly TypeScriptImport[] {
|
|
23
|
-
const imports: TypeScriptImport[] = [];
|
|
24
|
-
|
|
25
|
-
for (const child of tree.rootNode.children) {
|
|
26
|
-
if (child.type === "import_statement") {
|
|
27
|
-
parseImportStatement(child, imports);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return imports;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Parses an import statement into module path, named imports, and default import. */
|
|
35
|
-
function parseImportStatement(node: TreeSitterNode, results: TypeScriptImport[]): void {
|
|
36
|
-
const fromClause = node.children.find((c) => c.type === "string");
|
|
37
|
-
if (!fromClause) return;
|
|
38
|
-
|
|
39
|
-
const contentNode = fromClause.children.find((c) => c.type === "string_fragment");
|
|
40
|
-
const module = contentNode?.text ?? "";
|
|
41
|
-
const isRelative = module.startsWith(".");
|
|
42
|
-
|
|
43
|
-
const importClause = node.children.find((c) => c.type === "import_clause");
|
|
44
|
-
if (!importClause) return;
|
|
45
|
-
|
|
46
|
-
let defaultImport: string | undefined;
|
|
47
|
-
const names: string[] = [];
|
|
48
|
-
|
|
49
|
-
for (const child of importClause.children) {
|
|
50
|
-
if (child.type === "identifier") {
|
|
51
|
-
defaultImport = child.text;
|
|
52
|
-
} else if (child.type === "named_imports") {
|
|
53
|
-
for (const spec of child.children) {
|
|
54
|
-
if (spec.type === "import_specifier") {
|
|
55
|
-
const nameNode = spec.childForFieldName("name");
|
|
56
|
-
if (nameNode) names.push(nameNode.text);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
results.push({
|
|
63
|
-
module,
|
|
64
|
-
names,
|
|
65
|
-
defaultImport,
|
|
66
|
-
isRelative,
|
|
67
|
-
line: node.startPosition.row + 1,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export { extractTypeScriptImports, type TypeScriptImport };
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import type { Node, NodeKind } from "../../types/graph.ts";
|
|
2
|
-
import type { TreeSitterNode, TreeSitterTree } from "../parser.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Extracts symbols (functions, classes, methods, interfaces, type aliases)
|
|
6
|
-
* from a TypeScript AST.
|
|
7
|
-
*
|
|
8
|
-
* @param tree - Parsed tree-sitter tree
|
|
9
|
-
* @param filePath - Relative file path for node ID construction
|
|
10
|
-
* @param _source - Original source text (unused, kept for interface consistency)
|
|
11
|
-
* @returns Extracted nodes with deterministic IDs
|
|
12
|
-
*/
|
|
13
|
-
function extractTypeScriptSymbols(
|
|
14
|
-
tree: TreeSitterTree,
|
|
15
|
-
filePath: string,
|
|
16
|
-
_source: string,
|
|
17
|
-
): readonly Node[] {
|
|
18
|
-
const nodes: Node[] = [];
|
|
19
|
-
visitNode(tree.rootNode, filePath, [], nodes);
|
|
20
|
-
return nodes;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Scope entry for tracking parent context. */
|
|
24
|
-
type ScopeEntry = { readonly name: string; readonly isClass: boolean };
|
|
25
|
-
|
|
26
|
-
/** Recursively visits AST nodes to extract symbols. */
|
|
27
|
-
function visitNode(
|
|
28
|
-
node: TreeSitterNode,
|
|
29
|
-
filePath: string,
|
|
30
|
-
scopeStack: readonly ScopeEntry[],
|
|
31
|
-
results: Node[],
|
|
32
|
-
): void {
|
|
33
|
-
switch (node.type) {
|
|
34
|
-
case "function_declaration":
|
|
35
|
-
extractFunction(node, filePath, scopeStack, results);
|
|
36
|
-
return;
|
|
37
|
-
case "class_declaration":
|
|
38
|
-
extractClass(node, filePath, scopeStack, results);
|
|
39
|
-
return;
|
|
40
|
-
case "interface_declaration":
|
|
41
|
-
extractTypeDecl(node, filePath, scopeStack, results, "type");
|
|
42
|
-
return;
|
|
43
|
-
case "type_alias_declaration":
|
|
44
|
-
extractTypeDecl(node, filePath, scopeStack, results, "type");
|
|
45
|
-
return;
|
|
46
|
-
case "export_statement": {
|
|
47
|
-
// Unwrap: export function X, export class X, export const X = ...
|
|
48
|
-
for (const child of node.children) {
|
|
49
|
-
visitNode(child, filePath, scopeStack, results);
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
case "lexical_declaration": {
|
|
54
|
-
// const handler = async () => { ... }
|
|
55
|
-
extractArrowFunctions(node, filePath, scopeStack, results);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
case "method_definition": {
|
|
59
|
-
extractMethod(node, filePath, scopeStack, results);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
default:
|
|
63
|
-
for (const child of node.children) {
|
|
64
|
-
visitNode(child, filePath, scopeStack, results);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Extracts a function declaration. */
|
|
70
|
-
function extractFunction(
|
|
71
|
-
node: TreeSitterNode,
|
|
72
|
-
filePath: string,
|
|
73
|
-
scopeStack: readonly ScopeEntry[],
|
|
74
|
-
results: Node[],
|
|
75
|
-
): void {
|
|
76
|
-
const nameNode = node.childForFieldName("name");
|
|
77
|
-
if (!nameNode) return;
|
|
78
|
-
|
|
79
|
-
const name = nameNode.text;
|
|
80
|
-
const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
|
|
81
|
-
const id = `${filePath}::${qualifiedName}`;
|
|
82
|
-
|
|
83
|
-
results.push({
|
|
84
|
-
id,
|
|
85
|
-
kind: "function",
|
|
86
|
-
name,
|
|
87
|
-
file: filePath,
|
|
88
|
-
lineStart: node.startPosition.row + 1,
|
|
89
|
-
lineEnd: node.endPosition.row + 1,
|
|
90
|
-
language: "typescript",
|
|
91
|
-
signature: buildSignature(node, name),
|
|
92
|
-
isTest: false,
|
|
93
|
-
metadata: undefined,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Extracts a class declaration and recurses into its body for methods. */
|
|
98
|
-
function extractClass(
|
|
99
|
-
node: TreeSitterNode,
|
|
100
|
-
filePath: string,
|
|
101
|
-
scopeStack: readonly ScopeEntry[],
|
|
102
|
-
results: Node[],
|
|
103
|
-
): void {
|
|
104
|
-
const nameNode = node.childForFieldName("name");
|
|
105
|
-
if (!nameNode) return;
|
|
106
|
-
|
|
107
|
-
const name = nameNode.text;
|
|
108
|
-
const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
|
|
109
|
-
const id = `${filePath}::${qualifiedName}`;
|
|
110
|
-
|
|
111
|
-
results.push({
|
|
112
|
-
id,
|
|
113
|
-
kind: "class",
|
|
114
|
-
name,
|
|
115
|
-
file: filePath,
|
|
116
|
-
lineStart: node.startPosition.row + 1,
|
|
117
|
-
lineEnd: node.endPosition.row + 1,
|
|
118
|
-
language: "typescript",
|
|
119
|
-
signature: undefined,
|
|
120
|
-
isTest: false,
|
|
121
|
-
metadata: undefined,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Recurse into class body for methods
|
|
125
|
-
const body = node.childForFieldName("body");
|
|
126
|
-
if (body) {
|
|
127
|
-
const newScope = [...scopeStack, { name, isClass: true }];
|
|
128
|
-
for (const child of body.children) {
|
|
129
|
-
visitNode(child, filePath, newScope, results);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Extracts a method definition inside a class. */
|
|
135
|
-
function extractMethod(
|
|
136
|
-
node: TreeSitterNode,
|
|
137
|
-
filePath: string,
|
|
138
|
-
scopeStack: readonly ScopeEntry[],
|
|
139
|
-
results: Node[],
|
|
140
|
-
): void {
|
|
141
|
-
const nameNode = node.childForFieldName("name");
|
|
142
|
-
if (!nameNode) return;
|
|
143
|
-
|
|
144
|
-
const name = nameNode.text;
|
|
145
|
-
const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
|
|
146
|
-
const id = `${filePath}::${qualifiedName}`;
|
|
147
|
-
|
|
148
|
-
results.push({
|
|
149
|
-
id,
|
|
150
|
-
kind: "method",
|
|
151
|
-
name,
|
|
152
|
-
file: filePath,
|
|
153
|
-
lineStart: node.startPosition.row + 1,
|
|
154
|
-
lineEnd: node.endPosition.row + 1,
|
|
155
|
-
language: "typescript",
|
|
156
|
-
signature: buildSignature(node, name),
|
|
157
|
-
isTest: false,
|
|
158
|
-
metadata: undefined,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Extracts an interface or type alias declaration. */
|
|
163
|
-
function extractTypeDecl(
|
|
164
|
-
node: TreeSitterNode,
|
|
165
|
-
filePath: string,
|
|
166
|
-
scopeStack: readonly ScopeEntry[],
|
|
167
|
-
results: Node[],
|
|
168
|
-
kind: NodeKind,
|
|
169
|
-
): void {
|
|
170
|
-
const nameNode = node.childForFieldName("name");
|
|
171
|
-
if (!nameNode) return;
|
|
172
|
-
|
|
173
|
-
const name = nameNode.text;
|
|
174
|
-
const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
|
|
175
|
-
const id = `${filePath}::${qualifiedName}`;
|
|
176
|
-
|
|
177
|
-
results.push({
|
|
178
|
-
id,
|
|
179
|
-
kind,
|
|
180
|
-
name,
|
|
181
|
-
file: filePath,
|
|
182
|
-
lineStart: node.startPosition.row + 1,
|
|
183
|
-
lineEnd: node.endPosition.row + 1,
|
|
184
|
-
language: "typescript",
|
|
185
|
-
signature: undefined,
|
|
186
|
-
isTest: false,
|
|
187
|
-
metadata: undefined,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Extracts arrow functions assigned to const/let variables. */
|
|
192
|
-
function extractArrowFunctions(
|
|
193
|
-
node: TreeSitterNode,
|
|
194
|
-
filePath: string,
|
|
195
|
-
scopeStack: readonly ScopeEntry[],
|
|
196
|
-
results: Node[],
|
|
197
|
-
): void {
|
|
198
|
-
for (const child of node.children) {
|
|
199
|
-
if (child.type === "variable_declarator") {
|
|
200
|
-
const nameNode = child.childForFieldName("name");
|
|
201
|
-
const valueNode = child.childForFieldName("value");
|
|
202
|
-
if (nameNode && valueNode?.type === "arrow_function") {
|
|
203
|
-
const name = nameNode.text;
|
|
204
|
-
const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
|
|
205
|
-
const id = `${filePath}::${qualifiedName}`;
|
|
206
|
-
|
|
207
|
-
results.push({
|
|
208
|
-
id,
|
|
209
|
-
kind: "function",
|
|
210
|
-
name,
|
|
211
|
-
file: filePath,
|
|
212
|
-
lineStart: node.startPosition.row + 1,
|
|
213
|
-
lineEnd: node.endPosition.row + 1,
|
|
214
|
-
language: "typescript",
|
|
215
|
-
signature: buildArrowSignature(valueNode, name),
|
|
216
|
-
isTest: false,
|
|
217
|
-
metadata: undefined,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Builds a signature string from a function/method declaration. */
|
|
225
|
-
function buildSignature(node: TreeSitterNode, name: string): string {
|
|
226
|
-
const params = node.childForFieldName("parameters");
|
|
227
|
-
const returnType =
|
|
228
|
-
node.childForFieldName("return_type") ??
|
|
229
|
-
node.children.find((c) => c.type === "type_annotation");
|
|
230
|
-
|
|
231
|
-
const paramsStr = params?.text ?? "()";
|
|
232
|
-
const returnStr = returnType?.text ?? "";
|
|
233
|
-
|
|
234
|
-
return `${name}${paramsStr}${returnStr ? ` ${returnStr}` : ""}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Builds a signature string from an arrow function. */
|
|
238
|
-
function buildArrowSignature(node: TreeSitterNode, name: string): string {
|
|
239
|
-
const params =
|
|
240
|
-
node.childForFieldName("parameters") ??
|
|
241
|
-
node.children.find((c) => c.type === "formal_parameters");
|
|
242
|
-
const returnType =
|
|
243
|
-
node.childForFieldName("return_type") ??
|
|
244
|
-
node.children.find((c) => c.type === "type_annotation");
|
|
245
|
-
|
|
246
|
-
const paramsStr = params?.text ?? "()";
|
|
247
|
-
const returnStr = returnType?.text ?? "";
|
|
248
|
-
|
|
249
|
-
return `${name}${paramsStr}${returnStr ? ` ${returnStr}` : ""}`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export { extractTypeScriptSymbols };
|