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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency injection: the edge between a value a class receives and the code that provides it,
|
|
3
|
+
* which the compiler does not see. In NestJS a constructor parameter marked `@Inject(TOKEN)` gets
|
|
4
|
+
* whatever a module registered as `{ provide: TOKEN, useClass | useExisting | useFactory | useValue }`;
|
|
5
|
+
* its declared type is often an interface or a union, so neither the type checker nor grep for the
|
|
6
|
+
* type name leads to the implementation.
|
|
7
|
+
*
|
|
8
|
+
* Recognised from the indexed source (TypeScript):
|
|
9
|
+
* - providers: an object with `provide:` and one of `useClass`, `useExisting`, `useFactory`,
|
|
10
|
+
* `useValue`, on one line or over the lines that follow;
|
|
11
|
+
* - injection points: `@Inject(TOKEN)` on a constructor parameter or property.
|
|
12
|
+
* Test files are left out: their providers are mocks.
|
|
13
|
+
* A token is the definition the index binds it to (a constant, a class, a symbol); a string, a
|
|
14
|
+
* constant declared as that string and `Class.name` are one token. An expression such as
|
|
15
|
+
* `getRepositoryToken(User)` is matched by its text, only when written the same way, and marked so;
|
|
16
|
+
* tokens computed at run time (`Object.values(Namespace).map(ns => ({ provide: ns, … }))`) are not
|
|
17
|
+
* found. Microservice clients registered with `ClientsModule.register([{ name: TOKEN }])` are
|
|
18
|
+
* providers too.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { collectSpring } from './injection-spring.mjs';
|
|
22
|
+
|
|
23
|
+
const PROVIDE = /\bprovide\s*:\s*([^,}\n]+?)\s*(?:,|}|$)/;
|
|
24
|
+
const USE = /\buse(Class|Existing|Factory|Value)\s*:\s*([^,}\n]*)/;
|
|
25
|
+
const INJECT = /@Inject\(\s*([^)]*?)\s*\)/g;
|
|
26
|
+
const PARAM = /^\s*(?:(?:private|public|protected|readonly|override)\s+)*([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*([^,=)]+)/;
|
|
27
|
+
|
|
28
|
+
const strip = (s) => String(s ?? '').trim().replace(/\s+/g, ' ');
|
|
29
|
+
|
|
30
|
+
/** The definition a name on a line is bound to, through the index. */
|
|
31
|
+
function bound(intel, fileId, line, name) {
|
|
32
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return null;
|
|
33
|
+
const r = intel.store.get('SELECT dst_id FROM refs WHERE file_id = ? AND line = ? AND name = ? AND dst_id IS NOT NULL LIMIT 1', fileId, line, name);
|
|
34
|
+
return r ? symRow(intel, r.dst_id) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function symRow(intel, id) {
|
|
38
|
+
return intel.store.get('SELECT s.id, s.name, s.qname, s.kind, s.start_line, s.end_line, s.parent_id, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id = ?', id) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A constant's string value (`const REFLECTOR = 'Reflector'`), if it is declared as one. */
|
|
42
|
+
function stringValue(intel, s) {
|
|
43
|
+
if (s.kind !== 'constant' && s.kind !== 'variable') return null;
|
|
44
|
+
const l = intel.fileLines(s.path)?.[s.start_line - 1] ?? '';
|
|
45
|
+
return new RegExp(`\\b${s.name}\\s*(?::[^=]+)?=\\s*(['"\`])([^'"\`]*)\\1\\s*;?\\s*$`).exec(l)?.[2] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A token's identity: the definition it names, or its text (strings and expressions). A string,
|
|
50
|
+
* a constant holding one and `Class.name` are the same token when the strings are equal; `p.provide`
|
|
51
|
+
* is the token of the provider declared as `p`.
|
|
52
|
+
*/
|
|
53
|
+
function tokenKey(intel, fileId, line, text) {
|
|
54
|
+
const t = strip(text);
|
|
55
|
+
const dotted = /^([\w$]+)\.(name|provide)$/.exec(t);
|
|
56
|
+
if (dotted) {
|
|
57
|
+
const head = bound(intel, fileId, line, dotted[1]);
|
|
58
|
+
if (head && dotted[2] === 'name' && head.kind === 'class') return { key: `str:${head.name}`, token: t, sym: head, exact: true };
|
|
59
|
+
if (head && dotted[2] === 'provide') return { key: `holder:${head.id}`, token: t, sym: head, exact: true };
|
|
60
|
+
}
|
|
61
|
+
const last = /^(?:[\w$]+\.)*([\w$]+)$/.exec(t)?.[1];
|
|
62
|
+
let s = last ? bound(intel, fileId, line, last) : null;
|
|
63
|
+
// a member of a type (`FactoryProvider.provide`) is a shape, not a token
|
|
64
|
+
if (s && (s.kind === 'field' || s.kind === 'property')) s = null;
|
|
65
|
+
if (s) {
|
|
66
|
+
const v = stringValue(intel, s);
|
|
67
|
+
return { key: v != null ? `str:${v}` : `id:${s.id}`, token: s.qname, sym: s, exact: true };
|
|
68
|
+
}
|
|
69
|
+
const lit = /^(['"`])(.*)\1$/.exec(t);
|
|
70
|
+
return { key: lit ? `str:${lit[2]}` : `text:${t}`, token: t, sym: null, exact: !!lit };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The variable or constant a line is declared in (`const catsProvider = { provide: … }`). */
|
|
74
|
+
function holderAt(intel, fileId, line) {
|
|
75
|
+
return intel.store.get(`SELECT id FROM symbols WHERE file_id = ? AND kind IN ('variable', 'constant') AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1`, fileId, line, line)?.id ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Is the line inside an interface or a type alias? */
|
|
79
|
+
function inTypeDeclaration(intel, fileId, line) {
|
|
80
|
+
const s = intel.store.get(`SELECT kind FROM symbols WHERE file_id = ? AND start_line <= ? AND end_line >= ? AND kind IN ('class', 'interface', 'type', 'function', 'method', 'variable', 'constant')
|
|
81
|
+
ORDER BY start_line DESC LIMIT 1`, fileId, line, line);
|
|
82
|
+
return s?.kind === 'interface' || s?.kind === 'type';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The innermost class around a line. */
|
|
86
|
+
function classAt(intel, fileId, line) {
|
|
87
|
+
return intel.store.get(`SELECT s.id, s.name, s.qname, s.kind, s.start_line, s.end_line, s.parent_id, f.path FROM symbols s JOIN files f ON f.id = s.file_id
|
|
88
|
+
WHERE s.file_id = ? AND s.kind = 'class' AND s.start_line <= ? AND s.end_line >= ? ORDER BY s.start_line DESC LIMIT 1`, fileId, line, line) ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Providers and injection points of the repository, keyed by what is asked for (a NestJS token, a
|
|
93
|
+
* Spring type): { providers: [{ key, token, exact, how, target, targetText, file, line }],
|
|
94
|
+
* injections: [{ key, token, exact, owner, param, type, file, line, from, why? }] }, `from`
|
|
95
|
+
* being the providers the point may receive (Spring: the beans chosen, see injection-spring.mjs).
|
|
96
|
+
*/
|
|
97
|
+
export function collectInjection(intel) {
|
|
98
|
+
if (intel._injection?.version === intel.ix?.version) return intel._injection.data;
|
|
99
|
+
const providers = [], injections = [];
|
|
100
|
+
// test modules register mocks (`{ provide: Service, useValue: { get: jest.fn() } }`): not the app's wiring
|
|
101
|
+
const files = intel.store.all(`SELECT id, path FROM files WHERE lang IN ('typescript', 'tsx') AND is_test = 0`);
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
const lines = intel.fileLines(f.path);
|
|
104
|
+
if (!lines) continue;
|
|
105
|
+
const text = lines.join('\n');
|
|
106
|
+
if (!text.includes('provide') && !text.includes('@Inject(') && !text.includes('ClientsModule')) continue;
|
|
107
|
+
lines.forEach((l, i) => {
|
|
108
|
+
const line = i + 1;
|
|
109
|
+
if (/^\s*(\/\/|\/?\*)/.test(l)) return;
|
|
110
|
+
const p = PROVIDE.exec(l);
|
|
111
|
+
// `provide: InjectionToken;` in an interface or type declares the shape, it provides nothing
|
|
112
|
+
if (p && !inTypeDeclaration(intel, f.id, line)) {
|
|
113
|
+
// the `use…` of this provider: on its line, or on the lines below before the next one
|
|
114
|
+
let u = USE.exec(l.slice(p.index)), at = line;
|
|
115
|
+
for (let j = i + 1; !u && j < Math.min(lines.length, i + 15) && !PROVIDE.test(lines[j]); j++) { u = USE.exec(lines[j]); at = j + 1; }
|
|
116
|
+
if (u) {
|
|
117
|
+
const tk = tokenKey(intel, f.id, line, p[1]);
|
|
118
|
+
const targetText = strip(u[2]) || `(${u[1] === 'Value' ? 'a value' : 'code'} on the lines below)`;
|
|
119
|
+
const name = /^(?:new\s+)?(?:[\w$]+\.)*([\w$]+)\b/.exec(targetText)?.[1];
|
|
120
|
+
let target = name && !/^(async|function)$/.test(name) && !/=>/.test(targetText.split('(')[0]) ? bound(intel, f.id, at, name) : null;
|
|
121
|
+
// `useClass: options.useClass` passes along what a caller gave: not a definition here
|
|
122
|
+
if (target && (target.kind === 'field' || target.kind === 'property')) target = null;
|
|
123
|
+
providers.push({ ...tk, how: `use${u[1]}`, target, targetText, file: f.path, line, holder: holderAt(intel, f.id, line) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// a microservice client: `ClientsModule.register([{ name: MATH_SERVICE, transport: … }])`
|
|
127
|
+
const client = /^\s*name\s*:\s*([^,}\n]+?)\s*(?:,|}|$)/.exec(l) ?? /\{\s*name\s*:\s*([^,}\n]+?)\s*[,}]/.exec(l);
|
|
128
|
+
if (client && lines.slice(Math.max(0, i - 6), i + 1).some(x => /\bClientsModule\.register(Async)?\s*\(/.test(x))) {
|
|
129
|
+
providers.push({ ...tokenKey(intel, f.id, line, client[1]), how: 'ClientsModule', target: null, targetText: 'a client proxy for the microservice', file: f.path, line, holder: null });
|
|
130
|
+
}
|
|
131
|
+
for (const m of l.matchAll(INJECT)) {
|
|
132
|
+
if (!m[1]) continue;
|
|
133
|
+
const tk = tokenKey(intel, f.id, line, m[1]);
|
|
134
|
+
// the parameter or property it decorates: the rest of the line, or the next line
|
|
135
|
+
const rest = l.slice(m.index + m[0].length);
|
|
136
|
+
const pm = PARAM.exec(rest.trim() ? rest : (lines[i + 1] ?? ''));
|
|
137
|
+
injections.push({ ...tk, owner: classAt(intel, f.id, line), param: pm?.[1] ?? null, type: pm ? strip(pm[2]).replace(/;$/, '') : null, file: f.path, line });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// `@Inject(p.provide)`: the token of the provider declared as `p`
|
|
142
|
+
for (const j of injections) {
|
|
143
|
+
if (!j.key.startsWith('holder:')) continue;
|
|
144
|
+
const p = providers.find(x => x.holder === Number(j.key.slice(7)));
|
|
145
|
+
if (p) Object.assign(j, { key: p.key, token: `${j.token} (${p.token})` });
|
|
146
|
+
}
|
|
147
|
+
for (const j of injections) j.from = providers.filter(p => p.key === j.key);
|
|
148
|
+
const spring = collectSpring(intel);
|
|
149
|
+
const data = { providers: [...providers, ...spring.providers], injections: [...injections, ...spring.injections] };
|
|
150
|
+
intel._injection = { version: intel.ix?.version, data };
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** For a definition: the tokens it provides, each with where it is injected. */
|
|
155
|
+
export function providedBy(intel, id) {
|
|
156
|
+
const { providers, injections } = collectInjection(intel);
|
|
157
|
+
return providers.filter(p => p.target?.id === id).map(p => ({ ...p, into: injections.filter(j => j.from.includes(p)) }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** For a token or a type asked for by injection: what provides it and where it is injected. */
|
|
161
|
+
export function tokenUses(intel, id) {
|
|
162
|
+
const { providers, injections } = collectInjection(intel);
|
|
163
|
+
const key = `id:${id}`;
|
|
164
|
+
const js = injections.filter(j => j.key === key);
|
|
165
|
+
// a NestJS token's providers name it; a Spring type's are the beans its injection points receive
|
|
166
|
+
const ps = [...new Set([...providers.filter(p => p.key === key && !p.types), ...js.flatMap(j => j.from)])];
|
|
167
|
+
return { providers: ps, injections: js };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What a class receives by injection (`@Inject(TOKEN)`, Spring's injection points), each with its providers. */
|
|
171
|
+
export function injectedInto(intel, classId) {
|
|
172
|
+
return collectInjection(intel).injections.filter(j => j.owner?.id === classId);
|
|
173
|
+
}
|
package/src/query/intel.mjs
CHANGED
|
@@ -173,9 +173,18 @@ export class CodeIntel {
|
|
|
173
173
|
}
|
|
174
174
|
const norm = t.replace(/::|#|->/g, '.');
|
|
175
175
|
if (norm.includes('.')) {
|
|
176
|
-
const
|
|
177
|
-
const rows = this.store.all(`SELECT id, qname FROM symbols
|
|
178
|
-
|
|
176
|
+
const qual = norm.split('.'), name = qual.pop();
|
|
177
|
+
const rows = this.store.all(`SELECT s.id, s.qname, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.name = ?`, name);
|
|
178
|
+
const exact = rows.filter(r => r.qname === norm || r.qname.endsWith('.' + norm));
|
|
179
|
+
if (exact.length) return { matches: this.#rank(exact.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'qualified' };
|
|
180
|
+
// `module.func`, `pkg.mod.func`, `ns::Type::method`, `Foo.prototype.bar`: the owner named
|
|
181
|
+
// last before the name is one of the definition's enclosing names, its file or a directory
|
|
182
|
+
const owner = qual[qual.length - 1];
|
|
183
|
+
const inModule = rows.filter(r => r.qname.split('.').slice(0, -1).includes(owner) || r.path.replace(/\.[^./]+$/, '').split('/').includes(owner));
|
|
184
|
+
if (inModule.length) return { matches: this.#rank(inModule.map(r => r.id)).slice(0, limit).map(id => this.sym(id)), how: 'module' };
|
|
185
|
+
// the qualifier names nothing that declares `name`: answering with another owner's
|
|
186
|
+
// `name` would present the wrong definition's uses as the asked one's
|
|
187
|
+
if (rows.length) return { matches: [], how: 'qualifier-unknown', sameName: this.#rank(rows.map(r => r.id)).slice(0, limit).map(id => this.sym(id)) };
|
|
179
188
|
}
|
|
180
189
|
let rows = this.store.all('SELECT id FROM symbols WHERE name = ?', norm.split('.').pop());
|
|
181
190
|
if (!rows.length) rows = this.store.all('SELECT id FROM symbols WHERE name_lc = ?', norm.split('.').pop().toLowerCase());
|
|
@@ -198,6 +207,32 @@ export class CodeIntel {
|
|
|
198
207
|
return [...ids].sort((a, b) => score(b) - score(a) || a - b);
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Where `word` occurs in code the parser could not read (an ERROR region, give or take a
|
|
212
|
+
* line): a declaration or a use of it there may be missing from the index or bound wrongly.
|
|
213
|
+
* The first such line per file.
|
|
214
|
+
* @returns {{ path: string, line: number }[]}
|
|
215
|
+
*/
|
|
216
|
+
filesWithParseErrors(word, { lang = null, max = 50 } = {}) {
|
|
217
|
+
if (!word || !/^[\w$]+$/.test(word)) return [];
|
|
218
|
+
const re = new RegExp(`(?<![\\w$])${escapeRegExp(word)}(?![\\w$])`);
|
|
219
|
+
const rows = this.store.all(`SELECT path, parse_error_lines FROM files WHERE parse_errors > 0${lang ? ' AND lang = ?' : ''} ORDER BY is_test, path LIMIT 5000`, ...(lang ? [lang] : []));
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const r of rows) {
|
|
222
|
+
let regions;
|
|
223
|
+
try { regions = JSON.parse(r.parse_error_lines ?? '[]'); } catch { regions = []; }
|
|
224
|
+
const lines = regions.length ? this.fileLines(r.path) : null;
|
|
225
|
+
if (!lines) continue;
|
|
226
|
+
hit: for (const [a, b] of regions) {
|
|
227
|
+
for (let ln = Math.max(1, a - 1); ln <= Math.min(lines.length, b + 1); ln++) {
|
|
228
|
+
if (re.test(lines[ln - 1])) { out.push({ path: r.path, line: ln }); break hit; }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (out.length >= max) break;
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
201
236
|
members(id) {
|
|
202
237
|
return this.store.all('SELECT id, name, qname, kind, start_line, end_line, sig, visibility FROM symbols WHERE parent_id = ? ORDER BY start_line', id);
|
|
203
238
|
}
|
|
@@ -235,12 +270,14 @@ export class CodeIntel {
|
|
|
235
270
|
|
|
236
271
|
/**
|
|
237
272
|
* The subtypes of a member's type, transitively, split into those that inherit the member — they
|
|
238
|
-
* run it as their own, so a change to it reaches them —
|
|
239
|
-
* version
|
|
240
|
-
*
|
|
273
|
+
* run it as their own, so a change to it reaches them — those that extend it (they redeclare it,
|
|
274
|
+
* but their version uses this one: `TRANSFORMS = {**Base.TRANSFORMS, …}`, `super().m()`; a change
|
|
275
|
+
* still reaches them, and their subtypes that inherit their version), and those that override it
|
|
276
|
+
* (their own version runs without this one).
|
|
277
|
+
* @returns {{ inherit: object[], extend: object[], override: object[] }} index rows of the subtypes
|
|
241
278
|
*/
|
|
242
279
|
inheritance(id, { max = 30 } = {}) {
|
|
243
|
-
const out = { inherit: [], override: [] };
|
|
280
|
+
const out = { inherit: [], extend: [], override: [] };
|
|
244
281
|
const sym = this.sym(id);
|
|
245
282
|
if (!sym) return out;
|
|
246
283
|
const type = this.ix.resolver.enclosingType(sym.id);
|
|
@@ -263,20 +300,30 @@ export class CodeIntel {
|
|
|
263
300
|
frontier = next;
|
|
264
301
|
}
|
|
265
302
|
const declares = new Map();
|
|
266
|
-
const declared = (tid) => { if (!declares.has(tid)) declares.set(tid, this.#membersNamed(tid, sym.name)
|
|
303
|
+
const declared = (tid) => { if (!declares.has(tid)) declares.set(tid, this.#membersNamed(tid, sym.name)); return declares.get(tid).length > 0; };
|
|
304
|
+
// a redeclaration that reads the member of a type after it in the resolution order uses that version
|
|
305
|
+
const usesNext = (tid, rest) => {
|
|
306
|
+
const later = new Set(rest.flatMap(t => declared(t) ? declares.get(t) : []));
|
|
307
|
+
return declares.get(tid).some(id => this.store.all('SELECT dst_id FROM refs WHERE src_id = ? AND dst_id IS NOT NULL', id).some(r => later.has(r.dst_id)));
|
|
308
|
+
};
|
|
267
309
|
for (const sid of subs) {
|
|
268
310
|
const order = this.ix.resolver.mro(sid);
|
|
269
311
|
if (!order.includes(type.id)) continue;
|
|
270
|
-
const
|
|
312
|
+
const declarers = order.filter(declared);
|
|
313
|
+
const owner = declarers[0];
|
|
314
|
+
// follow the redeclarations that use the next version down to this one
|
|
315
|
+
let k = 0;
|
|
316
|
+
while (declarers[k] !== type.id && k + 1 < declarers.length && usesNext(declarers[k], order.slice(order.indexOf(declarers[k]) + 1))) k++;
|
|
271
317
|
let bucket = null;
|
|
272
318
|
if (owner === type.id) bucket = out.inherit;
|
|
319
|
+
else if (declarers[k] === type.id) bucket = out.extend;
|
|
273
320
|
// its own version, where it would otherwise run this one
|
|
274
|
-
else if (owner === sid &&
|
|
321
|
+
else if (owner === sid && declarers[1] === type.id) bucket = out.override;
|
|
275
322
|
if (!bucket) continue;
|
|
276
323
|
const st = this.sym(sid);
|
|
277
324
|
if (!st) continue;
|
|
278
325
|
bucket.push(st);
|
|
279
|
-
if (out.inherit.length + out.override.length >= max) return out;
|
|
326
|
+
if (out.inherit.length + out.extend.length + out.override.length >= max) return out;
|
|
280
327
|
}
|
|
281
328
|
return out;
|
|
282
329
|
}
|
|
@@ -308,8 +355,23 @@ export class CodeIntel {
|
|
|
308
355
|
overloads(sym) {
|
|
309
356
|
// where each overload is a method of its own (Java, C#, Kotlin…), calls are bound to the one they reach
|
|
310
357
|
if (this.ix.resolver.specs[sym.lang]?.distinctOverloads) return [];
|
|
311
|
-
|
|
312
|
-
|
|
358
|
+
// methods of one name from impls of different traits or types (Rust `Debug::fmt` / `Display::fmt`,
|
|
359
|
+
// `impl AsyncWrite for Cursor<Vec<u8>>` / `… for Cursor<&mut [u8]>`) are not redeclarations
|
|
360
|
+
return this.store.all('SELECT id FROM symbols WHERE file_id = ? AND qname = ? AND kind = ? AND (parent_id IS ? OR parent_id = ?) AND is_static = ? AND (kind != \'method\' OR (bases IS ? AND (? != \'rust\' OR decorators IS ?)))',
|
|
361
|
+
sym.file_id, sym.qname, sym.kind, sym.parent_id, sym.parent_id, sym.is_static ? 1 : 0, sym.bases ?? null, sym.lang, sym.decorators ?? null).map(r => r.id);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A Rust trait method's implementations (`fn m` in each `impl Trait for X`): calls of them are
|
|
366
|
+
* calls of the trait's method, as rust-analyzer counts them, so they are its references.
|
|
367
|
+
*/
|
|
368
|
+
#traitImpls(sym) {
|
|
369
|
+
if (sym.lang !== 'rust' || sym.kind !== 'method' || sym.parent_id == null) return [];
|
|
370
|
+
const trait = this.sym(sym.parent_id);
|
|
371
|
+
if (trait?.kind !== 'trait') return [];
|
|
372
|
+
const r = this.ix.resolver;
|
|
373
|
+
return this.store.all("SELECT s.id, s.file_id FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.name = ? AND s.kind = 'method' AND f.lang = 'rust' AND s.bases LIKE ?", sym.name, `%"${trait.name}"%`)
|
|
374
|
+
.filter(m => m.id !== sym.id && r.resolveTypeName(trait.name, m.file_id)?.id === trait.id).map(m => m.id);
|
|
313
375
|
}
|
|
314
376
|
|
|
315
377
|
/** Is `memberId` one of the members `sym` overrides (declared in a supertype)? */
|
|
@@ -391,7 +453,7 @@ export class CodeIntel {
|
|
|
391
453
|
references(id, { kinds = null, includeTests = true, minConf = 0, family = true } = {}) {
|
|
392
454
|
const sym = this.sym(id);
|
|
393
455
|
if (!sym) return null;
|
|
394
|
-
const ids = [...new Set([id, ...this.overloads(sym)])];
|
|
456
|
+
const ids = [...new Set([id, ...this.overloads(sym), ...this.#traitImpls(sym)])];
|
|
395
457
|
const via = new Map();
|
|
396
458
|
if (family && !sym.is_static && (sym.kind === 'method' || sym.kind === 'property' || sym.kind === 'field')) {
|
|
397
459
|
for (const s of this.#supertypeMembers(sym)) if (!ids.includes(s.id)) { ids.push(s.id); via.set(s.id, s.via); }
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an issue points in the code. An issue names things in code spans, examples and tracebacks
|
|
3
|
+
* (`DATE_DIFF`, `rich_club_coefficient(normalized=True)`, `exp.Agg`); the code to change is where
|
|
4
|
+
* several of those names meet. Each name is matched against identifiers by their words, so
|
|
5
|
+
* `DATE_DIFF`, `DateDiff` and `_date_diff_sql` are one term, and each definition scores the
|
|
6
|
+
* rarity (inverse document frequency) of the distinct terms its lines carry, plus the terms its
|
|
7
|
+
* own name or its file's path is made of. Library code only: tests are left out.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
// words of examples that name the language, not the code under it
|
|
13
|
+
const NOISE = new Set(('true false none null undefined self cls this def class return import from print len str int float bool list dict set tuple range '
|
|
14
|
+
+ 'isinstance assert lambda and or not in is if else elif for while with as pass raise try except finally yield async await new let const var '
|
|
15
|
+
+ 'function public private static void string object type repr').split(' '));
|
|
16
|
+
const IDENT = /[A-Za-z_][A-Za-z0-9_]*/g;
|
|
17
|
+
const WORD = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
|
|
18
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
19
|
+
// examples, benchmarks and docs are not where an issue gets fixed
|
|
20
|
+
const ASIDE_PATH = /(^|\/)(examples?|samples?|demos?|sandbox|playground|docs?(_src|_source|_examples?)?|documentation|benchmarks?|bench|scripts?|fixtures?|testdata|tutorials?|website|\.github)\//i;
|
|
21
|
+
// measured on the dev split of bench/agentic/localize.mjs: mild length normalisation (BM25's b),
|
|
22
|
+
// the title's terms four times an example's, tables and lists of names at 0.6 of a definition
|
|
23
|
+
const B = 0.2;
|
|
24
|
+
const TITLE_W = 4;
|
|
25
|
+
const VALUE_W = 0.6;
|
|
26
|
+
const VALUE_KINDS = new Set(['field', 'property', 'variable', 'constant']);
|
|
27
|
+
|
|
28
|
+
const partsCache = new Map();
|
|
29
|
+
function parts(id) {
|
|
30
|
+
let p = partsCache.get(id);
|
|
31
|
+
if (!p) {
|
|
32
|
+
p = (id.match(WORD) ?? []).map(w => w.toLowerCase());
|
|
33
|
+
if (partsCache.size > 200_000) partsCache.clear();
|
|
34
|
+
partsCache.set(id, p);
|
|
35
|
+
}
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether an issue reads like one: long enough, or carrying code. */
|
|
40
|
+
export function isIssueText(text) {
|
|
41
|
+
const t = String(text ?? '');
|
|
42
|
+
return t.length >= 240 || /```/.test(t) || (t.match(/`[^`\n]+`/g) ?? []).length >= 3;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The code-like names an issue uses: from its code spans and blocks, and identifier-shaped words of its prose. */
|
|
46
|
+
export function issueTerms(text) {
|
|
47
|
+
const t = String(text ?? '');
|
|
48
|
+
const code = [];
|
|
49
|
+
// a fence's language tag (```python) is not part of the code
|
|
50
|
+
const prose = t.replace(/```[^\n]*\n([\s\S]*?)```/g, (_, m) => { code.push(m); return ' '; })
|
|
51
|
+
.replace(/`([^`\n]+)`/g, (_, m) => { code.push(m); return ' '; });
|
|
52
|
+
// the title names what the issue is about; examples name everything they touch
|
|
53
|
+
const title = new Set((t.trim().split('\n')[0].match(IDENT) ?? []).map(x => parts(x).join('_')));
|
|
54
|
+
const ids = [];
|
|
55
|
+
for (const c of code) for (const m of c.matchAll(IDENT)) ids.push(m[0]);
|
|
56
|
+
// prose words that can only be code: snake_case, camelCase, dotted paths
|
|
57
|
+
for (const m of prose.matchAll(/[A-Za-z_][\w]*(?:\.[A-Za-z_]\w*)+|\b[a-z]+_[a-z_\d]+\b|\b[a-z]+[A-Z]\w*\b|\b[A-Z][a-z]+[A-Z]\w*\b/g)) for (const x of m[0].split('.')) ids.push(x);
|
|
58
|
+
const terms = new Map();
|
|
59
|
+
for (const id of ids) {
|
|
60
|
+
const p = parts(id);
|
|
61
|
+
if (!p.length || NOISE.has(id.toLowerCase())) continue;
|
|
62
|
+
if (p.length === 1 && p[0].length < 3) continue;
|
|
63
|
+
if (p.every(w => /^\d+$/.test(w))) continue;
|
|
64
|
+
const key = p.join('_');
|
|
65
|
+
if (!terms.has(key)) terms.set(key, { key, parts: p, forms: new Set(), title: title.has(key) });
|
|
66
|
+
terms.get(key).forms.add(id);
|
|
67
|
+
}
|
|
68
|
+
return [...terms.values()];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Definitions ranked by the issue's terms they carry. Returns [{ id, score, terms }] (best first).
|
|
73
|
+
* @param {import('./intel.mjs').CodeIntel} intel
|
|
74
|
+
*/
|
|
75
|
+
export function issueSeeds(intel, text, { limit = 12, path: prefix = null } = {}) {
|
|
76
|
+
const terms = issueTerms(text);
|
|
77
|
+
if (!terms.length) return [];
|
|
78
|
+
const byFirst = new Map();
|
|
79
|
+
for (const t of terms) { if (!byFirst.has(t.parts[0])) byFirst.set(t.parts[0], []); byFirst.get(t.parts[0]).push(t); }
|
|
80
|
+
const files = intel.store.all('SELECT id, path, size FROM files WHERE is_test = 0');
|
|
81
|
+
const hits = new Map(); // symbol id -> Set(term key)
|
|
82
|
+
const owned = new Map(); // symbol id -> lines it owns (its own, not its members')
|
|
83
|
+
const df = new Map(); // term key -> number of definitions carrying it
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
if (prefix && !f.path.startsWith(prefix)) continue;
|
|
87
|
+
if (f.size > MAX_FILE_BYTES) continue;
|
|
88
|
+
if (ASIDE_PATH.test(f.path) && !(prefix && ASIDE_PATH.test(prefix))) continue;
|
|
89
|
+
const syms = intel.store.all('SELECT id, start_line, end_line FROM symbols WHERE file_id = ? ORDER BY start_line', f.id);
|
|
90
|
+
total += syms.length;
|
|
91
|
+
if (!syms.length) continue;
|
|
92
|
+
let text;
|
|
93
|
+
try { text = fs.readFileSync(path.join(intel.root, f.path), 'utf8'); } catch { continue; }
|
|
94
|
+
const lines = text.split('\n');
|
|
95
|
+
// innermost definition of each line
|
|
96
|
+
const owner = new Int32Array(lines.length + 2).fill(-1);
|
|
97
|
+
const span = new Int32Array(lines.length + 2).fill(1 << 30);
|
|
98
|
+
for (const s of syms) {
|
|
99
|
+
const w = s.end_line - s.start_line;
|
|
100
|
+
for (let l = s.start_line; l <= Math.min(s.end_line, lines.length); l++) if (w < span[l]) { span[l] = w; owner[l] = s.id; }
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < lines.length; i++) {
|
|
103
|
+
const o = owner[i + 1];
|
|
104
|
+
if (o < 0) continue;
|
|
105
|
+
owned.set(o, (owned.get(o) ?? 0) + 1);
|
|
106
|
+
for (const m of lines[i].matchAll(IDENT)) {
|
|
107
|
+
const p = parts(m[0]);
|
|
108
|
+
for (let k = 0; k < p.length; k++) {
|
|
109
|
+
const cands = byFirst.get(p[k]);
|
|
110
|
+
if (!cands) continue;
|
|
111
|
+
for (const t of cands) {
|
|
112
|
+
if (k + t.parts.length > p.length) continue;
|
|
113
|
+
let ok = true;
|
|
114
|
+
for (let j = 1; j < t.parts.length; j++) if (p[k + j] !== t.parts[j]) { ok = false; break; }
|
|
115
|
+
if (!ok) continue;
|
|
116
|
+
let set = hits.get(o);
|
|
117
|
+
if (!set) hits.set(o, (set = new Set()));
|
|
118
|
+
if (!set.has(t.key)) { set.add(t.key); df.set(t.key, (df.get(t.key) ?? 0) + 1); }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!hits.size) return [];
|
|
125
|
+
const idf = (k) => Math.log(1 + total / (df.get(k) ?? total));
|
|
126
|
+
const rows = new Map();
|
|
127
|
+
const ids = [...hits.keys()];
|
|
128
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
129
|
+
const chunk = ids.slice(i, i + 500);
|
|
130
|
+
for (const r of intel.store.all(`SELECT s.id, s.name, s.kind, s.start_line, s.end_line, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.id IN (${chunk.map(() => '?').join(',')})`, ...chunk)) rows.set(r.id, r);
|
|
131
|
+
}
|
|
132
|
+
// longer definitions carry more terms by chance: BM25's length normalisation, over owned lines
|
|
133
|
+
let sumLen = 0;
|
|
134
|
+
for (const v of owned.values()) sumLen += v;
|
|
135
|
+
const avgLen = sumLen / Math.max(1, owned.size);
|
|
136
|
+
const byKey = new Map(terms.map(t => [t.key, t]));
|
|
137
|
+
const scored = [];
|
|
138
|
+
for (const [id, set] of hits) {
|
|
139
|
+
const r = rows.get(id);
|
|
140
|
+
if (!r) continue;
|
|
141
|
+
let score = 0;
|
|
142
|
+
for (const k of set) score += idf(k) * (byKey.get(k)?.title ? TITLE_W : 1);
|
|
143
|
+
// its own name is a term the issue uses: the issue names this definition
|
|
144
|
+
const own = parts(r.name).join('_');
|
|
145
|
+
if (set.has(own)) score += idf(own);
|
|
146
|
+
// its file's path is made of a term (a dialect, a format, a module named in the issue)
|
|
147
|
+
const pathWords = new Set(r.path.split(/[/._-]+/).flatMap(parts));
|
|
148
|
+
for (const t of terms) if (t.parts.length === 1 && pathWords.has(t.parts[0]) && !set.has(t.key)) score += 0.5 * idf(t.key);
|
|
149
|
+
score /= 1 - B + B * ((owned.get(id) ?? 1) / avgLen);
|
|
150
|
+
// a table or list of names (`__all__`, a function map) enumerates terms rather than combining them
|
|
151
|
+
if (VALUE_KINDS.has(r.kind)) score *= VALUE_W;
|
|
152
|
+
scored.push({ id, score, terms: [...set] });
|
|
153
|
+
}
|
|
154
|
+
scored.sort((a, b) => b.score - a.score || a.id - b.id);
|
|
155
|
+
return scored.slice(0, limit);
|
|
156
|
+
}
|