@event4u/agent-config 17.2.1 → 17.3.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +152 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/cmd_conformance.js +20 -14
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +21 -11
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +1 -1
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +5561 -2463
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +9 -9
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +16 -1
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_dispatch.bash +1 -1
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +80 -44
- package/src/scripts/code_graph/cli.ts +260 -16
- package/src/scripts/code_graph/conventions_default.ts +228 -0
- package/src/scripts/code_graph/extract.ts +5 -2
- package/src/scripts/code_graph/extract_more.ts +105 -1
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +174 -41
- package/src/scripts/code_graph/map_model.ts +53 -15
- package/src/scripts/code_graph/map_verify.ts +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +290 -0
- package/src/scripts/code_graph/providers/django.ts +163 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +101 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +138 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +193 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +10 -4
- package/src/scripts/code_graph/types.ts +75 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +9 -9
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +4 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -7
- package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
- package/src/scripts/mcp_server/graph_tools.ts +5 -1
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +6 -5
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The building blocks every convention provider shares: emitting deduplicated
|
|
3
|
+
* edges, declaring route and table nodes, finding the symbol a line belongs to
|
|
4
|
+
* or the one a decorator precedes, and resolving a handler name the way the
|
|
5
|
+
* file itself states it (same file, then its imports).
|
|
6
|
+
*
|
|
7
|
+
* A provider stays a parameter table plus the semantics only it knows; the
|
|
8
|
+
* mechanics live here once (road-to-graph-providers-for-every-stack 4.1).
|
|
9
|
+
*/
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import type { SourceFile } from '../../build.js';
|
|
13
|
+
import type { CodeEdge, CodeNode, ResolvedVia } from '../../types.js';
|
|
14
|
+
import type { ProviderInput, ProviderResult } from '../types.js';
|
|
15
|
+
|
|
16
|
+
/** A string matched literally inside a `RegExp` built from a symbol name. */
|
|
17
|
+
export function escapeRe(s: string): string {
|
|
18
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Line-start offsets, so many `lineAt` lookups stay linear. */
|
|
22
|
+
export function lineIndex(src: string): (index: number) => number {
|
|
23
|
+
const starts = [0];
|
|
24
|
+
for (let i = 0; i < src.length; i++) if (src.charCodeAt(i) === 10) starts.push(i + 1);
|
|
25
|
+
return (index: number): number => {
|
|
26
|
+
let lo = 0;
|
|
27
|
+
let hi = starts.length - 1;
|
|
28
|
+
while (lo < hi) {
|
|
29
|
+
const mid = (lo + hi + 1) >> 1;
|
|
30
|
+
if ((starts[mid] as number) <= index) lo = mid;
|
|
31
|
+
else hi = mid - 1;
|
|
32
|
+
}
|
|
33
|
+
return lo + 1;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Kit {
|
|
38
|
+
files: readonly SourceFile[];
|
|
39
|
+
fileIds: ReadonlySet<string>;
|
|
40
|
+
nodeById: ReadonlyMap<string, CodeNode>;
|
|
41
|
+
/** Every project path under the provider's roots, code or not. */
|
|
42
|
+
paths: readonly string[];
|
|
43
|
+
/** The package roots the provider was detected in (`''` is the repository root). */
|
|
44
|
+
roots: readonly string[];
|
|
45
|
+
/** The code files whose path ends with one of `exts`. */
|
|
46
|
+
filesWithExt(...exts: string[]): SourceFile[];
|
|
47
|
+
/** Make sure a file node exists for a non-code target (a template). */
|
|
48
|
+
fileNode(p: string): void;
|
|
49
|
+
/** Every base node declared in `file` (indexed once; never a scan of the whole graph). */
|
|
50
|
+
nodesInFile(file: string): readonly CodeNode[];
|
|
51
|
+
/** Every base node with this label (indexed once). */
|
|
52
|
+
nodesNamed(label: string): readonly CodeNode[];
|
|
53
|
+
/** Whether a project path is in the inventory (indexed once). */
|
|
54
|
+
hasPath(p: string): boolean;
|
|
55
|
+
emit(source: string, target: string, relation: CodeEdge['relation'], via: ResolvedVia): void;
|
|
56
|
+
/** Declare a route node in `file`; returns its id (`<file>#route:<VERB> <path>`, `@n` when repeated). */
|
|
57
|
+
route(file: string, verb: string, routePath: string, line: number): string;
|
|
58
|
+
/** Declare (once) the table node `table:<name>`, first seen in `file`. */
|
|
59
|
+
table(name: string, file: string, line: number): string;
|
|
60
|
+
/** The innermost function or method node whose span holds `line`, else the file. */
|
|
61
|
+
enclosing(file: string, line: number): string;
|
|
62
|
+
/** The first function, method or class node starting at or after `line` (what a decorator decorates). */
|
|
63
|
+
nextSymbol(file: string, line: number, kinds?: readonly CodeNode['kind'][]): CodeNode | null;
|
|
64
|
+
/** The class node whose span holds `line`. */
|
|
65
|
+
classAt(file: string, line: number): CodeNode | null;
|
|
66
|
+
/**
|
|
67
|
+
* A name as written in `file`, resolved the way the file states it: a
|
|
68
|
+
* same-file symbol first, then a symbol an import edge of the file binds.
|
|
69
|
+
*/
|
|
70
|
+
resolveName(file: string, name: string): string | null;
|
|
71
|
+
/** A method `member` of the class node `classId`, if the class declares it. */
|
|
72
|
+
methodOf(classId: string, member: string): string | null;
|
|
73
|
+
result(): ProviderResult;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function makeKit(input: ProviderInput): Kit {
|
|
77
|
+
const files = input.files;
|
|
78
|
+
const fileIds = new Set(files.map((f) => f.path));
|
|
79
|
+
const nodeById = new Map(input.baseNodes.map((n) => [n.id, n] as const));
|
|
80
|
+
const nodes: CodeNode[] = [];
|
|
81
|
+
const edges: CodeEdge[] = [];
|
|
82
|
+
const seenEdge = new Set<string>();
|
|
83
|
+
const seenNode = new Set<string>();
|
|
84
|
+
const routeOrdinal = new Map<string, number>();
|
|
85
|
+
let byFile: Map<string, CodeNode[]> | null = null;
|
|
86
|
+
let byLabel: Map<string, CodeNode[]> | null = null;
|
|
87
|
+
let pathSet: Set<string> | null = null;
|
|
88
|
+
const index = (key: (n: CodeNode) => string): Map<string, CodeNode[]> => {
|
|
89
|
+
const m = new Map<string, CodeNode[]>();
|
|
90
|
+
for (const n of input.baseNodes) {
|
|
91
|
+
const k = key(n);
|
|
92
|
+
const list = m.get(k);
|
|
93
|
+
if (list) list.push(n);
|
|
94
|
+
else m.set(k, [n]);
|
|
95
|
+
}
|
|
96
|
+
return m;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const symbolsByFile = new Map<string, CodeNode[]>();
|
|
100
|
+
for (const n of input.baseNodes) {
|
|
101
|
+
if (n.kind === 'file' || n.source_location.length === 0) continue;
|
|
102
|
+
const list = symbolsByFile.get(n.source_file) ?? [];
|
|
103
|
+
list.push(n);
|
|
104
|
+
symbolsByFile.set(n.source_file, list);
|
|
105
|
+
}
|
|
106
|
+
for (const list of symbolsByFile.values()) list.sort((a, b) => (a.source_location[0] as number) - (b.source_location[0] as number));
|
|
107
|
+
|
|
108
|
+
const importsByFile = new Map<string, Map<string, string>>();
|
|
109
|
+
for (const e of input.baseEdges) {
|
|
110
|
+
if (e.relation !== 'imports') continue;
|
|
111
|
+
const from = nodeById.get(e.source)?.source_file;
|
|
112
|
+
const to = nodeById.get(e.target);
|
|
113
|
+
if (!from || !to) continue;
|
|
114
|
+
const m = importsByFile.get(from) ?? new Map<string, string>();
|
|
115
|
+
if (!m.has(to.label)) m.set(to.label, to.id);
|
|
116
|
+
importsByFile.set(from, m);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const emit: Kit['emit'] = (source, target, relation, via) => {
|
|
120
|
+
const key = `${source}\0${relation}\0${target}`;
|
|
121
|
+
if (seenEdge.has(key)) return;
|
|
122
|
+
seenEdge.add(key);
|
|
123
|
+
edges.push({ source, target, relation, confidence: 'EXTRACTED', resolved_via: via, provider: 'native' });
|
|
124
|
+
};
|
|
125
|
+
const span = (n: CodeNode): [number, number] => [n.source_location[0] as number, (n.source_location[2] ?? n.source_location[0]) as number];
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
files,
|
|
129
|
+
fileIds,
|
|
130
|
+
nodeById,
|
|
131
|
+
paths: input.paths,
|
|
132
|
+
roots: input.roots,
|
|
133
|
+
nodesInFile: (file) => (byFile ??= index((n) => n.source_file)).get(file) ?? [],
|
|
134
|
+
nodesNamed: (label) => (byLabel ??= index((n) => n.label)).get(label) ?? [],
|
|
135
|
+
hasPath: (p) => (pathSet ??= new Set(input.paths)).has(p),
|
|
136
|
+
filesWithExt: (...exts) => files.filter((f) => exts.some((x) => f.path.endsWith(x))),
|
|
137
|
+
fileNode(p) {
|
|
138
|
+
if (nodeById.has(p) || seenNode.has(p)) return;
|
|
139
|
+
seenNode.add(p);
|
|
140
|
+
nodes.push({ id: p, label: p, kind: 'file', source_file: p, source_location: [] });
|
|
141
|
+
},
|
|
142
|
+
emit,
|
|
143
|
+
route(file, verb, routePath, line) {
|
|
144
|
+
const base = `${file}#route:${verb} ${routePath}`;
|
|
145
|
+
const n = (routeOrdinal.get(base) ?? 0) + 1;
|
|
146
|
+
routeOrdinal.set(base, n);
|
|
147
|
+
const id = n === 1 ? base : `${base}@${n}`;
|
|
148
|
+
nodes.push({ id, label: `${verb} ${routePath}`, kind: 'route', source_file: file, source_location: [line, 1, line, 1] });
|
|
149
|
+
emit(file, id, 'member', 'same-file');
|
|
150
|
+
return id;
|
|
151
|
+
},
|
|
152
|
+
table(name, file, line) {
|
|
153
|
+
const id = `table:${name}`;
|
|
154
|
+
if (!seenNode.has(id)) {
|
|
155
|
+
seenNode.add(id);
|
|
156
|
+
nodes.push({ id, label: name, kind: 'table', source_file: file, source_location: [line, 1, line, 1] });
|
|
157
|
+
}
|
|
158
|
+
return id;
|
|
159
|
+
},
|
|
160
|
+
enclosing(file, line) {
|
|
161
|
+
let best: CodeNode | null = null;
|
|
162
|
+
for (const n of symbolsByFile.get(file) ?? []) {
|
|
163
|
+
if (n.kind !== 'method' && n.kind !== 'function') continue;
|
|
164
|
+
const [s, e] = span(n);
|
|
165
|
+
if (s <= line && line <= e && (!best || s >= (best.source_location[0] as number))) best = n;
|
|
166
|
+
}
|
|
167
|
+
return best ? best.id : file;
|
|
168
|
+
},
|
|
169
|
+
nextSymbol(file, line, kinds = ['method', 'function', 'class']) {
|
|
170
|
+
for (const n of symbolsByFile.get(file) ?? []) if (kinds.includes(n.kind) && span(n)[0] >= line) return n;
|
|
171
|
+
return null;
|
|
172
|
+
},
|
|
173
|
+
classAt(file, line) {
|
|
174
|
+
let best: CodeNode | null = null;
|
|
175
|
+
for (const n of symbolsByFile.get(file) ?? []) {
|
|
176
|
+
if (n.kind !== 'class') continue;
|
|
177
|
+
const [s, e] = span(n);
|
|
178
|
+
if (s <= line && line <= e && (!best || s >= (best.source_location[0] as number))) best = n;
|
|
179
|
+
}
|
|
180
|
+
return best;
|
|
181
|
+
},
|
|
182
|
+
resolveName(file, name) {
|
|
183
|
+
const local = (symbolsByFile.get(file) ?? []).find((n) => n.label === name && n.kind !== 'method');
|
|
184
|
+
if (local) return local.id;
|
|
185
|
+
return importsByFile.get(file)?.get(name) ?? null;
|
|
186
|
+
},
|
|
187
|
+
methodOf(classId, member) {
|
|
188
|
+
const id = `${classId}::${member}`;
|
|
189
|
+
return nodeById.has(id) ? id : null;
|
|
190
|
+
},
|
|
191
|
+
result: () => ({ nodes, edges }),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The one symbol named `name` declared by a file in `file`'s own directory with
|
|
197
|
+
* the same extension — what a language whose package is its directory (Go, a
|
|
198
|
+
* Java package) sees without an import. Null when none or several declare it.
|
|
199
|
+
*/
|
|
200
|
+
export function packageSymbol(kit: Kit, file: string, name: string, kinds: readonly CodeNode['kind'][]): string | null {
|
|
201
|
+
const dir = path.posix.dirname(file);
|
|
202
|
+
const ext = path.posix.extname(file);
|
|
203
|
+
const hits: string[] = [];
|
|
204
|
+
for (const n of kit.nodesNamed(name)) {
|
|
205
|
+
if (!kinds.includes(n.kind)) continue;
|
|
206
|
+
if (path.posix.dirname(n.source_file) !== dir || path.posix.extname(n.source_file) !== ext) continue;
|
|
207
|
+
hits.push(n.id);
|
|
208
|
+
}
|
|
209
|
+
return hits.length === 1 ? (hits[0] as string) : null;
|
|
210
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHP name resolution shared by the PHP providers: a class reference read the
|
|
3
|
+
* way its file states it (namespace + `use` aliases), then located through the
|
|
4
|
+
* project's PSR-4 map. A reference that resolves to no class node in the graph
|
|
5
|
+
* resolves to nothing — never to a same-named class in another namespace.
|
|
6
|
+
*/
|
|
7
|
+
import { resolvePsr4, type ResolutionConfig } from '../../resolution_tiers.js';
|
|
8
|
+
import type { Kit } from './kit.js';
|
|
9
|
+
|
|
10
|
+
export interface PhpScope {
|
|
11
|
+
namespace: string;
|
|
12
|
+
uses: Map<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function phpScope(src: string): PhpScope {
|
|
16
|
+
const ns = /^\s*namespace\s+([\\\w]+)\s*;/m.exec(src)?.[1] ?? '';
|
|
17
|
+
const uses = new Map<string, string>();
|
|
18
|
+
for (const m of src.matchAll(/^\s*use\s+([\\\w]+)(?:\s+as\s+(\w+))?\s*;/gm)) {
|
|
19
|
+
const fq = (m[1] as string).replace(/^\\/, '');
|
|
20
|
+
uses.set(m[2] ?? fq.split('\\').pop() ?? fq, fq);
|
|
21
|
+
}
|
|
22
|
+
return { namespace: ns, uses };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fully-qualified name of a class reference as written under `scope`. */
|
|
26
|
+
export function qualify(ref: string, scope: PhpScope): string {
|
|
27
|
+
if (ref.startsWith('\\')) return ref.slice(1);
|
|
28
|
+
const [head, ...rest] = ref.split('\\');
|
|
29
|
+
const aliased = scope.uses.get(head as string);
|
|
30
|
+
if (aliased) return [aliased, ...rest].join('\\');
|
|
31
|
+
return scope.namespace ? `${scope.namespace}\\${ref}` : ref;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shortName(fq: string): string {
|
|
35
|
+
return fq.split('\\').pop() ?? fq;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The class node a fully-qualified name names, through PSR-4; null when absent. */
|
|
39
|
+
export function classNode(kit: Kit, fq: string, resolution: ResolutionConfig): string | null {
|
|
40
|
+
const file = resolvePsr4(fq, resolution, kit.fileIds);
|
|
41
|
+
const id = file ? `${file}#${shortName(fq)}` : null;
|
|
42
|
+
return id && kit.nodeById.has(id) ? id : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Offset of the bracket closing the one at `open` (`(`, `[` or `{`), skipping string literals. */
|
|
46
|
+
export function matchingClose(src: string, open: number): number {
|
|
47
|
+
const pair: Record<string, string> = { '(': ')', '[': ']', '{': '}' };
|
|
48
|
+
const o = src[open] as string;
|
|
49
|
+
const c = pair[o];
|
|
50
|
+
let depth = 0;
|
|
51
|
+
for (let i = open; i < src.length; i++) {
|
|
52
|
+
const ch = src[i];
|
|
53
|
+
if (ch === "'" || ch === '"') {
|
|
54
|
+
for (i += 1; i < src.length && src[i] !== ch; i++) if (src[i] === '\\') i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === o) depth += 1;
|
|
58
|
+
else if (ch === c) {
|
|
59
|
+
depth -= 1;
|
|
60
|
+
if (depth === 0) return i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return src.length;
|
|
64
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python source helpers the Python providers share: balanced call arguments,
|
|
3
|
+
* the variables a constructor call is assigned to, keyword-argument literals,
|
|
4
|
+
* a def's parameter list and a class's declared bases. Text rules only — the
|
|
5
|
+
* extractor already supplies the symbols and their line spans.
|
|
6
|
+
*/
|
|
7
|
+
import type { CodeNode } from '../../types.js';
|
|
8
|
+
import { type Kit, lineIndex } from './kit.js';
|
|
9
|
+
|
|
10
|
+
/** The text inside the parentheses opened at `open` (the index of a `(`), skipping string literals. */
|
|
11
|
+
export function balanced(src: string, open: number): string {
|
|
12
|
+
let depth = 0;
|
|
13
|
+
for (let i = open; i < src.length; i++) {
|
|
14
|
+
const c = src[i];
|
|
15
|
+
if (c === "'" || c === '"') {
|
|
16
|
+
const q = c;
|
|
17
|
+
for (i += 1; i < src.length && src[i] !== q && src[i] !== '\n'; i++) if (src[i] === '\\') i += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (c === '#') {
|
|
21
|
+
while (i < src.length && src[i] !== '\n') i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === '(' || c === '[' || c === '{') depth += 1;
|
|
25
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
26
|
+
depth -= 1;
|
|
27
|
+
if (depth === 0) return src.slice(open + 1, i);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return src.slice(open + 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AssignedCall {
|
|
34
|
+
/** The call's argument text. */
|
|
35
|
+
args: string;
|
|
36
|
+
index: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Module-level `name = Ctor(...)` / `name = mod.Ctor(...)` for one of `ctors`, by variable name. */
|
|
40
|
+
export function assignedCalls(src: string, ctors: readonly string[]): Map<string, AssignedCall> {
|
|
41
|
+
const out = new Map<string, AssignedCall>();
|
|
42
|
+
const re = new RegExp(`^(\\w+)\\s*(?::\\s*[\\w.]+\\s*)?=\\s*(?:[\\w.]+\\.)?(?:${ctors.join('|')})\\s*\\(`, 'gm');
|
|
43
|
+
for (const m of src.matchAll(re)) {
|
|
44
|
+
const open = (m.index as number) + m[0].length - 1;
|
|
45
|
+
out.set(m[1] as string, { args: balanced(src, open), index: m.index as number });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The string literal a keyword argument is given (`prefix="/api"`), if it is one. */
|
|
51
|
+
export function kwargString(args: string, key: string): string | undefined {
|
|
52
|
+
return new RegExp(`(?:^|[,(\\s])${key}\\s*=\\s*[rbuf]?(['"])([^'"]*)\\1`).exec(args)?.[2];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Offset of the first character of 1-based `line`. */
|
|
56
|
+
export function lineOffset(src: string, line: number): number {
|
|
57
|
+
let off = 0;
|
|
58
|
+
for (let l = 1; l < line; l++) {
|
|
59
|
+
const nl = src.indexOf('\n', off);
|
|
60
|
+
if (nl < 0) return src.length;
|
|
61
|
+
off = nl + 1;
|
|
62
|
+
}
|
|
63
|
+
return off;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The parameter list text of the def a function or method node starts at. */
|
|
67
|
+
export function paramsOf(src: string, fn: CodeNode): string {
|
|
68
|
+
const from = lineOffset(src, fn.source_location[0] as number);
|
|
69
|
+
const def = /\bdef\s+\w+\s*\(/g;
|
|
70
|
+
def.lastIndex = from;
|
|
71
|
+
const m = def.exec(src);
|
|
72
|
+
if (!m) return '';
|
|
73
|
+
return balanced(src, (m.index as number) + m[0].length - 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The source lines of a node's span, joined. */
|
|
77
|
+
export function spanText(src: string, n: CodeNode): string {
|
|
78
|
+
const lines = src.split('\n');
|
|
79
|
+
const start = n.source_location[0] as number;
|
|
80
|
+
const end = (n.source_location[2] ?? start) as number;
|
|
81
|
+
return lines.slice(start - 1, end).join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The base-class expressions a class statement declares (`class A(models.Model, Mixin):` → `['models.Model', 'Mixin']`). */
|
|
85
|
+
export function classBases(src: string, cls: CodeNode): string[] {
|
|
86
|
+
const head = /^\s*class\s+\w+\s*\(/.exec(spanText(src, cls));
|
|
87
|
+
if (!head) return [];
|
|
88
|
+
const text = spanText(src, cls);
|
|
89
|
+
return balanced(text, head[0].length - 1)
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((b) => b.trim())
|
|
92
|
+
.filter((b) => b.length > 0 && !b.includes('='));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RouteDecorator {
|
|
96
|
+
/** The decorated object (`app` in `@app.get(...)`). */
|
|
97
|
+
receiver: string;
|
|
98
|
+
/** The decorator attribute (`get`, `route`). */
|
|
99
|
+
method: string;
|
|
100
|
+
/** The route path: the first positional string literal, else a `path=` literal. */
|
|
101
|
+
path: string | null;
|
|
102
|
+
/** The decorator's argument text. */
|
|
103
|
+
args: string;
|
|
104
|
+
line: number;
|
|
105
|
+
/** The function or method the decorator decorates. */
|
|
106
|
+
handler: CodeNode | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every `@<receiver>.<method>(...)` decorator in `file` whose method is one of `methods`. */
|
|
110
|
+
export function routeDecorators(kit: Kit, file: { path: string; source: string }, methods: readonly string[]): RouteDecorator[] {
|
|
111
|
+
const at = lineIndex(file.source);
|
|
112
|
+
const out: RouteDecorator[] = [];
|
|
113
|
+
const re = new RegExp(`^[ \\t]*@(\\w+)\\.(${methods.join('|')})\\s*\\(`, 'gm');
|
|
114
|
+
for (const m of file.source.matchAll(re)) {
|
|
115
|
+
const line = at(m.index as number);
|
|
116
|
+
const args = balanced(file.source, (m.index as number) + m[0].length - 1);
|
|
117
|
+
const positional = /^\s*[rbuf]?(['"])([^'"]*)\1/.exec(args)?.[2];
|
|
118
|
+
const handler = kit.nextSymbol(file.path, line, ['function', 'method']);
|
|
119
|
+
out.push({
|
|
120
|
+
receiver: m[1] as string,
|
|
121
|
+
method: m[2] as string,
|
|
122
|
+
path: positional ?? kwargString(args, 'path') ?? null,
|
|
123
|
+
args,
|
|
124
|
+
line,
|
|
125
|
+
handler: handler && handler.kind !== 'class' ? handler : null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A call's argument text split at its top-level commas, each trimmed. */
|
|
132
|
+
export function splitArgs(args: string): string[] {
|
|
133
|
+
const out: string[] = [];
|
|
134
|
+
let depth = 0;
|
|
135
|
+
let start = 0;
|
|
136
|
+
for (let i = 0; i < args.length; i++) {
|
|
137
|
+
const c = args[i];
|
|
138
|
+
if (c === "'" || c === '"') {
|
|
139
|
+
const q = c;
|
|
140
|
+
for (i += 1; i < args.length && args[i] !== q; i++) if (args[i] === '\\') i += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (c === '(' || c === '[' || c === '{') depth += 1;
|
|
144
|
+
else if (c === ')' || c === ']' || c === '}') depth -= 1;
|
|
145
|
+
else if (c === ',' && depth === 0) {
|
|
146
|
+
out.push(args.slice(start, i).trim());
|
|
147
|
+
start = i + 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const last = args.slice(start).trim();
|
|
151
|
+
if (last) out.push(last);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A string literal argument's value (`"x"`, `r"^x$"`), or null when the argument is no plain literal. */
|
|
156
|
+
export function stringLiteral(arg: string): string | null {
|
|
157
|
+
return /^[rbu]?(['"])([^'"]*)\1$/.exec(arg.trim())?.[2] ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The module file a dotted import names (`a.b` → `a/b.py` or `a/b/__init__.py`),
|
|
162
|
+
* searched under each root and, for a relative import, beside `file`.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveModule(file: string, dotted: string, roots: readonly string[], inventory: ReadonlySet<string>): string | null {
|
|
165
|
+
const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '';
|
|
166
|
+
const rel = /^\.+/.exec(dotted)?.[0].length ?? 0;
|
|
167
|
+
const parts = dotted.slice(rel).split('.').filter(Boolean);
|
|
168
|
+
let bases: string[];
|
|
169
|
+
if (rel > 0) {
|
|
170
|
+
const segs = dir ? dir.split('/') : [];
|
|
171
|
+
bases = [segs.slice(0, Math.max(0, segs.length - (rel - 1))).join('/')];
|
|
172
|
+
} else bases = [...roots];
|
|
173
|
+
for (const b of bases) {
|
|
174
|
+
const stem = [b, ...parts].filter(Boolean).join('/');
|
|
175
|
+
// `from . import x` names the package itself: its `__init__.py`.
|
|
176
|
+
const candidates = parts.length > 0 ? [`${stem}.py`, `${stem}/__init__.py`] : rel > 0 ? [stem ? `${stem}/__init__.py` : '__init__.py'] : [];
|
|
177
|
+
for (const c of candidates) if (inventory.has(c)) return c;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Every `from m import a, b as c` item, as the module text, the imported name and the local name. */
|
|
183
|
+
function fromImports(src: string): Array<{ mod: string; name: string; local: string }> {
|
|
184
|
+
const out: Array<{ mod: string; name: string; local: string }> = [];
|
|
185
|
+
for (const m of src.matchAll(/^\s*from\s+([.\w]+)\s+import\s+\(?([^)\n]+)\)?/gm)) {
|
|
186
|
+
for (const item of (m[2] as string).split(',')) {
|
|
187
|
+
const [name, alias] = item.trim().split(/\s+as\s+/) as [string, string | undefined];
|
|
188
|
+
if (/^\w+$/.test(name)) out.push({ mod: m[1] as string, name, local: alias ?? name });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Module files the file imports by name, by local name: `from a import b` and
|
|
196
|
+
* `import a.b as b` map `b` to `a/b.py` (or `a/b/__init__.py`) when that file is in
|
|
197
|
+
* `inventory`, searched under each root and, for a relative import, beside `file`.
|
|
198
|
+
*/
|
|
199
|
+
export function importedModules(file: string, src: string, roots: readonly string[], inventory: ReadonlySet<string>): Map<string, string> {
|
|
200
|
+
const out = new Map<string, string>();
|
|
201
|
+
for (const { mod, name, local } of fromImports(src)) {
|
|
202
|
+
const hit = resolveModule(file, mod.endsWith('.') ? `${mod}${name}` : `${mod}.${name}`, roots, inventory);
|
|
203
|
+
if (hit) out.set(local, hit);
|
|
204
|
+
}
|
|
205
|
+
for (const m of src.matchAll(/^\s*import\s+([\w.]+)\s+as\s+(\w+)\s*$/gm)) {
|
|
206
|
+
const hit = resolveModule(file, m[1] as string, roots, inventory);
|
|
207
|
+
if (hit) out.set(m[2] as string, hit);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Names the file imports out of a module it can resolve, by local name:
|
|
214
|
+
* `from app.main import bp as main_bp` maps `main_bp` to `{ file: 'app/main/__init__.py', name: 'bp' }`.
|
|
215
|
+
*/
|
|
216
|
+
export function importedNames(file: string, src: string, roots: readonly string[], inventory: ReadonlySet<string>): Map<string, { file: string; name: string }> {
|
|
217
|
+
const out = new Map<string, { file: string; name: string }>();
|
|
218
|
+
for (const { mod, name, local } of fromImports(src)) {
|
|
219
|
+
const hit = resolveModule(file, mod, roots, inventory);
|
|
220
|
+
if (hit) out.set(local, { file: hit, name });
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring Boot: the links a Spring application makes by annotation.
|
|
3
|
+
*
|
|
4
|
+
* @GetMapping("/x") · @RequestMapping(value = "/x", method = RequestMethod.GET)
|
|
5
|
+
* under a class-level @RequestMapping("/api") route --routes-to--> the method
|
|
6
|
+
* @Entity class, @Table(name = "t") or the default class --persists--> table:t
|
|
7
|
+
* interface R extends JpaRepository<E, Id> R --relates--> E
|
|
8
|
+
* @Autowired field · constructor parameter class --injects--> the in-repo type
|
|
9
|
+
*
|
|
10
|
+
* The Java extractor spans a method or class from its first annotation, so a
|
|
11
|
+
* mapping annotation belongs to the method whose span holds it; one outside
|
|
12
|
+
* every method span (on the class) is never a route. With no table named, the
|
|
13
|
+
* table is Spring Boot's default physical name: the entity class in snake_case
|
|
14
|
+
* (`OrderLine` → `order_line`); `@Entity(name = "X")` is not read. A method-level
|
|
15
|
+
* `@RequestMapping` without `method =` and a constructor of a class carrying no
|
|
16
|
+
* stereotype annotation are not followed.
|
|
17
|
+
*/
|
|
18
|
+
import { jvmRoots } from './shared/detect.js';
|
|
19
|
+
import { joinRoute, tableMapping } from './shared/extractors.js';
|
|
20
|
+
import { escapeRe, type Kit, lineIndex, makeKit, packageSymbol } from './shared/kit.js';
|
|
21
|
+
import type { ConventionProvider } from './types.js';
|
|
22
|
+
|
|
23
|
+
const VERB_MAPPING = /@(?<verb>Get|Post|Put|Delete|Patch)Mapping\b(?:\s*\(\s*(?:(?:value|path)\s*=\s*)?(?:\{\s*)?"(?<path>[^"]*)")?/g;
|
|
24
|
+
const REQUEST_MAPPING = /@RequestMapping\s*\((?<args>[^)]*)\)/g;
|
|
25
|
+
const STEREOTYPE = /@(?:Component|Service|Repository|Controller|RestController|Configuration)\b/;
|
|
26
|
+
const AUTOWIRED_FIELD = /@Autowired\s+(?:(?:private|protected|public|final|static|transient)\s+)*(?<type>[A-Z]\w*)(?:<[^>;]*>)?\s+\w+\s*[;=]/g;
|
|
27
|
+
const REPOSITORY =
|
|
28
|
+
/\binterface\s+(?<repo>\w+)\s+extends\s+(?:[\w.]+\.)?(?:JpaRepository|CrudRepository|ListCrudRepository|PagingAndSortingRepository|ReactiveCrudRepository|MongoRepository)\s*<\s*(?<entity>[A-Z]\w*)/g;
|
|
29
|
+
|
|
30
|
+
function snakeCase(name: string): string {
|
|
31
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An in-repo class or interface named `name` as `file` sees it: same file, an import, or the same package. */
|
|
35
|
+
function resolveType(kit: Kit, file: string, name: string): string | null {
|
|
36
|
+
const id = kit.resolveName(file, name);
|
|
37
|
+
const n = id ? kit.nodeById.get(id) : undefined;
|
|
38
|
+
if (n && (n.kind === 'class' || n.kind === 'interface')) return n.id;
|
|
39
|
+
return packageSymbol(kit, file, name, ['class', 'interface']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The method whose span holds `line`, or null — never the file. */
|
|
43
|
+
function methodAt(kit: Kit, file: string, line: number) {
|
|
44
|
+
const id = kit.enclosing(file, line);
|
|
45
|
+
const n = kit.nodeById.get(id);
|
|
46
|
+
return n && n.kind === 'method' ? n : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function routes(kit: Kit): void {
|
|
50
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
51
|
+
const at = lineIndex(f.source);
|
|
52
|
+
const lines = f.source.split('\n');
|
|
53
|
+
const hits: Array<{ line: number; verb: string; path: string | undefined }> = [];
|
|
54
|
+
const prefixes: Array<{ line: number; path: string }> = [];
|
|
55
|
+
for (const m of f.source.matchAll(VERB_MAPPING)) {
|
|
56
|
+
hits.push({ line: at(m.index as number), verb: m.groups?.['verb'] as string, path: m.groups?.['path'] });
|
|
57
|
+
}
|
|
58
|
+
for (const m of f.source.matchAll(REQUEST_MAPPING)) {
|
|
59
|
+
const args = m.groups?.['args'] ?? '';
|
|
60
|
+
const line = at(m.index as number);
|
|
61
|
+
const p = /^\s*(?:(?:value|path)\s*=\s*)?(?:\{\s*)?"(?<path>[^"]*)"/.exec(args)?.groups?.['path']
|
|
62
|
+
?? /\b(?:value|path)\s*=\s*(?:\{\s*)?"(?<path>[^"]*)"/.exec(args)?.groups?.['path'];
|
|
63
|
+
const verb = /\bmethod\s*=\s*(?:\{\s*)?RequestMethod\.(?<verb>[A-Z]+)/.exec(args)?.groups?.['verb'];
|
|
64
|
+
if (methodAt(kit, f.path, line)) {
|
|
65
|
+
if (verb) hits.push({ line, verb, path: p });
|
|
66
|
+
} else if (p !== undefined) prefixes.push({ line, path: p });
|
|
67
|
+
}
|
|
68
|
+
for (const hit of hits) {
|
|
69
|
+
const handler = methodAt(kit, f.path, hit.line);
|
|
70
|
+
if (!handler) continue;
|
|
71
|
+
const cls = kit.classAt(f.path, handler.source_location[0] as number);
|
|
72
|
+
let prefix: string | undefined;
|
|
73
|
+
if (cls) {
|
|
74
|
+
const start = cls.source_location[0] as number;
|
|
75
|
+
const decl = lines.findIndex((l, i) => i >= start - 1 && new RegExp(`\\bclass\\s+${cls.label}\\b`).test(l)) + 1;
|
|
76
|
+
prefix = prefixes.filter((p) => p.line >= start && p.line <= decl).pop()?.path;
|
|
77
|
+
}
|
|
78
|
+
const id = kit.route(f.path, hit.verb.toUpperCase(), joinRoute(prefix, hit.path), hit.line);
|
|
79
|
+
kit.emit(id, handler.id, 'routes-to', 'route-table');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function repositories(kit: Kit): void {
|
|
85
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
86
|
+
for (const m of f.source.matchAll(REPOSITORY)) {
|
|
87
|
+
const repo = resolveType(kit, f.path, m.groups?.['repo'] as string);
|
|
88
|
+
const entity = resolveType(kit, f.path, m.groups?.['entity'] as string);
|
|
89
|
+
if (repo && entity) kit.emit(repo, entity, 'relates', 'orm-relation');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function injections(kit: Kit): void {
|
|
95
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
96
|
+
const at = lineIndex(f.source);
|
|
97
|
+
const lines = f.source.split('\n');
|
|
98
|
+
for (const m of f.source.matchAll(AUTOWIRED_FIELD)) {
|
|
99
|
+
const cls = kit.classAt(f.path, at(m.index as number));
|
|
100
|
+
const target = resolveType(kit, f.path, m.groups?.['type'] as string);
|
|
101
|
+
if (cls && target && target !== cls.id) kit.emit(cls.id, target, 'injects', 'di-binding');
|
|
102
|
+
}
|
|
103
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
104
|
+
if (n.kind !== 'class') continue;
|
|
105
|
+
const ctorId = kit.methodOf(n.id, n.label);
|
|
106
|
+
const ctor = ctorId ? kit.nodeById.get(ctorId) : undefined;
|
|
107
|
+
if (!ctor) continue;
|
|
108
|
+
const start = n.source_location[0] as number;
|
|
109
|
+
const head = lines.slice(start - 1, (ctor.source_location[0] as number) - 1).join('\n');
|
|
110
|
+
const ctorText = lines.slice((ctor.source_location[0] as number) - 1, ctor.source_location[2] as number).join('\n');
|
|
111
|
+
if (!STEREOTYPE.test(head.split(new RegExp(`\\bclass\\s+${escapeRe(n.label)}\\b`))[0] ?? '') && !/@Autowired\b/.test(ctorText)) continue;
|
|
112
|
+
const params = new RegExp(`\\b${escapeRe(n.label)}\\s*\\((?<params>[^)]*)\\)`).exec(ctorText)?.groups?.['params'] ?? '';
|
|
113
|
+
for (const param of params.split(',')) {
|
|
114
|
+
const type = /(?:^|\s)(?<type>[A-Z]\w*)(?:<[^>]*>)?\s+\w+\s*$/.exec(param.replace(/@\w+(?:\([^)]*\))?/g, '').trim())?.groups?.['type'];
|
|
115
|
+
const target = type ? resolveType(kit, f.path, type) : null;
|
|
116
|
+
if (target && target !== n.id) kit.emit(n.id, target, 'injects', 'di-binding');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const springProvider: ConventionProvider = {
|
|
123
|
+
id: 'spring',
|
|
124
|
+
detect: (probe) => jvmRoots(probe, /spring-boot|org\.springframework\.boot/),
|
|
125
|
+
build: (input) => {
|
|
126
|
+
const kit = makeKit(input);
|
|
127
|
+
routes(kit);
|
|
128
|
+
tableMapping(kit, {
|
|
129
|
+
exts: ['.java'],
|
|
130
|
+
entity: /@Entity\b/g,
|
|
131
|
+
tableName: /@Table\s*\(\s*(?:name\s*=\s*)?"(?<name>[^"]+)"/,
|
|
132
|
+
convention: snakeCase,
|
|
133
|
+
});
|
|
134
|
+
repositories(kit);
|
|
135
|
+
injections(kit);
|
|
136
|
+
return kit.result();
|
|
137
|
+
},
|
|
138
|
+
};
|