@opensip-cli/graph 0.1.1 → 0.1.2
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/README.md +2 -2
- package/dist/__tests__/rules/always-throws-branch.test.js +94 -0
- package/dist/__tests__/rules/always-throws-branch.test.js.map +1 -1
- package/dist/__tests__/rules/duplicated-function-body-self-match.test.d.ts +18 -0
- package/dist/__tests__/rules/duplicated-function-body-self-match.test.d.ts.map +1 -0
- package/dist/__tests__/rules/duplicated-function-body-self-match.test.js +167 -0
- package/dist/__tests__/rules/duplicated-function-body-self-match.test.js.map +1 -0
- package/dist/__tests__/rules/no-side-effect-path.test.js +115 -0
- package/dist/__tests__/rules/no-side-effect-path.test.js.map +1 -1
- package/dist/__tests__/rules/orphan-subtree-dynamic-import.test.d.ts +27 -0
- package/dist/__tests__/rules/orphan-subtree-dynamic-import.test.d.ts.map +1 -0
- package/dist/__tests__/rules/orphan-subtree-dynamic-import.test.js +189 -0
- package/dist/__tests__/rules/orphan-subtree-dynamic-import.test.js.map +1 -0
- package/dist/rules/_entry-points.d.ts +16 -2
- package/dist/rules/_entry-points.d.ts.map +1 -1
- package/dist/rules/_entry-points.js +147 -2
- package/dist/rules/_entry-points.js.map +1 -1
- package/dist/rules/always-throws-branch.d.ts.map +1 -1
- package/dist/rules/always-throws-branch.js +99 -5
- package/dist/rules/always-throws-branch.js.map +1 -1
- package/dist/rules/duplicated-function-body.js +42 -12
- package/dist/rules/duplicated-function-body.js.map +1 -1
- package/dist/rules/no-side-effect-path.d.ts.map +1 -1
- package/dist/rules/no-side-effect-path.js +73 -9
- package/dist/rules/no-side-effect-path.js.map +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: dynamic `import()` must count as a reachability edge.
|
|
3
|
+
*
|
|
4
|
+
* Verified false positive (the bug this guards): a CLI command module
|
|
5
|
+
* reached only through
|
|
6
|
+
* `const { runReplay } = await import('../commands/.../replay.js')`
|
|
7
|
+
* was flagged as an orphan — and so were the helpers it calls
|
|
8
|
+
* (`parseArgs` / `formatOutcomeTag`) — because the static call-graph
|
|
9
|
+
* resolver cannot trace a binding through a dynamic import, so neither
|
|
10
|
+
* the importing call nor a static import edge ever marked the target
|
|
11
|
+
* module reachable.
|
|
12
|
+
*
|
|
13
|
+
* The fix (heuristic 6 in `_entry-points.ts`) treats the dynamic-import
|
|
14
|
+
* target the same way a static import makes the imported module's
|
|
15
|
+
* exported surface reachable: it resolves the relative specifier to the
|
|
16
|
+
* target file and seeds that file's EXPORTED occurrences as entry points.
|
|
17
|
+
*
|
|
18
|
+
* These tests mirror the real catalog edge topology observed from the
|
|
19
|
+
* TypeScript pipeline for the `const { x } = await import('./mod.js')`
|
|
20
|
+
* form: the importing function carries an UNRESOLVED `import('./mod.js')`
|
|
21
|
+
* call edge (`to: []`) plus an UNRESOLVED `runReplay(...)` call edge
|
|
22
|
+
* (the destructured binding the resolver could not trace), while the
|
|
23
|
+
* target module's exported function resolves edges to its own private
|
|
24
|
+
* helpers.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, expect, it } from 'vitest';
|
|
27
|
+
import { buildIndexes } from '../../pipeline/indexes.js';
|
|
28
|
+
import { inferEntryPoints } from '../../rules/_entry-points.js';
|
|
29
|
+
import { orphanSubtreeRule } from '../../rules/orphan-subtree.js';
|
|
30
|
+
import { edge, makeCatalog, occ, staticCall } from './_helpers.js';
|
|
31
|
+
/**
|
|
32
|
+
* Build the `registry.ts` + `replay.ts` cluster:
|
|
33
|
+
* - `dispatch` (exported, registry.ts): dynamic-imports replay.js and
|
|
34
|
+
* calls the destructured `runReplay`; both edges are UNRESOLVED.
|
|
35
|
+
* - `runReplay` (exported, replay.js): calls its private helpers.
|
|
36
|
+
* - `parseArgs` / `formatOutcomeTag` (module-local, replay.js).
|
|
37
|
+
*/
|
|
38
|
+
function dynamicImportCluster() {
|
|
39
|
+
const dispatch = occ({
|
|
40
|
+
bodyHash: 'dispatch',
|
|
41
|
+
simpleName: 'dispatch',
|
|
42
|
+
visibility: 'exported',
|
|
43
|
+
filePath: 'src/registry.ts',
|
|
44
|
+
// Unresolved (to: []) — exactly what the TS resolver emits for a
|
|
45
|
+
// dynamic import and a binding it cannot trace.
|
|
46
|
+
calls: [edge("import('./replay.js')"), edge('runReplay([])')],
|
|
47
|
+
});
|
|
48
|
+
const runReplay = occ({
|
|
49
|
+
bodyHash: 'runReplay',
|
|
50
|
+
simpleName: 'runReplay',
|
|
51
|
+
visibility: 'exported',
|
|
52
|
+
filePath: 'src/replay.ts',
|
|
53
|
+
calls: [staticCall('parseArgs'), staticCall('formatOutcomeTag')],
|
|
54
|
+
});
|
|
55
|
+
const parseArgs = occ({
|
|
56
|
+
bodyHash: 'parseArgs',
|
|
57
|
+
simpleName: 'parseArgs',
|
|
58
|
+
visibility: 'module-local',
|
|
59
|
+
filePath: 'src/replay.ts',
|
|
60
|
+
});
|
|
61
|
+
const formatOutcomeTag = occ({
|
|
62
|
+
bodyHash: 'formatOutcomeTag',
|
|
63
|
+
simpleName: 'formatOutcomeTag',
|
|
64
|
+
visibility: 'module-local',
|
|
65
|
+
filePath: 'src/replay.ts',
|
|
66
|
+
});
|
|
67
|
+
return makeCatalog([dispatch, runReplay, parseArgs, formatOutcomeTag]);
|
|
68
|
+
}
|
|
69
|
+
describe('orphan-subtree — dynamic import reachability (heuristic 6)', () => {
|
|
70
|
+
it('seeds an exported dynamic-import target as an entry point', () => {
|
|
71
|
+
const catalog = dynamicImportCluster();
|
|
72
|
+
const eps = inferEntryPoints(catalog, buildIndexes(catalog));
|
|
73
|
+
const runReplayEp = eps.find((e) => e.bodyHash === 'runReplay');
|
|
74
|
+
expect(runReplayEp).toBeDefined();
|
|
75
|
+
// It is seeded specifically by the dynamic-import heuristic (it has no
|
|
76
|
+
// in-project caller, so it would also satisfy no-callers-exported; the
|
|
77
|
+
// dynamic-import reason is what survives dedup for the FIRST classifier
|
|
78
|
+
// that fires — assert the symbol is an entry point either way).
|
|
79
|
+
expect(['dynamic-import', 'no-callers-exported']).toContain(runReplayEp.reason);
|
|
80
|
+
});
|
|
81
|
+
it('does NOT flag a symbol reachable only via `await import()` destructure', () => {
|
|
82
|
+
const catalog = dynamicImportCluster();
|
|
83
|
+
const orphans = orphanSubtreeRule
|
|
84
|
+
// flagExportedOrphans + flagTestOrphans left default; the helpers are
|
|
85
|
+
// module-local so the exported filter does not mask them.
|
|
86
|
+
.evaluate(catalog, buildIndexes(catalog), {})
|
|
87
|
+
.map((s) => s.metadata.simpleName);
|
|
88
|
+
// The whole dynamic-import subtree is reachable — none are orphans.
|
|
89
|
+
expect(orphans).not.toContain('runReplay');
|
|
90
|
+
expect(orphans).not.toContain('parseArgs');
|
|
91
|
+
expect(orphans).not.toContain('formatOutcomeTag');
|
|
92
|
+
});
|
|
93
|
+
it('keeps flagging a genuinely unreferenced exported function (true positive preserved)', () => {
|
|
94
|
+
// A dead exported function whose file is NOT a dynamic-import target,
|
|
95
|
+
// reached only by a dead module-local caller. With flagExportedOrphans
|
|
96
|
+
// it must still be flagged — the dynamic-import heuristic only adds
|
|
97
|
+
// reachability for actually-imported files, never suppresses orphans.
|
|
98
|
+
const deadCaller = occ({
|
|
99
|
+
bodyHash: 'dc',
|
|
100
|
+
simpleName: 'deadCaller',
|
|
101
|
+
visibility: 'module-local',
|
|
102
|
+
filePath: 'src/dead.ts',
|
|
103
|
+
calls: [staticCall('ue')],
|
|
104
|
+
});
|
|
105
|
+
const unusedExport = occ({
|
|
106
|
+
bodyHash: 'ue',
|
|
107
|
+
simpleName: 'unusedExport',
|
|
108
|
+
visibility: 'exported',
|
|
109
|
+
filePath: 'src/dead.ts',
|
|
110
|
+
});
|
|
111
|
+
// Plus the dynamic-import cluster, to prove the two are isolated.
|
|
112
|
+
const cluster = dynamicImportCluster();
|
|
113
|
+
const catalog = makeCatalog([
|
|
114
|
+
deadCaller,
|
|
115
|
+
unusedExport,
|
|
116
|
+
...Object.values(cluster.functions).flat(),
|
|
117
|
+
]);
|
|
118
|
+
const indexes = buildIndexes(catalog);
|
|
119
|
+
const orphans = orphanSubtreeRule
|
|
120
|
+
.evaluate(catalog, indexes, { flagExportedOrphans: true })
|
|
121
|
+
.map((s) => s.metadata.simpleName);
|
|
122
|
+
expect(orphans).toContain('deadCaller'); // dead module-local caller
|
|
123
|
+
expect(orphans).toContain('unusedExport'); // genuine exported orphan
|
|
124
|
+
// ...and the dynamic-import subtree stays reachable.
|
|
125
|
+
expect(orphans).not.toContain('runReplay');
|
|
126
|
+
expect(orphans).not.toContain('parseArgs');
|
|
127
|
+
expect(orphans).not.toContain('formatOutcomeTag');
|
|
128
|
+
});
|
|
129
|
+
it('seeds the dynamic-import target even when no-callers-exported does NOT apply', () => {
|
|
130
|
+
// Isolate heuristic 6 from heuristic 5: the dynamic-import target has a
|
|
131
|
+
// (dead) in-project caller, so `no-callers-exported` does NOT classify it.
|
|
132
|
+
// Only the dynamic-import heuristic can seed it as an entry point.
|
|
133
|
+
const importer = occ({
|
|
134
|
+
bodyHash: 'imp',
|
|
135
|
+
simpleName: 'lazyDispatch',
|
|
136
|
+
visibility: 'exported',
|
|
137
|
+
filePath: 'src/registry.ts',
|
|
138
|
+
calls: [edge("import('./replay.js')"), edge('runReplay()')],
|
|
139
|
+
});
|
|
140
|
+
const runReplay = occ({
|
|
141
|
+
bodyHash: 'runReplay',
|
|
142
|
+
simpleName: 'runReplay',
|
|
143
|
+
visibility: 'exported',
|
|
144
|
+
filePath: 'src/replay.ts',
|
|
145
|
+
});
|
|
146
|
+
// A dead module-local caller gives runReplay an in-project caller, so
|
|
147
|
+
// hasExternalCaller() is true and no-callers-exported is suppressed.
|
|
148
|
+
const deadCaller = occ({
|
|
149
|
+
bodyHash: 'dead',
|
|
150
|
+
simpleName: 'deadStaticCaller',
|
|
151
|
+
visibility: 'module-local',
|
|
152
|
+
filePath: 'src/replay.ts',
|
|
153
|
+
calls: [staticCall('runReplay')],
|
|
154
|
+
});
|
|
155
|
+
const catalog = makeCatalog([importer, runReplay, deadCaller]);
|
|
156
|
+
const eps = inferEntryPoints(catalog, buildIndexes(catalog));
|
|
157
|
+
const ep = eps.find((e) => e.bodyHash === 'runReplay');
|
|
158
|
+
expect(ep?.reason).toBe('dynamic-import');
|
|
159
|
+
});
|
|
160
|
+
it('tolerates the `.js → .ts` ESM extension rewrite and bare specifiers', () => {
|
|
161
|
+
// Importing file uses an extensionless specifier; target is a .tsx file.
|
|
162
|
+
const importer = occ({
|
|
163
|
+
bodyHash: 'imp',
|
|
164
|
+
simpleName: 'lazyLoad',
|
|
165
|
+
visibility: 'exported',
|
|
166
|
+
filePath: 'src/app.ts',
|
|
167
|
+
calls: [edge("import('./views/panel')"), edge('mountPanel()')],
|
|
168
|
+
});
|
|
169
|
+
const mountPanel = occ({
|
|
170
|
+
bodyHash: 'mp',
|
|
171
|
+
simpleName: 'mountPanel',
|
|
172
|
+
visibility: 'exported',
|
|
173
|
+
filePath: 'src/views/panel.tsx',
|
|
174
|
+
});
|
|
175
|
+
// A bare/workspace dynamic import resolves outside the catalog — must be
|
|
176
|
+
// ignored without error.
|
|
177
|
+
const bare = occ({
|
|
178
|
+
bodyHash: 'bare',
|
|
179
|
+
simpleName: 'loadExternal',
|
|
180
|
+
visibility: 'exported',
|
|
181
|
+
filePath: 'src/ext.ts',
|
|
182
|
+
calls: [edge("import('node:fs')")],
|
|
183
|
+
});
|
|
184
|
+
const catalog = makeCatalog([importer, mountPanel, bare]);
|
|
185
|
+
const eps = inferEntryPoints(catalog, buildIndexes(catalog));
|
|
186
|
+
expect(eps.find((e) => e.bodyHash === 'mp')).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
//# sourceMappingURL=orphan-subtree-dynamic-import.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-subtree-dynamic-import.test.js","sourceRoot":"","sources":["../../../src/__tests__/rules/orphan-subtree-dynamic-import.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,14 +14,28 @@
|
|
|
14
14
|
* does not resolve) is still an external entry point — counting its
|
|
15
15
|
* own recursion as a "caller" would wrongly hide it (and its whole
|
|
16
16
|
* file-local helper subtree) as an orphan.
|
|
17
|
+
* 6. It's an EXPORTED symbol of a module reached via a dynamic
|
|
18
|
+
* `import('<spec>')` / `await import('<spec>')` expression (including
|
|
19
|
+
* the `const { x } = await import(...)` destructure form). The call
|
|
20
|
+
* graph's static resolver cannot trace a binding through a dynamic
|
|
21
|
+
* import, so a function reached only that way would otherwise look
|
|
22
|
+
* orphaned. We treat the dynamic-import target the same way a static
|
|
23
|
+
* import edge makes the imported module's exported surface reachable:
|
|
24
|
+
* resolve the relative specifier to the target file and seed its
|
|
25
|
+
* exported occurrences as entry points. This is conservative — it
|
|
26
|
+
* only adds reachability (never suppresses a genuine orphan), and it
|
|
27
|
+
* fires solely on exported symbols of an actually-imported file.
|
|
17
28
|
*
|
|
18
29
|
* v0.2 ships heuristics 3, 4, 5; 1 and 2 are project-specific and
|
|
19
|
-
* deferred until cross-package call resolution is reliable.
|
|
30
|
+
* deferred until cross-package call resolution is reliable. Heuristic 6
|
|
31
|
+
* (dynamic-import reachability) was added to fix a verified false
|
|
32
|
+
* positive: a CLI command reached only via
|
|
33
|
+
* `const { runReplay } = await import('../commands/.../replay.js')`.
|
|
20
34
|
*/
|
|
21
35
|
import type { Catalog, Indexes } from '../types.js';
|
|
22
36
|
export interface EntryPoint {
|
|
23
37
|
readonly bodyHash: string;
|
|
24
|
-
readonly reason: 'module-init' | 'name-match' | 'no-callers-exported';
|
|
38
|
+
readonly reason: 'module-init' | 'name-match' | 'no-callers-exported' | 'dynamic-import';
|
|
25
39
|
}
|
|
26
40
|
export declare function inferEntryPoints(catalog: Catalog, indexes: Indexes): readonly EntryPoint[];
|
|
27
41
|
//# sourceMappingURL=_entry-points.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_entry-points.d.ts","sourceRoot":"","sources":["../../src/rules/_entry-points.ts"],"names":[],"mappings":"AACA
|
|
1
|
+
{"version":3,"file":"_entry-points.d.ts","sourceRoot":"","sources":["../../src/rules/_entry-points.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAsB,OAAO,EAAE,MAAM,aAAa,CAAC;AAYxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,qBAAqB,GAAG,gBAAgB,CAAC;CAC1F;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAyB1F"}
|
|
@@ -15,9 +15,23 @@
|
|
|
15
15
|
* does not resolve) is still an external entry point — counting its
|
|
16
16
|
* own recursion as a "caller" would wrongly hide it (and its whole
|
|
17
17
|
* file-local helper subtree) as an orphan.
|
|
18
|
+
* 6. It's an EXPORTED symbol of a module reached via a dynamic
|
|
19
|
+
* `import('<spec>')` / `await import('<spec>')` expression (including
|
|
20
|
+
* the `const { x } = await import(...)` destructure form). The call
|
|
21
|
+
* graph's static resolver cannot trace a binding through a dynamic
|
|
22
|
+
* import, so a function reached only that way would otherwise look
|
|
23
|
+
* orphaned. We treat the dynamic-import target the same way a static
|
|
24
|
+
* import edge makes the imported module's exported surface reachable:
|
|
25
|
+
* resolve the relative specifier to the target file and seed its
|
|
26
|
+
* exported occurrences as entry points. This is conservative — it
|
|
27
|
+
* only adds reachability (never suppresses a genuine orphan), and it
|
|
28
|
+
* fires solely on exported symbols of an actually-imported file.
|
|
18
29
|
*
|
|
19
30
|
* v0.2 ships heuristics 3, 4, 5; 1 and 2 are project-specific and
|
|
20
|
-
* deferred until cross-package call resolution is reliable.
|
|
31
|
+
* deferred until cross-package call resolution is reliable. Heuristic 6
|
|
32
|
+
* (dynamic-import reachability) was added to fix a verified false
|
|
33
|
+
* positive: a CLI command reached only via
|
|
34
|
+
* `const { runReplay } = await import('../commands/.../replay.js')`.
|
|
21
35
|
*/
|
|
22
36
|
const NAME_HEURISTICS = new Set([
|
|
23
37
|
'main',
|
|
@@ -30,10 +44,23 @@ const NAME_HEURISTICS = new Set([
|
|
|
30
44
|
]);
|
|
31
45
|
export function inferEntryPoints(catalog, indexes) {
|
|
32
46
|
const out = [];
|
|
47
|
+
const seen = new Set();
|
|
33
48
|
for (const occ of indexes.byBodyHash.values()) {
|
|
34
49
|
const reason = classify(occ, indexes);
|
|
35
|
-
if (reason !== null)
|
|
50
|
+
if (reason !== null) {
|
|
36
51
|
out.push({ bodyHash: occ.bodyHash, reason });
|
|
52
|
+
seen.add(occ.bodyHash);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Heuristic 6: exported symbols of any dynamically-imported module are
|
|
56
|
+
// entry points (the static call resolver cannot trace a binding through
|
|
57
|
+
// `import(...)`). Deduped against the heuristics above so a function is
|
|
58
|
+
// never emitted twice.
|
|
59
|
+
for (const hash of dynamicImportEntryHashes(indexes)) {
|
|
60
|
+
if (!seen.has(hash)) {
|
|
61
|
+
out.push({ bodyHash: hash, reason: 'dynamic-import' });
|
|
62
|
+
seen.add(hash);
|
|
63
|
+
}
|
|
37
64
|
}
|
|
38
65
|
// Honor caller-supplied override at the rule level via GraphConfig
|
|
39
66
|
// (handled by the consuming rule). This module returns the inferred
|
|
@@ -78,4 +105,122 @@ function hasExternalCaller(occ, indexes) {
|
|
|
78
105
|
}
|
|
79
106
|
return false;
|
|
80
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Matches a dynamic-import call expression and captures its specifier:
|
|
110
|
+
* `import('./x.js')`, `await import("./x")`, and the
|
|
111
|
+
* `const { y } = await import('./x.js')` destructure form all surface in
|
|
112
|
+
* the catalog as a call edge whose `text` begins with `import(` followed
|
|
113
|
+
* by the string-literal specifier. We capture the specifier verbatim.
|
|
114
|
+
*/
|
|
115
|
+
const DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
116
|
+
/**
|
|
117
|
+
* Body hashes of every EXPORTED occurrence that lives in a file targeted
|
|
118
|
+
* by a dynamic `import('<spec>')` anywhere in the project. Built by:
|
|
119
|
+
* 1. collecting every relative dynamic-import specifier (from call-edge
|
|
120
|
+
* text), resolved to a project-relative target path against the
|
|
121
|
+
* importing occurrence's directory;
|
|
122
|
+
* 2. mapping those target paths to the catalog's actual file paths
|
|
123
|
+
* (tolerating the `.js → .ts/.tsx` ESM extension rewrite and
|
|
124
|
+
* `index` directory imports);
|
|
125
|
+
* 3. emitting the exported occurrences declared in each matched file.
|
|
126
|
+
*
|
|
127
|
+
* Bare/workspace specifiers (`@scope/pkg`, `node:fs`) are ignored — they
|
|
128
|
+
* resolve outside the catalog and the exported-no-caller heuristic
|
|
129
|
+
* already covers cross-package surface.
|
|
130
|
+
*/
|
|
131
|
+
function dynamicImportEntryHashes(indexes) {
|
|
132
|
+
const targetFiles = collectDynamicImportTargetFiles(indexes);
|
|
133
|
+
if (targetFiles.size === 0)
|
|
134
|
+
return [];
|
|
135
|
+
const exportedByFile = buildExportedByFile(indexes);
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const target of targetFiles) {
|
|
138
|
+
for (const candidate of candidateFilePaths(target)) {
|
|
139
|
+
const occs = exportedByFile.get(candidate);
|
|
140
|
+
if (occs) {
|
|
141
|
+
for (const o of occs)
|
|
142
|
+
out.push(o.bodyHash);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
/** Resolve every relative dynamic-import specifier to a project-relative
|
|
149
|
+
* (extension-bearing) target path, keyed off the importing occurrence's
|
|
150
|
+
* directory. */
|
|
151
|
+
function collectDynamicImportTargetFiles(indexes) {
|
|
152
|
+
const targets = new Set();
|
|
153
|
+
for (const occ of indexes.byBodyHash.values()) {
|
|
154
|
+
for (const call of occ.calls) {
|
|
155
|
+
const match = DYNAMIC_IMPORT_RE.exec(call.text);
|
|
156
|
+
if (!match)
|
|
157
|
+
continue;
|
|
158
|
+
const specifier = match[1];
|
|
159
|
+
// Only relative specifiers resolve into the catalog.
|
|
160
|
+
if (!specifier.startsWith('.'))
|
|
161
|
+
continue;
|
|
162
|
+
targets.add(resolveRelative(dirOf(occ.filePath), specifier));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return targets;
|
|
166
|
+
}
|
|
167
|
+
/** filePath → exported function occurrences declared in that file. */
|
|
168
|
+
function buildExportedByFile(indexes) {
|
|
169
|
+
const byFile = new Map();
|
|
170
|
+
for (const occ of indexes.byBodyHash.values()) {
|
|
171
|
+
if (occ.visibility !== 'exported')
|
|
172
|
+
continue;
|
|
173
|
+
if (!occ.filePath)
|
|
174
|
+
continue;
|
|
175
|
+
const bucket = byFile.get(occ.filePath);
|
|
176
|
+
if (bucket)
|
|
177
|
+
bucket.push(occ);
|
|
178
|
+
else
|
|
179
|
+
byFile.set(occ.filePath, [occ]);
|
|
180
|
+
}
|
|
181
|
+
return byFile;
|
|
182
|
+
}
|
|
183
|
+
/** Project-relative posix directory of a file ('' for a root-level file). */
|
|
184
|
+
function dirOf(filePath) {
|
|
185
|
+
const slash = filePath.lastIndexOf('/');
|
|
186
|
+
return slash === -1 ? '' : filePath.slice(0, slash);
|
|
187
|
+
}
|
|
188
|
+
/** Resolve a relative specifier against a posix directory, collapsing
|
|
189
|
+
* `.` / `..` segments. Returns a project-relative posix path. */
|
|
190
|
+
function resolveRelative(dir, specifier) {
|
|
191
|
+
const segments = dir.length > 0 ? dir.split('/') : [];
|
|
192
|
+
for (const part of specifier.split('/')) {
|
|
193
|
+
if (part === '' || part === '.')
|
|
194
|
+
continue;
|
|
195
|
+
if (part === '..')
|
|
196
|
+
segments.pop();
|
|
197
|
+
else
|
|
198
|
+
segments.push(part);
|
|
199
|
+
}
|
|
200
|
+
return segments.join('/');
|
|
201
|
+
}
|
|
202
|
+
/** Candidate catalog file paths a resolved specifier may map to —
|
|
203
|
+
* tolerating the ESM `.js → .ts/.tsx` rewrite and `index` imports.
|
|
204
|
+
* Order is irrelevant; lookups are exact-match against real files. */
|
|
205
|
+
function candidateFilePaths(target) {
|
|
206
|
+
const base = stripKnownExtension(target);
|
|
207
|
+
return [
|
|
208
|
+
target,
|
|
209
|
+
`${base}.ts`,
|
|
210
|
+
`${base}.tsx`,
|
|
211
|
+
`${base}.js`,
|
|
212
|
+
`${base}.jsx`,
|
|
213
|
+
`${base}.mts`,
|
|
214
|
+
`${base}.cts`,
|
|
215
|
+
`${base}/index.ts`,
|
|
216
|
+
`${base}/index.tsx`,
|
|
217
|
+
`${base}/index.js`,
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
/** Drop a trailing JS/TS module extension so the `.js → .ts` ESM rewrite
|
|
221
|
+
* can be re-applied as candidates. Leaves an extensionless path intact. */
|
|
222
|
+
function stripKnownExtension(path) {
|
|
223
|
+
const match = /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/.exec(path);
|
|
224
|
+
return match ? path.slice(0, path.length - match[0].length) : path;
|
|
225
|
+
}
|
|
81
226
|
//# sourceMappingURL=_entry-points.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_entry-points.js","sourceRoot":"","sources":["../../src/rules/_entry-points.ts"],"names":[],"mappings":"AAAA,2HAA2H;AAC3H
|
|
1
|
+
{"version":3,"file":"_entry-points.js","sourceRoot":"","sources":["../../src/rules/_entry-points.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"always-throws-branch.d.ts","sourceRoot":"","sources":["../../src/rules/always-throws-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;
|
|
1
|
+
{"version":3,"file":"always-throws-branch.d.ts","sourceRoot":"","sources":["../../src/rules/always-throws-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA6FH,eAAO,MAAM,sBAAsB,4BAqDjC,CAAC"}
|
|
@@ -25,11 +25,88 @@
|
|
|
25
25
|
import { createGraphSignal } from './create-graph-signal.js';
|
|
26
26
|
import { defineRule } from './define-rule.js';
|
|
27
27
|
const TYPESCRIPT_FALLBACK_THROW_REGEX = /^\s*throw\s+(?:new\s+)?[A-Z]\w*/;
|
|
28
|
+
/**
|
|
29
|
+
* Prefix the adapters stamp onto a *creation* edge (`[creates] () => …`),
|
|
30
|
+
* mirroring `CREATION_EDGE_PREFIX` in `lang-adapter/edge-helpers.ts`. An
|
|
31
|
+
* occurrence that is the TARGET of such an edge is an inline callable
|
|
32
|
+
* (arrow / function-expression) that some enclosing scope merely *creates*
|
|
33
|
+
* and returns/passes — not a standalone helper the program calls for its
|
|
34
|
+
* always-throwing effect. Inlined here (rather than imported) to keep this
|
|
35
|
+
* rule module dependency-free of the edge-helper surface; the literal is the
|
|
36
|
+
* documented producer-side contract.
|
|
37
|
+
*/
|
|
38
|
+
const CREATION_EDGE_PREFIX = '[creates] ';
|
|
39
|
+
/**
|
|
40
|
+
* A function "appears to always throw" only when a throw is reachable in its
|
|
41
|
+
* OWN control flow. A `throw` that lives inside a NESTED function expression /
|
|
42
|
+
* arrow / returned closure is NOT the outer function's control flow — the outer
|
|
43
|
+
* function merely RETURNS or PASSES the closure; the throw fires only when the
|
|
44
|
+
* inner callable is later invoked.
|
|
45
|
+
*
|
|
46
|
+
* Two boundaries enforce that, both conservative (genuine always-throw helpers
|
|
47
|
+
* keep firing):
|
|
48
|
+
*
|
|
49
|
+
* 1. **Nested-body boundary.** When an adapter attributes a throw-shaped edge
|
|
50
|
+
* to an enclosing occurrence even though the throw textually lives inside a
|
|
51
|
+
* nested function declared within that occurrence's source span, the edge is
|
|
52
|
+
* not part of the enclosing function's own control flow. We drop any edge
|
|
53
|
+
* whose source line falls inside a nested occurrence's `[line, endLine]`
|
|
54
|
+
* span before testing "every remaining edge is a throw."
|
|
55
|
+
*
|
|
56
|
+
* 2. **Returned-closure boundary.** The flagged occurrence is itself an inline
|
|
57
|
+
* callable that an enclosing scope created (the target of a `[creates] …`
|
|
58
|
+
* edge). A Proxy `get` trap of the form `get() { return () => { throw … } }`
|
|
59
|
+
* produces exactly this: the inner arrow's only edge is the throw, but the
|
|
60
|
+
* arrow is a lazily-throwing closure the trap returns — invoking the trap
|
|
61
|
+
* does NOT throw. We never flag an occurrence that is a created inline
|
|
62
|
+
* callable.
|
|
63
|
+
*/
|
|
64
|
+
function collectCreatedInlineCallableHashes(catalog) {
|
|
65
|
+
const created = new Set();
|
|
66
|
+
for (const occurrences of Object.values(catalog.functions)) {
|
|
67
|
+
for (const occ of occurrences) {
|
|
68
|
+
for (const e of occ.calls) {
|
|
69
|
+
if (!e.text.startsWith(CREATION_EDGE_PREFIX))
|
|
70
|
+
continue;
|
|
71
|
+
for (const targetHash of e.to)
|
|
72
|
+
created.add(targetHash);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Nested occurrences declared *inside* `outer`'s source span (same file,
|
|
80
|
+
* strictly contained `[line, endLine]`, not `outer` itself). A throw edge whose
|
|
81
|
+
* source line falls within one of these spans belongs to the nested function's
|
|
82
|
+
* control flow, not `outer`'s.
|
|
83
|
+
*/
|
|
84
|
+
function nestedSpansWithin(outer, catalog) {
|
|
85
|
+
const spans = [];
|
|
86
|
+
for (const occurrences of Object.values(catalog.functions)) {
|
|
87
|
+
for (const occ of occurrences) {
|
|
88
|
+
if (occ.filePath !== outer.filePath)
|
|
89
|
+
continue;
|
|
90
|
+
if (occ.bodyHash === outer.bodyHash && occ.line === outer.line && occ.column === outer.column)
|
|
91
|
+
continue;
|
|
92
|
+
// Strictly contained within the outer's declaration span.
|
|
93
|
+
if (occ.line >= outer.line && occ.endLine <= outer.endLine && occ.line > outer.line) {
|
|
94
|
+
spans.push({ line: occ.line, endLine: occ.endLine });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return spans;
|
|
99
|
+
}
|
|
100
|
+
/** True when `edge`'s source line falls inside any nested-function span. */
|
|
101
|
+
function edgeIsInsideNestedFunction(edge, nestedSpans) {
|
|
102
|
+
return nestedSpans.some((s) => edge.line >= s.line && edge.line <= s.endLine);
|
|
103
|
+
}
|
|
28
104
|
export const alwaysThrowsBranchRule = defineRule({
|
|
29
105
|
slug: 'graph:always-throws-branch',
|
|
30
106
|
defaultSeverity: 'warning',
|
|
31
|
-
evaluate({ indexes, hints, config }) {
|
|
107
|
+
evaluate({ catalog, indexes, hints, config }) {
|
|
32
108
|
const throwRegex = hints?.throwSyntaxRegex ?? TYPESCRIPT_FALLBACK_THROW_REGEX;
|
|
109
|
+
const createdInlineCallables = collectCreatedInlineCallableHashes(catalog);
|
|
33
110
|
const signals = [];
|
|
34
111
|
for (const occ of indexes.byBodyHash.values()) {
|
|
35
112
|
if (occ.kind === 'module-init')
|
|
@@ -41,11 +118,28 @@ export const alwaysThrowsBranchRule = defineRule({
|
|
|
41
118
|
// rule applies.
|
|
42
119
|
if (occ.inTestFile)
|
|
43
120
|
continue;
|
|
121
|
+
// Returned-closure boundary: this occurrence is an inline callable that
|
|
122
|
+
// an enclosing scope created and returns/passes. A throw in its body
|
|
123
|
+
// fires only when the closure is later invoked — it is NOT the creating
|
|
124
|
+
// function's control flow (the Proxy-`get`-trap false positive).
|
|
125
|
+
if (createdInlineCallables.has(occ.bodyHash))
|
|
126
|
+
continue;
|
|
44
127
|
if (occ.calls.length === 0)
|
|
45
128
|
continue;
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
129
|
+
// Nested-body boundary: drop edges that textually live inside a nested
|
|
130
|
+
// function declared within this occurrence's span — they are the nested
|
|
131
|
+
// function's control flow, not this one's.
|
|
132
|
+
const nestedSpans = nestedSpansWithin(occ, catalog);
|
|
133
|
+
const ownControlFlowEdges = nestedSpans.length === 0
|
|
134
|
+
? occ.calls
|
|
135
|
+
: occ.calls.filter((e) => !edgeIsInsideNestedFunction(e, nestedSpans));
|
|
136
|
+
// If every throw-shaped edge was inside a nested function, this
|
|
137
|
+
// function has no own-control-flow throw — do not flag.
|
|
138
|
+
if (ownControlFlowEdges.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
// All remaining (own-control-flow) edges look like throw shapes — every
|
|
141
|
+
// documented call site is a throw / raise / panic per the adapter's regex.
|
|
142
|
+
const everyCallIsThrow = ownControlFlowEdges.every((e) => throwRegex.test(e.text));
|
|
49
143
|
if (!everyCallIsThrow)
|
|
50
144
|
continue;
|
|
51
145
|
signals.push(createGraphSignal('graph:always-throws-branch', config, {
|
|
@@ -56,7 +150,7 @@ export const alwaysThrowsBranchRule = defineRule({
|
|
|
56
150
|
suggestion: 'Inline the throw at every caller, or document the precondition this function enforces.',
|
|
57
151
|
metadata: {
|
|
58
152
|
qualifiedName: occ.qualifiedName,
|
|
59
|
-
edgeCount:
|
|
153
|
+
edgeCount: ownControlFlowEdges.length,
|
|
60
154
|
},
|
|
61
155
|
}));
|
|
62
156
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"always-throws-branch.js","sourceRoot":"","sources":["../../src/rules/always-throws-branch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"always-throws-branch.js","sourceRoot":"","sources":["../../src/rules/always-throws-branch.ts"],"names":[],"mappings":"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"}
|