@starci/eslint-canon-fe 9.5.23 → 9.6.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/CHANGELOG.md +12 -0
- package/docs/button-intent.md +1 -1
- package/package.json +1 -1
- package/runtime/bundle.json +192 -0
- package/runtime/engine/by-code-unit.mjs +6 -6
- package/runtime/engine/model-config.mjs +98 -76
- package/runtime/engine/orca-config.mjs +22 -17
- package/runtime/engine/release-config.mjs +22 -18
- package/runtime/engine/removed-vocabulary.mjs +21 -20
- package/runtime/engine/resources-config.mjs +42 -35
- package/runtime/engine/runtime-root.mjs +11 -13
- package/runtime/engine/secrets.mjs +170 -102
- package/runtime/engine/sonar-config.mjs +16 -13
- package/runtime/engine/temp-root.mjs +3 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
- package/runtime/knowledge/hfs/slots.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -1
- package/runtime/scripts/api/git/cat-file.mjs +0 -1
- package/runtime/scripts/api/git/config-get.mjs +1 -2
- package/runtime/scripts/api/git/diff.mjs +0 -1
- package/runtime/scripts/api/git/lib.mjs +32 -32
- package/runtime/scripts/api/git/ls-files.mjs +0 -1
- package/runtime/scripts/api/git/ls-tree.mjs +0 -1
- package/runtime/scripts/api/git/merge-base.mjs +2 -3
- package/runtime/scripts/api/git/remote.mjs +0 -1
- package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
- package/runtime/scripts/api/git/show.mjs +0 -1
- package/runtime/scripts/hfs/allows.mjs +37 -38
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
- package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
- package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
- package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
- package/runtime/scripts/hfs/architecture/config.mjs +302 -268
- package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
- package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
- package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
- package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
- package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
- package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
- package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
- package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
- package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
- package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
- package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
- package/runtime/scripts/hfs/architecture/index.mjs +272 -272
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
- package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
- package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
- package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
- package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
- package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
- package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
- package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
- package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
- package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
- package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
- package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
- package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
- package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
- package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
- package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
- package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
- package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
- package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
- package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
- package/runtime/scripts/hfs/edition-slots.mjs +43 -40
- package/runtime/scripts/hfs/linear-text.mjs +20 -20
- package/runtime/scripts/hfs/param-names.mjs +6 -11
- package/runtime/scripts/hfs/path-findings.mjs +120 -100
- package/runtime/scripts/hfs/repo-identity.mjs +55 -42
- package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
- package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
- package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
- package/runtime/scripts/hfs/rules/read.mjs +19 -15
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
- package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
- package/runtime/scripts/hfs/slot-classify.mjs +42 -38
- package/runtime/scripts/hfs/slot-errors.mjs +8 -7
- package/runtime/scripts/hfs/slot-imports.mjs +70 -63
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
- package/runtime/scripts/hfs/slot-match.mjs +122 -111
- package/runtime/scripts/hfs/slot-path.mjs +0 -2
- package/runtime/scripts/hfs/slot-required.mjs +65 -59
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
- package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
- package/runtime/scripts/hfs/slots.mjs +175 -152
- package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
- package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
- package/runtime/scripts/hfs/view.mjs +38 -40
- package/runtime/scripts/lib/dockerfile.mjs +131 -114
- package/runtime/scripts/lib/env.mjs +3 -6
- package/runtime/scripts/lib/event-contract.mjs +73 -78
- package/runtime/scripts/lib/git.mjs +15 -16
- package/runtime/scripts/lib/glob.mjs +23 -14
- package/runtime/scripts/lib/grammar-lint.mjs +33 -37
- package/runtime/scripts/lib/i18n.mjs +32 -35
- package/runtime/scripts/lib/in-order.mjs +57 -38
- package/runtime/scripts/lib/json-clone.mjs +45 -36
- package/runtime/scripts/lib/json.mjs +26 -14
- package/runtime/scripts/lib/language.mjs +133 -129
- package/runtime/scripts/lib/list.mjs +9 -14
- package/runtime/scripts/lib/mutation-fence.mjs +10 -5
- package/runtime/scripts/lib/path-key.mjs +53 -38
- package/runtime/scripts/lib/read-text.mjs +30 -17
- package/runtime/scripts/lib/regex.mjs +1 -2
- package/runtime/scripts/lib/same-text.mjs +0 -1
- package/runtime/scripts/lib/sops-envelope.mjs +148 -120
- package/runtime/scripts/lib/stack-services.mjs +128 -129
- package/runtime/scripts/lib/ts-analysis.mjs +28 -26
- package/runtime/scripts/lib/ts-ast.mjs +36 -42
- package/runtime/scripts/lib/walk.mjs +33 -27
- package/runtime/scripts/lib/yaml-cached.mjs +4 -4
|
@@ -1,233 +1,250 @@
|
|
|
1
|
-
// ast-walks.mjs - AST walks the architecture checkers share (moved out of typescript.mjs, which keeps the program loading and module resolution).
|
|
2
1
|
import { canonical, isInside } from './config.mjs';
|
|
3
2
|
import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
3
|
import { UNPROVEN_FRAMEWORK, isUnshadowedCommonJsRequire, relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
|
-
|
|
6
4
|
/** `expression` unwrapped by the caller's own predicate set (a predicate absent on an older ts reads as never). */
|
|
7
|
-
export function unwrapEach(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
export function unwrapEach(_ts, expression, predicates) {
|
|
6
|
+
let current = expression;
|
|
7
|
+
// The predicates the caller passes select wrapper nodes, which all carry an `expression` child.
|
|
8
|
+
while (current && predicates.some((predicate) => predicate?.(current)))
|
|
9
|
+
current = current.expression;
|
|
10
|
+
return current;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
12
|
/** `value` with every TS alias hop resolved (a re-exported import reads as its target symbol). */
|
|
14
13
|
export function normalizedSymbolValue(ts, checker, value = null) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
let symbol = value;
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
while (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
17
|
+
seen.add(symbol);
|
|
18
|
+
const target = checker.getAliasedSymbol(symbol);
|
|
19
|
+
if (!target || target === symbol)
|
|
20
|
+
break;
|
|
21
|
+
symbol = target;
|
|
22
|
+
}
|
|
23
|
+
return symbol;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
25
|
/** The symbol `node` names, aliases resolved. */
|
|
27
26
|
export function normalizedSymbol(ts, checker, node) {
|
|
28
|
-
|
|
27
|
+
return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
|
|
29
28
|
}
|
|
30
|
-
|
|
31
29
|
/** The identifier or member-name node an expression selects through unwrapExpression, or null. */
|
|
32
30
|
export function selectedNode(ts, expression) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const unwrapped = unwrapExpression(ts, expression);
|
|
32
|
+
if (ts.isIdentifier(unwrapped))
|
|
33
|
+
return unwrapped;
|
|
34
|
+
if (ts.isPropertyAccessExpression(unwrapped))
|
|
35
|
+
return unwrapped.name;
|
|
36
|
+
if (ts.isElementAccessExpression(unwrapped) && ts.isStringLiteralLike(unwrapped.argumentExpression))
|
|
37
|
+
return unwrapped.argumentExpression;
|
|
38
|
+
return null;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
40
|
/**
|
|
41
41
|
* The symbol `node` ultimately evaluates to: its own symbol, followed through a single `const` variable
|
|
42
42
|
* initializer (and so on). `unselected: 'null'` answers null when `node` selects no name at all.
|
|
43
43
|
*/
|
|
44
44
|
export function valueSymbol(ts, checker, node, seen = new Set(), { unselected = 'node' } = {}) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
45
|
+
const selected = selectedNode(ts, node);
|
|
46
|
+
if (!selected && unselected === 'null')
|
|
47
|
+
return null;
|
|
48
|
+
const symbol = normalizedSymbol(ts, checker, selected ?? node);
|
|
49
|
+
if (!symbol || seen.has(symbol))
|
|
50
|
+
return symbol;
|
|
51
|
+
seen.add(symbol);
|
|
52
|
+
const declarations = symbol.getDeclarations?.() ?? [];
|
|
53
|
+
const [only] = declarations;
|
|
54
|
+
if (declarations.length === 1 && only && ts.isVariableDeclaration(only) && only.initializer
|
|
55
|
+
&& isConstVariable(ts, only)) {
|
|
56
|
+
return valueSymbol(ts, checker, only.initializer, seen, { unselected });
|
|
57
|
+
}
|
|
58
|
+
return symbol;
|
|
59
|
+
}
|
|
58
60
|
/**
|
|
59
61
|
* The expressions `declaration` returns: its body when it is an expression-bodied arrow/function, else every
|
|
60
62
|
* `return`'s argument without descending into nested functions. `functionLike` overrides which node kinds bound
|
|
61
63
|
* that descent (default the four callable declaration kinds; pass `ts.isFunctionLike` for the wider set).
|
|
62
64
|
*/
|
|
63
65
|
export function returnedExpressions(ts, declaration, { functionLike = null } = {}) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const boundary = functionLike ?? ((node) => ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)
|
|
67
|
+
|| ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
68
|
+
if ((ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) && !ts.isBlock(declaration.body))
|
|
69
|
+
return [declaration.body];
|
|
70
|
+
// A node the boundary predicate accepts is a function-like, which carries a `body`.
|
|
71
|
+
const body = boundary(declaration) ? declaration.body : null;
|
|
72
|
+
if (!body || !ts.isBlock(body))
|
|
73
|
+
return [];
|
|
74
|
+
const returned = [];
|
|
75
|
+
const visit = (node) => {
|
|
76
|
+
if (node !== body && boundary(node))
|
|
77
|
+
return;
|
|
78
|
+
if (ts.isReturnStatement(node) && node.expression)
|
|
79
|
+
returned.push(node.expression);
|
|
80
|
+
else
|
|
81
|
+
ts.forEachChild(node, visit);
|
|
82
|
+
};
|
|
83
|
+
visit(body);
|
|
84
|
+
return returned;
|
|
85
|
+
}
|
|
79
86
|
/** A node's decorators across TS versions (ts.getDecorators, else the legacy `decorators` property). */
|
|
80
87
|
export function nodeDecorators(ts, node) {
|
|
81
|
-
|
|
88
|
+
return ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : node.decorators ?? [];
|
|
82
89
|
}
|
|
83
|
-
|
|
84
90
|
/** One finding row: rule id, root-relative path, node position and message, plus rule extras. */
|
|
85
91
|
export function violation(config, sourceFile, node, ruleId, message, extra = {}) {
|
|
86
|
-
|
|
92
|
+
return { ruleId, path: relativePath(config.root, sourceFile.fileName), ...sourceLocation(sourceFile, node), message, ...extra };
|
|
87
93
|
}
|
|
88
|
-
|
|
89
94
|
/** Whether `match` holds for `node` or a descendant (the walk stops at the first match). */
|
|
90
95
|
export function anyDescendant(ts, node, match) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
let found = false;
|
|
97
|
+
const visit = (current) => {
|
|
98
|
+
if (found)
|
|
99
|
+
return;
|
|
100
|
+
if (match(current)) {
|
|
101
|
+
found = true;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
ts.forEachChild(current, visit);
|
|
105
|
+
};
|
|
106
|
+
visit(node);
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
101
109
|
/**
|
|
102
110
|
* The framework kinds `expression` can produce: `directOf(ts, checker, {symbol, selected, targets})` maps a
|
|
103
111
|
* selected node's symbols to a kind (default `targets.get(symbol)`); `shorthandOf` (when set) also resolves
|
|
104
112
|
* shorthand-property declarations and descends property assignments; `newExpression` descends `new X()` callees.
|
|
105
113
|
*/
|
|
106
|
-
const addKinds = (target, source) => { for (const kind of source)
|
|
107
|
-
|
|
114
|
+
const addKinds = (target, source) => { for (const kind of source)
|
|
115
|
+
target.add(kind); };
|
|
108
116
|
const kindsFromReturns = (ts, checker, declaration, targets, options) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
const kinds = new Set();
|
|
118
|
+
for (const returned of returnedExpressions(ts, declaration))
|
|
119
|
+
addKinds(kinds, tracedFrameworkKinds(ts, checker, returned, targets, options));
|
|
120
|
+
return kinds;
|
|
112
121
|
};
|
|
113
|
-
|
|
114
122
|
const kindsFromDeclaration = (ts, checker, declaration, targets, options, shorthandOf) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
const kinds = new Set();
|
|
124
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer)
|
|
125
|
+
addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
|
|
126
|
+
if (shorthandOf && ts.isShorthandPropertyAssignment(declaration)) {
|
|
127
|
+
const target = normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol?.(declaration));
|
|
128
|
+
const kind = shorthandOf(ts, checker, target, targets);
|
|
129
|
+
if (kind)
|
|
130
|
+
kinds.add(kind);
|
|
131
|
+
}
|
|
132
|
+
if (shorthandOf && ts.isPropertyAssignment(declaration))
|
|
133
|
+
addKinds(kinds, tracedFrameworkKinds(ts, checker, declaration.initializer, targets, options));
|
|
134
|
+
addKinds(kinds, kindsFromReturns(ts, checker, declaration, targets, options));
|
|
135
|
+
return kinds;
|
|
127
136
|
};
|
|
128
|
-
|
|
129
137
|
const kindsFromSymbol = (ts, checker, symbol, targets, seen, options) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
const nextSeen = new Set(seen).add(symbol);
|
|
139
|
+
const declarationOptions = { ...options, seen: nextSeen };
|
|
140
|
+
const kinds = new Set();
|
|
141
|
+
for (const declaration of symbol.getDeclarations?.() ?? [])
|
|
142
|
+
addKinds(kinds, kindsFromDeclaration(ts, checker, declaration, targets, declarationOptions, options.shorthandOf));
|
|
143
|
+
return kinds;
|
|
136
144
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
145
|
+
const defaultDirectOf = (_ts, _checker, { symbol, targets }) => targets.get(symbol) ?? null;
|
|
146
|
+
export function tracedFrameworkKinds(ts, checker, expression, targets, { seen = new Set(), depth = 0, directOf = null, shorthandOf = null, newExpression = false } = {}) {
|
|
147
|
+
if (!expression)
|
|
148
|
+
return new Set();
|
|
149
|
+
if (depth > 10)
|
|
150
|
+
return new Set([UNPROVEN_FRAMEWORK]);
|
|
151
|
+
const unwrapped = unwrapExpression(ts, expression);
|
|
152
|
+
const selected = selectedNode(ts, unwrapped);
|
|
153
|
+
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
154
|
+
const direct = symbol && selected ? (directOf ?? defaultDirectOf)(ts, checker, { symbol, selected, targets }) : null;
|
|
155
|
+
if (direct)
|
|
156
|
+
return new Set([direct]);
|
|
157
|
+
const options = { depth: depth + 1, directOf, shorthandOf, newExpression };
|
|
158
|
+
if (ts.isArrowFunction(unwrapped) || ts.isFunctionExpression(unwrapped))
|
|
159
|
+
return kindsFromReturns(ts, checker, unwrapped, targets, { ...options, seen });
|
|
160
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
161
|
+
const kinds = tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
|
|
162
|
+
if (kinds.size)
|
|
163
|
+
return kinds;
|
|
164
|
+
}
|
|
165
|
+
if (newExpression && ts.isNewExpression(unwrapped))
|
|
166
|
+
return tracedFrameworkKinds(ts, checker, unwrapped.expression, targets, { ...options, seen });
|
|
167
|
+
if (!symbol || seen.has(symbol))
|
|
168
|
+
return new Set();
|
|
169
|
+
return kindsFromSymbol(ts, checker, symbol, targets, seen, options);
|
|
170
|
+
}
|
|
161
171
|
/** 'unproven framework' | 'multiple framework' | the one kind | null, for a decorator whose expression constructs. */
|
|
162
172
|
export function constructedDecoratorKind(ts, checker, decorator, targets, traceOpts = {}) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
174
|
+
const kinds = tracedFrameworkKinds(ts, checker, callee, targets, traceOpts);
|
|
175
|
+
if (kinds.has(UNPROVEN_FRAMEWORK))
|
|
176
|
+
return 'unproven framework';
|
|
177
|
+
if (kinds.size === 1)
|
|
178
|
+
return [...kinds][0] ?? null;
|
|
179
|
+
return kinds.size ? 'multiple framework' : null;
|
|
168
180
|
}
|
|
169
|
-
|
|
170
181
|
/** The target a decorator resolves to through a `let`/`var` alias's single initializer, or null. */
|
|
171
182
|
export function mutableDecoratorKind(ts, checker, decorator, targets) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
||
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
184
|
+
const selected = selectedNode(ts, callee);
|
|
185
|
+
const symbol = selected ? normalizedSymbol(ts, checker, selected) : null;
|
|
186
|
+
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
187
|
+
const [only] = declarations;
|
|
188
|
+
if (declarations.length !== 1 || !only || !ts.isVariableDeclaration(only) || !only.initializer
|
|
189
|
+
|| isConstVariable(ts, only))
|
|
190
|
+
return null;
|
|
191
|
+
const resolved = valueSymbol(ts, checker, only.initializer);
|
|
192
|
+
return (resolved ? targets.get(resolved) : undefined) ?? null;
|
|
193
|
+
}
|
|
181
194
|
/** The callee expression of a decorator (the called expression of `@x(...)`, else the expression itself). */
|
|
182
195
|
export function decoratorCallee(ts, decorator) {
|
|
183
|
-
|
|
196
|
+
return ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
184
197
|
}
|
|
185
|
-
|
|
186
198
|
/**
|
|
187
199
|
* The module `statement` (import, export-from or import-equals) statically binds: its specifier text, the
|
|
188
200
|
* module's symbol and its exported symbols by name (aliases resolved); `symbol` is null when the module does
|
|
189
201
|
* not resolve. Null when the statement names no static specifier.
|
|
190
202
|
*/
|
|
191
203
|
export function moduleExportsOf(ts, checker, statement) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
let moduleSpecifier = null;
|
|
205
|
+
if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement))
|
|
206
|
+
&& statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier))
|
|
207
|
+
moduleSpecifier = statement.moduleSpecifier;
|
|
208
|
+
let importEquals = null;
|
|
209
|
+
if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)
|
|
210
|
+
&& statement.moduleReference.expression && ts.isStringLiteralLike(statement.moduleReference.expression))
|
|
211
|
+
importEquals = statement.moduleReference.expression;
|
|
212
|
+
const specifierNode = moduleSpecifier ?? importEquals;
|
|
213
|
+
if (!specifierNode)
|
|
214
|
+
return null;
|
|
215
|
+
const specifier = specifierNode.text;
|
|
216
|
+
const symbol = checker.getSymbolAtLocation(specifierNode) ?? null;
|
|
217
|
+
const exports = new Map((symbol ? checker.getExportsOfModule(symbol) : [])
|
|
218
|
+
.map((item) => [item.getName(), normalizedSymbolValue(ts, checker, item) ?? null]));
|
|
219
|
+
return { specifierNode, specifier, symbol, exports };
|
|
220
|
+
}
|
|
206
221
|
/**
|
|
207
222
|
* The program source files a framework-target scan of `checker` judges: every file in `localFiles`, plus (when
|
|
208
223
|
* `root` is given) declaration files inside the root but outside node_modules. Null when no program of the
|
|
209
224
|
* context owns the checker.
|
|
210
225
|
*/
|
|
211
226
|
export function programSourcesOf(context, checker, localFiles, { root = null } = {}) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
const program = context.programs.find(candidate => candidate.getTypeChecker() === checker) ?? null;
|
|
228
|
+
if (!program)
|
|
229
|
+
return null;
|
|
230
|
+
return program.getSourceFiles().filter(sourceFile => localFiles.has(canonical(sourceFile.fileName))
|
|
231
|
+
|| (root && sourceFile.isDeclarationFile && isInside(root, sourceFile.fileName)
|
|
232
|
+
&& !sourceFile.fileName.replaceAll('\\', '/').includes('/node_modules/')));
|
|
217
233
|
}
|
|
218
|
-
|
|
219
234
|
/**
|
|
220
235
|
* The CommonJS require() caveat every framework scan reports: each `require('<specifier>')` `accepted` pushes
|
|
221
236
|
* `${filePath} uses a CommonJS <label ?? specifier> <detail>` (the import-shape reasoning cannot prove that binding).
|
|
222
237
|
*/
|
|
223
|
-
export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
export function commonJsRequireReasons(ts, checker, sourceFile, accepted, reasons, filePath, detail, labelArgument) {
|
|
239
|
+
const label = labelArgument ?? null;
|
|
240
|
+
const visit = (node) => {
|
|
241
|
+
if (ts.isCallExpression(node) && isUnshadowedCommonJsRequire(ts, checker, node.expression) && node.arguments.length === 1) {
|
|
242
|
+
const [argument] = node.arguments;
|
|
243
|
+
if (argument && ts.isStringLiteralLike(argument) && accepted(argument.text)) {
|
|
244
|
+
reasons.push(`${filePath} uses a CommonJS ${label ?? argument.text} ${detail}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ts.forEachChild(node, visit);
|
|
248
|
+
};
|
|
249
|
+
visit(sourceFile);
|
|
233
250
|
}
|
|
@@ -1,96 +1,103 @@
|
|
|
1
1
|
import { readTextFile } from '../../lib/read-text.mjs';
|
|
2
|
-
|
|
3
2
|
// Owner ruling 2026-09-29 (layout 2026-09-30): integration, e2e and contract run MANUALLY only. No hook, default typecheck,
|
|
4
3
|
// coverage run or automatic CI trigger may include those trees or run those projects. Linting the e2e files is not running them: ESLint reads them
|
|
5
4
|
// as syntax in the one repository-wide lint run (the factory's e2e block), so no `lint:e2e` command exists to judge.
|
|
6
5
|
const E2E_COMMAND = new RegExp([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
String.raw `\btest:(?:e2e|integration|contract)\b|`,
|
|
7
|
+
String.raw `\btypecheck:tests\b|`,
|
|
8
|
+
String.raw `--selectProjects\s+(?:e2e|integration|contract)\b|`,
|
|
9
|
+
String.raw `src\/tests\/(?:world|integration|e2e|contract)\b|`,
|
|
10
|
+
String.raw `jest[^\n|&;]*(?:e2e|integration|contract)`,
|
|
12
11
|
].join(''), 'u');
|
|
13
12
|
const UNIT_RUN_SCRIPTS = ['test', 'test:unit', 'test:ci', 'test:coverage', 'test:cov'];
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const withoutComments = text => text.split('\n').filter(line => !/^\s*#/u.test(line)).join('\n');
|
|
17
|
-
|
|
13
|
+
const runsE2e = (text) => E2E_COMMAND.test(String(text).replace(/--ignore-pattern[= ]+(?:"[^"]*"|'[^']*'|\S+)/gu, ''));
|
|
14
|
+
const withoutComments = (text) => text.split('\n').filter(line => !/^\s*#/u.test(line)).join('\n');
|
|
18
15
|
function inspectHooks(state) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
for (const hook of ['.husky/pre-commit', '.husky/pre-push']) {
|
|
17
|
+
const text = readTextFile(state.root, hook);
|
|
18
|
+
if (text === null)
|
|
19
|
+
continue;
|
|
20
|
+
const body = withoutComments(text);
|
|
21
|
+
if (runsE2e(body))
|
|
22
|
+
state.finding(state.rule, hook, `${hook} runs integration, e2e or contract. They are manual only; hooks run unit, lint and typecheck.`);
|
|
23
|
+
for (const match of body.matchAll(/npm\s+run\s+([\w:.-]+)/gu))
|
|
24
|
+
state.pending.push(match[1]);
|
|
25
|
+
}
|
|
26
26
|
}
|
|
27
|
-
|
|
28
27
|
function inspectLintStaged(state) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const lintStaged = state.pkg?.['lint-staged'];
|
|
29
|
+
if (lintStaged && runsE2e(Object.values(lintStaged).flat().join('\n')))
|
|
30
|
+
state.finding(state.rule, 'package.json', 'lint-staged runs an integration, e2e or contract command. They are manual only.');
|
|
32
31
|
}
|
|
33
|
-
|
|
34
32
|
function inspectHookScripts(state) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
for (const name of state.pending) {
|
|
34
|
+
if (state.called.has(name))
|
|
35
|
+
continue;
|
|
36
|
+
state.called.add(name);
|
|
37
|
+
const command = state.scripts[name];
|
|
38
|
+
if (typeof command !== 'string')
|
|
39
|
+
continue;
|
|
40
|
+
if (runsE2e(command))
|
|
41
|
+
state.finding(state.rule, 'package.json', `Script ${name} is run by a husky hook and touches integration, e2e or contract. They are manual only.`);
|
|
42
|
+
for (const match of command.matchAll(/npm\s+run\s+([\w:.-]+)/gu))
|
|
43
|
+
state.pending.push(match[1]);
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
|
-
|
|
45
46
|
function inspectUnitGates(state) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
if (state.jestConfig === null || !state.tree.hasFile(state.jestConfig))
|
|
48
|
+
return;
|
|
49
|
+
for (const name of UNIT_RUN_SCRIPTS) {
|
|
50
|
+
const command = state.scripts[name];
|
|
51
|
+
if (typeof command === 'string' && /\bjest\b/u.test(command) && !/--selectProjects\s+unit\b/u.test(command))
|
|
52
|
+
state.finding(state.rule, 'package.json', `Script ${name} runs jest without --selectProjects unit and would run the integration, e2e or contract project.`);
|
|
53
|
+
}
|
|
54
|
+
const jestText = readTextFile(state.root, state.jestConfig) ?? '';
|
|
55
|
+
if (/collectCoverageFrom/u.test(jestText) && !/!src\/tests\/(?:\*\*|e2e)/u.test(jestText))
|
|
56
|
+
state.finding(state.rule, state.jestConfig, 'collectCoverageFrom must exclude src/tests/** so no integration, e2e or contract file counts toward coverage.');
|
|
55
57
|
}
|
|
56
|
-
|
|
57
58
|
function inspectWorkflows(state) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
for (const file of state.tree.files().filter((entry) => /^\.github\/workflows\/[^/]+\.ya?ml$/u.test(entry))) {
|
|
60
|
+
const text = readTextFile(state.root, file);
|
|
61
|
+
if (text === null)
|
|
62
|
+
continue;
|
|
63
|
+
const trigger = /^on:.*(?:\n(?:[ \t].*)?$)*/mu.exec(text)?.[0] ?? '';
|
|
64
|
+
if (!/\b(?:push|pull_request)\b/u.test(trigger))
|
|
65
|
+
continue;
|
|
66
|
+
if (runsE2e(withoutComments(text)))
|
|
67
|
+
state.finding(state.rule, file, `${file} runs e2e on push or pull_request. Move the e2e job to its own workflow with on: workflow_dispatch only.`);
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
|
-
|
|
68
70
|
/** Check hooks, transitive hook scripts, unit scripts, coverage and automatic workflows. */
|
|
69
71
|
export function e2eInAutomaticGate({ root, tree, jestConfig, finding }) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
|
|
73
|
+
let pkg = null;
|
|
74
|
+
try {
|
|
75
|
+
pkg = JSON.parse(readTextFile(root, 'package.json') ?? '');
|
|
76
|
+
}
|
|
77
|
+
catch { /* the package checks own invalid JSON */ }
|
|
78
|
+
const state = { root, tree, jestConfig, finding, rule, pkg, scripts: pkg?.scripts ?? {}, pending: [], called: new Set() };
|
|
79
|
+
inspectHooks(state);
|
|
80
|
+
inspectLintStaged(state);
|
|
81
|
+
inspectHookScripts(state);
|
|
82
|
+
inspectUnitGates(state);
|
|
83
|
+
inspectWorkflows(state);
|
|
79
84
|
}
|
|
80
|
-
|
|
81
85
|
/** The be side's default tsconfig excludes the world, integration, e2e and contract trees (they are checked by src/tests/tsconfig.json). */
|
|
82
86
|
export function testTreesOutOfDefaultProgram({ root, tree, finding }) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
const rule = 'HFS_E2E_IN_AUTOMATIC_GATE';
|
|
88
|
+
const tsconfigText = readTextFile(root, 'tsconfig.json');
|
|
89
|
+
let tsconfig = null;
|
|
90
|
+
try {
|
|
91
|
+
tsconfig = tsconfigText === null ? null : JSON.parse(tsconfigText);
|
|
92
|
+
}
|
|
93
|
+
catch { /* the typecheck itself owns parsing */ }
|
|
94
|
+
if (tsconfig) {
|
|
95
|
+
const excludedText = JSON.stringify(tsconfig.exclude ?? []);
|
|
96
|
+
const excludesTestTrees = ['world', 'integration', 'e2e', 'contract'].every(tree => excludedText.includes(`src/tests/${tree}`));
|
|
97
|
+
const defaultAll = tsconfig.include === undefined && tsconfig.files === undefined;
|
|
98
|
+
const files = tree.files();
|
|
99
|
+
if (files.some((file) => /^src\/tests\/(?:world|integration|e2e|contract)\/.+\.[cm]?tsx?$/u.test(file)) && !excludesTestTrees &&
|
|
100
|
+
(defaultAll || JSON.stringify(tsconfig.include ?? []).includes('src')))
|
|
101
|
+
finding(rule, 'tsconfig.json', 'The default tsconfig includes src/tests/{world,integration,e2e,contract}/**. Exclude those trees and check them with src/tests/tsconfig.json (typecheck:tests).');
|
|
102
|
+
}
|
|
96
103
|
}
|