@starci/eslint-canon-fe 8.2.18 → 8.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/CHANGELOG.md +15 -0
- package/README.md +19 -1
- package/callback-shape.mjs +344 -0
- package/client-boundary.mjs +1 -1
- package/docs/hooks-folder.md +1 -1
- package/docs/size-and-state-budget.md +6 -6
- package/drawing.mjs +65 -28
- package/env-owner.mjs +120 -9
- package/function-budget.mjs +49 -0
- package/hooks-folder.mjs +149 -18
- package/icon.mjs +67 -0
- package/index.mjs +47 -0
- package/lib/own-hook-origin.mjs +53 -0
- package/lib/program-for.mjs +22 -0
- package/lib/pure-owner-helper.mjs +148 -0
- package/lib/redux-hook-origin.mjs +44 -0
- package/next-conventions.mjs +38 -35
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +54 -8
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/project-rule.mjs +3 -1
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/lib/env.mjs +20 -0
- package/runtime/scripts/lib/event-contract.mjs +107 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/served-locale.mjs +80 -9
- package/size-and-state-budget.mjs +11 -8
- package/size-growth.mjs +10 -6
- package/swr-family.mjs +197 -0
- package/translation.mjs +135 -7
- package/transport.mjs +126 -59
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const keyOf = (ts, property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
|
|
4
|
+
const exported = (ts, statement) => statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
5
|
+
|
|
6
|
+
/** The directly defined functions among the declarations of an exported variable statement; other values are not callable. */
|
|
7
|
+
function callableVariables(ts, statement) {
|
|
8
|
+
const names = [];
|
|
9
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
10
|
+
const initializer = declaration.initializer;
|
|
11
|
+
if (ts.isIdentifier(declaration.name) && initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) names.push(declaration.name.text);
|
|
12
|
+
}
|
|
13
|
+
return names;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The callable exports of one statement: an overload signature has no body and collapses into its implementation. */
|
|
17
|
+
function callableExports(ts, statement) {
|
|
18
|
+
if (!exported(ts, statement)) return [];
|
|
19
|
+
if (ts.isFunctionDeclaration(statement)) return statement.name && statement.body ? [statement.name.text] : [];
|
|
20
|
+
return ts.isVariableStatement(statement) ? callableVariables(ts, statement) : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The callable subset of a function subject's exports. Constants, enums, objects, types, `export ... from` re-exports and overload
|
|
25
|
+
* signatures do not change which functions the spec must provide; a class or a default-export assignment keeps the class-provider
|
|
26
|
+
* check (null), as does a module with no callable export.
|
|
27
|
+
*/
|
|
28
|
+
export function functionSubjectExports(ts, sourceFile) {
|
|
29
|
+
const names = [];
|
|
30
|
+
for (const statement of sourceFile.statements) {
|
|
31
|
+
if (ts.isClassDeclaration(statement) || ts.isExportAssignment(statement)) return null;
|
|
32
|
+
for (const name of callableExports(ts, statement)) if (!names.includes(name)) names.push(name);
|
|
33
|
+
}
|
|
34
|
+
return names.length ? names : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const decoratorsOf = (ts, node) => (ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : []);
|
|
38
|
+
|
|
39
|
+
/** A value object: an undecorated class whose constructor parameters carry no injection token, so Nest never builds it. */
|
|
40
|
+
function isValueObjectClass(ts, declaration) {
|
|
41
|
+
if (decoratorsOf(ts, declaration).length > 0) return false;
|
|
42
|
+
const constructors = declaration.members.filter(member => ts.isConstructorDeclaration(member));
|
|
43
|
+
return constructors.every(constructor => constructor.parameters.every(parameter => decoratorsOf(ts, parameter).length === 0));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The names of the value-object classes a module exports, or null when it exports a class that is not one (or none). */
|
|
47
|
+
export function valueObjectSubjects(ts, sourceFile) {
|
|
48
|
+
const classes = sourceFile.statements.filter(statement => ts.isClassDeclaration(statement) && statement.name && exported(ts, statement));
|
|
49
|
+
if (!classes.length || !classes.every(declaration => isValueObjectClass(ts, declaration))) return null;
|
|
50
|
+
return classes.map(declaration => declaration.name.text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The value of one literal object property. */
|
|
54
|
+
const propertyValue = (ts, object, key) => {
|
|
55
|
+
const property = object.properties.find(item => keyOf(ts, item) === key);
|
|
56
|
+
return property && ts.isPropertyAssignment(property) ? property.initializer : null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A literal value object maps every exported function to its real sibling import. */
|
|
60
|
+
function realFunctions(ts, value, names, imports, rel, serviceRel) {
|
|
61
|
+
if (!value || !ts.isObjectLiteralExpression(value) || value.properties.length !== names.length) return false;
|
|
62
|
+
const remaining = new Set(names);
|
|
63
|
+
for (const property of value.properties) {
|
|
64
|
+
const key = keyOf(ts, property);
|
|
65
|
+
const identifier = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
66
|
+
if (!remaining.delete(key) || !identifier || !ts.isIdentifier(identifier)) return false;
|
|
67
|
+
const binding = imports.get(identifier.text);
|
|
68
|
+
if (!binding || binding.name !== key || !binding.module.startsWith('.')) return false;
|
|
69
|
+
const target = path.posix.normalize(path.posix.join(path.posix.dirname(rel), binding.module)).replace(/\.[cm]?[jt]s$/u, '');
|
|
70
|
+
if (target !== serviceRel.replace(/\.ts$/u, '')) return false;
|
|
71
|
+
}
|
|
72
|
+
return remaining.size === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Every node satisfying a predicate, within one spec. */
|
|
76
|
+
function matchingNodes(ts, root, predicate) {
|
|
77
|
+
const found = [];
|
|
78
|
+
const visit = node => {
|
|
79
|
+
if (predicate(node)) found.push(node);
|
|
80
|
+
ts.forEachChild(node, visit);
|
|
81
|
+
};
|
|
82
|
+
visit(root);
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The declared variable initialized by an expression, including its await and call chain. */
|
|
87
|
+
function initializedVariable(ts, expression) {
|
|
88
|
+
let cursor = expression;
|
|
89
|
+
let compiled = false;
|
|
90
|
+
let awaited = false;
|
|
91
|
+
while (cursor.parent && (ts.isAwaitExpression(cursor.parent) || ts.isPropertyAccessExpression(cursor.parent) || ts.isCallExpression(cursor.parent))) {
|
|
92
|
+
cursor = cursor.parent;
|
|
93
|
+
if (ts.isAwaitExpression(cursor)) awaited = true;
|
|
94
|
+
if (ts.isCallExpression(cursor) && ts.isPropertyAccessExpression(cursor.expression) && cursor.expression.name.text === 'compile') compiled = true;
|
|
95
|
+
}
|
|
96
|
+
if (!compiled || !awaited) return null;
|
|
97
|
+
const declaration = cursor.parent;
|
|
98
|
+
return declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) ? declaration.name.text : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The subject variable receiving module.get, whether declared there or assigned in beforeEach. */
|
|
102
|
+
function retrievedVariable(ts, call) {
|
|
103
|
+
let cursor = call;
|
|
104
|
+
while (cursor.parent && (ts.isAwaitExpression(cursor.parent) || ts.isParenthesizedExpression(cursor.parent))) cursor = cursor.parent;
|
|
105
|
+
const parent = cursor.parent;
|
|
106
|
+
if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
|
|
107
|
+
if (parent && ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) return parent.left.text;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A helper returning the retrieved subject is followed to the variable receiving its result. */
|
|
112
|
+
function retrievedSubjects(ts, read, sourceFile) {
|
|
113
|
+
const direct = retrievedVariable(ts, read);
|
|
114
|
+
if (direct) return [direct];
|
|
115
|
+
if (!read.parent || !ts.isReturnStatement(read.parent)) return [];
|
|
116
|
+
let helper = read.parent;
|
|
117
|
+
while (helper.parent && !ts.isFunctionLike(helper)) helper = helper.parent;
|
|
118
|
+
const name = helper.name?.text ?? helper.parent?.name?.text;
|
|
119
|
+
if (!name) return [];
|
|
120
|
+
return matchingNodes(ts, sourceFile, node => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name)
|
|
121
|
+
.map(node => retrievedVariable(ts, node)).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The same compiled module retrieves the matching token and its result actually calls a subject function. */
|
|
125
|
+
function retrievedSubjectUsed(ts, call, token, names) {
|
|
126
|
+
const moduleName = initializedVariable(ts, call);
|
|
127
|
+
if (!moduleName) return false;
|
|
128
|
+
const sourceFile = call.getSourceFile();
|
|
129
|
+
const reads = matchingNodes(ts, sourceFile, node => {
|
|
130
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false;
|
|
131
|
+
const member = node.expression;
|
|
132
|
+
return ts.isIdentifier(member.expression) && member.expression.text === moduleName && member.name.text === 'get'
|
|
133
|
+
&& node.arguments.length === 1 && ts.isIdentifier(node.arguments[0]) && node.arguments[0].text === token.text;
|
|
134
|
+
});
|
|
135
|
+
return reads.some(read => {
|
|
136
|
+
const subjects = retrievedSubjects(ts, read, sourceFile);
|
|
137
|
+
return matchingNodes(ts, sourceFile, node => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
|
|
138
|
+
&& ts.isIdentifier(node.expression.expression) && subjects.includes(node.expression.expression.text) && names.includes(node.expression.name.text)).length > 0;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A pure function service has exactly one real subject value provider, retrieved and exercised through Nest. */
|
|
143
|
+
export function checkFunctionSubject({ ts, rel, serviceRel, imports, call, list, names, report }) {
|
|
144
|
+
const provider = list.elements[0];
|
|
145
|
+
const shape = provider && ts.isObjectLiteralExpression(provider) && provider.properties.length === 2;
|
|
146
|
+
const token = shape ? propertyValue(ts, provider, 'provide') : null;
|
|
147
|
+
const value = shape ? propertyValue(ts, provider, 'useValue') : null;
|
|
148
|
+
if (list.elements.length !== 1 || !token || !ts.isIdentifier(token) || !realFunctions(ts, value, names, imports, rel, serviceRel)) {
|
|
149
|
+
report(provider ?? list, `${rel}: a function-only subject needs exactly one { provide: TOKEN, useValue: { ...realFunctions } } provider containing every callable export imported from ./${path.posix.basename(serviceRel, '.ts')}.`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!retrievedSubjectUsed(ts, call, token, names)) report(list, `${rel}: retrieve the function subject with moduleRef.get(TOKEN) from this compiled testing module and exercise a function on the returned subject.`);
|
|
153
|
+
}
|
|
@@ -297,11 +297,11 @@ function checkTestTopology(state) {
|
|
|
297
297
|
// Test kinds and folders that do not exist: int-spec, harness-spec, src/tests/harness and live/;
|
|
298
298
|
// a backend spec sits by its kind (beside its subject, or under src/tests/{integration,e2e,contract}/).
|
|
299
299
|
for (const file of state.tree.files()) {
|
|
300
|
-
if (UNSUPPORTED_TEST_SUFFIX.test(file))
|
|
300
|
+
if (state.backend && state.profile === 'be' && UNSUPPORTED_TEST_SUFFIX.test(file))
|
|
301
301
|
state.finding('BE_TEST_TOPOLOGY', file, `${file} uses an unsupported test kind. Only unit *.spec.ts, *.integration-spec.ts, *.e2e-spec.ts and *.contract-spec.ts exist, each in its own folder under src/tests/.`);
|
|
302
|
-
else if (state.backend && UNSUPPORTED_TEST_FOLDER.test(file))
|
|
302
|
+
else if (state.backend && state.profile === 'be' && UNSUPPORTED_TEST_FOLDER.test(file))
|
|
303
303
|
state.finding('BE_TEST_TOPOLOGY', file, `${file} sits in an unsupported test folder. Unit specs sit beside their subject, flows go under src/tests/e2e/<area>/ and test infrastructure under src/tests/world/.`);
|
|
304
|
-
else if (state.backend && file.startsWith('src/tests/') && isExtraTestConfig(file))
|
|
304
|
+
else if (state.backend && state.profile === 'be' && file.startsWith('src/tests/') && isExtraTestConfig(file))
|
|
305
305
|
state.finding('BE_TEST_TOPOLOGY', file, `${file} is a per-lane test config. One root jest.config.js declares exactly the unit, integration, e2e and contract projects.`);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
|
+
import { escapeRegExp } from '../../lib/regex.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R106 `i18n-keys` (FE_I18N_KEYS). The catalogs of an app (`apps/<app>/src/modules/i18n/messages/<locale>.json`, slot fe.modules.i18n) and
|
|
@@ -12,7 +13,7 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
12
13
|
* own source (both branches of a conditional key are read). A key no locale's catalog holds is FE_I18N_KEYS, once per call. A computed
|
|
13
14
|
* key (a template with an interpolation, a variable) is not judged here; it counts as a read.
|
|
14
15
|
* - a key nobody reads is dead: a leaf of the app's catalogs that no string in the app's or the shared packages' source can be reading.
|
|
15
|
-
* A leaf counts as read when a string literal or a template pattern (each interpolation a wildcard
|
|
16
|
+
* A leaf counts as read when a string literal or a template pattern (each interpolation a wildcard within its dotted segment) equals a
|
|
16
17
|
* tail of its dotted path (`"title"`, `"card.title"`, `"orders.card.title"` all read `orders.card.title`); the test is deliberately loose
|
|
17
18
|
* so a key handed through props or a table of keys is never reported, and only a key that appears nowhere is.
|
|
18
19
|
*
|
|
@@ -39,7 +40,10 @@ const held = (leaves, full) => leaves.has(full) || [...leaves].some(leaf => leaf
|
|
|
39
40
|
|
|
40
41
|
// A pattern that is only wildcards (`${a}`, `${a}.${b}`) names nothing: it would read every key, so it reads none.
|
|
41
42
|
const tailMatches = (pattern, leafSegments) => pattern.some(segment => segment !== '*' && segment !== '') && pattern.length <= leafSegments.length
|
|
42
|
-
&& pattern.every((segment, index) =>
|
|
43
|
+
&& pattern.every((segment, index) => {
|
|
44
|
+
const leaf = leafSegments[leafSegments.length - pattern.length + index];
|
|
45
|
+
return segment instanceof RegExp ? segment.test(leaf) : segment === '*' || segment === leaf;
|
|
46
|
+
});
|
|
43
47
|
|
|
44
48
|
function catalogsPerApp(config, graph) {
|
|
45
49
|
const catalogs = new Map();
|
|
@@ -56,12 +60,21 @@ function catalogsPerApp(config, graph) {
|
|
|
56
60
|
return catalogs;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
/** Each interpolation can fill part or all of a segment; the template's static fragments keep their literal spelling. */
|
|
64
|
+
function templatePatternOf(node) {
|
|
65
|
+
return [node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('.').map(segment => {
|
|
66
|
+
if (!segment.includes('\u0000')) return segment;
|
|
67
|
+
const parts = segment.split('\u0000');
|
|
68
|
+
return parts.every(part => part === '') ? '*' : new RegExp(`^${parts.map(escapeRegExp).join('.*')}$`, 'su');
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
function patternsOf(ts, files) {
|
|
60
73
|
const patterns = [];
|
|
61
74
|
for (const file of files) {
|
|
62
75
|
const visit = node => {
|
|
63
76
|
if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
|
|
64
|
-
else if (ts.isTemplateExpression(node)) patterns.push(
|
|
77
|
+
else if (ts.isTemplateExpression(node)) patterns.push(templatePatternOf(node));
|
|
65
78
|
ts.forEachChild(node, visit);
|
|
66
79
|
};
|
|
67
80
|
visit(file.sourceFile);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical } from './config.mjs';
|
|
3
3
|
import { hasAnyFlag, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
5
|
|
|
5
6
|
const HELPER_DEPTH = 4;
|
|
6
7
|
|
|
@@ -110,6 +111,21 @@ const at = (env, rel, sourceFile, node, extra = {}) => ({ path: rel, ...sourceLo
|
|
|
110
111
|
/** The app root file of an app (apps/<name>/src/app.module.ts), or null when the program does not hold it. */
|
|
111
112
|
const appRoot = (env, name) => graphFile(env, `apps/${name}/src/app.module.ts`);
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Whether a repository-relative file is a composition group of an app: `apps/<app>/src/compose/<area>.ts`, the module list of
|
|
116
|
+
* one area that the app root spreads into its imports (slot allows `compose/<area>.ts` of every be.app.* slot).
|
|
117
|
+
*/
|
|
118
|
+
export const isComposeGroup = rel => /^apps\/[^/]+\/src\/compose\/[^/]+\.ts$/.test(rel) && !/\.(?:spec|d)\.ts$/.test(rel);
|
|
119
|
+
|
|
120
|
+
/** The app root and the composition groups of an app, in path order (the root first); [] when the program holds no root. */
|
|
121
|
+
const appCompositionFiles = (env, name) => {
|
|
122
|
+
const root = appRoot(env, name);
|
|
123
|
+
if (!root) return [];
|
|
124
|
+
const prefix = `apps/${name}/src/compose/`;
|
|
125
|
+
const groups = [...env.graph.files.values()].filter(file => file.rel.startsWith(prefix) && isComposeGroup(file.rel));
|
|
126
|
+
return [root, ...groups.sort((a, b) => byCodeUnit(a.rel, b.rel))];
|
|
127
|
+
};
|
|
128
|
+
|
|
113
129
|
/** The body a call or a spread identifier resolves to when it is a function or a const declared in a file of the program (not a package), else null. */
|
|
114
130
|
function helperBodyOf(env, checker, node) {
|
|
115
131
|
const { ts } = env;
|
|
@@ -200,6 +216,7 @@ export function machineKit({ context, graph }) {
|
|
|
200
216
|
valueOfProperty: bound(valueOfProperty),
|
|
201
217
|
at: bound(at),
|
|
202
218
|
appRoot: bound(appRoot),
|
|
219
|
+
appCompositionFiles: bound(appCompositionFiles),
|
|
203
220
|
};
|
|
204
221
|
}
|
|
205
222
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { machineKit } from './machine-ast.mjs';
|
|
2
|
+
import { isComposeGroup, machineKit } from './machine-ast.mjs';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
4
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
5
5
|
import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
@@ -7,14 +7,17 @@ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
|
7
7
|
/**
|
|
8
8
|
* R45 `module-per-transport` (BE_MODULE_SHAPE, BE-CONVENTION 1.2 and 1.6). A feature has one Nest module for its
|
|
9
9
|
* application (`<f>.module.ts` at the feature root) and exactly one module per transport folder
|
|
10
|
-
* (`transport/<protocol>/<f>-<protocol>.module.ts`). Never one module per operation
|
|
10
|
+
* (`transport/<protocol>/<f>-<protocol>.module.ts`). Never one module per operation. Every one of these modules has its own
|
|
11
|
+
* module-definition beside it (owner ruling module-definition-everywhere, R295):
|
|
11
12
|
*
|
|
12
|
-
* - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts`
|
|
13
|
-
*
|
|
14
|
-
* feature file that is neither the application module
|
|
13
|
+
* - a `*.module.ts` file below a transport folder other than `<f>-<protocol>.module.ts`, a `*.module-definition.ts` that is
|
|
14
|
+
* not the sibling of the application module or of the transport module of its own folder, and an `@Module` class (or a
|
|
15
|
+
* `ConfigurableModuleBuilder`/`ConfigurableModuleClass` use) in a feature file that is neither the application module, the
|
|
16
|
+
* transport module of its own folder nor that module's definition are refused;
|
|
15
17
|
* - apps import only transport modules, of the kinds their app kind allows: the `composedBy` list of each transport slot
|
|
16
|
-
* in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root
|
|
17
|
-
* lists a feature's application module, or the transport module
|
|
18
|
+
* in knowledge/hfs/slots.yaml (api: graphql, http, websocket; worker: schedule, message). An app root (app.module.ts
|
|
19
|
+
* or a composition group of its compose/ folder) that lists a feature's application module, or the transport module
|
|
20
|
+
* of another kind, is refused;
|
|
18
21
|
* - a composed root (an owner slot with `composedBy`, be.cli) holds one static module per folder, named after the folder
|
|
19
22
|
* (cli.module.ts, <group>/<group>.module.ts); an app of a composing kind lists the root module only, and an app of any
|
|
20
23
|
* other kind lists none of its modules.
|
|
@@ -22,7 +25,6 @@ import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
|
22
25
|
export const MODULE_PER_TRANSPORT_RULE_IDS = ['BE_MODULE_SHAPE'];
|
|
23
26
|
|
|
24
27
|
const RULE = 'BE_MODULE_SHAPE';
|
|
25
|
-
const APP_ROOT_FILE = 'app.module.ts';
|
|
26
28
|
|
|
27
29
|
function featureOwnerOf(resolver, rel) {
|
|
28
30
|
const owner = resolver.ownerOf(rel);
|
|
@@ -53,13 +55,24 @@ function composedByProtocol(resolver) {
|
|
|
53
55
|
return composedBy;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
/** The module a feature file may declare: the application module, the transport module of its folder, or a cli folder's module. */
|
|
59
|
+
function ownModuleOf(place, rel, composedModuleOf) {
|
|
60
|
+
if (place.composed) return composedModuleOf(rel);
|
|
61
|
+
return place.protocol === null
|
|
62
|
+
? `${place.root}/${place.feature}.module.ts`
|
|
63
|
+
: `${place.root}/transport/${place.protocol}/${place.feature}-${place.protocol}.module.ts`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFINITION_FILE = /\.module-definition\.[cm]?tsx?$/u;
|
|
67
|
+
const definitionOf = modulePath => modulePath.replace(/\.module\.([cm]?tsx?)$/u, '.module-definition.$1');
|
|
68
|
+
|
|
69
|
+
function reportTransportFileShapes(tree, placeOf, plain, composedModuleOf) {
|
|
57
70
|
let transportModules = 0;
|
|
58
71
|
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
59
72
|
const place = placeOf(rel);
|
|
60
73
|
if (!place) continue;
|
|
61
74
|
const base = path.posix.basename(rel);
|
|
62
|
-
if (
|
|
75
|
+
if (DEFINITION_FILE.test(base) && rel !== definitionOf(ownModuleOf(place, rel, composedModuleOf))) plain(rel, `${rel} is a module-definition that is not beside a module of feature ${place.feature}; the definition of a module is its sibling ${path.posix.basename(definitionOf(ownModuleOf(place, rel, composedModuleOf)))}, one per application module, transport module or cli folder.`, { feature: place.feature });
|
|
63
76
|
if (place.protocol && /\.module\.[cm]?tsx?$/u.test(base)) {
|
|
64
77
|
const expected = `${place.feature}-${place.protocol}.module.ts`;
|
|
65
78
|
if (place.below.length !== 3 || base !== expected) plain(rel, `${rel} is a second module of the ${place.protocol} transport of ${place.feature}; a transport folder has exactly one module, transport/${place.protocol}/${expected}, never one module per operation. List the operation's providers in that module and delete this file.`, { feature: place.feature, protocol: place.protocol });
|
|
@@ -88,13 +101,15 @@ function reportModuleClassLocation(file, statement, place, composedModuleOf, rep
|
|
|
88
101
|
if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
function reportConfigurableModuleUses(file, checker, kit, ts, place,
|
|
104
|
+
function reportConfigurableModuleUses(file, checker, kit, ts, place, env) {
|
|
105
|
+
const own = ownModuleOf(place, file.rel, env.composedModuleOf);
|
|
106
|
+
const isDefinition = file.rel === definitionOf(own);
|
|
92
107
|
kit.walk(file.sourceFile, node => {
|
|
93
|
-
if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common')) {
|
|
94
|
-
report(file, node, `ConfigurableModuleBuilder builds
|
|
95
|
-
} else if (ts.isHeritageClause(node)) {
|
|
108
|
+
if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common') && !isDefinition) {
|
|
109
|
+
env.report(file, node, `ConfigurableModuleBuilder builds the module definition of one module; in feature ${place.feature} it is written only in ${path.posix.basename(definitionOf(own))}, beside ${path.posix.basename(own)}.`, { feature: place.feature });
|
|
110
|
+
} else if (ts.isHeritageClause(node) && file.rel !== own) {
|
|
96
111
|
for (const type of node.types) if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
|
|
97
|
-
report(file, type, `
|
|
112
|
+
env.report(file, type, `ConfigurableModuleClass is extended by the module of its own definition only (${path.posix.basename(own)}); feature ${place.feature} declares no other configurable module.`, { feature: place.feature });
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
return true;
|
|
@@ -118,7 +133,7 @@ function collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report
|
|
|
118
133
|
const checker = kit.checkerOf(file.sourceFile);
|
|
119
134
|
const place = placeOf(file.rel);
|
|
120
135
|
moduleClassesInFile(file, checker, place, modules, env);
|
|
121
|
-
if (place) reportConfigurableModuleUses(file, checker, kit, ts, place,
|
|
136
|
+
if (place) reportConfigurableModuleUses(file, checker, kit, ts, place, env);
|
|
122
137
|
}
|
|
123
138
|
return modules;
|
|
124
139
|
}
|
|
@@ -152,28 +167,37 @@ function inspectModuleDeclarations(checker, target, node, root, app, scan, seen)
|
|
|
152
167
|
return references;
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const seen = new Set();
|
|
170
|
+
/** The module references of one composition file: the root's `imports` lists, or every array of a composition group (its module list). */
|
|
171
|
+
function inspectCompositionFile(file, app, scan, seen) {
|
|
172
|
+
const { kit, ts } = scan;
|
|
173
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
174
|
+
const group = isComposeGroup(file.rel);
|
|
161
175
|
let references = 0;
|
|
162
176
|
const judge = node => {
|
|
163
177
|
const call = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register';
|
|
164
178
|
if (ts.isCallExpression(node) && !call) return;
|
|
165
179
|
const target = call ? node.expression.expression : node;
|
|
166
|
-
references += inspectModuleDeclarations(checker, target, node,
|
|
180
|
+
references += inspectModuleDeclarations(checker, target, node, file, app, scan, seen);
|
|
167
181
|
};
|
|
168
|
-
kit.walk(
|
|
182
|
+
kit.walk(file.sourceFile, node => {
|
|
169
183
|
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === 'imports' && ts.isArrayLiteralExpression(node.initializer)) {
|
|
170
184
|
for (const element of node.initializer.elements) judge(element);
|
|
185
|
+
} else if (group && ts.isArrayLiteralExpression(node)) {
|
|
186
|
+
for (const element of node.elements) judge(element);
|
|
171
187
|
} else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register') judge(node);
|
|
172
188
|
return true;
|
|
173
189
|
});
|
|
174
190
|
return references;
|
|
175
191
|
}
|
|
176
192
|
|
|
193
|
+
/** The module references of an app: its root `apps/<app>/src/app.module.ts` and the composition groups of `compose/` it spreads. */
|
|
194
|
+
function inspectAppRoot(app, scan) {
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
let references = 0;
|
|
197
|
+
for (const file of scan.kit.appCompositionFiles(app.name)) references += inspectCompositionFile(file, app, scan, seen);
|
|
198
|
+
return references;
|
|
199
|
+
}
|
|
200
|
+
|
|
177
201
|
function appModuleReferences(apps, scan) {
|
|
178
202
|
let references = 0;
|
|
179
203
|
for (const app of apps) references += inspectAppRoot(app, scan);
|
|
@@ -194,7 +218,7 @@ export function checkModulePerTransport(input) {
|
|
|
194
218
|
const composedBy = composedByProtocol(resolver);
|
|
195
219
|
|
|
196
220
|
const tree = treeOf(config.root);
|
|
197
|
-
const transportModules = reportTransportFileShapes(tree, placeOf, plain);
|
|
221
|
+
const transportModules = reportTransportFileShapes(tree, placeOf, plain, composedModuleOf);
|
|
198
222
|
|
|
199
223
|
const modules = collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report);
|
|
200
224
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Infer lexical request identities for SWR hooks without a declared lifecycle contract.
|
|
2
|
+
import { unwrapExpression } from './typescript.mjs';
|
|
3
|
+
import { anyDescendant, normalizedSymbol } from './ast-walks.mjs';
|
|
4
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
|
+
import { functionNode, constInitializer, inferredSymbol } from './next-data-symbols.mjs';
|
|
6
|
+
|
|
7
|
+
export function inferredFetcher(ts, checker, expression, seen = new Set()) {
|
|
8
|
+
if (!expression) return null;
|
|
9
|
+
expression = unwrapExpression(ts, expression);
|
|
10
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) return expression;
|
|
11
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
12
|
+
if (!symbol || seen.has(symbol)) return null;
|
|
13
|
+
for (const declaration of symbol.getDeclarations?.() ?? []) {
|
|
14
|
+
const fn = functionNode(ts, declaration);
|
|
15
|
+
if (fn) return fn;
|
|
16
|
+
}
|
|
17
|
+
const initializer = constInitializer(ts, symbol);
|
|
18
|
+
return initializer ? inferredFetcher(ts, checker, initializer, new Set(seen).add(symbol)) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function importedIdentityReference(ts, checker, expression) {
|
|
22
|
+
const reference = ts.isShorthandPropertyAssignment(expression.parent)
|
|
23
|
+
? checker.getShorthandAssignmentValueSymbol(expression.parent) : checker.getSymbolAtLocation(expression);
|
|
24
|
+
return (reference?.getDeclarations?.() ?? []).some(declaration => ts.isImportSpecifier(declaration)
|
|
25
|
+
|| ts.isImportClause(declaration) || ts.isNamespaceImport(declaration) || ts.isImportEqualsDeclaration(declaration));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function identifierIdentityAccess(ts, checker, expression, seen) {
|
|
29
|
+
const imported = importedIdentityReference(ts, checker, expression);
|
|
30
|
+
const symbol = inferredSymbol(ts, checker, expression);
|
|
31
|
+
const initializer = symbol && !seen.has(symbol) ? constInitializer(ts, symbol) : null;
|
|
32
|
+
if (initializer) {
|
|
33
|
+
const alias = identityAccess(ts, checker, initializer, new Set(seen).add(symbol));
|
|
34
|
+
if (alias) return { ...alias, imported: imported || alias.imported };
|
|
35
|
+
}
|
|
36
|
+
return { symbol, parts: [expression.text], imported };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSelectedMember(ts, expression) {
|
|
40
|
+
return ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)
|
|
41
|
+
&& ts.isStringLiteralLike(expression.argumentExpression);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function selectedMemberKey(ts, expression) {
|
|
45
|
+
return ts.isPropertyAccessExpression(expression) ? expression.name.text : expression.argumentExpression.text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function identityAccess(ts, checker, expression, seen = new Set()) {
|
|
49
|
+
expression = unwrapExpression(ts, expression);
|
|
50
|
+
if (ts.isIdentifier(expression)) return identifierIdentityAccess(ts, checker, expression, seen);
|
|
51
|
+
if (isSelectedMember(ts, expression)) {
|
|
52
|
+
const base = identityAccess(ts, checker, expression.expression, seen);
|
|
53
|
+
return base ? { ...base, parts: [...base.parts, selectedMemberKey(ts, expression)] } : null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function containerWritten(ts, checker, symbol, source) {
|
|
59
|
+
const rootSymbol = expression => {
|
|
60
|
+
expression = unwrapExpression(ts, expression);
|
|
61
|
+
while (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) expression = unwrapExpression(ts, expression.expression);
|
|
62
|
+
return ts.isIdentifier(expression) ? inferredSymbol(ts, checker, expression) : null;
|
|
63
|
+
};
|
|
64
|
+
return anyDescendant(ts, source, node => {
|
|
65
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
|
|
66
|
+
&& node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) return rootSymbol(node.left) === symbol;
|
|
67
|
+
if (ts.isDeleteExpression(node)) return rootSymbol(node.expression) === symbol;
|
|
68
|
+
if (ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node)
|
|
69
|
+
&& [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) return rootSymbol(node.operand) === symbol;
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function selectedRequestAlias(ts, checker, expression, seen) {
|
|
75
|
+
const symbol = inferredSymbol(ts, checker, expression);
|
|
76
|
+
const initializer = symbol && !seen.has(symbol) ? constInitializer(ts, symbol) : null;
|
|
77
|
+
if (!initializer) return null;
|
|
78
|
+
const value = unwrapExpression(ts, initializer);
|
|
79
|
+
if (ts.isObjectLiteralExpression(value) && (initializer.getSourceFile() !== expression.getSourceFile()
|
|
80
|
+
|| containerWritten(ts, checker, symbol, expression.getSourceFile()))) return null;
|
|
81
|
+
if (ts.isObjectLiteralExpression(value) || ts.isIdentifier(value) || ts.isPropertyAccessExpression(value)
|
|
82
|
+
|| ts.isElementAccessExpression(value)) return { symbol, value };
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function selectedRequestProperty(ts, object, key) {
|
|
87
|
+
if (!ts.isObjectLiteralExpression(object) || !object.properties.every(property => ts.isPropertyAssignment(property)
|
|
88
|
+
|| ts.isShorthandPropertyAssignment(property))) return null;
|
|
89
|
+
return [...object.properties].reverse().find(candidate => (ts.isIdentifier(candidate.name)
|
|
90
|
+
|| ts.isStringLiteralLike(candidate.name)) && candidate.name.text === key) ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function selectedRequestValue(ts, checker, expression, seen = new Set()) {
|
|
94
|
+
expression = unwrapExpression(ts, expression);
|
|
95
|
+
if (ts.isIdentifier(expression)) {
|
|
96
|
+
const alias = selectedRequestAlias(ts, checker, expression, seen);
|
|
97
|
+
return alias ? selectedRequestValue(ts, checker, alias.value, new Set(seen).add(alias.symbol)) : expression;
|
|
98
|
+
}
|
|
99
|
+
if (!isSelectedMember(ts, expression)) return expression;
|
|
100
|
+
const object = selectedRequestValue(ts, checker, expression.expression, seen);
|
|
101
|
+
const property = selectedRequestProperty(ts, object, selectedMemberKey(ts, expression));
|
|
102
|
+
if (!property) return expression;
|
|
103
|
+
const value = ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
104
|
+
return selectedRequestValue(ts, checker, value, seen);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function capturedInput(ts, access, region) {
|
|
108
|
+
const declarations = access.symbol?.getDeclarations?.() ?? [];
|
|
109
|
+
return declarations.some(declaration => {
|
|
110
|
+
if (declaration.getSourceFile() === region.getSourceFile() && declaration.pos >= region.pos && declaration.end <= region.end) return false;
|
|
111
|
+
if (declaration.getSourceFile().isDeclarationFile && !access.imported) return false;
|
|
112
|
+
return access.imported || ts.isVariableDeclaration(declaration) || ts.isParameter(declaration) || ts.isBindingElement(declaration);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function capturedIdentityAlias(ts, access, seen) {
|
|
117
|
+
// Follow transparent aliases and request objects, so {id} and an alias of id carry the same identity.
|
|
118
|
+
if (access.parts.length !== 1 || seen.has(access.symbol)) return null;
|
|
119
|
+
const initializer = constInitializer(ts, access.symbol);
|
|
120
|
+
if (!initializer) return null;
|
|
121
|
+
const value = unwrapExpression(ts, initializer);
|
|
122
|
+
return ts.isCallExpression(value) || ts.isAwaitExpression(value) || ts.isNewExpression(value) ? null : value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function recordInferredIdentity(scope, access, seen, visit) {
|
|
126
|
+
const { ts, call, region, reasons, identities } = scope;
|
|
127
|
+
if (access?.imported && !(access.symbol?.getDeclarations?.() ?? []).length) {
|
|
128
|
+
reasons.push(`${call.source.fileName}:${sourceLocation(call.source, call.node).line} imported SWR request input ${access.parts.join('.')} cannot be resolved; dataLifecycle is required to declare identities`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (!access || !capturedInput(ts, access, region)) return false;
|
|
132
|
+
const alias = capturedIdentityAlias(ts, access, seen);
|
|
133
|
+
if (alias) visit(alias, new Set(seen).add(access.symbol));
|
|
134
|
+
else {
|
|
135
|
+
const binding = access.parts.join('.');
|
|
136
|
+
identities.set(binding, { id: binding, binding, ...access, gatesRequest: false, resource: call.kind === 'mutation' });
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function visitRequestChildren(ts, node, seen, visit) {
|
|
142
|
+
if (ts.isPropertyAssignment(node)) { visit(node.initializer, seen); return; }
|
|
143
|
+
// A method name is not a request input; its receiver and arguments can be.
|
|
144
|
+
if (ts.isCallExpression(node)) {
|
|
145
|
+
const callee = unwrapExpression(ts, node.expression);
|
|
146
|
+
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) visit(callee.expression, seen);
|
|
147
|
+
for (const argument of node.arguments) visit(argument, seen);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (ts.isVariableDeclaration(node)) {
|
|
151
|
+
if (node.initializer) visit(node.initializer, seen);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
ts.forEachChild(node, child => { visit(child, seen); });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Infer lexical request inputs; parameters declared inside the fetcher are supplied by SWR. */
|
|
158
|
+
export function inferredIdentities(context, call, region, reasons) {
|
|
159
|
+
const { ts } = context;
|
|
160
|
+
const identities = new Map();
|
|
161
|
+
const scope = { ts, call, region, reasons, identities };
|
|
162
|
+
const visit = (node, seen = new Set()) => {
|
|
163
|
+
const selectedValue = selectedRequestValue(ts, call.checker, node);
|
|
164
|
+
if (selectedValue !== node && selectedValue !== unwrapExpression(ts, node)) { visit(selectedValue, seen); return; }
|
|
165
|
+
if (recordInferredIdentity(scope, identityAccess(ts, call.checker, node), seen, visit)) return;
|
|
166
|
+
visitRequestChildren(ts, node, seen, visit);
|
|
167
|
+
};
|
|
168
|
+
visit(region.body ?? region);
|
|
169
|
+
return [...identities.values()];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function inferredKeyBindings(context, call, reasons) {
|
|
173
|
+
const key = call.node.arguments[0];
|
|
174
|
+
// A key or its null guard can contain bindings that are not captured request inputs.
|
|
175
|
+
return key ? inferredIdentities(context, call, key, reasons).map(identity => ({ ...identity, id: `key:${identity.id}`, resource: false })) : [];
|
|
176
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Symbol reads shared by declared and inferred SWR lifecycle checks.
|
|
2
|
+
import { unwrapExpression } from './typescript.mjs';
|
|
3
|
+
import { normalizedSymbol, normalizedSymbolValue } from './ast-walks.mjs';
|
|
4
|
+
import { isConstVariable } from '../../lib/ts-ast.mjs';
|
|
5
|
+
|
|
6
|
+
export function functionNode(ts, declaration) {
|
|
7
|
+
if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
|
|
8
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
9
|
+
const value = unwrapExpression(ts, declaration.initializer);
|
|
10
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) return value;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function constInitializer(ts, symbol) {
|
|
16
|
+
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
17
|
+
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
18
|
+
|| !isConstVariable(ts, declarations[0])) return null;
|
|
19
|
+
return declarations[0].initializer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function inferredSymbol(ts, checker, node) {
|
|
23
|
+
return ts.isIdentifier(node) && ts.isShorthandPropertyAssignment(node.parent)
|
|
24
|
+
? normalizedSymbolValue(ts, checker, checker.getShorthandAssignmentValueSymbol(node.parent))
|
|
25
|
+
: normalizedSymbol(ts, checker, node);
|
|
26
|
+
}
|