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.
Files changed (52) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/README.md +61 -8
  3. package/bin/graph-indexer.mjs +46 -5
  4. package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
  5. package/integrations/claude-code/agents/code-structure.md +0 -3
  6. package/integrations/claude-code/skills/setup/SKILL.md +26 -0
  7. package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
  8. package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
  9. package/integrations/claude-code-local/README.md +10 -0
  10. package/integrations/claude-code-local/agents/code-structure.md +22 -0
  11. package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +3 -1
  12. package/package.json +1 -1
  13. package/src/cli/helper.mjs +15 -1
  14. package/src/cli/hook.mjs +123 -41
  15. package/src/cli/init.mjs +16 -1
  16. package/src/cli/resident.mjs +1 -1
  17. package/src/index/indexer.mjs +77 -41
  18. package/src/index/modules.mjs +76 -2
  19. package/src/index/parse-worker.mjs +12 -0
  20. package/src/index/parsepool.mjs +73 -0
  21. package/src/index/resolver.mjs +362 -23
  22. package/src/mcp/server.mjs +16 -12
  23. package/src/mcp/tools.mjs +417 -25
  24. package/src/parse/arity.mjs +39 -2
  25. package/src/parse/extract.mjs +269 -45
  26. package/src/parse/lang/ecma.mjs +2 -0
  27. package/src/parse/lang/go.mjs +2 -0
  28. package/src/parse/lang/jvm.mjs +33 -0
  29. package/src/parse/lang/markdown.mjs +92 -0
  30. package/src/parse/lang/python.mjs +43 -0
  31. package/src/parse/lang/rust.mjs +215 -15
  32. package/src/parse/lang/sfc.mjs +125 -0
  33. package/src/parse/languages.mjs +9 -2
  34. package/src/query/check.mjs +49 -13
  35. package/src/query/facts.mjs +3 -2
  36. package/src/query/injection-spring.mjs +405 -0
  37. package/src/query/injection.mjs +173 -0
  38. package/src/query/intel.mjs +76 -14
  39. package/src/query/issue.mjs +156 -0
  40. package/src/query/orm.mjs +286 -0
  41. package/src/query/read.mjs +6 -1
  42. package/src/query/report.mjs +53 -0
  43. package/src/query/routes.mjs +142 -0
  44. package/src/query/showcase.mjs +59 -0
  45. package/src/search/search.mjs +85 -17
  46. package/src/search/semantic.mjs +201 -0
  47. package/src/search/vectors.mjs +122 -0
  48. package/src/store/db.mjs +22 -4
  49. package/vendor/grammars/README.md +47 -0
  50. package/vendor/grammars/kotlin.wasm.gz +0 -0
  51. package/vendor/grammars/manifest.json +4 -4
  52. /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
@@ -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,
@@ -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
- // function_item inside (impl_item body: (declaration_list ...))
67
- const list = node.parent;
68
- const impl = list?.parent;
69
- if (list?.type === 'declaration_list' && impl?.type === 'impl_item') {
70
- const t = impl.childForFieldName('type');
71
- if (!t) return null;
72
- if (t.type === 'type_identifier') return t.text;
73
- const inner = t.childForFieldName('type') ?? t.childForFieldName('name');
74
- return inner?.type === 'type_identifier' ? inner.text : t.text.replace(/<.*$/s, '').split('::').pop();
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
- normalizeRefName: (name, r) => (r.node.type === 'macro_invocation' ? (BUILTIN_MACROS.has(name) ? null : name) : name),
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']);