@event4u/agent-config 17.2.1 → 17.4.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 +167 -193
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -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/registry.js +2 -2
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +190 -192
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.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_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- 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 +443 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +1001 -938
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +57 -7
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- 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 +71202 -63282
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +18 -15
- 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 +6 -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/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +46 -6
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +18 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +4 -3
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +14 -0
- package/src/scripts/check_egress_mutations.ts +1 -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 +145 -45
- package/src/scripts/code_graph/cli.ts +382 -35
- package/src/scripts/code_graph/conventions_default.ts +247 -0
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +8 -2
- package/src/scripts/code_graph/extract_more.ts +241 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +195 -53
- package/src/scripts/code_graph/map_model.ts +113 -20
- package/src/scripts/code_graph/map_verify.ts +80 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -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 +299 -0
- package/src/scripts/code_graph/providers/django.ts +206 -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 +103 -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/rails.ts +204 -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 +175 -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/query.ts +9 -9
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +197 -0
- package/src/scripts/code_graph/repo_identity.ts +169 -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 +31 -28
- package/src/scripts/code_graph/types.ts +79 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +15 -15
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +11 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +43 -23
- package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +8 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- 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 +70 -15
- package/src/scripts/mcp_server/graph_tools.ts +165 -41
- package/src/scripts/network_sources.ts +24 -6
- 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/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- 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 +8 -6
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -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,175 @@
|
|
|
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
|
+
* @Controller method `return "a/b"` · ModelAndView("a/b")
|
|
10
|
+
* · a same-file `static final String` constant method --renders--> templates/a/b.html
|
|
11
|
+
*
|
|
12
|
+
* The Java extractor spans a method or class from its first annotation, so a
|
|
13
|
+
* mapping annotation belongs to the method whose span holds it; one outside
|
|
14
|
+
* every method span (on the class) is never a route. With no table named, the
|
|
15
|
+
* table is Spring Boot's default physical name: the entity class in snake_case
|
|
16
|
+
* (`OrderLine` → `order_line`); `@Entity(name = "X")` is not read. A method-level
|
|
17
|
+
* `@RequestMapping` without `method =` and a constructor of a class carrying no
|
|
18
|
+
* stereotype annotation are not followed. A view name is read only in a
|
|
19
|
+
* `@Controller` file — a `@RestController` returns response bodies — and never
|
|
20
|
+
* from a `redirect:` / `forward:`; it renders the one template of that name
|
|
21
|
+
* under the Thymeleaf, FreeMarker, Mustache or JSP folders, or nothing.
|
|
22
|
+
*/
|
|
23
|
+
import { jvmRoots } from './shared/detect.js';
|
|
24
|
+
import { joinRoute, tableMapping } from './shared/extractors.js';
|
|
25
|
+
import { escapeRe, type Kit, lineIndex, makeKit, packageSymbol } from './shared/kit.js';
|
|
26
|
+
import type { ConventionProvider } from './types.js';
|
|
27
|
+
|
|
28
|
+
const VERB_MAPPING = /@(?<verb>Get|Post|Put|Delete|Patch)Mapping\b(?:\s*\(\s*(?:(?:value|path)\s*=\s*)?(?:\{\s*)?"(?<path>[^"]*)")?/g;
|
|
29
|
+
const REQUEST_MAPPING = /@RequestMapping\s*\((?<args>[^)]*)\)/g;
|
|
30
|
+
const STEREOTYPE = /@(?:Component|Service|Repository|Controller|RestController|Configuration)\b/;
|
|
31
|
+
const AUTOWIRED_FIELD = /@Autowired\s+(?:(?:private|protected|public|final|static|transient)\s+)*(?<type>[A-Z]\w*)(?:<[^>;]*>)?\s+\w+\s*[;=]/g;
|
|
32
|
+
const REPOSITORY =
|
|
33
|
+
/\binterface\s+(?<repo>\w+)\s+extends\s+(?:[\w.]+\.)?(?:JpaRepository|CrudRepository|ListCrudRepository|PagingAndSortingRepository|ReactiveCrudRepository|MongoRepository)\s*<\s*(?<entity>[A-Z]\w*)/g;
|
|
34
|
+
|
|
35
|
+
function snakeCase(name: string): string {
|
|
36
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** An in-repo class or interface named `name` as `file` sees it: same file, an import, or the same package. */
|
|
40
|
+
function resolveType(kit: Kit, file: string, name: string): string | null {
|
|
41
|
+
const id = kit.resolveName(file, name);
|
|
42
|
+
const n = id ? kit.nodeById.get(id) : undefined;
|
|
43
|
+
if (n && (n.kind === 'class' || n.kind === 'interface')) return n.id;
|
|
44
|
+
return packageSymbol(kit, file, name, ['class', 'interface']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The method whose span holds `line`, or null — never the file. */
|
|
48
|
+
function methodAt(kit: Kit, file: string, line: number) {
|
|
49
|
+
const id = kit.enclosing(file, line);
|
|
50
|
+
const n = kit.nodeById.get(id);
|
|
51
|
+
return n && n.kind === 'method' ? n : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function routes(kit: Kit): void {
|
|
55
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
56
|
+
const at = lineIndex(f.source);
|
|
57
|
+
const lines = f.source.split('\n');
|
|
58
|
+
const hits: Array<{ line: number; verb: string; path: string | undefined }> = [];
|
|
59
|
+
const prefixes: Array<{ line: number; path: string }> = [];
|
|
60
|
+
for (const m of f.source.matchAll(VERB_MAPPING)) {
|
|
61
|
+
hits.push({ line: at(m.index as number), verb: m.groups?.['verb'] as string, path: m.groups?.['path'] });
|
|
62
|
+
}
|
|
63
|
+
for (const m of f.source.matchAll(REQUEST_MAPPING)) {
|
|
64
|
+
const args = m.groups?.['args'] ?? '';
|
|
65
|
+
const line = at(m.index as number);
|
|
66
|
+
const p = /^\s*(?:(?:value|path)\s*=\s*)?(?:\{\s*)?"(?<path>[^"]*)"/.exec(args)?.groups?.['path']
|
|
67
|
+
?? /\b(?:value|path)\s*=\s*(?:\{\s*)?"(?<path>[^"]*)"/.exec(args)?.groups?.['path'];
|
|
68
|
+
const verb = /\bmethod\s*=\s*(?:\{\s*)?RequestMethod\.(?<verb>[A-Z]+)/.exec(args)?.groups?.['verb'];
|
|
69
|
+
if (methodAt(kit, f.path, line)) {
|
|
70
|
+
if (verb) hits.push({ line, verb, path: p });
|
|
71
|
+
} else if (p !== undefined) prefixes.push({ line, path: p });
|
|
72
|
+
}
|
|
73
|
+
for (const hit of hits) {
|
|
74
|
+
const handler = methodAt(kit, f.path, hit.line);
|
|
75
|
+
if (!handler) continue;
|
|
76
|
+
const cls = kit.classAt(f.path, handler.source_location[0] as number);
|
|
77
|
+
let prefix: string | undefined;
|
|
78
|
+
if (cls) {
|
|
79
|
+
const start = cls.source_location[0] as number;
|
|
80
|
+
const decl = lines.findIndex((l, i) => i >= start - 1 && new RegExp(`\\bclass\\s+${cls.label}\\b`).test(l)) + 1;
|
|
81
|
+
prefix = prefixes.filter((p) => p.line >= start && p.line <= decl).pop()?.path;
|
|
82
|
+
}
|
|
83
|
+
const id = kit.route(f.path, hit.verb.toUpperCase(), joinRoute(prefix, hit.path), hit.line);
|
|
84
|
+
kit.emit(id, handler.id, 'routes-to', 'route-table');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function repositories(kit: Kit): void {
|
|
90
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
91
|
+
for (const m of f.source.matchAll(REPOSITORY)) {
|
|
92
|
+
const repo = resolveType(kit, f.path, m.groups?.['repo'] as string);
|
|
93
|
+
const entity = resolveType(kit, f.path, m.groups?.['entity'] as string);
|
|
94
|
+
if (repo && entity) kit.emit(repo, entity, 'relates', 'orm-relation');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function injections(kit: Kit): void {
|
|
100
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
101
|
+
const at = lineIndex(f.source);
|
|
102
|
+
const lines = f.source.split('\n');
|
|
103
|
+
for (const m of f.source.matchAll(AUTOWIRED_FIELD)) {
|
|
104
|
+
const cls = kit.classAt(f.path, at(m.index as number));
|
|
105
|
+
const target = resolveType(kit, f.path, m.groups?.['type'] as string);
|
|
106
|
+
if (cls && target && target !== cls.id) kit.emit(cls.id, target, 'injects', 'di-binding');
|
|
107
|
+
}
|
|
108
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
109
|
+
if (n.kind !== 'class') continue;
|
|
110
|
+
const ctorId = kit.methodOf(n.id, n.label);
|
|
111
|
+
const ctor = ctorId ? kit.nodeById.get(ctorId) : undefined;
|
|
112
|
+
if (!ctor) continue;
|
|
113
|
+
const start = n.source_location[0] as number;
|
|
114
|
+
const head = lines.slice(start - 1, (ctor.source_location[0] as number) - 1).join('\n');
|
|
115
|
+
const ctorText = lines.slice((ctor.source_location[0] as number) - 1, ctor.source_location[2] as number).join('\n');
|
|
116
|
+
if (!STEREOTYPE.test(head.split(new RegExp(`\\bclass\\s+${escapeRe(n.label)}\\b`))[0] ?? '') && !/@Autowired\b/.test(ctorText)) continue;
|
|
117
|
+
const params = new RegExp(`\\b${escapeRe(n.label)}\\s*\\((?<params>[^)]*)\\)`).exec(ctorText)?.groups?.['params'] ?? '';
|
|
118
|
+
for (const param of params.split(',')) {
|
|
119
|
+
const type = /(?:^|\s)(?<type>[A-Z]\w*)(?:<[^>]*>)?\s+\w+\s*$/.exec(param.replace(/@\w+(?:\([^)]*\))?/g, '').trim())?.groups?.['type'];
|
|
120
|
+
const target = type ? resolveType(kit, f.path, type) : null;
|
|
121
|
+
if (target && target !== n.id) kit.emit(n.id, target, 'injects', 'di-binding');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const VIEW_ROOTS = ['src/main/resources/templates', 'src/main/webapp/WEB-INF/views', 'src/main/webapp/WEB-INF/jsp'];
|
|
128
|
+
const VIEW_EXTS = ['.html', '.ftlh', '.ftl', '.mustache', '.jte', '.jsp'];
|
|
129
|
+
const VIEW_NAME = /\b(?:return\s+|new\s+ModelAndView\s*\(\s*)(?:"(?<lit>[^"]*)"|(?<const>[A-Z][A-Z0-9_]*))\s*[;),]/g;
|
|
130
|
+
const VIEW_CONSTANT = /\bstatic\s+final\s+String\s+(?<name>[A-Z][A-Z0-9_]*)\s*=\s*"(?<value>[^"]*)"/g;
|
|
131
|
+
|
|
132
|
+
function views(kit: Kit): void {
|
|
133
|
+
const inventory = new Set(kit.paths);
|
|
134
|
+
for (const f of kit.filesWithExt('.java')) {
|
|
135
|
+
if (!/@Controller\b/.test(f.source)) continue;
|
|
136
|
+
const at = lineIndex(f.source);
|
|
137
|
+
const constants = new Map([...f.source.matchAll(VIEW_CONSTANT)].map((m) => [m.groups?.['name'] as string, m.groups?.['value'] as string]));
|
|
138
|
+
for (const m of f.source.matchAll(VIEW_NAME)) {
|
|
139
|
+
const name = m.groups?.['lit'] ?? constants.get(m.groups?.['const'] ?? '');
|
|
140
|
+
if (!name || /^(?:redirect|forward):/.test(name)) continue;
|
|
141
|
+
const method = methodAt(kit, f.path, at(m.index as number));
|
|
142
|
+
if (!method) continue;
|
|
143
|
+
const hits = new Set<string>();
|
|
144
|
+
for (const r of kit.roots)
|
|
145
|
+
for (const dir of VIEW_ROOTS)
|
|
146
|
+
for (const ext of VIEW_EXTS) {
|
|
147
|
+
const c = [r, dir, `${name.replace(/^\/+/, '')}${ext}`].filter(Boolean).join('/');
|
|
148
|
+
if (inventory.has(c)) hits.add(c);
|
|
149
|
+
}
|
|
150
|
+
if (hits.size !== 1) continue;
|
|
151
|
+
const target = [...hits][0] as string;
|
|
152
|
+
kit.fileNode(target);
|
|
153
|
+
kit.emit(method.id, target, 'renders', 'directive-scan');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const springProvider: ConventionProvider = {
|
|
159
|
+
id: 'spring',
|
|
160
|
+
detect: (probe) => jvmRoots(probe, /spring-boot|org\.springframework\.boot/),
|
|
161
|
+
build: (input) => {
|
|
162
|
+
const kit = makeKit(input);
|
|
163
|
+
routes(kit);
|
|
164
|
+
tableMapping(kit, {
|
|
165
|
+
exts: ['.java'],
|
|
166
|
+
entity: /@Entity\b/g,
|
|
167
|
+
tableName: /@Table\s*\(\s*(?:name\s*=\s*)?"(?<name>[^"]+)"/,
|
|
168
|
+
convention: snakeCase,
|
|
169
|
+
});
|
|
170
|
+
repositories(kit);
|
|
171
|
+
injections(kit);
|
|
172
|
+
views(kit);
|
|
173
|
+
return kit.result();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLAlchemy (and Flask-SQLAlchemy, SQLModel): a declarative class with
|
|
3
|
+
* `__tablename__ = "x"` --persists--> `table:x`; a SQLModel class declared with
|
|
4
|
+
* `table=True` and no `__tablename__` gets SQLModel's default, its lowercased
|
|
5
|
+
* class name. `relationship("Other")` / `relationship(Other)` inside a mapped
|
|
6
|
+
* class --relates--> the mapped class named: a string resolves to the one mapped
|
|
7
|
+
* class of that name in the project, a bare name the way the file imports it.
|
|
8
|
+
*
|
|
9
|
+
* Not drawn: imperative `Table("x", metadata, ...)` mappings, Flask-SQLAlchemy's
|
|
10
|
+
* implicit table names (a `db.Model` without `__tablename__`), a
|
|
11
|
+
* `relationship(lambda: X)` or a target named only in a `Mapped[...]` annotation,
|
|
12
|
+
* and SQLModel's `Relationship(...)`.
|
|
13
|
+
*/
|
|
14
|
+
import type { CodeNode } from '../types.js';
|
|
15
|
+
import { pythonRoots } from './shared/detect.js';
|
|
16
|
+
import { lineIndex, makeKit } from './shared/kit.js';
|
|
17
|
+
import { balanced, spanText, splitArgs, stringLiteral } from './shared/python.js';
|
|
18
|
+
import type { ConventionProvider } from './types.js';
|
|
19
|
+
|
|
20
|
+
export const sqlalchemyProvider: ConventionProvider = {
|
|
21
|
+
id: 'sqlalchemy',
|
|
22
|
+
detect: (probe) => pythonRoots(probe, ['sqlalchemy', 'flask-sqlalchemy', 'sqlmodel']),
|
|
23
|
+
build: (input) => {
|
|
24
|
+
const kit = makeKit(input);
|
|
25
|
+
const mapped = new Map<string, { node: CodeNode; source: string }>();
|
|
26
|
+
for (const f of kit.filesWithExt('.py')) {
|
|
27
|
+
const at = lineIndex(f.source);
|
|
28
|
+
for (const m of f.source.matchAll(/^[ \t]+__tablename__\s*(?::[^=\n]+)?=\s*(['"])(\w+)\1/gm)) {
|
|
29
|
+
const cls = kit.classAt(f.path, at(m.index as number));
|
|
30
|
+
if (!cls || mapped.has(cls.id)) continue;
|
|
31
|
+
mapped.set(cls.id, { node: cls, source: f.source });
|
|
32
|
+
kit.emit(cls.id, kit.table(m[2] as string, f.path, cls.source_location[0] as number), 'persists', 'schema-call');
|
|
33
|
+
}
|
|
34
|
+
for (const n of kit.nodesInFile(f.path)) {
|
|
35
|
+
if (n.kind !== 'class' || mapped.has(n.id)) continue;
|
|
36
|
+
const head = /^\s*class\s+\w+\s*\(([^)]*)\)/.exec(spanText(f.source, n))?.[1] ?? '';
|
|
37
|
+
if (!/\btable\s*=\s*True\b/.test(head)) continue;
|
|
38
|
+
mapped.set(n.id, { node: n, source: f.source });
|
|
39
|
+
kit.emit(n.id, kit.table(n.label.toLowerCase(), f.path, n.source_location[0] as number), 'persists', 'schema-call');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const byLabel = (label: string): string | null => {
|
|
43
|
+
const hits = [...mapped.values()].filter((x) => x.node.label === label);
|
|
44
|
+
return hits.length === 1 ? (hits[0]?.node.id ?? null) : null;
|
|
45
|
+
};
|
|
46
|
+
for (const { node, source } of mapped.values()) {
|
|
47
|
+
const region = spanText(source, node);
|
|
48
|
+
for (const r of region.matchAll(/\brelationship\s*\(/g)) {
|
|
49
|
+
const first = splitArgs(balanced(region, (r.index as number) + r[0].length - 1))[0] ?? '';
|
|
50
|
+
const lit = stringLiteral(first);
|
|
51
|
+
let target: string | null = null;
|
|
52
|
+
if (lit !== null) target = byLabel(lit.slice(lit.lastIndexOf('.') + 1));
|
|
53
|
+
else if (/^\w+$/.test(first)) {
|
|
54
|
+
const resolved = kit.resolveName(node.source_file, first);
|
|
55
|
+
target = resolved && mapped.has(resolved) ? resolved : null;
|
|
56
|
+
}
|
|
57
|
+
if (target && target !== node.id) kit.emit(node.id, target, 'relates', 'orm-relation');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return kit.result();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit file routing under `src/routes/` of a package root:
|
|
3
|
+
*
|
|
4
|
+
* +page.svelte · +page.ts PAGE /a/[slug] --routes-to--> that module
|
|
5
|
+
* +server.ts GET /api/x · POST … --routes-to--> the exported verb function
|
|
6
|
+
* +layout.svelte --includes--> every +page.svelte below its directory
|
|
7
|
+
*
|
|
8
|
+
* Route groups drop out of the path; route ids match the default pass's, so a
|
|
9
|
+
* page drawn here replaces the generic one. `+page.server.ts` load functions,
|
|
10
|
+
* `+layout@group` resets and a custom `kit.files.routes` directory are not
|
|
11
|
+
* followed; the default pass still adds its generic `ROUTE` node for a
|
|
12
|
+
* `+server.ts`.
|
|
13
|
+
*/
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { npmRoots } from './shared/detect.js';
|
|
17
|
+
import { cleanRoute, exportedVerbRoutes, underRoot } from './shared/js.js';
|
|
18
|
+
import { makeKit } from './shared/kit.js';
|
|
19
|
+
import type { ConventionProvider } from './types.js';
|
|
20
|
+
|
|
21
|
+
const PAGE = /^\+page\.(svelte|ts|js)$/;
|
|
22
|
+
const SERVER = /^\+server\.(ts|js)$/;
|
|
23
|
+
const LAYOUT = /^\+layout\.svelte$/;
|
|
24
|
+
|
|
25
|
+
export const sveltekitProvider: ConventionProvider = {
|
|
26
|
+
id: 'sveltekit',
|
|
27
|
+
detect: (probe) => npmRoots(probe, ['@sveltejs/kit']),
|
|
28
|
+
build: (input) => {
|
|
29
|
+
const kit = makeKit(input);
|
|
30
|
+
const pages: string[] = [];
|
|
31
|
+
const layouts: string[] = [];
|
|
32
|
+
for (const p of kit.paths) {
|
|
33
|
+
const at = underRoot(kit.roots, p);
|
|
34
|
+
if (!at) continue;
|
|
35
|
+
const segs = at.rel.split('/');
|
|
36
|
+
if (segs[0] !== 'src' || segs[1] !== 'routes') continue;
|
|
37
|
+
const base = segs[segs.length - 1] as string;
|
|
38
|
+
const route = cleanRoute(segs.slice(2, -1));
|
|
39
|
+
if (PAGE.test(base)) {
|
|
40
|
+
kit.fileNode(p);
|
|
41
|
+
kit.emit(kit.route(p, 'PAGE', route, 1), p, 'routes-to', 'route-table');
|
|
42
|
+
if (base.endsWith('.svelte')) pages.push(p);
|
|
43
|
+
} else if (SERVER.test(base)) {
|
|
44
|
+
if (kit.fileIds.has(p)) exportedVerbRoutes(kit, p, route);
|
|
45
|
+
} else if (LAYOUT.test(base)) {
|
|
46
|
+
layouts.push(p);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const layout of layouts) {
|
|
50
|
+
const dir = `${path.posix.dirname(layout)}/`;
|
|
51
|
+
for (const page of pages) {
|
|
52
|
+
if (!page.startsWith(dir)) continue;
|
|
53
|
+
kit.fileNode(layout);
|
|
54
|
+
kit.emit(layout, page, 'includes', 'directive-scan');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return kit.result();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symfony: the links a Symfony application makes by attribute or convention.
|
|
3
|
+
*
|
|
4
|
+
* #[Route('/x', methods: ['GET'])] · @Route("/x") route --routes-to--> the method below it,
|
|
5
|
+
* under the class-level `#[Route('/prefix')]`
|
|
6
|
+
* $this->render('a/b.html.twig') method --renders--> templates/a/b.html.twig
|
|
7
|
+
* #[ORM\Entity] (+ #[ORM\Table(name: 't')]) entity --persists--> table:t (default: snake_case short name)
|
|
8
|
+
* #[ORM\ManyToOne(targetEntity: X::class)] … entity --relates--> X (else the property's type)
|
|
9
|
+
* migrations/*.php $this->addSql('CREATE TABLE t') migration --migrates--> table:t
|
|
10
|
+
* __construct(private Mailer $m) class --injects--> Mailer (an in-repo class only)
|
|
11
|
+
*
|
|
12
|
+
* Class references resolve through the file's `use` aliases and the PSR-4
|
|
13
|
+
* map, so a same-named class in another namespace is never the target. A route
|
|
14
|
+
* with several `methods` is declared once, under the first. Twig's own
|
|
15
|
+
* `{% include %}` / `{% extends %}` are not followed: a provider reads only
|
|
16
|
+
* code files, and `.twig` is not one.
|
|
17
|
+
*/
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import type { FileRole } from '../map_model.js';
|
|
21
|
+
import { composerRoots } from './shared/detect.js';
|
|
22
|
+
import { annotationRoutes, tableMapping, templateRoots } from './shared/extractors.js';
|
|
23
|
+
import { type Kit, lineIndex, makeKit } from './shared/kit.js';
|
|
24
|
+
import { classNode, matchingClose, phpScope, qualify } from './shared/php.js';
|
|
25
|
+
import type { ConventionProvider } from './types.js';
|
|
26
|
+
import type { ResolutionConfig } from '../resolution_tiers.js';
|
|
27
|
+
|
|
28
|
+
/** Arguments of an attribute or annotation, one level of nested brackets deep. */
|
|
29
|
+
const ARGS = String.raw`(?<args>(?:[^()\[\]{}]|\[[^\]]*\]|\{[^}]*\})*)`;
|
|
30
|
+
/** The next declaration after the match is a method, not a class (`Post::class` is no declaration). */
|
|
31
|
+
const BEFORE = (decl: string): string =>
|
|
32
|
+
String.raw`(?=(?:(?!(?<!::)\bclass\s+[A-Za-z_]|\bfunction\b)[\s\S]){0,800}${decl})`;
|
|
33
|
+
const ROUTE_HEAD = String.raw`(?:#\[|@)Route\(\s*(?:path\s*[:=]\s*)?(['"])(?<path>[^'"]*)\1${ARGS}\)`;
|
|
34
|
+
const METHOD_ROUTE = new RegExp(ROUTE_HEAD + BEFORE(String.raw`\bfunction\b`), 'g');
|
|
35
|
+
const CLASS_ROUTE = new RegExp(ROUTE_HEAD + BEFORE(String.raw`(?<!::)\bclass\s+[A-Za-z_]`), 'g');
|
|
36
|
+
|
|
37
|
+
const RELATION = /(?:#\[|@)ORM\\(?:ManyToOne|OneToMany|ManyToMany|OneToOne)\b(?<args>\((?:[^()]|\([^()]*\))*\))?/g;
|
|
38
|
+
const TARGET_ENTITY = /targetEntity\s*[:=]\s*(?:(?<cls>[\\\w]+)::class|(['"])(?<str>[\\\w]+)\2)/;
|
|
39
|
+
const PROPERTY = /(?:public|protected|private|var)\s+(?:readonly\s+)?(?:(?<type>\??[\\\w]+)\s+)?\$\w+/;
|
|
40
|
+
|
|
41
|
+
const snake = (name: string): string => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
42
|
+
|
|
43
|
+
function verbOf(m: RegExpMatchArray): string {
|
|
44
|
+
const args = m.groups?.['args'] ?? '';
|
|
45
|
+
return /methods\s*[:=]\s*[[{]\s*['"](\w+)['"]/.exec(args)?.[1] ?? 'ANY';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function relations(kit: Kit, resolution: ResolutionConfig): void {
|
|
49
|
+
for (const f of kit.filesWithExt('.php')) {
|
|
50
|
+
const at = lineIndex(f.source);
|
|
51
|
+
const scope = phpScope(f.source);
|
|
52
|
+
for (const m of f.source.matchAll(RELATION)) {
|
|
53
|
+
const line = at(m.index as number);
|
|
54
|
+
const cls = kit.classAt(f.path, line);
|
|
55
|
+
if (!cls) continue;
|
|
56
|
+
const t = TARGET_ENTITY.exec(m.groups?.['args'] ?? '');
|
|
57
|
+
let fq: string | null = null;
|
|
58
|
+
if (t?.groups?.['cls']) fq = qualify(t.groups['cls'], scope);
|
|
59
|
+
else if (t?.groups?.['str']) {
|
|
60
|
+
const s = t.groups['str'];
|
|
61
|
+
fq = s.includes('\\') ? s.replace(/^\\/, '') : qualify(s, { namespace: scope.namespace, uses: new Map() });
|
|
62
|
+
} else {
|
|
63
|
+
const type = PROPERTY.exec(f.source.slice((m.index as number) + m[0].length))?.groups?.['type'];
|
|
64
|
+
if (type) fq = qualify(type.replace(/^\?/, ''), scope);
|
|
65
|
+
}
|
|
66
|
+
const target = fq ? classNode(kit, fq, resolution) : null;
|
|
67
|
+
if (target && target !== cls.id) kit.emit(cls.id, target, 'relates', 'orm-relation');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Top-level comma split of a parameter list. */
|
|
73
|
+
function params(list: string): string[] {
|
|
74
|
+
const out: string[] = [];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let cur = '';
|
|
77
|
+
for (const ch of list) {
|
|
78
|
+
if (ch === '(' || ch === '[' || ch === '{') depth += 1;
|
|
79
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth -= 1;
|
|
80
|
+
if (ch === ',' && depth === 0) {
|
|
81
|
+
out.push(cur);
|
|
82
|
+
cur = '';
|
|
83
|
+
} else cur += ch;
|
|
84
|
+
}
|
|
85
|
+
out.push(cur);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function injections(kit: Kit, resolution: ResolutionConfig): void {
|
|
90
|
+
for (const f of kit.filesWithExt('.php')) {
|
|
91
|
+
const at = lineIndex(f.source);
|
|
92
|
+
const scope = phpScope(f.source);
|
|
93
|
+
for (const m of f.source.matchAll(/\bfunction\s+__construct\s*\(/g)) {
|
|
94
|
+
const open = (m.index as number) + m[0].length - 1;
|
|
95
|
+
const cls = kit.classAt(f.path, at(m.index as number));
|
|
96
|
+
if (!cls) continue;
|
|
97
|
+
for (const p of params(f.source.slice(open + 1, matchingClose(f.source, open)))) {
|
|
98
|
+
const type = /^\s*(?:#\[[^\]]*\]\s*)*(?:(?:public|protected|private|readonly)\s+)*(?<type>\??[\\\w|]+)\s+&?(?:\.\.\.)?\$\w+/.exec(p)?.groups?.['type'];
|
|
99
|
+
for (const ref of (type ?? '').replace(/^\?/, '').split('|').filter(Boolean)) {
|
|
100
|
+
const target = classNode(kit, qualify(ref, scope), resolution);
|
|
101
|
+
if (target && target !== cls.id) kit.emit(cls.id, target, 'injects', 'di-binding');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function symfonyRole(p: string): FileRole | undefined {
|
|
109
|
+
if (p.startsWith('templates/')) return 'view';
|
|
110
|
+
if (p === 'public/index.php' || p === 'bin/console') return 'entry';
|
|
111
|
+
if (p.startsWith('migrations/')) return 'migration';
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const symfonyProvider: ConventionProvider = {
|
|
116
|
+
id: 'symfony',
|
|
117
|
+
detect: (probe) => composerRoots(probe, ['symfony/framework-bundle', 'symfony/symfony']),
|
|
118
|
+
build: (input) => {
|
|
119
|
+
const kit = makeKit(input);
|
|
120
|
+
annotationRoutes(kit, { exts: ['.php'], route: METHOD_ROUTE, verb: verbOf, prefix: CLASS_ROUTE, prefixReach: 12 });
|
|
121
|
+
templateRoots(kit, {
|
|
122
|
+
exts: ['.php'],
|
|
123
|
+
call: /\$this->render(?:View|Block)?\(\s*(['"])(?<name>[\w./@-]+)\1/g,
|
|
124
|
+
roots: ['templates'],
|
|
125
|
+
extensions: ['.html.twig', '.twig'],
|
|
126
|
+
});
|
|
127
|
+
tableMapping(kit, {
|
|
128
|
+
exts: ['.php'],
|
|
129
|
+
entity: /(?:#\[|@)ORM\\Entity\b/g,
|
|
130
|
+
tableName: /(?:#\[|@)ORM\\Table\(\s*(?:name\s*[:=]\s*)?(['"])(?<name>\w+)\1/,
|
|
131
|
+
convention: snake,
|
|
132
|
+
migration: {
|
|
133
|
+
files: (p) => /(?:^|\/)migrations\/[^/]+\.php$/.test(p),
|
|
134
|
+
create: /addSql\(\s*(['"])\s*(?:CREATE|ALTER)\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?(?<name>\w+)/gi,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
relations(kit, input.resolution);
|
|
138
|
+
injections(kit, input.resolution);
|
|
139
|
+
return kit.result();
|
|
140
|
+
},
|
|
141
|
+
roleOf: (p) => symfonyRole(path.posix.normalize(p)),
|
|
142
|
+
};
|