forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlua.ts
More file actions
152 lines (138 loc) · 6.06 KB
/
lua.ts
File metadata and controls
152 lines (138 loc) · 6.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';
// Node names follow the vendored ABI-15 grammar (@tree-sitter-grammars/
// tree-sitter-lua), NOT the older tree-sitter-wasms build — see grammars.ts.
/** First descendant of a given type (breadth-first), or null. */
function findDescendant(node: SyntaxNode, type: string): SyntaxNode | null {
const queue: SyntaxNode[] = [...node.namedChildren];
while (queue.length) {
const n = queue.shift()!;
if (n.type === type) return n;
queue.push(...n.namedChildren);
}
return null;
}
/**
* If `callNode` is a `require(...)` call, return the module name; otherwise null.
* Lua/Luau have no import statement — modules are loaded by calling the global
* `require`. Handles both:
* - string requires: `require("net.http")` / `require "net.http"` → "net.http"
* - Roblox/Luau path requires: `require(script.Parent.Signal)` → "Signal"
* (the dominant idiom in Roblox code, where the argument is an instance path
* rather than a string — use the trailing field as the module name).
*/
function requireModule(callNode: SyntaxNode, source: string): string | null {
// function_call > name: <callee>, arguments: arguments
const name = getChildByField(callNode, 'name');
// A dotted/colon callee (e.g. `socket.connect`) is dot/method_index_expression,
// never a bare `require`.
if (!name || name.type !== 'identifier') return null;
if (getNodeText(name, source) !== 'require') return null;
const args = getChildByField(callNode, 'arguments');
if (!args) return null;
// String require — `string > content: string_content` gives the bare name.
const content = findDescendant(args, 'string_content');
if (content) return getNodeText(content, source).trim() || null;
const str = findDescendant(args, 'string');
if (str) {
const mod = getNodeText(str, source)
.trim()
.replace(/^\[\[/, '')
.replace(/\]\]$/, '')
.replace(/^["']/, '')
.replace(/["']$/, '');
if (mod) return mod;
}
// Roblox/Luau instance-path require: `require(script.Parent.Signal)` → "Signal".
const idx = findDescendant(args, 'dot_index_expression') ?? findDescendant(args, 'method_index_expression');
if (idx) {
const field = getChildByField(idx, 'field') ?? getChildByField(idx, 'method');
if (field) return getNodeText(field, source).trim() || null;
}
return null;
}
export const luaExtractor: LanguageExtractor = {
// function_declaration covers global (`function f`), table (`function t.f`),
// method (`function t:m`), and local (`local function f`) forms — the form is
// distinguished by the `name:` child (identifier / dot_index_expression /
// method_index_expression) and a `local` token, not by separate node types.
// Anonymous `function() ... end` (function_definition) has no name and is
// captured via its enclosing variable instead.
functionTypes: ['function_declaration'],
classTypes: [], // Lua has no classes/structs/interfaces/enums — tables are used for everything
methodTypes: [],
interfaceTypes: [],
structTypes: [],
enumTypes: [],
typeAliasTypes: [],
importTypes: [], // `require` is a function_call — handled in visitNode below
callTypes: ['function_call'],
variableTypes: ['variable_declaration'], // see the `lua` branch in extractVariable
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
getSignature: (node, source) => {
const params = getChildByField(node, 'parameters');
return params ? getNodeText(params, source) : undefined;
},
// `function t.f()` / `function t:m()` are methods on table `t`: return the
// table as the receiver so they extract as methods with a `t::f` qualified
// name. Plain `function f()` / `local function f()` have no receiver and stay
// functions. (For `a.b.c`, the receiver is the nested `a.b`.)
getReceiverType: (node, source) => {
const name = getChildByField(node, 'name');
if (name && (name.type === 'dot_index_expression' || name.type === 'method_index_expression')) {
const table = getChildByField(name, 'table');
if (table) return getNodeText(table, source);
}
return undefined;
},
// Emit import nodes for `require(...)`. The local-declaration form is handled
// explicitly because the variable branch skips the initializer subtree; bare
// and global `require` calls are caught when the walker reaches the
// function_call node.
visitNode: (node, ctx) => {
const source = ctx.source;
const emit = (callNode: SyntaxNode): void => {
const mod = requireModule(callNode, source);
if (!mod) return;
const imp = ctx.createNode('import', mod, callNode, {
signature: getNodeText(callNode, source).trim().slice(0, 100),
});
if (imp && ctx.nodeStack.length > 0) {
const parentId = ctx.nodeStack[ctx.nodeStack.length - 1];
if (parentId) {
ctx.addUnresolvedReference({
fromNodeId: parentId,
referenceName: mod,
referenceKind: 'imports',
line: callNode.startPosition.row + 1,
column: callNode.startPosition.column,
});
}
}
};
// Bare / global `require("x")` — claim it so it isn't double-counted as a call.
if (node.type === 'function_call') {
if (requireModule(node, source)) {
emit(node);
return true;
}
return false;
}
// `local x = require("x")` — variable_declaration wraps an assignment_statement
// whose initializer subtree the variable branch will skip, so dig it out here.
if (node.type === 'variable_declaration') {
const assign = node.namedChildren.find((c) => c.type === 'assignment_statement');
const exprList = assign?.namedChildren.find((c) => c.type === 'expression_list');
if (exprList) {
for (const val of exprList.namedChildren) {
if (val.type === 'function_call') emit(val);
}
}
return false;
}
return false;
},
};