graph-indexer 3.0.1 → 3.1.1
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 +456 -0
- package/README.md +61 -8
- package/SECURITY.md +35 -20
- 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 +15 -4
- package/integrations/opencode/graph-indexer.js +10 -3
- package/package.json +1 -1
- package/src/cli/helper.mjs +15 -1
- package/src/cli/hook.mjs +124 -40
- package/src/cli/init.mjs +32 -5
- package/src/cli/resident.mjs +19 -4
- package/src/index/discover.mjs +52 -30
- package/src/index/indexer.mjs +78 -42
- package/src/index/modules.mjs +75 -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 +43 -13
- package/src/mcp/tools.mjs +418 -26
- 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 +126 -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 +406 -0
- package/src/query/injection.mjs +174 -0
- package/src/query/intel.mjs +85 -15
- package/src/query/issue.mjs +156 -0
- package/src/query/orm.mjs +288 -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/query/textsearch.mjs +30 -9
- 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 +33 -4
- package/src/util/git.mjs +60 -11
- package/src/util/paths.mjs +10 -3
- 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/languages.mjs
CHANGED
|
@@ -12,8 +12,15 @@ import { rust } from './lang/rust.mjs';
|
|
|
12
12
|
import { c, cpp } from './lang/native.mjs';
|
|
13
13
|
import { ruby, php, bash } from './lang/scripting.mjs';
|
|
14
14
|
import { scala, css } from './lang/misc.mjs';
|
|
15
|
+
import { vue, svelte } from './lang/sfc.mjs';
|
|
16
|
+
import { markdown } from './lang/markdown.mjs';
|
|
15
17
|
|
|
16
|
-
export const LANGUAGES = [javascript, typescript, tsx, python, go, java, kotlin, scala, csharp, rust, c, cpp, ruby, php, bash, css];
|
|
18
|
+
export const LANGUAGES = [javascript, typescript, tsx, vue, svelte, python, go, java, kotlin, scala, csharp, rust, c, cpp, ruby, php, bash, css, markdown];
|
|
19
|
+
|
|
20
|
+
/** A code language (parsed with a grammar), not documentation. */
|
|
21
|
+
export function isCodeSpec(spec) { return !!spec && !spec.docs; }
|
|
22
|
+
/** The spec of a code file at this path, or null (documentation and unknown files). */
|
|
23
|
+
export function codeSpecForPath(p) { const s = specForPath(p); return s && !s.docs ? s : null; }
|
|
17
24
|
|
|
18
25
|
const byExt = new Map();
|
|
19
26
|
for (const spec of LANGUAGES) for (const ext of spec.extensions) byExt.set(ext, spec);
|
|
@@ -33,7 +40,7 @@ export function languageById(id) { return LANGUAGES.find(l => l.id === id) ?? nu
|
|
|
33
40
|
// constructors named by the language rather than declared as such: a method called `initialize` is
|
|
34
41
|
// Ruby's constructor (`Foo.new`) but an ordinary method in TypeScript or Python
|
|
35
42
|
const CONSTRUCTOR_NAMES = {
|
|
36
|
-
javascript: ['constructor'], typescript: ['constructor'], tsx: ['constructor'],
|
|
43
|
+
javascript: ['constructor'], typescript: ['constructor'], tsx: ['constructor'], vue: ['constructor'], svelte: ['constructor'],
|
|
37
44
|
python: ['__init__', '__new__'], ruby: ['initialize'], php: ['__construct'],
|
|
38
45
|
};
|
|
39
46
|
|
package/src/query/check.mjs
CHANGED
|
@@ -9,10 +9,15 @@
|
|
|
9
9
|
* removed definitions that were deleted or renamed but are still used or imported
|
|
10
10
|
* untouched callers of a changed signature in files the change did not touch (to review)
|
|
11
11
|
* inherited subclasses that inherit a changed member (they run the new code) or override it
|
|
12
|
-
* tests the test files that exercise the changed code, and the command to run them
|
|
12
|
+
* tests the test files that exercise the changed code, and the command to run them;
|
|
13
|
+
* then, apart, the test files that name a changed module in a string (dialect,
|
|
14
|
+
* plugin or backend lookups such as `read="mysql"`, which no reference shows)
|
|
13
15
|
*/
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { codeSpecForPath as specForPath, isConstructor } from '../parse/languages.mjs'; // documentation is not checked
|
|
20
|
+
import { prepareLanguage, extractFile, parseSource } from '../parse/extract.mjs';
|
|
16
21
|
import { paramArity, callAt, callArgc, fits, describeArity } from '../parse/arity.mjs';
|
|
17
22
|
import { testCommands, testCaseCommands } from './testcmd.mjs';
|
|
18
23
|
import { escapeRegExp } from '../util/regex.mjs';
|
|
@@ -63,7 +68,7 @@ function changedFiles(root, base) {
|
|
|
63
68
|
|
|
64
69
|
/** Parse a source text: tree + extracted symbols, each symbol with its definition node's arity. */
|
|
65
70
|
async function analyse(spec, source, rel, tree = null) {
|
|
66
|
-
if (!tree) { const { parser } = await prepareLanguage(spec); tree = parser
|
|
71
|
+
if (!tree) { const { parser } = await prepareLanguage(spec); tree = parseSource(spec, parser, source); }
|
|
67
72
|
const ex = await extractFile(spec, source, rel);
|
|
68
73
|
for (const s of ex.symbols) {
|
|
69
74
|
if (!CALLABLE.has(s.kind) && !(s.kind === 'field' && /=>|function/.test(s.sig ?? ''))) continue;
|
|
@@ -142,7 +147,32 @@ function unionFits(argc, arities) {
|
|
|
142
147
|
return list.some(r => fits(argc, r));
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
|
|
150
|
+
const GENERIC_STEMS = new Set(['__init__', 'index', 'main', 'mod', 'lib', 'core', 'base', 'utils', 'util', 'helpers', 'common', 'types']);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Test files that mention a changed source module's name as a string literal ("mysql" for
|
|
154
|
+
* generators/mysql.py): code reached by a registry lookup — a dialect, plugin or backend chosen by
|
|
155
|
+
* name — that no reference shows. A second tier: broader than the files the index links to.
|
|
156
|
+
*/
|
|
157
|
+
function testsNamingModules(intel, files) {
|
|
158
|
+
const stems = new Set();
|
|
159
|
+
for (const f of files) {
|
|
160
|
+
if (intel.store.get('SELECT is_test FROM files WHERE path = ?', f)?.is_test) continue;
|
|
161
|
+
const stem = path.posix.basename(f).replace(/\.[^.]+$/, '');
|
|
162
|
+
if (stem.length >= 3 && !GENERIC_STEMS.has(stem) && /^\w+$/.test(stem)) stems.add(stem);
|
|
163
|
+
}
|
|
164
|
+
if (!stems.size) return [];
|
|
165
|
+
const re = new RegExp(`["'](${[...stems].join('|')})["']`);
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const { path: p } of intel.store.all('SELECT path FROM files WHERE is_test = 1 ORDER BY path')) {
|
|
168
|
+
let text;
|
|
169
|
+
try { text = fs.readFileSync(path.join(intel.root, p), 'utf8'); } catch { continue; }
|
|
170
|
+
if (re.test(text)) out.push(p);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function checkChanges(intel, { base = 'HEAD', files = null, tests = true, byName = false } = {}) {
|
|
146
176
|
const root = intel.root;
|
|
147
177
|
if (!isRevision(base)) return { error: `Not a git revision: ${base}` };
|
|
148
178
|
const changed = changedFiles(root, base);
|
|
@@ -151,7 +181,7 @@ export async function checkChanges(intel, { base = 'HEAD', files = null, tests =
|
|
|
151
181
|
await intel.ensureFresh();
|
|
152
182
|
await intel.ix.syncPaths([...changed.keys()]);
|
|
153
183
|
const targets = [...changed.keys()].filter(f => specForPath(f) && (!files || files.some(x => f === x || f.startsWith(x.replace(/\/?$/, '/')))));
|
|
154
|
-
const report = { base, files: targets.length, syntax: [], arity: [], removed: [], untouched: [], inherited: [], tests: [], commands: [], cases: [], casesTotal: 0, caseCommands: [], notes: [] };
|
|
184
|
+
const report = { base, files: targets.length, syntax: [], arity: [], removed: [], untouched: [], inherited: [], tests: [], commands: [], byName: [], byNameCommands: [], cases: [], casesTotal: 0, caseCommands: [], notes: [] };
|
|
155
185
|
if (!targets.length) return report;
|
|
156
186
|
// callers only need a syntax tree; definitions (changed files, callees) also need symbols + arities
|
|
157
187
|
const treeCache = new Map(), symCache = new Map();
|
|
@@ -163,7 +193,7 @@ export async function checkChanges(intel, { base = 'HEAD', files = null, tests =
|
|
|
163
193
|
const src = spec ? readRepoFile(root, rel, MAX_FILE_BYTES) : null;
|
|
164
194
|
if (src !== null) {
|
|
165
195
|
const { parser } = await prepareLanguage(spec);
|
|
166
|
-
v = { tree: parser
|
|
196
|
+
v = { tree: parseSource(spec, parser, src), lines: src.split(/\r?\n/), src, spec };
|
|
167
197
|
}
|
|
168
198
|
} catch { v = null; }
|
|
169
199
|
treeCache.set(rel, v);
|
|
@@ -299,19 +329,19 @@ export async function checkChanges(intel, { base = 'HEAD', files = null, tests =
|
|
|
299
329
|
const members = new Map();
|
|
300
330
|
for (const id of seeds) { const m = memberOf(intel, id); if (m && !m.is_test) members.set(m.id, m); }
|
|
301
331
|
for (const m of members.values()) {
|
|
302
|
-
const { inherit, override } = intel.inheritance(m.id);
|
|
303
|
-
if (!inherit.length && !override.length) continue;
|
|
332
|
+
const { inherit, extend, override } = intel.inheritance(m.id);
|
|
333
|
+
if (!inherit.length && !extend.length && !override.length) continue;
|
|
304
334
|
const owner = m.parent_id != null ? intel.sym(m.parent_id) : null;
|
|
305
335
|
const type = owner?.qname ?? m.qname.split('.').slice(0, -1).join('.');
|
|
306
|
-
const e = byType.get(type) ?? byType.set(type, { type, path: m.path, members: [], inherit: new Map(), override: [] }).get(type);
|
|
336
|
+
const e = byType.get(type) ?? byType.set(type, { type, path: m.path, members: [], inherit: new Map(), extend: new Map(), override: [] }).get(type);
|
|
307
337
|
e.members.push(m.name);
|
|
308
|
-
for (const
|
|
338
|
+
for (const [list, into] of [[inherit, e.inherit], [extend, e.extend]]) for (const s of list) {
|
|
309
339
|
inheritorFiles.add(s.path); // a test subclass is not listed, but its file is a test to run
|
|
310
|
-
if (!s.is_test)
|
|
340
|
+
if (!s.is_test) into.set(s.id, { qname: s.qname, path: s.path });
|
|
311
341
|
}
|
|
312
342
|
for (const s of override) if (!s.is_test) e.override.push({ qname: `${s.qname}.${m.name}`, path: s.path });
|
|
313
343
|
}
|
|
314
|
-
report.inherited = [...byType.values()].map(e => ({ ...e, inherit: [...e.inherit.values()] })).filter(e => e.inherit.length || e.override.length);
|
|
344
|
+
report.inherited = [...byType.values()].map(e => ({ ...e, inherit: [...e.inherit.values()], extend: [...e.extend.values()] })).filter(e => e.inherit.length || e.extend.length || e.override.length);
|
|
315
345
|
|
|
316
346
|
// ── tests to run ─────────────────────────────────────────────────────────────
|
|
317
347
|
if (seeds.length) {
|
|
@@ -324,6 +354,12 @@ export async function checkChanges(intel, { base = 'HEAD', files = null, tests =
|
|
|
324
354
|
for (const rel of targets) if (intel.store.get('SELECT is_test FROM files WHERE path = ?', rel)?.is_test) tests.add(rel);
|
|
325
355
|
report.tests = [...tests].sort();
|
|
326
356
|
report.commands = testCommands(root, report.tests.slice(0, 40));
|
|
357
|
+
// opt-in: the test files that name a changed module in a string (measured: more of the tests
|
|
358
|
+
// a fix breaks are found, fewer test files are avoided, and nothing gained outside sqlglot)
|
|
359
|
+
if (byName) {
|
|
360
|
+
report.byName = testsNamingModules(intel, [...targets, ...inheritorFiles]).filter(t => !tests.has(t));
|
|
361
|
+
report.byNameCommands = testCommands(root, report.byName.slice(0, 40));
|
|
362
|
+
}
|
|
327
363
|
// the test functions closest to the change: changed themselves, calling it, or calling a
|
|
328
364
|
// function that calls it — running these first verifies a fix in seconds
|
|
329
365
|
const near = new Map(seeds.map(id => [id, 0]));
|
package/src/query/facts.mjs
CHANGED
|
@@ -219,10 +219,11 @@ export function factsForRange(intel, file, from, to, { max = 6, shown = new Set(
|
|
|
219
219
|
if (fam.up.length) parts.push(`overrides ${fam.up.slice(0, 2).map(u => `${short(u)} (${loc(u)})`).join(', ')}`);
|
|
220
220
|
if (inh.override.length) parts.push(`overridden in ${inh.override.length} subclass${inh.override.length > 1 ? 'es' : ''}: ${inh.override.slice(0, 4).map(o => `${o.name} (${o.path})`).join(', ')}${inh.override.length > 4 ? ', …' : ''}`);
|
|
221
221
|
if (inh.inherit.length) parts.push(`inherited unchanged by ${inh.inherit.length}: ${inh.inherit.slice(0, 5).map(o => o.name).join(', ')}${inh.inherit.length > 5 ? ', …' : ''}`);
|
|
222
|
-
if (
|
|
222
|
+
if (inh.extend.length) parts.push(`extended (their version uses this one) by ${inh.extend.length}: ${inh.extend.slice(0, 5).map(o => o.name).join(', ')}${inh.extend.length > 5 ? ', …' : ''}`);
|
|
223
|
+
if (parts.length && (fam.up.length || inh.override.length || inh.inherit.length >= 1 || inh.extend.length)) {
|
|
223
224
|
const downIds = fam.down.map(d => d.id);
|
|
224
225
|
push({ kind: 'overrides', tier: 'exact', key: `ovr:${s.id}`, targets: [...fam.up.map(u => u.id), ...downIds],
|
|
225
|
-
text: `${s.qname}: ${parts.join('; ')}` }, inh.override.length || inh.inherit.length ? 4 : 2);
|
|
226
|
+
text: `${s.qname}: ${parts.join('; ')}` }, inh.override.length || inh.inherit.length || inh.extend.length ? 4 : 2);
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
// decorators
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring dependency injection: which bean an injection point receives. A field, constructor or
|
|
3
|
+
* `@Bean` parameter declares a type, usually an interface; Spring gives it the bean of that type
|
|
4
|
+
* (a class annotated as a component, or what a `@Bean` method returns), choosing among several by
|
|
5
|
+
* `@Qualifier`, `@Primary` and then the parameter's name. The compiler sees the interface, not the
|
|
6
|
+
* implementation that runs.
|
|
7
|
+
*
|
|
8
|
+
* Recognised from the indexed source (Java):
|
|
9
|
+
* - beans: classes annotated @Component, @Service, @Repository, @Controller, @RestController,
|
|
10
|
+
* @Configuration, @SpringBootApplication (and the advice variants), @ConfigurationProperties classes
|
|
11
|
+
* that something registers (@EnableConfigurationProperties naming them, or a
|
|
12
|
+
* @ConfigurationPropertiesScan), with the name Spring gives them (the annotation's
|
|
13
|
+
* value or the decapitalised class name), @Primary, @Qualifier and the conditions that may
|
|
14
|
+
* leave them out (@Profile, @Conditional…); `@Bean` methods, by return type, with their names;
|
|
15
|
+
* - beans the framework generates at run time, as such: Feign clients (@FeignClient, a primary
|
|
16
|
+
* proxy), Spring Data repositories (interfaces extending a `…Repository`) and MyBatis mappers
|
|
17
|
+
* (@Mapper, or interfaces in a package named by @MapperScan);
|
|
18
|
+
* - injection points: fields and setters with @Autowired, @Inject or @Resource; the parameters of
|
|
19
|
+
* a component's constructor (its only one, or the @Autowired one) and of `@Bean` methods; the
|
|
20
|
+
* final fields of a component that Lombok's @RequiredArgsConstructor injects. @Value parameters
|
|
21
|
+
* are configuration, not beans.
|
|
22
|
+
* A type a bean is assignable to is its class or anything it extends or implements, followed
|
|
23
|
+
* through the index; a type the index does not bind (a library type) is matched by its name.
|
|
24
|
+
* Collections (`List<T>`, `Map<String, T>`…) receive every bean of the element type. A build module
|
|
25
|
+
* (the nearest directory with a pom.xml or build.gradle) holding a @SpringBootApplication is one
|
|
26
|
+
* application, made of itself and the modules its build file depends on (Maven artifactIds and
|
|
27
|
+
* Gradle `project(':…')` of this repository): an injection point sees the beans of the
|
|
28
|
+
* applications its module is part of, never another application's; a shared module's point is
|
|
29
|
+
* filled by each application that includes it. Not modelled:
|
|
30
|
+
* custom stereotype annotations, beans declared in XML, auto-configuration from libraries and
|
|
31
|
+
* generics as a qualifier — what a library provides is reported as not in the indexed code.
|
|
32
|
+
* Test files are left out.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const STEREOTYPE = /^(Component|Service|Repository|Controller|RestController|Configuration|ControllerAdvice|RestControllerAdvice|SpringBootApplication|ConfigurationProperties)$/;
|
|
36
|
+
const POINT = /^(Autowired|Inject|Resource)$/;
|
|
37
|
+
const CONDITION = /^(Profile|Conditional\w*)$/;
|
|
38
|
+
const MODIFIER = /^(private|public|protected|static|final|transient|volatile|abstract|default|synchronized)$/;
|
|
39
|
+
const MANY = /^(List|Set|Collection|Iterable|Map|SortedSet|SortedMap|LinkedHashMap|HashMap)$/;
|
|
40
|
+
const WRAP = /^(Optional|ObjectProvider|ObjectFactory|Provider|Lazy)$/;
|
|
41
|
+
const ANNOTATION = /@[\w.]+(?:\s*\((?:[^()"]|"[^"]*"|\((?:[^()"]|"[^"]*")*\))*\))?/g;
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { readRepoFile } from '../util/paths.mjs';
|
|
46
|
+
|
|
47
|
+
const BUILD_FILES = ['pom.xml', 'build.gradle', 'build.gradle.kts'];
|
|
48
|
+
|
|
49
|
+
const decorators = (s) => { try { return JSON.parse(s.decorators || '[]'); } catch { return []; } };
|
|
50
|
+
const bases = (s) => { try { return JSON.parse(s.bases || '[]'); } catch { return []; } };
|
|
51
|
+
const strip = (s) => String(s ?? '').trim().replace(/\s+/g, ' ');
|
|
52
|
+
|
|
53
|
+
/** Module directory → the module directories its build file depends on (pom.xml artifactIds, Gradle project paths). */
|
|
54
|
+
function moduleDeps(root, mods) {
|
|
55
|
+
const info = new Map();
|
|
56
|
+
for (const m of mods) {
|
|
57
|
+
const pom = read(root, m, 'pom.xml');
|
|
58
|
+
if (pom != null) {
|
|
59
|
+
let body = pom;
|
|
60
|
+
for (let prev; prev !== body;) { prev = body; body = body.replace(/<!--[\s\S]*?-->/g, ''); }
|
|
61
|
+
const own = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(body.replace(/<parent>[\s\S]*?<\/parent>/, ''))?.[1];
|
|
62
|
+
const ds = [...(body.match(/<dependencies>[\s\S]*?<\/dependencies>/g) ?? []).join('\n').matchAll(/<artifactId>\s*([^<\s]+)\s*<\/artifactId>/g)].map(x => x[1]);
|
|
63
|
+
info.set(m, { names: [own, path.posix.basename(m)].filter(Boolean), deps: ds });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const gradle = read(root, m, 'build.gradle') ?? read(root, m, 'build.gradle.kts');
|
|
67
|
+
if (gradle != null) info.set(m, { names: [path.posix.basename(m)], deps: [...gradle.matchAll(/project\(\s*(?:path\s*[:=]\s*)?['"]:?([\w:.-]+)['"]/g)].map(x => x[1].split(':').pop()) });
|
|
68
|
+
}
|
|
69
|
+
const byName = new Map();
|
|
70
|
+
for (const [m, i] of info) for (const n of i.names) if (!byName.has(n)) byName.set(n, m);
|
|
71
|
+
const out = new Map();
|
|
72
|
+
for (const [m, i] of info) out.set(m, new Set(i.deps.map(d => byName.get(d)).filter(d => d && d !== m)));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A build file of module `m`: a small regular file inside the repository, or null. */
|
|
77
|
+
const read = (root, m, name) => readRepoFile(root, path.posix.join(m || '.', name));
|
|
78
|
+
|
|
79
|
+
/** Spring's default bean name: `FooService` → `fooService`, but `URLMapper` stays `URLMapper`. */
|
|
80
|
+
const beanName = (cls) => /^[A-Z]{2}/.test(cls) ? cls : cls[0].toLowerCase() + cls.slice(1);
|
|
81
|
+
|
|
82
|
+
/** The source of a declaration from its first line (annotations included) to its name, or to `upto`. */
|
|
83
|
+
function headText(lines, s, upto = null) {
|
|
84
|
+
const end = upto ?? s.name_line;
|
|
85
|
+
const out = [];
|
|
86
|
+
for (let l = s.start_line; l <= end && l <= lines.length; l++) out.push(l === s.name_line && upto == null ? lines[l - 1].slice(0, s.name_col) : lines[l - 1]);
|
|
87
|
+
return out.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The annotations in a text: [{ name, args }] with `args` the parenthesised text, if any. */
|
|
91
|
+
function annotations(text) {
|
|
92
|
+
return [...text.matchAll(ANNOTATION)].map(m => {
|
|
93
|
+
const a = /^@([\w.]+)\s*(?:\(([\s\S]*)\))?$/.exec(m[0]);
|
|
94
|
+
return { name: a[1].split('.').pop(), args: a[2] ?? null, index: m.index, length: m[0].length };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The string values of an annotation's `value`/`name` (or bare) argument: `("x")`, `(name = "x")`, `({"a", "b"})`. */
|
|
99
|
+
function annotationNames(args, keys = ['value', 'name']) {
|
|
100
|
+
if (args == null) return [];
|
|
101
|
+
const t = args.trim();
|
|
102
|
+
const kv = new RegExp(`\\b(?:${keys.join('|')})\\s*=\\s*(\\{[^}]*\\}|"[^"]*")`).exec(t);
|
|
103
|
+
const v = kv ? kv[1] : (/^(\{[^}]*\}|"[^"]*")/.exec(t)?.[1] ?? '');
|
|
104
|
+
return [...v.matchAll(/"([^"]*)"/g)].map(m => m[1]).filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A declared type → { raw, many, text }: `List<Foo>` → raw Foo, many; `Optional<Foo>` → Foo. */
|
|
108
|
+
function typeOf(text) {
|
|
109
|
+
let t = strip(text).replace(/\.\.\.$/, '');
|
|
110
|
+
let many = false;
|
|
111
|
+
for (let i = 0; i < 3; i++) {
|
|
112
|
+
const g = /^([\w.]+)\s*<(.*)>$/.exec(t);
|
|
113
|
+
if (!g) break;
|
|
114
|
+
const head = g[1].split('.').pop();
|
|
115
|
+
if (MANY.test(head)) { many = true; t = splitTop(g[2]).pop().trim(); continue; }
|
|
116
|
+
if (WRAP.test(head)) { t = g[2].trim(); continue; }
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (t.endsWith('[]')) { many = true; t = t.slice(0, -2); }
|
|
120
|
+
const raw = /^([\w.]+)/.exec(t)?.[1]?.split('.').pop() ?? null;
|
|
121
|
+
return { raw, many, text: strip(text) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Split at commas outside <>, () and {}. */
|
|
125
|
+
function splitTop(s) {
|
|
126
|
+
const out = [];
|
|
127
|
+
let depth = 0, cur = '', q = false;
|
|
128
|
+
for (const ch of s) {
|
|
129
|
+
if (ch === '"') q = !q;
|
|
130
|
+
if (!q && '<({'.includes(ch)) depth++;
|
|
131
|
+
if (!q && '>)}'.includes(ch)) depth--;
|
|
132
|
+
if (!q && ch === ',' && depth === 0) { out.push(cur); cur = ''; } else cur += ch;
|
|
133
|
+
}
|
|
134
|
+
if (cur.trim()) out.push(cur);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A parameter list's text (`(…)` after the name) → the parameters, with their line. */
|
|
139
|
+
function params(lines, s) {
|
|
140
|
+
let text = '', depth = 0, started = false;
|
|
141
|
+
const lineAt = [];
|
|
142
|
+
for (let l = s.name_line; l <= Math.min(lines.length, s.name_line + 40); l++) {
|
|
143
|
+
const src = l === s.name_line ? lines[l - 1].slice(s.name_col) : lines[l - 1];
|
|
144
|
+
for (const ch of src) {
|
|
145
|
+
if (ch === '(') { depth++; if (!started) { started = true; continue; } }
|
|
146
|
+
if (ch === ')') { depth--; if (started && depth === 0) return parse(); }
|
|
147
|
+
if (started) { text += ch; lineAt.push(l); }
|
|
148
|
+
}
|
|
149
|
+
if (started) { text += ' '; lineAt.push(l); }
|
|
150
|
+
}
|
|
151
|
+
return [];
|
|
152
|
+
function parse() {
|
|
153
|
+
const out = [];
|
|
154
|
+
let at = 0;
|
|
155
|
+
for (const part of splitTop(text)) {
|
|
156
|
+
const offset = text.indexOf(part, at);
|
|
157
|
+
at = offset + part.length;
|
|
158
|
+
const anns = annotations(part);
|
|
159
|
+
let rest = part;
|
|
160
|
+
for (const a of [...anns].reverse()) rest = rest.slice(0, a.index) + rest.slice(a.index + a.length);
|
|
161
|
+
const words = strip(rest).replace(/\bfinal\s+/, '').split(' ');
|
|
162
|
+
if (words.length < 2) continue;
|
|
163
|
+
const name = words.pop();
|
|
164
|
+
const lead = part.length - part.trimStart().length;
|
|
165
|
+
out.push({ name, type: words.join(' '), anns, line: lineAt[Math.min(lineAt.length - 1, offset + lead)] ?? s.name_line });
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Beans and injection points of the repository's Java code. Each injection point carries the
|
|
173
|
+
* beans it may receive (`from`) and why (`why`); see injection.mjs for the shapes.
|
|
174
|
+
*/
|
|
175
|
+
export function collectSpring(intel) {
|
|
176
|
+
const files = intel.store.all(`SELECT id, path, package FROM files WHERE lang = 'java' AND is_test = 0`);
|
|
177
|
+
if (!files.length) return { providers: [], injections: [] };
|
|
178
|
+
const fileOf = new Map(files.map(f => [f.id, f]));
|
|
179
|
+
const ids = files.map(f => f.id);
|
|
180
|
+
const syms = [];
|
|
181
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
182
|
+
const chunk = ids.slice(i, i + 500);
|
|
183
|
+
syms.push(...intel.store.all(`SELECT s.*, f.path FROM symbols s JOIN files f ON f.id = s.file_id WHERE s.file_id IN (${chunk.map(() => '?').join(',')})`, ...chunk));
|
|
184
|
+
}
|
|
185
|
+
const byId = new Map(syms.map(s => [s.id, s]));
|
|
186
|
+
const children = new Map();
|
|
187
|
+
for (const s of syms) if (s.parent_id != null) (children.get(s.parent_id) ?? children.set(s.parent_id, []).get(s.parent_id)).push(s);
|
|
188
|
+
|
|
189
|
+
// what extends or implements what (inherit references bound by the index), for assignability
|
|
190
|
+
const subs = new Map();
|
|
191
|
+
for (const r of intel.store.all(`SELECT r.src_id, r.dst_id FROM refs r JOIN files f ON f.id = r.file_id WHERE r.kind = 'inherit' AND f.lang = 'java' AND r.dst_id IS NOT NULL AND r.src_id IS NOT NULL`)) {
|
|
192
|
+
(subs.get(r.dst_id) ?? subs.set(r.dst_id, new Set()).get(r.dst_id)).add(r.src_id);
|
|
193
|
+
}
|
|
194
|
+
const supertypesCache = new Map();
|
|
195
|
+
const supersOf = new Map();
|
|
196
|
+
for (const [base, ss] of subs) for (const x of ss) (supersOf.get(x) ?? supersOf.set(x, []).get(x)).push(base);
|
|
197
|
+
/** A type's id and the ids of everything it extends or implements (through the index). */
|
|
198
|
+
const supers = (id) => {
|
|
199
|
+
if (supertypesCache.has(id)) return supertypesCache.get(id);
|
|
200
|
+
const out = new Set([id]);
|
|
201
|
+
const stack = [id];
|
|
202
|
+
while (stack.length) for (const b of supersOf.get(stack.pop()) ?? []) if (!out.has(b)) { out.add(b); stack.push(b); }
|
|
203
|
+
supertypesCache.set(id, out);
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
/** The names of a type's supertypes the index does not bind (library types), for name matching. */
|
|
207
|
+
const superNames = (id) => {
|
|
208
|
+
const out = new Set();
|
|
209
|
+
for (const x of supers(id)) { const s = byId.get(x); if (!s) continue; out.add(s.name); for (const b of bases(s)) out.add(/^([\w.]+)/.exec(b)?.[1]?.split('.').pop()); }
|
|
210
|
+
return out;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** The definition a type name on lines [l0, l1] of a file is bound to. */
|
|
214
|
+
const bindType = (fileId, l0, l1, name) => {
|
|
215
|
+
if (!name) return null;
|
|
216
|
+
const r = intel.store.get(`SELECT dst_id FROM refs WHERE file_id = ? AND name = ? AND line BETWEEN ? AND ? AND kind IN ('type', 'inherit') AND dst_id IS NOT NULL LIMIT 1`, fileId, name, l0, l1);
|
|
217
|
+
return r ? (byId.get(r.dst_id) ?? intel.sym(r.dst_id)) : null;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// @MapperScan packages: the interfaces there are MyBatis mappers
|
|
221
|
+
const mapperPackages = [];
|
|
222
|
+
for (const s of syms) if (decorators(s).includes('MapperScan')) {
|
|
223
|
+
const lines = intel.fileLines(s.path) ?? [];
|
|
224
|
+
for (const a of annotations(headText(lines, s))) if (a.name === 'MapperScan') mapperPackages.push(...annotationNames(a.args, ['value', 'basePackages']));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// build modules: an application's beans are not another application's
|
|
228
|
+
const moduleCache = new Map();
|
|
229
|
+
const moduleOf = (rel) => {
|
|
230
|
+
let dir = path.posix.dirname(rel);
|
|
231
|
+
const seen = [];
|
|
232
|
+
while (true) {
|
|
233
|
+
if (moduleCache.has(dir)) { const m = moduleCache.get(dir); for (const d of seen) moduleCache.set(d, m); return m; }
|
|
234
|
+
seen.push(dir);
|
|
235
|
+
if (BUILD_FILES.some(b => fs.existsSync(path.join(intel.root, dir, b))) || dir === '.' || dir === '') {
|
|
236
|
+
const m = dir === '' ? '.' : dir;
|
|
237
|
+
for (const d of seen) moduleCache.set(d, m);
|
|
238
|
+
return m;
|
|
239
|
+
}
|
|
240
|
+
dir = path.posix.dirname(dir);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const apps = new Set(syms.filter(s => s.kind === 'class' && decorators(s).includes('SpringBootApplication')).map(s => moduleOf(s.path)));
|
|
244
|
+
// which modules an application includes: its build file's dependencies on other modules here
|
|
245
|
+
const mods = new Set(syms.map(s => moduleOf(s.path)));
|
|
246
|
+
const deps = moduleDeps(intel.root, mods);
|
|
247
|
+
const closure = (m) => { const out = new Set([m]), st = [m]; while (st.length) for (const d of deps.get(st.pop()) ?? []) if (!out.has(d)) { out.add(d); st.push(d); } return out; };
|
|
248
|
+
const appMods = new Map([...apps].map(a => [a, closure(a)]));
|
|
249
|
+
const known = [...deps.values()].some(d => d.size);
|
|
250
|
+
// the applications a module is part of: itself if it is one, else those that include it
|
|
251
|
+
// (without dependency information, every application, as a shared library)
|
|
252
|
+
const appsOf = (m) => apps.has(m) ? [m] : [...apps].filter(a => !known || appMods.get(a).has(m));
|
|
253
|
+
const visible = (pointFile, p) => {
|
|
254
|
+
const a = moduleOf(pointFile), b = moduleOf(p.file);
|
|
255
|
+
if (a === b || !apps.size) return true;
|
|
256
|
+
const pa = appsOf(a);
|
|
257
|
+
// a module no application includes: its own beans and those of other such modules
|
|
258
|
+
if (!pa.length) return !appsOf(b).length || !known;
|
|
259
|
+
return pa.some(x => known ? appMods.get(x).has(b) : (b === x || !apps.has(b)));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// @ConfigurationProperties classes are beans only when registered
|
|
263
|
+
const enabledProps = new Set();
|
|
264
|
+
let propsScan = false;
|
|
265
|
+
for (const s of syms) {
|
|
266
|
+
const dec = decorators(s);
|
|
267
|
+
if (dec.includes('ConfigurationPropertiesScan')) propsScan = true;
|
|
268
|
+
if (!dec.includes('EnableConfigurationProperties')) continue;
|
|
269
|
+
for (const a of annotations(headText(intel.fileLines(s.path) ?? [], s))) if (a.name === 'EnableConfigurationProperties') for (const m of (a.args ?? '').matchAll(/([\w.]+)\.class/g)) enabledProps.add(m[1].split('.').pop());
|
|
270
|
+
}
|
|
271
|
+
const isBeanClass = (s, dec) => dec.some(d => STEREOTYPE.test(d) && d !== 'ConfigurationProperties') || (dec.includes('ConfigurationProperties') && (propsScan || enabledProps.has(s.name)));
|
|
272
|
+
|
|
273
|
+
const providers = [];
|
|
274
|
+
const conditionsOf = (anns) => anns.filter(a => CONDITION.test(a.name)).map(a => `@${a.name}${a.args != null ? `(${strip(a.args)})` : ''}`);
|
|
275
|
+
for (const s of syms) {
|
|
276
|
+
const dec = decorators(s);
|
|
277
|
+
const lines = intel.fileLines(s.path) ?? [];
|
|
278
|
+
if (s.kind === 'class' && isBeanClass(s, dec) && !/\babstract\b/.test(s.sig ?? '')) {
|
|
279
|
+
const anns = annotations(headText(lines, s));
|
|
280
|
+
const st = anns.find(a => STEREOTYPE.test(a.name) && (a.name !== 'ConfigurationProperties' || !dec.some(d => STEREOTYPE.test(d) && d !== 'ConfigurationProperties')));
|
|
281
|
+
// @ConfigurationProperties("prefix") names a prefix, not the bean
|
|
282
|
+
const name = (st?.name !== 'ConfigurationProperties' && annotationNames(st?.args)[0]) || beanName(s.name);
|
|
283
|
+
providers.push({ how: `@${st?.name ?? dec.find(d => STEREOTYPE.test(d))}`, target: s, targetText: s.qname, name, primary: dec.includes('Primary'),
|
|
284
|
+
qualifiers: anns.filter(a => a.name === 'Qualifier' || a.name === 'Named').flatMap(a => annotationNames(a.args)),
|
|
285
|
+
conditions: conditionsOf(anns), types: supers(s.id), typeNames: superNames(s.id), file: s.path, line: s.start_line, exact: true });
|
|
286
|
+
} else if (s.kind === 'method' && dec.includes('Bean')) {
|
|
287
|
+
const anns = annotations(headText(lines, s));
|
|
288
|
+
const ret = typeOf(s.type ?? '');
|
|
289
|
+
const rt = bindType(s.file_id, s.start_line, s.name_line, ret.raw);
|
|
290
|
+
const names = annotationNames(anns.find(a => a.name === 'Bean')?.args);
|
|
291
|
+
providers.push({ how: '@Bean', target: s, targetText: `${s.qname}()`, name: names[0] ?? s.name, aliases: names.slice(1), primary: dec.includes('Primary'),
|
|
292
|
+
qualifiers: anns.filter(a => a.name === 'Qualifier' || a.name === 'Named').flatMap(a => annotationNames(a.args)),
|
|
293
|
+
conditions: conditionsOf(anns), types: rt ? supers(rt.id) : new Set(), typeNames: new Set([ret.raw, ...(rt ? superNames(rt.id) : [])]), returns: ret.raw, file: s.path, line: s.start_line, exact: !!rt });
|
|
294
|
+
} else if (s.kind === 'interface') {
|
|
295
|
+
// beans the framework generates: Feign proxies, Spring Data repositories, MyBatis mappers
|
|
296
|
+
const pkg = fileOf.get(s.file_id)?.package ?? '';
|
|
297
|
+
const repo = [...superNames(s.id)].some(n => n && /^(Repository|CrudRepository|ListCrudRepository|PagingAndSortingRepository|JpaRepository|MongoRepository|ElasticsearchRepository|ReactiveCrudRepository|R2dbcRepository|Neo4jRepository|CassandraRepository|QuerydslPredicateExecutor|JpaSpecificationExecutor|KeyValueRepository|RedisRepository)$/.test(n));
|
|
298
|
+
const how = dec.includes('FeignClient') ? 'Feign client proxy'
|
|
299
|
+
: repo ? 'Spring Data repository'
|
|
300
|
+
: dec.includes('Mapper') || mapperPackages.some(p => pkg === p || pkg.startsWith(p + '.')) ? 'MyBatis mapper'
|
|
301
|
+
: null;
|
|
302
|
+
if (how) providers.push({ how, target: s, targetText: `${s.qname} (generated at run time)`, name: beanName(s.name), primary: how === 'Feign client proxy', qualifiers: [], conditions: [],
|
|
303
|
+
types: supers(s.id), typeNames: superNames(s.id), generated: true, file: s.path, line: s.start_line, exact: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const p of providers) p.key = `id:${p.target.id}`;
|
|
307
|
+
|
|
308
|
+
/** The beans an injection point may receive, and why that one. */
|
|
309
|
+
const resolve = ({ type, typeSym, qualifier, name, byName, file }) => {
|
|
310
|
+
let cands = providers.filter(p => (typeSym ? p.types.has(typeSym.id) : p.typeNames.has(type.raw)) && visible(file, p));
|
|
311
|
+
// a shared module's injection point: each application that includes it brings its own bean
|
|
312
|
+
const perApp = !apps.has(moduleOf(file)) && new Set(cands.flatMap(p => appsOf(moduleOf(p.file)).length === 1 && apps.has(moduleOf(p.file)) ? [moduleOf(p.file)] : [])).size > 1;
|
|
313
|
+
let why = typeSym ? 'by type' : `by type name ${type.raw} (a type the index does not bind)`;
|
|
314
|
+
if (byName) {
|
|
315
|
+
const n = providers.filter(p => p.name === name && (cands.includes(p)));
|
|
316
|
+
if (n.length === 1) return { from: n, why: `by name "${name}" (@Resource)` };
|
|
317
|
+
}
|
|
318
|
+
if (qualifier) {
|
|
319
|
+
cands = cands.filter(p => p.name === qualifier || p.aliases?.includes(qualifier) || p.qualifiers.includes(qualifier));
|
|
320
|
+
return { from: cands, why: `@Qualifier("${qualifier}")` };
|
|
321
|
+
}
|
|
322
|
+
if (type.many) return { from: cands, why: `every bean of type ${type.raw}` };
|
|
323
|
+
if (perApp) return { from: cands, why: 'the bean of the application that includes this module (one per application)' };
|
|
324
|
+
if (cands.length > 1) {
|
|
325
|
+
const prim = cands.filter(p => p.primary);
|
|
326
|
+
if (prim.length === 1) return { from: prim, why: `@Primary, over ${cands.filter(p => p !== prim[0]).map(p => p.target.name).join(', ')}`, others: cands.filter(p => p !== prim[0]) };
|
|
327
|
+
const byParam = cands.filter(p => p.name === name || p.aliases?.includes(name));
|
|
328
|
+
if (byParam.length === 1) return { from: byParam, why: `by the name "${name}"`, others: cands.filter(p => p !== byParam[0]) };
|
|
329
|
+
const why2 = cands.every(p => p.conditions.length) ? 'one of these, by the active profile or conditions' : 'ambiguous: several beans match and none is @Primary or named so';
|
|
330
|
+
return { from: cands, why: why2 };
|
|
331
|
+
}
|
|
332
|
+
return { from: cands, why };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const injections = [];
|
|
336
|
+
const point = (owner, s, decl, line, anns, extra = {}) => {
|
|
337
|
+
if (anns.some(a => a.name === 'Value')) return;
|
|
338
|
+
const type = typeOf(decl.type);
|
|
339
|
+
if (!type.raw || /^(String|int|long|boolean|double|float|short|byte|char|Integer|Long|Boolean|Double|Object)$/.test(type.raw)) return;
|
|
340
|
+
const typeSym = bindType(s.file_id, Math.min(line, s.start_line), Math.max(line, s.name_line) + 1, type.raw);
|
|
341
|
+
const qualifier = anns.filter(a => a.name === 'Qualifier' || a.name === 'Named').flatMap(a => annotationNames(a.args))[0] ?? null;
|
|
342
|
+
const res = anns.find(a => a.name === 'Resource');
|
|
343
|
+
const name = (res && annotationNames(res.args, ['name'])[0]) || decl.name;
|
|
344
|
+
const r = resolve({ type, typeSym, qualifier, name, byName: !!res, file: s.path });
|
|
345
|
+
injections.push({ key: typeSym ? `id:${typeSym.id}` : `text:${type.raw}`, token: typeSym?.qname ?? type.raw, exact: !!typeSym, owner, param: decl.name, type: type.text,
|
|
346
|
+
file: s.path, line, framework: 'spring', ...r, ...extra });
|
|
347
|
+
};
|
|
348
|
+
// Spring injects only into beans: a class that is one, that a @Bean method returns, or that a bean extends
|
|
349
|
+
const beanIds = new Set(providers.filter(p => !p.generated && p.how !== '@Bean').map(p => p.target.id));
|
|
350
|
+
const returned = new Set(providers.filter(p => p.how === '@Bean').map(p => p.returns));
|
|
351
|
+
// a @Bean declared as an interface but returning `new Impl(…)`: Impl is the bean
|
|
352
|
+
for (const p of providers) if (p.how === '@Bean') {
|
|
353
|
+
const body = (intel.fileLines(p.target.path) ?? []).slice(p.target.start_line - 1, p.target.end_line).join('\n');
|
|
354
|
+
for (const m of body.matchAll(/\bnew\s+([A-Z][\w]*)\s*[(<]/g)) returned.add(m[1]);
|
|
355
|
+
}
|
|
356
|
+
const isBeanOwner = (c) => {
|
|
357
|
+
if (!c) return false;
|
|
358
|
+
if (beanIds.has(c.id) || returned.has(c.name)) return true;
|
|
359
|
+
const stack = [c.id], seen = new Set(stack);
|
|
360
|
+
while (stack.length) for (const x of subs.get(stack.pop()) ?? []) {
|
|
361
|
+
const k = byId.get(x);
|
|
362
|
+
if (beanIds.has(x) || (k && returned.has(k.name))) return true;
|
|
363
|
+
if (!seen.has(x)) { seen.add(x); stack.push(x); }
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
};
|
|
367
|
+
for (const s of syms) {
|
|
368
|
+
const dec = decorators(s);
|
|
369
|
+
const lines = intel.fileLines(s.path) ?? [];
|
|
370
|
+
const owner = s.parent_id != null ? byId.get(s.parent_id) ?? null : null;
|
|
371
|
+
if (!isBeanOwner(owner)) continue;
|
|
372
|
+
if (s.kind === 'field' && dec.some(d => POINT.test(d)) && !s.is_static) {
|
|
373
|
+
const head = headText(lines, s);
|
|
374
|
+
const anns = annotations(head);
|
|
375
|
+
let rest = head;
|
|
376
|
+
for (const a of [...anns].reverse()) rest = rest.slice(0, a.index) + rest.slice(a.index + a.length);
|
|
377
|
+
const words = strip(rest).split(' ').filter(w => w && !MODIFIER.test(w));
|
|
378
|
+
point(owner, s, { name: s.name, type: words.join(' ') || s.type || '' }, s.name_line, anns);
|
|
379
|
+
} else if ((s.kind === 'method' || s.kind === 'constructor') && (dec.some(d => POINT.test(d)) || dec.includes('Bean'))) {
|
|
380
|
+
for (const p of params(lines, s)) point(owner, s, p, p.line, p.anns, dec.includes('Bean') ? { via: `@Bean ${s.name}()` } : {});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// constructor injection: a component's only constructor, or its @Autowired one (handled above)
|
|
384
|
+
for (const p of providers) {
|
|
385
|
+
// constructor binding of @ConfigurationProperties takes properties, not beans
|
|
386
|
+
if (p.how === '@Bean' || p.how === '@ConfigurationProperties' || p.generated) continue;
|
|
387
|
+
const kids = children.get(p.target.id) ?? [];
|
|
388
|
+
const ctors = kids.filter(k => k.kind === 'constructor');
|
|
389
|
+
const lines = intel.fileLines(p.target.path) ?? [];
|
|
390
|
+
if (ctors.length === 1 && !decorators(ctors[0]).some(d => POINT.test(d))) {
|
|
391
|
+
for (const x of params(lines, ctors[0])) point(p.target, ctors[0], x, x.line, x.anns);
|
|
392
|
+
} else if (!ctors.length && decorators(p.target).some(d => d === 'RequiredArgsConstructor' || d === 'AllArgsConstructor')) {
|
|
393
|
+
const all = decorators(p.target).includes('AllArgsConstructor');
|
|
394
|
+
for (const f of kids.filter(k => k.kind === 'field' && !k.is_static && !decorators(k).some(d => POINT.test(d)))) {
|
|
395
|
+
const head = headText(lines, f);
|
|
396
|
+
if (!all && !/\bfinal\b/.test(head)) continue;
|
|
397
|
+
const anns = annotations(head);
|
|
398
|
+
let rest = head;
|
|
399
|
+
for (const a of [...anns].reverse()) rest = rest.slice(0, a.index) + rest.slice(a.index + a.length);
|
|
400
|
+
const words = strip(rest).split(' ').filter(w => w && !MODIFIER.test(w));
|
|
401
|
+
point(p.target, f, { name: f.name, type: words.join(' ') || f.type || '' }, f.name_line, anns, { via: '@RequiredArgsConstructor' });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { providers, injections };
|
|
406
|
+
}
|