graph-indexer 3.0.1 → 3.1.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/CHANGELOG.md +404 -0
- package/README.md +61 -8
- package/bin/graph-indexer.mjs +46 -5
- package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
- package/integrations/claude-code/agents/code-structure.md +0 -3
- package/integrations/claude-code/skills/setup/SKILL.md +26 -0
- package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
- package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
- package/integrations/claude-code-local/README.md +10 -0
- package/integrations/claude-code-local/agents/code-structure.md +22 -0
- package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +3 -1
- package/package.json +1 -1
- package/src/cli/helper.mjs +15 -1
- package/src/cli/hook.mjs +123 -41
- package/src/cli/init.mjs +16 -1
- package/src/cli/resident.mjs +1 -1
- package/src/index/indexer.mjs +77 -41
- package/src/index/modules.mjs +76 -2
- package/src/index/parse-worker.mjs +12 -0
- package/src/index/parsepool.mjs +73 -0
- package/src/index/resolver.mjs +362 -23
- package/src/mcp/server.mjs +16 -12
- package/src/mcp/tools.mjs +417 -25
- package/src/parse/arity.mjs +39 -2
- package/src/parse/extract.mjs +269 -45
- package/src/parse/lang/ecma.mjs +2 -0
- package/src/parse/lang/go.mjs +2 -0
- package/src/parse/lang/jvm.mjs +33 -0
- package/src/parse/lang/markdown.mjs +92 -0
- package/src/parse/lang/python.mjs +43 -0
- package/src/parse/lang/rust.mjs +215 -15
- package/src/parse/lang/sfc.mjs +125 -0
- package/src/parse/languages.mjs +9 -2
- package/src/query/check.mjs +49 -13
- package/src/query/facts.mjs +3 -2
- package/src/query/injection-spring.mjs +405 -0
- package/src/query/injection.mjs +173 -0
- package/src/query/intel.mjs +76 -14
- package/src/query/issue.mjs +156 -0
- package/src/query/orm.mjs +286 -0
- package/src/query/read.mjs +6 -1
- package/src/query/report.mjs +53 -0
- package/src/query/routes.mjs +142 -0
- package/src/query/showcase.mjs +59 -0
- package/src/search/search.mjs +85 -17
- package/src/search/semantic.mjs +201 -0
- package/src/search/vectors.mjs +122 -0
- package/src/store/db.mjs +22 -4
- package/vendor/grammars/README.md +47 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +4 -4
- /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
package/src/parse/lang/jvm.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simpleParamType } from '../arity.mjs';
|
|
1
2
|
/** Java extraction spec (Kotlin/Scala live in their own modules). */
|
|
2
3
|
|
|
3
4
|
const T = (cap) => `[(type_identifier) @${cap} (generic_type (type_identifier) @${cap}) (scoped_type_identifier (type_identifier) @${cap} .) (generic_type (scoped_type_identifier (type_identifier) @${cap} .))]`;
|
|
@@ -34,6 +35,9 @@ const JAVA_QUERY = `
|
|
|
34
35
|
(method_invocation object: ((identifier) @name @ref.type (#match? @name "^[A-Z]\\w*[a-z]")))
|
|
35
36
|
(field_access object: ((identifier) @name @ref.type (#match? @name "^[A-Z]\\w*[a-z]")))
|
|
36
37
|
(method_reference . ((identifier) @name @ref.type (#match? @name "^[A-Z]\\w*[a-z]")))
|
|
38
|
+
; a nested class named through its outer one: Token.TokenType.EOF, Document.QuirksMode.quirks
|
|
39
|
+
(field_access object: (field_access object: (identifier) @recv field: (identifier) @name (#match? @name "^[A-Z]\\w*[a-z]")) @ref.type)
|
|
40
|
+
(method_invocation object: (field_access object: (identifier) @recv field: (identifier) @name (#match? @name "^[A-Z]\\w*[a-z]")) @ref.type)
|
|
37
41
|
(argument_list (identifier) @name) @ref.value
|
|
38
42
|
|
|
39
43
|
(lambda_expression) @scope
|
|
@@ -63,6 +67,34 @@ function javaImport(node) {
|
|
|
63
67
|
return [{ source: parts.join('.'), imported: name, local: name, static: isStatic }];
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/** The static type of a call argument where the source says it (literals, typed locals, `new`, casts), else null. */
|
|
71
|
+
function javaArgType(n, rawOfLocal) {
|
|
72
|
+
switch (n.type) {
|
|
73
|
+
case 'string_literal': case 'text_block': return 'String';
|
|
74
|
+
case 'character_literal': return 'char';
|
|
75
|
+
case 'decimal_integer_literal': case 'hex_integer_literal': case 'octal_integer_literal': case 'binary_integer_literal': return /[lL]$/.test(n.text) ? 'long' : 'int';
|
|
76
|
+
case 'decimal_floating_point_literal': case 'hex_floating_point_literal': return /[fF]$/.test(n.text) ? 'float' : 'double';
|
|
77
|
+
case 'true': case 'false': return 'boolean';
|
|
78
|
+
case 'null_literal': return '*';
|
|
79
|
+
case 'identifier': return simpleParamType(rawOfLocal(n.text));
|
|
80
|
+
case 'object_creation_expression': return simpleParamType(n.childForFieldName('type')?.text);
|
|
81
|
+
case 'cast_expression': return simpleParamType(n.childForFieldName('type')?.text);
|
|
82
|
+
case 'array_creation_expression': { const t = simpleParamType(n.childForFieldName('type')?.text); return t ? t + '[]'.repeat(Math.max(1, n.namedChildren.filter(c => c.type === 'dimensions_expr' || c.type === 'dimensions').length)) : null; }
|
|
83
|
+
case 'array_access': {
|
|
84
|
+
const arr = n.childForFieldName('array');
|
|
85
|
+
const t = arr?.type === 'identifier' ? simpleParamType(rawOfLocal(arr.text)) : null;
|
|
86
|
+
return t?.endsWith('[]') ? t.slice(0, -2) : null;
|
|
87
|
+
}
|
|
88
|
+
case 'parenthesized_expression': return n.namedChildren[0] ? javaArgType(n.namedChildren[0], rawOfLocal) : null;
|
|
89
|
+
case 'binary_expression': {
|
|
90
|
+
// `"a" + x` is a String
|
|
91
|
+
if (n.childForFieldName('operator')?.text === '+' && [n.childForFieldName('left'), n.childForFieldName('right')].some(c => c?.type === 'string_literal')) return 'String';
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
default: return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function modifiersText(node) {
|
|
67
99
|
const m = node.namedChildren.find(c => c.type === 'modifiers');
|
|
68
100
|
return m ? m.text : '';
|
|
@@ -76,6 +108,7 @@ function javaFileInfo(root) {
|
|
|
76
108
|
export const java = {
|
|
77
109
|
id: 'java',
|
|
78
110
|
distinctOverloads: true, // each overload is a method of its own: calls bind by argument count
|
|
111
|
+
argType: javaArgType, // … and, among overloads of one count, by the argument types the source shows
|
|
79
112
|
localValues: true, // an argument named like a local or parameter is that variable
|
|
80
113
|
nestedTypes: true, // `Token.Comment` names the class Comment nested in Token
|
|
81
114
|
// an anonymous class (`new Transformer<>() {…}`) is `<anonymous>`, a subclass of what it instantiates
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown documentation: one `section` definition per heading, spanning the text up to the next
|
|
3
|
+
* heading of the same or a higher level, nested like the headings. Sections are searchable
|
|
4
|
+
* (search_code, explore) and readable by name (read_code "docs/ARCHITECTURE.md:Storage"), so a
|
|
5
|
+
* question about why or how something is designed finds the design notes next to the code.
|
|
6
|
+
* No grammar: headings are read line by line, skipping fenced code blocks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ATX = /^ {0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
10
|
+
const SETEXT = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
11
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
12
|
+
|
|
13
|
+
/** Heading text without inline markup: `code`, **bold**, [links](…), <tags>. */
|
|
14
|
+
function plain(s) {
|
|
15
|
+
let t = s;
|
|
16
|
+
for (let prev; prev !== t;) { prev = t; t = t.replace(/<[^>]*>/g, ''); }
|
|
17
|
+
return t.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/[`*_~]/g, '').replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function extractMarkdown(source) {
|
|
21
|
+
const lines = source.split('\n');
|
|
22
|
+
const heads = []; // { level, text, line }
|
|
23
|
+
let fence = null;
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const l = lines[i];
|
|
26
|
+
const f = FENCE.exec(l);
|
|
27
|
+
if (f) {
|
|
28
|
+
if (!fence) fence = f[1][0];
|
|
29
|
+
else if (f[1][0] === fence) fence = null;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (fence) continue;
|
|
33
|
+
const m = ATX.exec(l);
|
|
34
|
+
if (m) { const text = plain(m[2]); if (text) heads.push({ level: m[1].length, text, line: i + 1 }); continue; }
|
|
35
|
+
// setext: a non-blank paragraph line underlined with === or ---
|
|
36
|
+
if (i + 1 < lines.length && SETEXT.test(lines[i + 1]) && l.trim() && !/^ {0,3}([-*+>]|\d+[.)]|\|)/.test(l) && (i === 0 || !lines[i - 1].trim())) {
|
|
37
|
+
const text = plain(l);
|
|
38
|
+
if (text) { heads.push({ level: lines[i + 1].trim()[0] === '=' ? 1 : 2, text, line: i + 1 }); i++; }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const symbols = [];
|
|
42
|
+
const stack = []; // indices into symbols, by nesting
|
|
43
|
+
for (let h = 0; h < heads.length; h++) {
|
|
44
|
+
const { level, text, line } = heads[h];
|
|
45
|
+
let end = lines.length;
|
|
46
|
+
for (let k = h + 1; k < heads.length; k++) if (heads[k].level <= level) { end = heads[k].line - 1; break; }
|
|
47
|
+
while (end > line && !lines[end - 1].trim()) end--;
|
|
48
|
+
while (stack.length && symbols[stack[stack.length - 1]].level >= level) stack.pop();
|
|
49
|
+
const parentIdx = stack.length ? stack[stack.length - 1] : -1;
|
|
50
|
+
const parent = parentIdx >= 0 ? symbols[parentIdx] : null;
|
|
51
|
+
// first paragraph under the heading, as its doc line
|
|
52
|
+
let doc = '';
|
|
53
|
+
for (let k = line; k < Math.min(end, line + 30); k++) {
|
|
54
|
+
const t = lines[k].trim();
|
|
55
|
+
if (!t) { if (doc) break; continue; }
|
|
56
|
+
if (ATX.test(lines[k]) || FENCE.test(lines[k]) || /^[|<]/.test(t)) { if (doc) break; continue; }
|
|
57
|
+
doc += (doc ? ' ' : '') + plain(t.replace(/^([-*+>]|\d+[.)])\s+/, ''));
|
|
58
|
+
if (doc.length > 240) break;
|
|
59
|
+
}
|
|
60
|
+
symbols.push({
|
|
61
|
+
name: text,
|
|
62
|
+
qname: parent ? `${parent.qname} › ${text}` : text,
|
|
63
|
+
kind: 'section',
|
|
64
|
+
level,
|
|
65
|
+
parentIdx,
|
|
66
|
+
lexParent: parentIdx,
|
|
67
|
+
owner: null,
|
|
68
|
+
startLine: line, startCol: 0,
|
|
69
|
+
endLine: Math.max(line, end), endCol: (lines[Math.max(line, end) - 1] ?? '').length,
|
|
70
|
+
nameLine: line, nameCol: lines[line - 1].indexOf(text[0]) >= 0 ? lines[line - 1].indexOf(text[0]) : 0,
|
|
71
|
+
sig: `${'#'.repeat(level)} ${text}`,
|
|
72
|
+
doc: doc.slice(0, 300) || null,
|
|
73
|
+
exported: true,
|
|
74
|
+
visibility: null,
|
|
75
|
+
decorators: [],
|
|
76
|
+
bases: [],
|
|
77
|
+
type: null,
|
|
78
|
+
isStatic: false,
|
|
79
|
+
});
|
|
80
|
+
stack.push(symbols.length - 1);
|
|
81
|
+
}
|
|
82
|
+
for (const s of symbols) delete s.level;
|
|
83
|
+
return { symbols, refs: [], imports: [], fields: [], fileInfo: null, errors: 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const markdown = {
|
|
87
|
+
id: 'markdown',
|
|
88
|
+
docs: true, // prose: indexed for search and reading, never parsed as code
|
|
89
|
+
extensions: ['.md', '.mdx', '.markdown'],
|
|
90
|
+
testFile: null,
|
|
91
|
+
extract: (source) => extractMarkdown(source),
|
|
92
|
+
};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Python extraction spec. See ../extract.mjs for the capture convention. */
|
|
2
2
|
|
|
3
|
+
// PEP 604 unions in annotations (`A | B.C | None`) nest left to right: one pattern per depth
|
|
4
|
+
const UNION_TYPES = [1, 2, 3, 4].flatMap(d => [
|
|
5
|
+
`(type ${'(binary_operator '.repeat(d)}(identifier) @name${')'.repeat(d)}) @ref.type`,
|
|
6
|
+
`(type ${'(binary_operator '.repeat(d)}(attribute object: (_) @recv attribute: (identifier) @name)${')'.repeat(d)}) @ref.type`,
|
|
7
|
+
]).join('\n');
|
|
8
|
+
|
|
3
9
|
const QUERY = `
|
|
4
10
|
(function_definition name: (identifier) @name return_type: (type) @type) @def.function
|
|
5
11
|
(function_definition name: (identifier) @name) @def.function
|
|
@@ -22,11 +28,34 @@ const QUERY = `
|
|
|
22
28
|
(type (generic_type (identifier) @name)) @ref.type
|
|
23
29
|
(type (generic_type (type_parameter (type (identifier) @name)))) @ref.type
|
|
24
30
|
(type (string (string_content) @name)) @ref.type
|
|
31
|
+
${UNION_TYPES}
|
|
32
|
+
(call function: [(identifier) (attribute)] @_tv arguments: (argument_list (keyword_argument name: (identifier) @_bound value: (string (string_content) @name))) (#match? @_tv "TypeVar$") (#eq? @_bound "bound")) @ref.type
|
|
33
|
+
(expression_statement (assignment left: (identifier) @_all right: [(list (string (string_content) @name)) (tuple (string (string_content) @name))] (#eq? @_all "__all__"))) @ref.value
|
|
34
|
+
(attribute object: (identifier) @name (#match? @name "^[A-Z]")) @ref.value
|
|
25
35
|
(argument_list (identifier) @name) @ref.value
|
|
26
36
|
(keyword_argument value: (identifier) @name) @ref.value
|
|
27
37
|
(list (identifier) @name) @ref.value
|
|
38
|
+
(tuple (identifier) @name) @ref.value
|
|
39
|
+
(set (identifier) @name) @ref.value
|
|
28
40
|
(pair value: (identifier) @name) @ref.value
|
|
29
41
|
(assignment right: (identifier) @name) @ref.value
|
|
42
|
+
(return_statement (identifier) @name) @ref.value
|
|
43
|
+
(pair key: (identifier) @name) @ref.value
|
|
44
|
+
(subscript value: (identifier) @_generic subscript: (identifier) @name (#match? @_generic "^[A-Z]")) @ref.type
|
|
45
|
+
(subscript value: (identifier) @_generic subscript: (attribute object: (_) @recv attribute: (identifier) @name) (#match? @_generic "^[A-Z]")) @ref.type
|
|
46
|
+
(subscript value: (attribute) subscript: (identifier) @name (#match? @name "^[A-Z]")) @ref.type
|
|
47
|
+
(subscript value: (attribute) subscript: (attribute object: (_) @recv attribute: (identifier) @name) (#match? @name "^[A-Z]")) @ref.type
|
|
48
|
+
(subscript value: [(identifier) (attribute)] subscript: (list (identifier) @name) (#match? @name "^[A-Z]")) @ref.type
|
|
49
|
+
(subscript value: [(identifier) (attribute)] subscript: (string (string_content) @name) (#match? @name "^[A-Z][A-Za-z0-9_]*$")) @ref.type
|
|
50
|
+
(except_clause value: (identifier) @name) @ref.type
|
|
51
|
+
(except_clause value: (attribute object: (_) @recv attribute: (identifier) @name)) @ref.type
|
|
52
|
+
(except_clause value: (tuple (identifier) @name)) @ref.type
|
|
53
|
+
(except_clause value: (tuple (attribute object: (_) @recv attribute: (identifier) @name))) @ref.type
|
|
54
|
+
(except_clause value: (as_pattern (identifier) @name)) @ref.type
|
|
55
|
+
(except_clause value: (as_pattern (attribute object: (_) @recv attribute: (identifier) @name))) @ref.type
|
|
56
|
+
(except_clause value: (as_pattern (tuple (identifier) @name))) @ref.type
|
|
57
|
+
(except_clause value: (as_pattern (tuple (attribute object: (_) @recv attribute: (identifier) @name)))) @ref.type
|
|
58
|
+
(return_statement (expression_list (identifier) @name)) @ref.value
|
|
30
59
|
(attribute object: (_) @recv attribute: (identifier) @name) @ref.read
|
|
31
60
|
|
|
32
61
|
[(lambda) (list_comprehension) (set_comprehension) (dictionary_comprehension) (generator_expression)] @scope
|
|
@@ -37,6 +66,11 @@ const QUERY = `
|
|
|
37
66
|
(assignment left: (identifier) @bind.name right: (_) @bind.expr) @bind
|
|
38
67
|
(assignment left: (identifier) @bind.name type: (type) @bind.type) @bind
|
|
39
68
|
(typed_parameter (identifier) @bind.name type: (type) @bind.type) @bind
|
|
69
|
+
(parameters (identifier) @bind.name @bind.local) @bind
|
|
70
|
+
(default_parameter name: (identifier) @bind.name @bind.local) @bind
|
|
71
|
+
(parameters (list_splat_pattern (identifier) @bind.name @bind.local)) @bind
|
|
72
|
+
(parameters (dictionary_splat_pattern (identifier) @bind.name @bind.local)) @bind
|
|
73
|
+
(lambda_parameters (identifier) @bind.name @bind.local) @bind
|
|
40
74
|
(typed_default_parameter name: (identifier) @bind.name type: (type) @bind.type) @bind
|
|
41
75
|
(for_statement left: (identifier) @bind.name right: (_) @bind.elem) @bind
|
|
42
76
|
(for_in_clause left: (identifier) @bind.name right: (_) @bind.elem) @bind
|
|
@@ -87,6 +121,9 @@ function parseImport(node) {
|
|
|
87
121
|
if (c.type === 'dotted_name') {
|
|
88
122
|
const mod = dotted(c);
|
|
89
123
|
out.push({ source: mod, imported: '*', local: mod });
|
|
124
|
+
// `import a.b` also binds `a`, to the package a
|
|
125
|
+
const top = mod.split('.')[0];
|
|
126
|
+
if (top !== mod && !out.some(o => o.local === top)) out.push({ source: top, imported: '*', local: top });
|
|
90
127
|
} else if (c.type === 'aliased_import') {
|
|
91
128
|
const mod = dotted(c.childForFieldName('name'));
|
|
92
129
|
out.push({ source: mod, imported: '*', local: c.childForFieldName('alias')?.text || mod });
|
|
@@ -118,7 +155,13 @@ export const python = {
|
|
|
118
155
|
id: 'python',
|
|
119
156
|
// `self.x = …` inside a method declares an instance attribute of the class (first assignment wins)
|
|
120
157
|
hoistMemberFields: true,
|
|
158
|
+
// a bare name bound in the function (parameter, assignment, loop variable) is that local
|
|
159
|
+
localNames: true,
|
|
121
160
|
grammar: 'python',
|
|
161
|
+
// Python 3.15's lazy imports (PEP 810): `lazy` is a soft keyword before `import`/`from` at the
|
|
162
|
+
// start of a statement, which tree-sitter-python 0.25 does not know; blanked, the import
|
|
163
|
+
// statement is read as written (same length, so positions are unchanged)
|
|
164
|
+
rewriteSource: (src) => (src.includes('lazy') ? src.replace(/^([ \t]*)lazy(?=[ \t]+(?:import|from)[ \t])/gm, '$1 ') : src),
|
|
122
165
|
extensions: ['.py', '.pyi'],
|
|
123
166
|
family: 'python',
|
|
124
167
|
query: QUERY,
|
package/src/parse/lang/rust.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Rust extraction spec. `impl Type { fn m() }` methods are qualified as Type.m. */
|
|
2
2
|
|
|
3
|
-
const TH = (cap) => `[(type_identifier) @${cap} (generic_type type: (type_identifier) @${cap}) (scoped_type_identifier name: (type_identifier) @${cap}) (reference_type type: (type_identifier) @${cap}) (reference_type type: (generic_type type: (type_identifier) @${cap}))]`;
|
|
3
|
+
const TH = (cap) => `[(type_identifier) @${cap} (generic_type type: (type_identifier) @${cap}) (scoped_type_identifier path: (_) @recv name: (type_identifier) @${cap}) (reference_type type: (type_identifier) @${cap}) (reference_type type: (generic_type type: (type_identifier) @${cap}))]`;
|
|
4
4
|
// a whole type (normalizeType unwraps references, Box/Rc/Arc/Option/Result, and sees Vec<T> as T[])
|
|
5
|
-
const TW = (cap) => `[(type_identifier) (generic_type) (scoped_type_identifier) (reference_type) (array_type) (pointer_type)] @${cap}`;
|
|
5
|
+
const TW = (cap) => `[(type_identifier) (generic_type) (scoped_type_identifier) (reference_type) (array_type) (pointer_type) (tuple_type)] @${cap}`;
|
|
6
6
|
|
|
7
7
|
const QUERY = `
|
|
8
8
|
(function_item name: (identifier) @name return_type: ${TW('type')}) @def.function
|
|
@@ -32,6 +32,9 @@ const QUERY = `
|
|
|
32
32
|
(struct_expression name: (type_identifier) @name) @ref.new
|
|
33
33
|
(struct_expression name: (scoped_type_identifier path: (_) @recv name: (type_identifier) @name)) @ref.new
|
|
34
34
|
(type_identifier) @name @ref.type
|
|
35
|
+
(scoped_type_identifier path: (_) @recv name: (type_identifier) @name) @ref.type
|
|
36
|
+
(scoped_identifier path: [(identifier) @name (scoped_identifier path: (_) @recv name: (identifier) @name)]) @ref.type
|
|
37
|
+
(identifier) @name @ref.value
|
|
35
38
|
(impl_item trait: ${TH('name')}) @ref.inherit
|
|
36
39
|
(trait_item bounds: (trait_bounds ${TH('name')})) @ref.inherit
|
|
37
40
|
(arguments (identifier) @name) @ref.value
|
|
@@ -48,10 +51,24 @@ const QUERY = `
|
|
|
48
51
|
(let_declaration pattern: (mut_pattern (identifier) @bind.name) value: (_) @bind.expr) @bind
|
|
49
52
|
(for_expression pattern: (identifier) @bind.name value: (_) @bind.elem) @bind
|
|
50
53
|
(let_declaration pattern: (identifier) @bind.name value: (struct_expression name: (type_identifier) @bind.new)) @bind
|
|
51
|
-
(let_declaration pattern: (identifier) @bind.name value: (call_expression function: (scoped_identifier path: (identifier) @bind.new name: (identifier) @_ctor))) @bind
|
|
52
|
-
(let_declaration pattern: (mut_pattern (identifier) @bind.name) value: (call_expression function: (scoped_identifier path: (identifier) @bind.new name: (identifier) @_ctor))) @bind
|
|
54
|
+
(let_declaration pattern: (identifier) @bind.name value: (call_expression function: (scoped_identifier path: (identifier) @bind.new name: (identifier) @_ctor)) (#not-match? @bind.new "^(Arc|Rc|Box|Pin)$")) @bind
|
|
55
|
+
(let_declaration pattern: (mut_pattern (identifier) @bind.name) value: (call_expression function: (scoped_identifier path: (identifier) @bind.new name: (identifier) @_ctor)) (#not-match? @bind.new "^(Arc|Rc|Box|Pin)$")) @bind
|
|
53
56
|
(let_declaration pattern: (identifier) @bind.name value: (call_expression function: (identifier) @bind.call)) @bind
|
|
54
57
|
(parameter pattern: (identifier) @bind.name type: ${TW('bind.type')}) @bind
|
|
58
|
+
(parameter pattern: (mut_pattern (identifier) @bind.name) type: ${TW('bind.type')}) @bind
|
|
59
|
+
(let_declaration pattern: (tuple_pattern) @bind.tuple value: (_) @bind.expr) @bind
|
|
60
|
+
(let_condition pattern: (tuple_struct_pattern type: (identifier) @_wrap [(identifier) @bind.name (ref_pattern (identifier) @bind.name) (mut_pattern (identifier) @bind.name)]) value: (_) @bind.expr (#match? @_wrap "^(Some|Ok)$")) @bind
|
|
61
|
+
(struct_item name: (type_identifier) @tuple.owner body: (ordered_field_declaration_list) @tuple.fields)
|
|
62
|
+
(enum_variant name: (identifier) @tuple.variant body: (ordered_field_declaration_list) @tuple.fields)
|
|
63
|
+
(match_arm pattern: (match_pattern (tuple_struct_pattern) @bind.variant)) @bind
|
|
64
|
+
(let_condition pattern: (tuple_struct_pattern) @bind.variant) @bind
|
|
65
|
+
(match_expression value: (_) @bind.expr body: (match_block (match_arm pattern: (match_pattern . (identifier) @bind.name .)) @bind) (#match? @bind.name "^[a-z_]"))
|
|
66
|
+
(for_expression pattern: (tuple_pattern (_) (identifier) @bind.name .) value: (call_expression function: (field_expression value: (_) @bind.elem field: (field_identifier) @_en)) (#eq? @_en "enumerate")) @bind
|
|
67
|
+
(block) @scope.block
|
|
68
|
+
(match_arm) @scope.block
|
|
69
|
+
(if_expression) @scope.block
|
|
70
|
+
(while_expression) @scope.block
|
|
71
|
+
(for_expression) @scope.block
|
|
55
72
|
`;
|
|
56
73
|
|
|
57
74
|
const PRIMITIVES = new Set(['i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
|
|
@@ -62,16 +79,72 @@ const BUILTIN_MACROS = new Set(['println', 'print', 'eprintln', 'eprint', 'forma
|
|
|
62
79
|
'todo', 'matches', 'include_str', 'include_bytes', 'concat', 'stringify', 'env', 'cfg', 'dbg', 'format_args', 'Ok',
|
|
63
80
|
'Err', 'Some', 'None', 'Box', 'Default', 'drop', 'Into', 'From']);
|
|
64
81
|
|
|
82
|
+
/** The type a type expression names: `&'a mut Deserializer<R>`, `Box<T>` and `a::B<C>` name Deserializer, Box and B. */
|
|
83
|
+
function typeHead(t) {
|
|
84
|
+
for (let g = 0; t && g < 8; g++) {
|
|
85
|
+
if (t.type === 'type_identifier' || t.type === 'primitive_type') return t.text;
|
|
86
|
+
if (t.type === 'reference_type' || t.type === 'pointer_type' || t.type === 'generic_type') t = t.childForFieldName('type');
|
|
87
|
+
else if (t.type === 'scoped_type_identifier') t = t.childForFieldName('name');
|
|
88
|
+
else return null;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const implOf = (node) => (node.parent?.type === 'declaration_list' && node.parent.parent?.type === 'impl_item' ? node.parent.parent : null);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The type whose method a function in an impl block is. A blanket impl over a type parameter
|
|
97
|
+
* (`impl<'a, M: Matcher> Matcher for &'a M`, `… for Box<M>`) adds the trait's methods to every implementor:
|
|
98
|
+
* its methods belong to the trait.
|
|
99
|
+
*/
|
|
65
100
|
function implOwner(node) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
const impl = implOf(node);
|
|
102
|
+
if (!impl) return null;
|
|
103
|
+
const type = impl.childForFieldName('type');
|
|
104
|
+
const head = typeHead(type);
|
|
105
|
+
const params = (impl.childForFieldName('type_parameters')?.namedChildren ?? []).filter(p => p.type === 'type_parameter').map(p => p.childForFieldName('name')?.text);
|
|
106
|
+
// `impl<M: Matcher> Matcher for Box<M>`: a wrapper around the parameter is the parameter
|
|
107
|
+
let inner = head;
|
|
108
|
+
if (WRAPPERS.has(head) && type?.type === 'generic_type') inner = typeHead(type.childForFieldName('type_arguments')?.namedChildren[0]) ?? head;
|
|
109
|
+
const isParam = inner && params.includes(inner);
|
|
110
|
+
const trait = isParam ? impl.childForFieldName('trait') : null;
|
|
111
|
+
return trait ? typeHead(trait) ?? head : head;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The trait an impl block implements, for the functions declared in it (`impl fmt::Debug for Number` → Debug). */
|
|
115
|
+
function implTrait(node) {
|
|
116
|
+
if (node.type !== 'function_item' && node.type !== 'function_signature_item') return [];
|
|
117
|
+
const t = implOf(node)?.childForFieldName('trait');
|
|
118
|
+
const h = t ? typeHead(t) : null;
|
|
119
|
+
return h ? [h] : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// bounds that say nothing about the methods a value has
|
|
123
|
+
const MARKER_TRAITS = new Set(['Copy', 'Clone', 'Send', 'Sync', 'Sized', 'Unpin', 'Debug', 'Display', 'Default', 'Eq', 'PartialEq',
|
|
124
|
+
'Hash', 'Ord', 'PartialOrd', 'Any', 'UnwindSafe', 'RefUnwindSafe']);
|
|
125
|
+
const GENERIC_ITEMS = new Set(['function_item', 'function_signature_item', 'impl_item', 'trait_item', 'struct_item', 'enum_item']);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The traits a type parameter in scope at `node` is bound by (`fn f<R: Read>(r: &mut R)`,
|
|
129
|
+
* `impl<W, F: Formatter> Serializer<W, F>`, `where R: Read<'de>`), so `r.read()` and `R::read(x)`
|
|
130
|
+
* reach the trait's method (`A + B` when several give it methods: `W: AsyncWrite + AsyncSeek`);
|
|
131
|
+
* null when `name` is no type parameter there or its bounds are markers.
|
|
132
|
+
*/
|
|
133
|
+
function typeParamBound(node, name) {
|
|
134
|
+
for (let n = node; n; n = n.parent) {
|
|
135
|
+
if (!GENERIC_ITEMS.has(n.type)) continue;
|
|
136
|
+
let declared = false;
|
|
137
|
+
const bounds = [];
|
|
138
|
+
const take = (list) => { for (const b of list?.namedChildren ?? []) { const t = b.type === 'higher_ranked_trait_bound' ? b.childForFieldName('type') : b; const h = typeHead(t); if (h && !MARKER_TRAITS.has(h) && !bounds.includes(t.text)) bounds.push(t.text); } };
|
|
139
|
+
for (const p of n.childForFieldName('type_parameters')?.namedChildren ?? []) {
|
|
140
|
+
if (p.type !== 'type_parameter' || p.childForFieldName('name')?.text !== name) continue;
|
|
141
|
+
declared = true;
|
|
142
|
+
take(p.childForFieldName('bounds'));
|
|
143
|
+
}
|
|
144
|
+
for (const w of n.namedChildren.find(c => c.type === 'where_clause')?.namedChildren ?? []) {
|
|
145
|
+
if (w.type === 'where_predicate' && w.childForFieldName('left')?.text === name) { declared ||= GENERIC_ITEMS.has(n.type); take(w.childForFieldName('bounds')); }
|
|
146
|
+
}
|
|
147
|
+
if (declared) return bounds.length ? bounds.join(' + ') : null;
|
|
75
148
|
}
|
|
76
149
|
return null;
|
|
77
150
|
}
|
|
@@ -116,6 +189,102 @@ function parseImport(node) {
|
|
|
116
189
|
return out;
|
|
117
190
|
}
|
|
118
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Macro arguments are a token tree to tree-sitter: `tri!(self.parse(x))` and `assert_eq!(a.f(), 1)`
|
|
194
|
+
* hold calls the index would not see, and `cfg_rt! { pub struct Runtime … }` holds whole items. Before
|
|
195
|
+
* extraction, invocations whose arguments are plain Rust are rewritten in place, to text of the same
|
|
196
|
+
* length (so every position stays where it is): `m!(args)` → `m (args)` (a call of `m`), `m![args]`
|
|
197
|
+
* → ` [args]` (an array) and, where an item may stand, `m! { items }` → the items unwrapped.
|
|
198
|
+
* A rewrite is kept only if its text parses without errors on its own; macros inside the arguments
|
|
199
|
+
* become nodes once the outer one is rewritten, so this repeats a few rounds. Macro definitions
|
|
200
|
+
* (`macro_rules!`, with `$x` fragments) are left alone.
|
|
201
|
+
*/
|
|
202
|
+
function rewriteMacros(source, parse) {
|
|
203
|
+
let text = source;
|
|
204
|
+
for (let round = 0; round < 4; round++) {
|
|
205
|
+
const tree = parse(text);
|
|
206
|
+
const edits = [];
|
|
207
|
+
try {
|
|
208
|
+
const stack = [tree.rootNode];
|
|
209
|
+
while (stack.length) {
|
|
210
|
+
const n = stack.pop();
|
|
211
|
+
if (n.type === 'macro_definition') continue;
|
|
212
|
+
if (n.type === 'macro_invocation') {
|
|
213
|
+
const e = macroEdit(n, text, parse);
|
|
214
|
+
if (e) edits.push(e);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) stack.push(n.namedChild(i));
|
|
218
|
+
}
|
|
219
|
+
} finally { tree.delete(); }
|
|
220
|
+
if (!edits.length) break;
|
|
221
|
+
let out = '', at = 0;
|
|
222
|
+
for (const e of edits.sort((a, b) => a.from - b.from)) { out += text.slice(at, e.from) + e.text; at = e.to; }
|
|
223
|
+
text = out + text.slice(at);
|
|
224
|
+
}
|
|
225
|
+
return text;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ');
|
|
229
|
+
const ITEM_PARENTS = new Set(['source_file', 'declaration_list']);
|
|
230
|
+
|
|
231
|
+
/** The same-length rewrite of one macro invocation, or null when its arguments are not plain Rust. */
|
|
232
|
+
function macroEdit(node, text, parse) {
|
|
233
|
+
const macro = node.childForFieldName('macro');
|
|
234
|
+
const tt = node.namedChildren[node.namedChildCount - 1];
|
|
235
|
+
if (!macro || tt?.type !== 'token_tree' || macro.text === 'macro_rules') return null;
|
|
236
|
+
const bang = text.indexOf('!', macro.endIndex);
|
|
237
|
+
if (bang < 0 || bang >= tt.startIndex || text.slice(macro.endIndex, bang).trim()) return null;
|
|
238
|
+
const open = text[tt.startIndex], inner = text.slice(tt.startIndex + 1, tt.endIndex - 1);
|
|
239
|
+
let edit, probe;
|
|
240
|
+
if (open === '(') {
|
|
241
|
+
edit = { from: bang, to: bang + 1, text: ' ' };
|
|
242
|
+
probe = `fn __() { let _ = __(${inner}); }`;
|
|
243
|
+
} else if (open === '[') {
|
|
244
|
+
edit = { from: node.startIndex, to: bang + 1, text: blank(text.slice(node.startIndex, bang + 1)) };
|
|
245
|
+
probe = `fn __() { let _ = [${inner}]; }`;
|
|
246
|
+
} else if (open === '{' && ITEM_PARENTS.has(node.parent?.type) && inner.trim()) {
|
|
247
|
+
edit = { from: node.startIndex, to: tt.endIndex, text: blank(text.slice(node.startIndex, tt.startIndex + 1)) + inner + ' ' };
|
|
248
|
+
probe = `mod __ {${inner}}`;
|
|
249
|
+
} else return null;
|
|
250
|
+
// `$x` metavariables belong to a macro definition's body (a nested one is not a node here)
|
|
251
|
+
if (/\$[\w(]/.test(inner)) return null;
|
|
252
|
+
const t = parse(probe);
|
|
253
|
+
try { return t.rootNode.hasError ? null : edit; } finally { t.delete(); }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// `Arc::new(x)`, `Box::pin(f)`, `Pin::new(&mut x)`, `Rc::clone(&x)`: a value of the wrapped type
|
|
257
|
+
const WRAPPERS = new Set(['Arc', 'Rc', 'Box', 'Pin']);
|
|
258
|
+
const WRAP_CTORS = new Set(['new', 'pin', 'clone', 'new_unchecked', 'from']);
|
|
259
|
+
|
|
260
|
+
function wrappedValue(node, describe) {
|
|
261
|
+
// `(*self).m()`, `(**self).m()`: method calls dereference on their own
|
|
262
|
+
if (node.type === 'unary_expression' && node.text.startsWith('*') && node.namedChildren[0]) return describe(node.namedChildren[0]);
|
|
263
|
+
if (node.type !== 'call_expression') return undefined;
|
|
264
|
+
const fn = node.childForFieldName('function');
|
|
265
|
+
if (fn?.type !== 'scoped_identifier') return undefined;
|
|
266
|
+
const owner = fn.childForFieldName('path')?.text.split('::').pop();
|
|
267
|
+
if (!WRAPPERS.has(owner) || !WRAP_CTORS.has(fn.childForFieldName('name')?.text)) return undefined;
|
|
268
|
+
const args = node.childForFieldName('arguments')?.namedChildren.filter(c => !c.type.endsWith('comment')) ?? [];
|
|
269
|
+
return args.length === 1 ? describe(args[0]) : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A file whose own crate defines a type named like a transparent wrapper (tokio's `Mutex`,
|
|
274
|
+
* `RwLock`, `Semaphore`…) keeps that type: `inner: Mutex<Inner>` is tokio's Mutex, whose
|
|
275
|
+
* methods (`get_mut`, `lock`) are the repository's, not a view of Inner.
|
|
276
|
+
*/
|
|
277
|
+
function specForSource(source) {
|
|
278
|
+
let shadowed = null;
|
|
279
|
+
for (const t of rust.transparentTypes) {
|
|
280
|
+
if (!source.includes(t)) continue;
|
|
281
|
+
const own = new RegExp(`\\b(?:struct|enum|type|trait|union)\\s+${t}\\b|\\buse\\s+(?:crate|self|super)::[^;]*\\b${t}\\b[^;]*;`).test(source);
|
|
282
|
+
if (own) (shadowed ??= new Set()).add(t);
|
|
283
|
+
}
|
|
284
|
+
if (!shadowed) return null;
|
|
285
|
+
return { ...rust, transparentTypes: new Set([...rust.transparentTypes].filter(t => !shadowed.has(t))) };
|
|
286
|
+
}
|
|
287
|
+
|
|
119
288
|
function visibility(node) {
|
|
120
289
|
const v = node.namedChildren.find(c => c.type === 'visibility_modifier');
|
|
121
290
|
return v ? 'public' : 'private';
|
|
@@ -135,7 +304,23 @@ export const rust = {
|
|
|
135
304
|
commentTypes: ['line_comment', 'block_comment'],
|
|
136
305
|
skipBeforeDoc: new Set(['attribute_item', 'inner_attribute_item']),
|
|
137
306
|
parseImport,
|
|
307
|
+
rewriteSource: rewriteMacros,
|
|
308
|
+
specForSource,
|
|
309
|
+
receiverDescriptor: wrappedValue,
|
|
310
|
+
typeParamBound,
|
|
311
|
+
tupleTypes: true,
|
|
312
|
+
shadowing: true,
|
|
313
|
+
pathCalls: true,
|
|
138
314
|
ownerOf: implOwner,
|
|
315
|
+
implicitBases: implTrait,
|
|
316
|
+
// the impl a method is declared in (`impl Cursor<Vec<u8>>`): methods of one name in impls of one
|
|
317
|
+
// type are `#[cfg]` variants of one method, in impls of different types (or traits) methods of their own
|
|
318
|
+
decoratorsOf: (node) => {
|
|
319
|
+
const impl = (node.type === 'function_item' || node.type === 'function_signature_item') ? implOf(node) : null;
|
|
320
|
+
const t = impl?.childForFieldName('type');
|
|
321
|
+
return t ? ['impl ' + t.text.replace(/\s+/g, ' ')] : [];
|
|
322
|
+
},
|
|
323
|
+
traitPathCalls: true,
|
|
139
324
|
refineKind(kind, d, parent) {
|
|
140
325
|
if (kind === 'function' && implOwner(d.node)) return 'method';
|
|
141
326
|
return kind;
|
|
@@ -143,6 +328,7 @@ export const rust = {
|
|
|
143
328
|
isExported: (node) => node.namedChildren.some(c => c.type === 'visibility_modifier') || node.parent?.parent?.type === 'trait_item',
|
|
144
329
|
visibility,
|
|
145
330
|
isPrimitiveType: (t) => PRIMITIVES.has(t),
|
|
331
|
+
markerTraits: MARKER_TRAITS,
|
|
146
332
|
transparentTypes: new Set(['Box', 'Rc', 'Arc', 'Cow', 'RefCell', 'Cell', 'Mutex', 'RwLock', 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', 'Ref', 'RefMut', 'Pin', 'Option', 'Result', 'Weak']),
|
|
147
333
|
elementTypes: new Set(['Vec', 'VecDeque', 'HashSet', 'BTreeSet', 'BinaryHeap', 'LinkedList', 'IntoIter', 'Iter', 'IterMut', 'Drain']),
|
|
148
334
|
elementMethods: new Set(['first', 'last', 'get', 'get_mut', 'first_mut', 'last_mut', 'pop', 'pop_front', 'pop_back', 'front', 'back', 'remove', 'swap_remove', 'next', 'peek', 'find']),
|
|
@@ -151,7 +337,21 @@ export const rust = {
|
|
|
151
337
|
mapValueMethods: new Set(['values', 'values_mut', 'into_values']),
|
|
152
338
|
// calls that hand back (a view of) the same value: unwrap, clone, borrow, lock, iter …
|
|
153
339
|
identityMethods: new Set(['unwrap', 'expect', 'unwrap_or_default', 'unwrap_or', 'unwrap_or_else', 'as_ref', 'as_mut', 'as_deref', 'as_deref_mut',
|
|
154
|
-
'clone', 'borrow', 'borrow_mut', 'lock', 'read', 'write', 'to_owned', 'deref', 'deref_mut', 'cloned', 'copied', 'iter', 'iter_mut', 'into_iter', 'drain', 'upgrade'
|
|
155
|
-
|
|
340
|
+
'clone', 'borrow', 'borrow_mut', 'lock', 'read', 'write', 'to_owned', 'deref', 'deref_mut', 'cloned', 'copied', 'iter', 'iter_mut', 'into_iter', 'drain', 'upgrade',
|
|
341
|
+
'get_mut', 'get_ref', 'into_inner', 'take',
|
|
342
|
+
// pin-project's projections have the fields of the value they project (`self.project().inner`)
|
|
343
|
+
'project', 'project_ref']),
|
|
344
|
+
normalizeRefName(name, r) {
|
|
345
|
+
if (r.node.type === 'macro_invocation') return BUILTIN_MACROS.has(name) ? null : name;
|
|
346
|
+
// a type named by the path of `Type::item` / `Enum::Variant`, and a unit struct or constant
|
|
347
|
+
// used as a value (`&Mmap`), are capitalised; modules and locals are not
|
|
348
|
+
if ((r.kind === 'type' && r.node.type === 'scoped_identifier') || (r.kind === 'value' && r.node.type === 'identifier')) {
|
|
349
|
+
if (!/^[A-Z]/.test(name)) return null;
|
|
350
|
+
// the last segment of a path (`Value::Number`) is the path's own reference
|
|
351
|
+
if (r.kind === 'value' && (r.nameNode.parent?.type === 'scoped_identifier' || r.nameNode.parent?.type === 'scoped_type_identifier')) return null;
|
|
352
|
+
for (let p = r.nameNode.parent; p; p = p.parent) if (p.type === 'token_tree' || p.type === 'use_declaration' || p.type === 'attribute_item' || p.type === 'macro_definition') return null;
|
|
353
|
+
}
|
|
354
|
+
return name;
|
|
355
|
+
},
|
|
156
356
|
bodyField: 'body',
|
|
157
357
|
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-file components: Vue (`.vue`) and Svelte (`.svelte`). The `<script>` blocks are TypeScript
|
|
3
|
+
* or JavaScript, extracted as such; the template is where a component uses others, the edge that
|
|
4
|
+
* "find the uses of this component" needs.
|
|
5
|
+
*
|
|
6
|
+
* The source is rewritten before parsing, every line kept where it is: the script blocks stay as
|
|
7
|
+
* they are, everything else becomes blank, and the template becomes a function — the component's
|
|
8
|
+
* default export, named in PascalCase after its file (`…Component` when the script imports another
|
|
9
|
+
* component of that name; `render` when the script has
|
|
10
|
+
* its own `export default {…}`, the Options API) — in which each tag of a component the script
|
|
11
|
+
* imports or declares is a call at the tag's line: `<UserCard :user="u" />` and
|
|
12
|
+
* `<user-card>` are calls to `UserCard`, bound through the script's import like any other.
|
|
13
|
+
* Tags of components registered globally (`app.component(…)`) and not named in the script are left
|
|
14
|
+
* out, as are HTML elements and the frameworks' own (`<Transition>`, `<svelte:self>`).
|
|
15
|
+
*/
|
|
16
|
+
import { typescript } from './ecma.mjs';
|
|
17
|
+
|
|
18
|
+
const BUILTIN = new Set(['Transition', 'TransitionGroup', 'KeepAlive', 'Teleport', 'Suspense', 'Component', 'Slot', 'Template', 'RouterView', 'RouterLink']);
|
|
19
|
+
|
|
20
|
+
const pascal = (tag) => tag.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());
|
|
21
|
+
|
|
22
|
+
/** [start, end) offsets of the content of each `<name …>…</name>` block at the top level. */
|
|
23
|
+
function blocks(source, name) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const open = new RegExp(`<${name}(\\s[^>]*)?>`, 'gi');
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = open.exec(source))) {
|
|
28
|
+
const start = m.index + m[0].length;
|
|
29
|
+
const close = source.toLowerCase().indexOf(`</${name}>`, start);
|
|
30
|
+
if (close < 0) break;
|
|
31
|
+
out.push([start, close, m.index]);
|
|
32
|
+
open.lastIndex = close + name.length + 3;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Vue's template: from its opening tag to the last `</template>` (templates nest, for slots). */
|
|
38
|
+
function vueTemplate(source) {
|
|
39
|
+
const m = /<template(\s[^>]*)?>/i.exec(source);
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
const end = source.toLowerCase().lastIndexOf('</template>');
|
|
42
|
+
return end > m.index ? [m.index + m[0].length, end, m.index] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rewrite(source, kind) {
|
|
46
|
+
const scripts = blocks(source, 'script');
|
|
47
|
+
const scriptText = scripts.map(([a, b]) => source.slice(a, b)).join('\n');
|
|
48
|
+
// keep the scripts' characters, blank the rest (newlines kept, so every position stays)
|
|
49
|
+
const keep = new Uint8Array(source.length);
|
|
50
|
+
for (const [a, b] of scripts) keep.fill(1, a, b);
|
|
51
|
+
let blanked = '';
|
|
52
|
+
for (let i = 0; i < source.length; i++) blanked += keep[i] || source[i] === '\n' ? source[i] : (source[i] === '\t' ? '\t' : ' ');
|
|
53
|
+
const lines = blanked.split('\n');
|
|
54
|
+
const lineOf = (off) => { let n = 0; for (let i = 0; i < off; i++) if (source[i] === '\n') n++; return n; };
|
|
55
|
+
|
|
56
|
+
// the markup: Vue's <template>, or for Svelte everything outside <script> and <style>
|
|
57
|
+
let ranges;
|
|
58
|
+
if (kind === 'vue') { const t = vueTemplate(source); ranges = t ? [t] : []; }
|
|
59
|
+
else {
|
|
60
|
+
const skip = [...scripts, ...blocks(source, 'style')].map(([a, b, tag]) => [tag, source.indexOf('>', b) + 1]).sort((x, y) => x[0] - y[0]);
|
|
61
|
+
ranges = [];
|
|
62
|
+
let at = 0;
|
|
63
|
+
for (const [a, b] of skip) { if (a > at) ranges.push([at, a, at]); at = Math.max(at, b); }
|
|
64
|
+
if (at < source.length) ranges.push([at, source.length, at]);
|
|
65
|
+
ranges = ranges.filter(([a, b]) => source.slice(a, b).trim());
|
|
66
|
+
}
|
|
67
|
+
if (!ranges.length) return lines.join('\n');
|
|
68
|
+
const known = (name) => !BUILTIN.has(name) && new RegExp(`\\b${name}\\b`).test(scriptText);
|
|
69
|
+
const tags = [];
|
|
70
|
+
for (const [a, b] of ranges) {
|
|
71
|
+
const markup = source.slice(a, b).replace(/<!--[\s\S]*?-->/g, (c) => c.replace(/[^\n]/g, ' '));
|
|
72
|
+
for (const m of markup.matchAll(/<([A-Za-z][\w-]*)/g)) {
|
|
73
|
+
const raw = m[1];
|
|
74
|
+
const name = kind === 'vue' && raw.includes('-') ? pascal(raw) : raw;
|
|
75
|
+
if (!/^[A-Z]/.test(name) || !known(name)) continue;
|
|
76
|
+
const off = a + m.index;
|
|
77
|
+
const line = lineOf(off);
|
|
78
|
+
tags.push({ name, line, col: off - (source.lastIndexOf('\n', off - 1) + 1) + 1 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// write `name` at `col` of a line, past what was written there already; a line grows if it must
|
|
82
|
+
const cursor = new Map();
|
|
83
|
+
const put = (line, col, text) => {
|
|
84
|
+
let l = lines[line];
|
|
85
|
+
const at = Math.max(col, cursor.get(line) ?? 0);
|
|
86
|
+
if (l.length < at + text.length) l = l.padEnd(at + text.length, ' ');
|
|
87
|
+
lines[line] = l.slice(0, at) + text + l.slice(at + text.length);
|
|
88
|
+
cursor.set(line, at + text.length + 1);
|
|
89
|
+
};
|
|
90
|
+
const first = lineOf(ranges[0][2]), last = lineOf(ranges[ranges.length - 1][1]);
|
|
91
|
+
const optionsApi = kind === 'vue' && /^\s*export\s+default\b/m.test(scriptText);
|
|
92
|
+
put(first, 0, optionsApi ? 'function render() {' : 'export default function () {');
|
|
93
|
+
for (const t of tags) put(t.line, t.col, `${t.name}();`);
|
|
94
|
+
put(last, 0, '}');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// compiler macros of `<script setup>` (Vue) and runes (Svelte 5): not functions anyone defines
|
|
99
|
+
const MACROS = {
|
|
100
|
+
vue: ['defineProps', 'defineEmits', 'defineExpose', 'defineSlots', 'defineModel', 'defineOptions', 'withDefaults'],
|
|
101
|
+
svelte: ['$state', '$derived', '$effect', '$props', '$bindable', '$inspect', '$host'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const sfc = (id, extensions) => ({
|
|
105
|
+
...typescript,
|
|
106
|
+
id,
|
|
107
|
+
extensions,
|
|
108
|
+
builtinCalls: new Set([...typescript.builtinCalls, ...MACROS[id]]),
|
|
109
|
+
// the component is named in PascalCase after its file (`user-card.vue` → UserCard), as importers name it
|
|
110
|
+
normalizeName: (name, d, relPath) => {
|
|
111
|
+
const n = typescript.normalizeName(name, d, relPath);
|
|
112
|
+
if (d.node.type !== 'export_statement' || !n) return n;
|
|
113
|
+
const comp = n[0].toUpperCase() + n.slice(1);
|
|
114
|
+
// a component that imports another of its own name (`Valves.svelte` using common/Valves):
|
|
115
|
+
// its template's tags are that import, so the component itself takes another name
|
|
116
|
+
const root = d.node.tree?.rootNode ?? d.node.parent;
|
|
117
|
+
const imports = (root?.namedChildren ?? []).filter(c => c.type === 'import_statement').map(c => c.text).join('\n');
|
|
118
|
+
return new RegExp(`\\b${comp}\\b`).test(imports) ? `${comp}Component` : comp;
|
|
119
|
+
},
|
|
120
|
+
rewriteSource: (source) => rewrite(source, id),
|
|
121
|
+
testFile: /(^|\/)(__tests__|tests?|spec|e2e)\//,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const vue = sfc('vue', ['.vue']);
|
|
125
|
+
export const svelte = sfc('svelte', ['.svelte']);
|