@violetflux/eslint-plugin-kerros 0.3.3 → 0.3.5
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/dist/index.cjs +267 -2371
- package/dist/index.d.cts +4 -19
- package/dist/index.d.mts +4 -19
- package/dist/index.mjs +267 -2370
- package/package.json +2 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import parser from "@typescript-eslint/parser";
|
|
2
|
-
import ts from "typescript";
|
|
3
2
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
4
3
|
//#region src/internal/ast.ts
|
|
5
4
|
const transparentExpressionTypes = /* @__PURE__ */ new Set([
|
|
@@ -39,299 +38,84 @@ function getReturnedExpressions(selector) {
|
|
|
39
38
|
}
|
|
40
39
|
//#endregion
|
|
41
40
|
//#region src/internal/rule.ts
|
|
42
|
-
const createRule = ESLintUtils.RuleCreator((
|
|
43
|
-
/** Require complete TypeScript parser services for every Kerros rule. */
|
|
44
|
-
function getTypeServices(context) {
|
|
45
|
-
try {
|
|
46
|
-
return ESLintUtils.getParserServices(context);
|
|
47
|
-
} catch {
|
|
48
|
-
throw new Error("Kerros ESLint rules require typed linting. Configure @typescript-eslint/parser with parserOptions.projectService.");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
41
|
+
const createRule = ESLintUtils.RuleCreator(() => "https://github.com/violetflux/kerros/tree/main/packages/eslint-plugin-kerros/src/lite/rules.ts");
|
|
51
42
|
//#endregion
|
|
52
|
-
//#region src/
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
43
|
+
//#region src/lite/syntax.ts
|
|
44
|
+
const factoryNames = /* @__PURE__ */ new Set(["bindStore", "createStore"]);
|
|
45
|
+
const syntaxToolsCache = /* @__PURE__ */ new WeakMap();
|
|
46
|
+
/** Visit ESTree descendants without following parent links. */
|
|
47
|
+
function visit(node, callback) {
|
|
48
|
+
callback(node);
|
|
49
|
+
for (const key of Object.keys(node)) {
|
|
50
|
+
if (key === "parent" || key === "range" || key === "loc") continue;
|
|
51
|
+
const value = node[key];
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
for (const child of value) if (child && typeof child === "object" && "type" in child) visit(child, callback);
|
|
54
|
+
} else if (value && typeof value === "object" && "type" in value) visit(value, callback);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Read one static member name. */
|
|
58
|
+
function getMemberName(node) {
|
|
59
|
+
if (!node.computed && node.property.type === "Identifier") return node.property.name;
|
|
60
|
+
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
|
|
64
61
|
}
|
|
65
|
-
/**
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
79
|
-
/** Verify that a computed property comes from Kerros' private unique symbol. */
|
|
80
|
-
const isMarkerProperty = (property, marker) => {
|
|
81
|
-
return property.declarations?.some((declaration) => {
|
|
82
|
-
if (!ts.isPropertySignature(declaration) || !ts.isComputedPropertyName(declaration.name)) return false;
|
|
83
|
-
const inputSymbol = checker.getSymbolAtLocation(declaration.name.expression);
|
|
84
|
-
if (!inputSymbol) return false;
|
|
85
|
-
const symbol = resolveSymbol(inputSymbol);
|
|
86
|
-
if (symbol.getName() !== markerNames[marker]) return false;
|
|
87
|
-
return symbol.declarations?.some((markerDeclaration) => {
|
|
88
|
-
if (!ts.isVariableDeclaration(markerDeclaration)) return false;
|
|
89
|
-
return isKerrosSourceFile(markerDeclaration.getSourceFile());
|
|
90
|
-
}) === true;
|
|
91
|
-
}) === true;
|
|
92
|
-
};
|
|
93
|
-
/** Test a TypeScript type for one nominal Kerros marker. */
|
|
94
|
-
const hasMarker = (type, marker) => {
|
|
95
|
-
return type.getProperties().some((property) => isMarkerProperty(property, marker));
|
|
96
|
-
};
|
|
97
|
-
/** Read the payload carried by one nominal Kerros marker. */
|
|
98
|
-
const getMarkerType = (type, marker, location) => {
|
|
99
|
-
const property = type.getProperties().find((candidate) => isMarkerProperty(candidate, marker));
|
|
100
|
-
return property ? checker.getTypeOfSymbolAtLocation(property, location) : void 0;
|
|
101
|
-
};
|
|
102
|
-
/** Resolve an inline, named, or imported model function. */
|
|
103
|
-
const getModelFunction = (input) => {
|
|
104
|
-
const seen = /* @__PURE__ */ new Set();
|
|
105
|
-
/** Follow syntax wrappers and variable aliases to a concrete function body. */
|
|
106
|
-
const resolve = (node) => {
|
|
107
|
-
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return node;
|
|
108
|
-
if (ts.isFunctionDeclaration(node) && node.body) return node;
|
|
109
|
-
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) return resolve(node.expression);
|
|
110
|
-
const symbol = getTsSymbol(node);
|
|
111
|
-
if (!symbol || seen.has(symbol)) return void 0;
|
|
112
|
-
seen.add(symbol);
|
|
113
|
-
for (const declaration of symbol.declarations ?? []) {
|
|
114
|
-
if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
|
|
115
|
-
if (ts.isMethodDeclaration(declaration) && declaration.body) return declaration;
|
|
116
|
-
if (ts.isPropertyAssignment(declaration)) {
|
|
117
|
-
const fn = resolve(declaration.initializer);
|
|
118
|
-
if (fn) return fn;
|
|
119
|
-
}
|
|
120
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
121
|
-
const fn = resolve(declaration.initializer);
|
|
122
|
-
if (fn) return fn;
|
|
123
|
-
}
|
|
62
|
+
/** Build file-local Kerros identities without TypeScript parser services. */
|
|
63
|
+
function createLiteSyntaxTools(context) {
|
|
64
|
+
const cached = syntaxToolsCache.get(context.sourceCode);
|
|
65
|
+
if (cached) return cached;
|
|
66
|
+
const factories = /* @__PURE__ */ new Map();
|
|
67
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
68
|
+
const storeHooks = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const statement of context.sourceCode.ast.body) {
|
|
70
|
+
if (statement.type !== "ImportDeclaration" || statement.source.value !== "@violetflux/kerros") continue;
|
|
71
|
+
for (const specifier of statement.specifiers) {
|
|
72
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
73
|
+
namespaces.add(specifier.local.name);
|
|
74
|
+
continue;
|
|
124
75
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (symbol?.getName() !== name) return false;
|
|
132
|
-
return symbol.declarations?.some((declaration) => {
|
|
133
|
-
return declaration.getSourceFile().fileName.replaceAll("\\", "/").includes("/node_modules/@types/react/");
|
|
134
|
-
}) === true;
|
|
135
|
-
};
|
|
136
|
-
/** Read a tuple member type from a factory return type. */
|
|
137
|
-
const getTupleMemberType = (type, index, location) => {
|
|
138
|
-
const property = checker.getPropertyOfType(type, String(index));
|
|
139
|
-
return property ? checker.getTypeOfSymbolAtLocation(property, location) : void 0;
|
|
140
|
-
};
|
|
141
|
-
/** Classify a call by the nominal markers on its resolved return tuple. */
|
|
142
|
-
const getFactoryKind = (node) => {
|
|
143
|
-
const inputSymbol = checker.getSymbolAtLocation(node.expression);
|
|
144
|
-
if (!inputSymbol) return void 0;
|
|
145
|
-
const symbol = resolveSymbol(inputSymbol);
|
|
146
|
-
const name = symbol.getName();
|
|
147
|
-
if (name !== "createStore" && name !== "bindStore" || !symbol.declarations?.some((declaration) => isKerrosSourceFile(declaration.getSourceFile()))) return;
|
|
148
|
-
const signature = checker.getResolvedSignature(node);
|
|
149
|
-
if (!signature) return void 0;
|
|
150
|
-
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
151
|
-
const hookType = getTupleMemberType(returnType, 0, node);
|
|
152
|
-
if (!hookType || !hasMarker(hookType, "storeHook")) return void 0;
|
|
153
|
-
const thirdType = getTupleMemberType(returnType, 2, node);
|
|
154
|
-
if (name === "createStore") return !thirdType || hasMarker(thirdType, "storeGetter") ? "createStore" : void 0;
|
|
155
|
-
if (name !== "bindStore" || !thirdType) return void 0;
|
|
156
|
-
const providerType = getTupleMemberType(returnType, 1, node);
|
|
157
|
-
return providerType && hasMarker(providerType, "externalStoreProvider") && hasMarker(thirdType, "storeInstanceHook") ? "bindStore" : void 0;
|
|
158
|
-
};
|
|
159
|
-
/** Test whether a call invokes a nominal Kerros Store Hook. */
|
|
160
|
-
const isStoreHookCall = (node) => {
|
|
161
|
-
return hasMarker(checker.getTypeAtLocation(node.expression), "storeHook");
|
|
162
|
-
};
|
|
163
|
-
/** Test whether a call invokes a nominal Kerros Store instance Hook. */
|
|
164
|
-
const isStoreInstanceHookCall = (node) => {
|
|
165
|
-
return hasMarker(checker.getTypeAtLocation(node.expression), "storeInstanceHook");
|
|
166
|
-
};
|
|
167
|
-
return {
|
|
168
|
-
checker,
|
|
169
|
-
getFactoryKind,
|
|
170
|
-
getMarkerType,
|
|
171
|
-
getModelFunction,
|
|
172
|
-
getTsSymbol,
|
|
173
|
-
hasMarker,
|
|
174
|
-
isReactCall,
|
|
175
|
-
isStoreInstanceHookCall,
|
|
176
|
-
isStoreHookCall,
|
|
177
|
-
resolveSymbol
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
/** Create ESTree adapters for the Kerros identity checks in one rule context. */
|
|
181
|
-
function createKerrosTypeTools(context) {
|
|
182
|
-
const services = getTypeServices(context);
|
|
183
|
-
const programTools = createKerrosProgramTools(services.program);
|
|
184
|
-
const { checker, getTsSymbol, resolveSymbol } = programTools;
|
|
185
|
-
/** Read an ESTree node's TypeScript type. */
|
|
186
|
-
const getType = (node) => {
|
|
187
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
188
|
-
return checker.getTypeAtLocation(tsNode);
|
|
189
|
-
};
|
|
190
|
-
/** Read the TypeScript node corresponding to an ESTree node. */
|
|
191
|
-
const getTsNode = (node) => {
|
|
192
|
-
return services.esTreeNodeToTSNodeMap.get(node);
|
|
193
|
-
};
|
|
194
|
-
/** Classify an ESTree call through the shared Program identity checks. */
|
|
76
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
77
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
78
|
+
if (factoryNames.has(imported)) factories.set(specifier.local.name, imported);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Classify direct and namespace Kerros factory calls. */
|
|
195
82
|
const getFactoryKind = (node) => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
83
|
+
const callee = unwrapExpression(node.callee);
|
|
84
|
+
if (callee.type === "Identifier") return factories.get(callee.name);
|
|
85
|
+
if (callee.type !== "MemberExpression" || callee.object.type !== "Identifier" || !namespaces.has(callee.object.name)) return;
|
|
86
|
+
const name = getMemberName(callee);
|
|
87
|
+
return name && factoryNames.has(name) ? name : void 0;
|
|
88
|
+
};
|
|
89
|
+
if (factories.size === 0 && namespaces.size === 0) {
|
|
90
|
+
const tools = {
|
|
91
|
+
getFactoryKind,
|
|
92
|
+
isStoreHookCall: () => false
|
|
93
|
+
};
|
|
94
|
+
syntaxToolsCache.set(context.sourceCode, tools);
|
|
95
|
+
return tools;
|
|
96
|
+
}
|
|
97
|
+
visit(context.sourceCode.ast, (node) => {
|
|
98
|
+
if (node.type !== "CallExpression" || !getFactoryKind(node)) return;
|
|
99
|
+
const expression = unwrapExpression(node);
|
|
100
|
+
const declarator = expression.parent;
|
|
101
|
+
if (declarator?.type !== "VariableDeclarator" || declarator.init !== expression || declarator.id.type !== "ArrayPattern") return;
|
|
102
|
+
const hook = declarator.id.elements[0];
|
|
103
|
+
if (hook?.type === "Identifier") storeHooks.add(hook.name);
|
|
104
|
+
});
|
|
105
|
+
/** Test whether a call targets a Store Hook created in the current file. */
|
|
204
106
|
const isStoreHookCall = (node) => {
|
|
205
|
-
const
|
|
206
|
-
return
|
|
207
|
-
};
|
|
208
|
-
/** Test whether an ESTree call invokes a nominal Kerros Store instance Hook. */
|
|
209
|
-
const isStoreInstanceHookCall = (node) => {
|
|
210
|
-
const tsNode = getTsNode(node);
|
|
211
|
-
return ts.isCallExpression(tsNode) && programTools.isStoreInstanceHookCall(tsNode);
|
|
212
|
-
};
|
|
213
|
-
/** Resolve an identifier to its non-alias TypeScript symbol. */
|
|
214
|
-
const getIdentifierSymbol = (node) => {
|
|
215
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
216
|
-
const symbol = (ts.isIdentifier(tsNode) && ts.isShorthandPropertyAssignment(tsNode.parent) ? checker.getShorthandAssignmentValueSymbol(tsNode.parent) : void 0) ?? checker.getSymbolAtLocation(tsNode);
|
|
217
|
-
return symbol ? resolveSymbol(symbol) : void 0;
|
|
107
|
+
const callee = unwrapExpression(node.callee);
|
|
108
|
+
return callee.type === "Identifier" && storeHooks.has(callee.name);
|
|
218
109
|
};
|
|
219
|
-
|
|
220
|
-
checker,
|
|
110
|
+
const tools = {
|
|
221
111
|
getFactoryKind,
|
|
222
|
-
|
|
223
|
-
getMarkerType: programTools.getMarkerType,
|
|
224
|
-
getModelFunction,
|
|
225
|
-
getTsNode,
|
|
226
|
-
getTsSymbol,
|
|
227
|
-
getType,
|
|
228
|
-
hasMarker: programTools.hasMarker,
|
|
229
|
-
isReactCall: programTools.isReactCall,
|
|
230
|
-
isStoreInstanceHookCall,
|
|
231
|
-
isStoreHookCall,
|
|
232
|
-
services
|
|
112
|
+
isStoreHookCall
|
|
233
113
|
};
|
|
114
|
+
syntaxToolsCache.set(context.sourceCode, tools);
|
|
115
|
+
return tools;
|
|
234
116
|
}
|
|
235
|
-
/** Test whether a declaration is owned directly by a source file. */
|
|
236
|
-
function isModuleDeclaration(declaration) {
|
|
237
|
-
let current = declaration;
|
|
238
|
-
while (current.parent && !ts.isSourceFile(current.parent)) {
|
|
239
|
-
if (ts.isFunctionLike(current.parent) || ts.isBlock(current.parent)) return false;
|
|
240
|
-
current = current.parent;
|
|
241
|
-
}
|
|
242
|
-
return ts.isSourceFile(current.parent);
|
|
243
|
-
}
|
|
244
|
-
/** Read a type's visible property across unions and generic constraints. */
|
|
245
|
-
function getTypeProperty(checker, type, name) {
|
|
246
|
-
const direct = checker.getPropertyOfType(type, name);
|
|
247
|
-
if (direct) return direct;
|
|
248
|
-
if (type.isUnion()) for (const member of type.types) {
|
|
249
|
-
const property = getTypeProperty(checker, member, name);
|
|
250
|
-
if (property) return property;
|
|
251
|
-
}
|
|
252
|
-
const constraint = checker.getBaseConstraintOfType(type);
|
|
253
|
-
return constraint && constraint !== type ? getTypeProperty(checker, constraint, name) : void 0;
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/rules/binding-naming.ts
|
|
257
|
-
/** Extract the model-derived Store name used by createStore bindings. */
|
|
258
|
-
function getCreateStoreName(call) {
|
|
259
|
-
const model = call.arguments[0];
|
|
260
|
-
if (!model || model.type !== "Identifier") return void 0;
|
|
261
|
-
return /^use(?<name>[A-Z][A-Za-z0-9]*)Model$/u.exec(model.name)?.groups?.name;
|
|
262
|
-
}
|
|
263
|
-
/** Extract the explicit display name used by bindStore bindings. */
|
|
264
|
-
function getBindStoreName(call) {
|
|
265
|
-
const input = call.arguments[0];
|
|
266
|
-
return input?.type === "Literal" && typeof input.value === "string" ? input.value : void 0;
|
|
267
|
-
}
|
|
268
|
-
/** Read one identifier from a binding tuple, leaving holes untouched. */
|
|
269
|
-
function getElement(pattern, index) {
|
|
270
|
-
const element = pattern.elements[index];
|
|
271
|
-
return element?.type === "Identifier" ? element : void 0;
|
|
272
|
-
}
|
|
273
|
-
const bindingNaming = createRule({
|
|
274
|
-
name: "binding-naming",
|
|
275
|
-
meta: {
|
|
276
|
-
type: "problem",
|
|
277
|
-
docs: { description: "Keep Kerros Hook, Provider, and getter binding names aligned." },
|
|
278
|
-
schema: [],
|
|
279
|
-
messages: {
|
|
280
|
-
destructureBinding: "Kerros factory results must be destructured.",
|
|
281
|
-
getterName: "The Store getter must be named getXxx.",
|
|
282
|
-
hookName: "The Store Hook must be named useXxx.",
|
|
283
|
-
providerName: "The Provider name must match the Store Hook.",
|
|
284
|
-
instanceName: "The instance Hook name must match the Store Hook."
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
defaultOptions: [],
|
|
288
|
-
create(context) {
|
|
289
|
-
const { getFactoryKind } = createKerrosTypeTools(context);
|
|
290
|
-
return { CallExpression(node) {
|
|
291
|
-
const kind = getFactoryKind(node);
|
|
292
|
-
if (!kind) return;
|
|
293
|
-
const expression = unwrapExpression(node);
|
|
294
|
-
const declarator = expression.parent;
|
|
295
|
-
if (declarator?.type !== "VariableDeclarator" || declarator.init !== expression) {
|
|
296
|
-
context.report({
|
|
297
|
-
node,
|
|
298
|
-
messageId: "destructureBinding"
|
|
299
|
-
});
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
if (declarator.id.type !== "ArrayPattern") {
|
|
303
|
-
context.report({
|
|
304
|
-
node: declarator.id,
|
|
305
|
-
messageId: "destructureBinding"
|
|
306
|
-
});
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const hook = getElement(declarator.id, 0);
|
|
310
|
-
const provider = getElement(declarator.id, 1);
|
|
311
|
-
const third = getElement(declarator.id, 2);
|
|
312
|
-
const explicitName = kind === "createStore" ? getCreateStoreName(node) : getBindStoreName(node);
|
|
313
|
-
const hookMatch = hook && /^use(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(hook.name);
|
|
314
|
-
const providerMatch = provider && /^(?<name>[A-Z][A-Za-z0-9]*)Provider$/u.exec(provider.name);
|
|
315
|
-
const thirdMatch = third && (kind === "createStore" ? /^get(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(third.name) : /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(third.name));
|
|
316
|
-
const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? thirdMatch?.groups?.name;
|
|
317
|
-
const name = explicitName ?? inferredName;
|
|
318
|
-
if (hook && (!hookMatch || name && hookMatch.groups?.name !== name)) context.report({
|
|
319
|
-
node: hook,
|
|
320
|
-
messageId: "hookName"
|
|
321
|
-
});
|
|
322
|
-
if (provider && (!providerMatch || name && providerMatch.groups?.name !== name)) context.report({
|
|
323
|
-
node: provider,
|
|
324
|
-
messageId: "providerName"
|
|
325
|
-
});
|
|
326
|
-
if (third && (!thirdMatch || name && thirdMatch.groups?.name !== name)) context.report({
|
|
327
|
-
node: third,
|
|
328
|
-
messageId: kind === "createStore" ? "getterName" : "instanceName"
|
|
329
|
-
});
|
|
330
|
-
} };
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
117
|
//#endregion
|
|
334
|
-
//#region src/rules
|
|
118
|
+
//#region src/lite/rules.ts
|
|
335
119
|
const nestedScopeTypes = /* @__PURE__ */ new Set([
|
|
336
120
|
"ArrowFunctionExpression",
|
|
337
121
|
"BlockStatement",
|
|
@@ -352,6 +136,12 @@ const nestedScopeTypes = /* @__PURE__ */ new Set([
|
|
|
352
136
|
"TryStatement",
|
|
353
137
|
"WhileStatement"
|
|
354
138
|
]);
|
|
139
|
+
const objectEnumerationMethods = /* @__PURE__ */ new Set([
|
|
140
|
+
"entries",
|
|
141
|
+
"keys",
|
|
142
|
+
"values"
|
|
143
|
+
]);
|
|
144
|
+
const modelNamePattern = /^use[A-Z][A-Za-z0-9]*Model$/u;
|
|
355
145
|
/** Test whether a factory call executes unconditionally at module scope. */
|
|
356
146
|
function isModuleScopeCall(node) {
|
|
357
147
|
let current = node.parent;
|
|
@@ -361,17 +151,43 @@ function isModuleScopeCall(node) {
|
|
|
361
151
|
}
|
|
362
152
|
return current?.type === "Program";
|
|
363
153
|
}
|
|
364
|
-
|
|
154
|
+
/** Read one identifier from a factory tuple. */
|
|
155
|
+
function getElement(pattern, index) {
|
|
156
|
+
const element = pattern.elements[index];
|
|
157
|
+
return element?.type === "Identifier" ? element : void 0;
|
|
158
|
+
}
|
|
159
|
+
/** Extract the Store name carried by a createStore model. */
|
|
160
|
+
function getCreateStoreName(call) {
|
|
161
|
+
const model = call.arguments[0];
|
|
162
|
+
if (!model || model.type !== "Identifier") return void 0;
|
|
163
|
+
return /^use(?<name>[A-Z][A-Za-z0-9]*)Model$/u.exec(model.name)?.groups?.name;
|
|
164
|
+
}
|
|
165
|
+
/** Extract the explicit bindStore display name. */
|
|
166
|
+
function getBindStoreName(call) {
|
|
167
|
+
const input = call.arguments[0];
|
|
168
|
+
return input?.type === "Literal" && typeof input.value === "string" ? input.value : void 0;
|
|
169
|
+
}
|
|
170
|
+
/** Resolve whether one identifier is declared in module scope. */
|
|
171
|
+
function isModuleBinding(context, node) {
|
|
172
|
+
let scope = context.sourceCode.getScope(node);
|
|
173
|
+
while (scope) {
|
|
174
|
+
const variable = scope.set.get(node.name);
|
|
175
|
+
if (variable) return variable.scope.type === "module";
|
|
176
|
+
scope = scope.upper;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const liteFactoryAtModuleScope = createRule({
|
|
365
181
|
name: "factory-at-module-scope",
|
|
366
182
|
meta: {
|
|
367
183
|
type: "problem",
|
|
368
|
-
docs: { description: "Require Kerros factories to run once at module scope." },
|
|
184
|
+
docs: { description: "Require directly imported Kerros factories to run once at module scope." },
|
|
369
185
|
schema: [],
|
|
370
186
|
messages: { moduleScope: "Kerros factories must be called at module scope." }
|
|
371
187
|
},
|
|
372
188
|
defaultOptions: [],
|
|
373
189
|
create(context) {
|
|
374
|
-
const { getFactoryKind } =
|
|
190
|
+
const { getFactoryKind } = createLiteSyntaxTools(context);
|
|
375
191
|
return { CallExpression(node) {
|
|
376
192
|
if (getFactoryKind(node) && !isModuleScopeCall(node)) context.report({
|
|
377
193
|
node,
|
|
@@ -380,14 +196,11 @@ const factoryAtModuleScope = createRule({
|
|
|
380
196
|
} };
|
|
381
197
|
}
|
|
382
198
|
});
|
|
383
|
-
|
|
384
|
-
//#region src/rules/model-convention.ts
|
|
385
|
-
const modelNamePattern = /^use[A-Z][A-Za-z0-9]*Model$/u;
|
|
386
|
-
const modelConvention = createRule({
|
|
199
|
+
const liteModelConvention = createRule({
|
|
387
200
|
name: "model-convention",
|
|
388
201
|
meta: {
|
|
389
202
|
type: "problem",
|
|
390
|
-
docs: { description: "Require createStore models to be named module-level Hooks." },
|
|
203
|
+
docs: { description: "Require direct createStore models to be named module-level Hooks." },
|
|
391
204
|
schema: [],
|
|
392
205
|
messages: {
|
|
393
206
|
anonymousModel: "createStore requires a named model Hook.",
|
|
@@ -397,7 +210,7 @@ const modelConvention = createRule({
|
|
|
397
210
|
},
|
|
398
211
|
defaultOptions: [],
|
|
399
212
|
create(context) {
|
|
400
|
-
const { getFactoryKind
|
|
213
|
+
const { getFactoryKind } = createLiteSyntaxTools(context);
|
|
401
214
|
return { CallExpression(node) {
|
|
402
215
|
if (getFactoryKind(node) !== "createStore") return;
|
|
403
216
|
const model = node.arguments[0];
|
|
@@ -413,2140 +226,224 @@ const modelConvention = createRule({
|
|
|
413
226
|
node: model,
|
|
414
227
|
messageId: "modelName"
|
|
415
228
|
});
|
|
416
|
-
|
|
417
|
-
if (symbol && symbol.declarations?.every((declaration) => !isModuleDeclaration(declaration))) context.report({
|
|
229
|
+
if (!isModuleBinding(context, model)) context.report({
|
|
418
230
|
node: model,
|
|
419
231
|
messageId: "moduleModel"
|
|
420
232
|
});
|
|
421
233
|
} };
|
|
422
234
|
}
|
|
423
235
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
]);
|
|
437
|
-
const mapMutationMethods = /* @__PURE__ */ new Set([
|
|
438
|
-
"clear",
|
|
439
|
-
"delete",
|
|
440
|
-
"set"
|
|
441
|
-
]);
|
|
442
|
-
const setMutationMethods = /* @__PURE__ */ new Set([
|
|
443
|
-
"add",
|
|
444
|
-
"clear",
|
|
445
|
-
"delete"
|
|
446
|
-
]);
|
|
447
|
-
/** Build a reusable index that streams dynamic call-site contexts without retaining every path. */
|
|
448
|
-
function createFunctionCallSiteContexts(edges) {
|
|
449
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
450
|
-
for (const edge of edges) {
|
|
451
|
-
const existing = incoming.get(edge.callee) ?? [];
|
|
452
|
-
existing.push(edge);
|
|
453
|
-
incoming.set(edge.callee, existing);
|
|
454
|
-
}
|
|
455
|
-
/** Visit paths independently while retaining only the current DFS path. */
|
|
456
|
-
const some = (target, predicate) => {
|
|
457
|
-
const calls = /* @__PURE__ */ new Map();
|
|
458
|
-
const active = /* @__PURE__ */ new Set([target]);
|
|
459
|
-
const frames = [{
|
|
460
|
-
advanced: false,
|
|
461
|
-
edges: incoming.get(target) ?? [],
|
|
462
|
-
fn: target,
|
|
463
|
-
index: 0
|
|
464
|
-
}];
|
|
465
|
-
while (frames.length > 0) {
|
|
466
|
-
const frame = frames.at(-1);
|
|
467
|
-
if (!frame) break;
|
|
468
|
-
const edge = frame.edges[frame.index];
|
|
469
|
-
if (edge) {
|
|
470
|
-
frame.index += 1;
|
|
471
|
-
if (active.has(edge.caller)) continue;
|
|
472
|
-
frame.advanced = true;
|
|
473
|
-
calls.set(frame.fn, edge.site);
|
|
474
|
-
active.add(edge.caller);
|
|
475
|
-
frames.push({
|
|
476
|
-
advanced: false,
|
|
477
|
-
edges: incoming.get(edge.caller) ?? [],
|
|
478
|
-
fn: edge.caller,
|
|
479
|
-
index: 0,
|
|
480
|
-
parent: frame.fn
|
|
481
|
-
});
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (!frame.advanced && predicate(calls)) return true;
|
|
485
|
-
frames.pop();
|
|
486
|
-
active.delete(frame.fn);
|
|
487
|
-
if (frame.parent) calls.delete(frame.parent);
|
|
488
|
-
}
|
|
489
|
-
return false;
|
|
490
|
-
};
|
|
491
|
-
return { some };
|
|
492
|
-
}
|
|
493
|
-
/** Track assignment sources and resolve definitions that reach a concrete reference point. */
|
|
494
|
-
function createReferenceOriginTracker(program) {
|
|
495
|
-
const events = /* @__PURE__ */ new Map();
|
|
496
|
-
/** Find the function execution scope containing one syntax node. */
|
|
497
|
-
const getOwner = (input) => {
|
|
498
|
-
let node = input.parent;
|
|
499
|
-
while (node) {
|
|
500
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
|
|
501
|
-
node = node.parent;
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
/** Test whether one syntax range contains another. */
|
|
505
|
-
const contains = (container, target) => {
|
|
506
|
-
return container.range[0] <= target.range[0] && container.range[1] >= target.range[1];
|
|
507
|
-
};
|
|
508
|
-
/** Merge branch states without duplicating the same reaching write. */
|
|
509
|
-
const merge = (left, right) => {
|
|
510
|
-
return [.../* @__PURE__ */ new Set([...left, ...right])];
|
|
511
|
-
};
|
|
512
|
-
/** Test whether a simple statement cannot continue into its following sibling. */
|
|
513
|
-
const terminates = (node) => {
|
|
514
|
-
if (node.type === "ReturnStatement" || node.type === "ThrowStatement") return true;
|
|
515
|
-
if (node.type === "BlockStatement") {
|
|
516
|
-
const last = node.body.at(-1);
|
|
517
|
-
return last ? terminates(last) : false;
|
|
518
|
-
}
|
|
519
|
-
if (node.type === "IfStatement" && node.alternate) return terminates(node.consequent) && terminates(node.alternate);
|
|
520
|
-
if (node.type === "LabeledStatement") return terminates(node.body);
|
|
521
|
-
return false;
|
|
522
|
-
};
|
|
523
|
-
/** Record one initializer or assignment after its right-hand side is evaluated. */
|
|
524
|
-
const record = (symbol, source, write) => {
|
|
525
|
-
const existing = events.get(symbol) ?? [];
|
|
526
|
-
existing.push({
|
|
527
|
-
owner: getOwner(write),
|
|
528
|
-
source,
|
|
529
|
-
write
|
|
530
|
-
});
|
|
531
|
-
events.set(symbol, existing);
|
|
532
|
-
};
|
|
533
|
-
/** Resolve the possible definitions reaching one symbol reference. */
|
|
534
|
-
const resolve = (symbol, reference, calls) => {
|
|
535
|
-
const symbolEvents = events.get(symbol) ?? [];
|
|
536
|
-
let functions = [];
|
|
537
|
-
let parent = reference.parent;
|
|
538
|
-
while (parent) {
|
|
539
|
-
if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") functions.push(parent);
|
|
540
|
-
parent = parent.parent;
|
|
236
|
+
const liteBindingNaming = createRule({
|
|
237
|
+
name: "binding-naming",
|
|
238
|
+
meta: {
|
|
239
|
+
type: "problem",
|
|
240
|
+
docs: { description: "Keep direct Kerros factory binding names aligned." },
|
|
241
|
+
schema: [],
|
|
242
|
+
messages: {
|
|
243
|
+
destructureBinding: "Kerros factory results must be destructured.",
|
|
244
|
+
getterName: "The Store getter must be named getXxx.",
|
|
245
|
+
hookName: "The Store Hook must be named useXxx.",
|
|
246
|
+
providerName: "The Provider name must match the Store Hook.",
|
|
247
|
+
instanceName: "The instance Hook name must match the Store Hook."
|
|
541
248
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
249
|
+
},
|
|
250
|
+
defaultOptions: [],
|
|
251
|
+
create(context) {
|
|
252
|
+
const { getFactoryKind } = createLiteSyntaxTools(context);
|
|
253
|
+
return { CallExpression(node) {
|
|
254
|
+
const kind = getFactoryKind(node);
|
|
255
|
+
if (!kind) return;
|
|
256
|
+
const expression = unwrapExpression(node);
|
|
257
|
+
const declarator = expression.parent;
|
|
258
|
+
if (declarator?.type !== "VariableDeclarator" || declarator.init !== expression || declarator.id.type !== "ArrayPattern") {
|
|
259
|
+
context.report({
|
|
260
|
+
node,
|
|
261
|
+
messageId: "destructureBinding"
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
552
264
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
265
|
+
const hook = getElement(declarator.id, 0);
|
|
266
|
+
const provider = getElement(declarator.id, 1);
|
|
267
|
+
const third = getElement(declarator.id, 2);
|
|
268
|
+
const explicitName = kind === "createStore" ? getCreateStoreName(node) : getBindStoreName(node);
|
|
269
|
+
const hookMatch = hook && /^use(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(hook.name);
|
|
270
|
+
const providerMatch = provider && /^(?<name>[A-Z][A-Za-z0-9]*)Provider$/u.exec(provider.name);
|
|
271
|
+
const thirdMatch = third && (kind === "createStore" ? /^get(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(third.name) : /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(third.name));
|
|
272
|
+
const name = explicitName ?? hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? thirdMatch?.groups?.name;
|
|
273
|
+
if (hook && (!hookMatch || name && hookMatch.groups?.name !== name)) context.report({
|
|
274
|
+
node: hook,
|
|
275
|
+
messageId: "hookName"
|
|
561
276
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
277
|
+
if (provider && (!providerMatch || name && providerMatch.groups?.name !== name)) context.report({
|
|
278
|
+
node: provider,
|
|
279
|
+
messageId: "providerName"
|
|
280
|
+
});
|
|
281
|
+
if (third && (!thirdMatch || name && thirdMatch.groups?.name !== name)) context.report({
|
|
282
|
+
node: third,
|
|
283
|
+
messageId: kind === "createStore" ? "getterName" : "instanceName"
|
|
284
|
+
});
|
|
285
|
+
} };
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const liteSelectorParameterName = createRule({
|
|
289
|
+
name: "selector-parameter-name",
|
|
290
|
+
meta: {
|
|
291
|
+
type: "suggestion",
|
|
292
|
+
docs: { description: "Use s for selectors on file-local Kerros Store Hooks." },
|
|
293
|
+
schema: [],
|
|
294
|
+
messages: { parameterName: "Name the Store selector parameter s." }
|
|
295
|
+
},
|
|
296
|
+
defaultOptions: [],
|
|
297
|
+
create(context) {
|
|
298
|
+
const { isStoreHookCall } = createLiteSyntaxTools(context);
|
|
299
|
+
return { CallExpression(node) {
|
|
300
|
+
if (!isStoreHookCall(node)) return;
|
|
301
|
+
const selector = node.arguments[0];
|
|
302
|
+
if (!selector || selector.type === "SpreadElement" || selector.type !== "ArrowFunctionExpression" && selector.type !== "FunctionExpression") return;
|
|
303
|
+
const parameter = selector.params[0];
|
|
304
|
+
if (parameter && (parameter.type !== "Identifier" || parameter.name !== "s")) context.report({
|
|
305
|
+
node: parameter,
|
|
306
|
+
messageId: "parameterName"
|
|
307
|
+
});
|
|
308
|
+
} };
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
/** Collect direct local aliases of the selector parameter. */
|
|
312
|
+
function getAliases(selector, parameter) {
|
|
313
|
+
const aliases = /* @__PURE__ */ new Set([parameter]);
|
|
314
|
+
if (selector.body.type !== "BlockStatement") return aliases;
|
|
315
|
+
let changed = true;
|
|
316
|
+
while (changed) {
|
|
317
|
+
changed = false;
|
|
318
|
+
for (const statement of selector.body.body) {
|
|
319
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
320
|
+
for (const declaration of statement.declarations) {
|
|
321
|
+
if (declaration.id.type !== "Identifier" || !declaration.init) continue;
|
|
322
|
+
const value = unwrapExpression(declaration.init);
|
|
323
|
+
if (value.type === "Identifier" && aliases.has(value.name) && !aliases.has(declaration.id.name)) {
|
|
324
|
+
aliases.add(declaration.id.name);
|
|
325
|
+
changed = true;
|
|
326
|
+
}
|
|
595
327
|
}
|
|
596
|
-
return state;
|
|
597
|
-
}
|
|
598
|
-
/** Evaluate one program or function scope up to a nested function/reference. */
|
|
599
|
-
const flowScope = (root, target, state) => {
|
|
600
|
-
const owner = root.type === "Program" ? void 0 : root;
|
|
601
|
-
if (root.type === "Program") return flowSequence(root.body, owner, state, target);
|
|
602
|
-
return root.body.type === "BlockStatement" ? flowSequence(root.body.body, owner, state, target) : flowUntil(root.body, owner, state, target);
|
|
603
|
-
};
|
|
604
|
-
let state = [];
|
|
605
|
-
let root = program;
|
|
606
|
-
for (const fn of functions) {
|
|
607
|
-
state = flowScope(root, calls?.get(fn) ?? fn, state);
|
|
608
|
-
root = fn;
|
|
609
|
-
}
|
|
610
|
-
state = flowScope(root, reference, state);
|
|
611
|
-
return state.map((event) => event.source);
|
|
612
|
-
};
|
|
613
|
-
return {
|
|
614
|
-
record,
|
|
615
|
-
resolve
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
/** Return an inline selector from a nominal Store Hook call. */
|
|
619
|
-
function getInlineSelector(node, isStoreHookCall) {
|
|
620
|
-
if (!isStoreHookCall(node)) return void 0;
|
|
621
|
-
const selector = node.arguments[0];
|
|
622
|
-
return selector?.type === "ArrowFunctionExpression" || selector?.type === "FunctionExpression" ? selector : void 0;
|
|
623
|
-
}
|
|
624
|
-
/** Visit a syntax subtree while ignoring parser metadata and optional nested functions. */
|
|
625
|
-
function visitSubtree(root, visitor, skipNestedFunctions = false) {
|
|
626
|
-
const visit = (node) => {
|
|
627
|
-
if (node !== root && skipNestedFunctions && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration")) return;
|
|
628
|
-
visitor(node);
|
|
629
|
-
for (const key of Object.keys(node)) {
|
|
630
|
-
if (key === "parent" || key === "range" || key === "loc") continue;
|
|
631
|
-
const value = node[key];
|
|
632
|
-
if (Array.isArray(value)) {
|
|
633
|
-
for (const child of value) if (child && typeof child === "object" && "type" in child) visit(child);
|
|
634
|
-
} else if (value && typeof value === "object" && "type" in value) visit(value);
|
|
635
|
-
}
|
|
636
|
-
};
|
|
637
|
-
visit(root);
|
|
638
|
-
}
|
|
639
|
-
/** Return the statically known property name for member access. */
|
|
640
|
-
function getMemberName(node) {
|
|
641
|
-
if (!node.computed && node.property.type === "Identifier") return node.property.name;
|
|
642
|
-
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
|
|
643
|
-
}
|
|
644
|
-
/** Classify runtime built-ins by TypeScript's default-library declarations. */
|
|
645
|
-
function getBuiltinTypeKind(checker, program, inputType) {
|
|
646
|
-
const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
|
|
647
|
-
if (type.isUnion() || type.isIntersection()) {
|
|
648
|
-
for (const member of type.types) {
|
|
649
|
-
const kind = getBuiltinTypeKind(checker, program, member);
|
|
650
|
-
if (kind) return kind;
|
|
651
328
|
}
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (checker.isArrayType(type) || checker.isTupleType(type)) return "array";
|
|
655
|
-
if ((type.flags & ts.TypeFlags.StringLike) !== 0) return "string";
|
|
656
|
-
for (const symbol of [type.getSymbol(), type.aliasSymbol]) {
|
|
657
|
-
const name = symbol?.getName();
|
|
658
|
-
if (!(symbol?.declarations?.some((declaration) => {
|
|
659
|
-
return program.isSourceFileDefaultLibrary(declaration.getSourceFile());
|
|
660
|
-
}) === true)) continue;
|
|
661
|
-
if (name === "Map" || name === "ReadonlyMap") return "map";
|
|
662
|
-
if (name === "Set" || name === "ReadonlySet") return "set";
|
|
663
|
-
if (name === "String") return "string";
|
|
664
329
|
}
|
|
330
|
+
return aliases;
|
|
665
331
|
}
|
|
666
|
-
/** Test whether
|
|
667
|
-
function
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
/** Test whether TypeScript proves a value is primitive across unions and constraints. */
|
|
679
|
-
function isPrimitiveType(checker, inputType) {
|
|
680
|
-
const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
|
|
681
|
-
if (type.isUnion()) return type.types.every((member) => isPrimitiveType(checker, member));
|
|
682
|
-
if (type.isIntersection()) return type.types.some((member) => isPrimitiveType(checker, member));
|
|
683
|
-
const primitiveFlags = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Never;
|
|
684
|
-
return (type.flags & primitiveFlags) !== 0;
|
|
685
|
-
}
|
|
686
|
-
//#endregion
|
|
687
|
-
//#region src/rules/no-broad-store-access.ts
|
|
688
|
-
const objectEnumerationMethods = /* @__PURE__ */ new Set([
|
|
689
|
-
"entries",
|
|
690
|
-
"keys",
|
|
691
|
-
"values"
|
|
692
|
-
]);
|
|
693
|
-
/** Read the tracked value argument from a broad enumeration or serialization call. */
|
|
694
|
-
function getBroadArgument(node) {
|
|
695
|
-
const [argument] = node.arguments;
|
|
696
|
-
if (!argument || argument.type === "SpreadElement") return false;
|
|
697
|
-
const { callee } = node;
|
|
698
|
-
if (callee.type !== "MemberExpression" || callee.computed) return;
|
|
699
|
-
if (callee.object.type === "Identifier" && callee.property.type === "Identifier") {
|
|
700
|
-
if (callee.object.name === "Object" && objectEnumerationMethods.has(callee.property.name)) return argument;
|
|
701
|
-
if (callee.object.name === "JSON" && callee.property.name === "stringify") return argument;
|
|
332
|
+
/** Test whether an expression returns or embeds a complete selector parameter. */
|
|
333
|
+
function containsWholeStore(node, aliases) {
|
|
334
|
+
const expression = unwrapExpression(node);
|
|
335
|
+
if (expression.type === "Identifier") return aliases.has(expression.name);
|
|
336
|
+
if (expression.type === "ObjectExpression") return expression.properties.some((property) => property.type === "SpreadElement" ? containsWholeStore(property.argument, aliases) : containsWholeStore(property.value, aliases));
|
|
337
|
+
if (expression.type === "ArrayExpression") return expression.elements.some((element) => element?.type === "SpreadElement" ? containsWholeStore(element.argument, aliases) : element ? containsWholeStore(element, aliases) : false);
|
|
338
|
+
if (expression.type === "ConditionalExpression") return containsWholeStore(expression.consequent, aliases) || containsWholeStore(expression.alternate, aliases);
|
|
339
|
+
if (expression.type === "LogicalExpression") return containsWholeStore(expression.left, aliases) || containsWholeStore(expression.right, aliases);
|
|
340
|
+
if (expression.type === "SequenceExpression") {
|
|
341
|
+
const result = expression.expressions.at(-1);
|
|
342
|
+
return result ? containsWholeStore(result, aliases) : false;
|
|
702
343
|
}
|
|
344
|
+
if (expression.type === "AssignmentExpression") return containsWholeStore(expression.right, aliases);
|
|
345
|
+
return false;
|
|
703
346
|
}
|
|
704
|
-
const
|
|
705
|
-
name: "no-
|
|
347
|
+
const liteNoWholeStoreSelector = createRule({
|
|
348
|
+
name: "no-whole-store-selector",
|
|
706
349
|
meta: {
|
|
707
350
|
type: "problem",
|
|
708
|
-
docs: { description: "Prevent
|
|
709
|
-
schema: [{
|
|
710
|
-
type: "object",
|
|
711
|
-
properties: {
|
|
712
|
-
includeObjectFields: { type: "boolean" },
|
|
713
|
-
includeStoreModels: { type: "boolean" }
|
|
714
|
-
},
|
|
715
|
-
additionalProperties: false
|
|
716
|
-
}],
|
|
717
|
-
messages: {
|
|
718
|
-
broadAccess: "Do not enumerate, serialize, or spread a complete selector-free Store snapshot.",
|
|
719
|
-
broadObjectField: "Do not enumerate, serialize, or spread an object field from a selector-free Store snapshot."
|
|
720
|
-
}
|
|
721
|
-
},
|
|
722
|
-
defaultOptions: [{
|
|
723
|
-
includeObjectFields: false,
|
|
724
|
-
includeStoreModels: false
|
|
725
|
-
}],
|
|
726
|
-
create(context, [options]) {
|
|
727
|
-
const { getFactoryKind, getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
|
|
728
|
-
const origins = createReferenceOriginTracker(context.sourceCode.ast);
|
|
729
|
-
const storeModels = /* @__PURE__ */ new Set();
|
|
730
|
-
const pendingReports = [];
|
|
731
|
-
/** Test whether an expression belongs to a function registered as a createStore model. */
|
|
732
|
-
const isInsideStoreModel = (expression) => {
|
|
733
|
-
let node = expression.parent;
|
|
734
|
-
while (node) {
|
|
735
|
-
if (node.type === "FunctionDeclaration" && node.id) {
|
|
736
|
-
const symbol = getIdentifierSymbol(node.id);
|
|
737
|
-
return symbol ? storeModels.has(symbol) : false;
|
|
738
|
-
}
|
|
739
|
-
if ((node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") && node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier") {
|
|
740
|
-
const symbol = getIdentifierSymbol(node.parent.id);
|
|
741
|
-
return symbol ? storeModels.has(symbol) : false;
|
|
742
|
-
}
|
|
743
|
-
node = node.parent;
|
|
744
|
-
}
|
|
745
|
-
return false;
|
|
746
|
-
};
|
|
747
|
-
/** Classify whether an expression is a complete Store snapshot or one object field from it. */
|
|
748
|
-
const readStoreOrigin = (input, seen = /* @__PURE__ */ new Set()) => {
|
|
749
|
-
const node = unwrapExpression(input);
|
|
750
|
-
if (node.type === "CallExpression") {
|
|
751
|
-
const selector = node.arguments[0];
|
|
752
|
-
return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & ts.TypeFlags.Undefined) !== 0) && isStoreHookCall(node) ? "snapshot" : void 0;
|
|
753
|
-
}
|
|
754
|
-
if (node.type === "AssignmentExpression") return readStoreOrigin(node.right, seen);
|
|
755
|
-
if (node.type === "MemberExpression") return readStoreOrigin(node.object, seen) ? "objectField" : void 0;
|
|
756
|
-
if (node.type !== "Identifier") return;
|
|
757
|
-
const symbol = getIdentifierSymbol(node);
|
|
758
|
-
if (!symbol || seen.has(symbol)) return;
|
|
759
|
-
seen.add(symbol);
|
|
760
|
-
let result;
|
|
761
|
-
for (const source of origins.resolve(symbol, node)) {
|
|
762
|
-
const origin = readStoreOrigin(source.expression, seen);
|
|
763
|
-
if (!origin) continue;
|
|
764
|
-
if (!source.objectField && origin === "snapshot") {
|
|
765
|
-
result = "snapshot";
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
result = "objectField";
|
|
769
|
-
}
|
|
770
|
-
seen.delete(symbol);
|
|
771
|
-
return result;
|
|
772
|
-
};
|
|
773
|
-
/** Report one operation that subscribes to every enumerable field. */
|
|
774
|
-
const reportBroadAccess = (expression) => {
|
|
775
|
-
const origin = readStoreOrigin(expression);
|
|
776
|
-
if (origin === "snapshot") pendingReports.push({
|
|
777
|
-
expression,
|
|
778
|
-
messageId: "broadAccess"
|
|
779
|
-
});
|
|
780
|
-
else if (origin === "objectField" && options.includeObjectFields) pendingReports.push({
|
|
781
|
-
expression,
|
|
782
|
-
messageId: "broadObjectField"
|
|
783
|
-
});
|
|
784
|
-
};
|
|
785
|
-
/** Track object-valued bindings destructured from a snapshot. */
|
|
786
|
-
const recordObjectBindings = (pattern, source, write, objectField = false) => {
|
|
787
|
-
if (pattern.type === "Identifier") {
|
|
788
|
-
if ((getType(pattern).flags & ts.TypeFlags.Object) === 0) return;
|
|
789
|
-
const symbol = getIdentifierSymbol(pattern);
|
|
790
|
-
if (symbol) origins.record(symbol, {
|
|
791
|
-
expression: source,
|
|
792
|
-
objectField
|
|
793
|
-
}, write);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
if (pattern.type === "AssignmentPattern") {
|
|
797
|
-
recordObjectBindings(pattern.left, source, write, objectField);
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
if (pattern.type === "RestElement") return;
|
|
801
|
-
if (pattern.type !== "ObjectPattern" && pattern.type !== "ArrayPattern") return;
|
|
802
|
-
const entries = pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements;
|
|
803
|
-
for (const entry of entries) {
|
|
804
|
-
if (!entry) continue;
|
|
805
|
-
if (entry.type === "Property") recordObjectBindings(entry.value, source, write, true);
|
|
806
|
-
else recordObjectBindings(entry, source, write, true);
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
return {
|
|
810
|
-
CallExpression(node) {
|
|
811
|
-
if (getFactoryKind(node) === "createStore") {
|
|
812
|
-
const model = node.arguments[0];
|
|
813
|
-
if (model?.type === "Identifier") {
|
|
814
|
-
const symbol = getIdentifierSymbol(model);
|
|
815
|
-
if (symbol) storeModels.add(symbol);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const argument = getBroadArgument(node);
|
|
819
|
-
if (argument) reportBroadAccess(argument);
|
|
820
|
-
},
|
|
821
|
-
SpreadElement(node) {
|
|
822
|
-
reportBroadAccess(node.argument);
|
|
823
|
-
},
|
|
824
|
-
VariableDeclarator(node) {
|
|
825
|
-
if (!node.init) return;
|
|
826
|
-
if (node.id.type === "Identifier") {
|
|
827
|
-
const symbol = getIdentifierSymbol(node.id);
|
|
828
|
-
if (symbol) origins.record(symbol, {
|
|
829
|
-
expression: node.init,
|
|
830
|
-
objectField: false
|
|
831
|
-
}, node);
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
if (node.id.type === "ObjectPattern" && node.id.properties.some((property) => property.type === "RestElement")) reportBroadAccess(node.init);
|
|
835
|
-
recordObjectBindings(node.id, node.init, node);
|
|
836
|
-
},
|
|
837
|
-
AssignmentExpression(node) {
|
|
838
|
-
if (node.left.type !== "Identifier") return;
|
|
839
|
-
const symbol = getIdentifierSymbol(node.left);
|
|
840
|
-
if (symbol) origins.record(symbol, {
|
|
841
|
-
expression: node.right,
|
|
842
|
-
objectField: false
|
|
843
|
-
}, node);
|
|
844
|
-
},
|
|
845
|
-
"Program:exit"() {
|
|
846
|
-
for (const { expression, messageId } of pendingReports) {
|
|
847
|
-
if (!options.includeStoreModels && isInsideStoreModel(expression)) continue;
|
|
848
|
-
context.report({
|
|
849
|
-
node: expression,
|
|
850
|
-
messageId
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
//#endregion
|
|
858
|
-
//#region src/internal/store-dependency-graph.ts
|
|
859
|
-
const programGraphCache = /* @__PURE__ */ new WeakMap();
|
|
860
|
-
const sourceDependencyCache = /* @__PURE__ */ new WeakMap();
|
|
861
|
-
/** Visit a TypeScript tree iteratively so large source files do not consume the call stack. */
|
|
862
|
-
function forEachTsNode(root, visit) {
|
|
863
|
-
const stack = [root];
|
|
864
|
-
while (stack.length > 0) {
|
|
865
|
-
const node = stack.pop();
|
|
866
|
-
if (!node) continue;
|
|
867
|
-
visit(node);
|
|
868
|
-
const children = [];
|
|
869
|
-
ts.forEachChild(node, (child) => {
|
|
870
|
-
children.push(child);
|
|
871
|
-
});
|
|
872
|
-
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
/** Resolve the first tuple binding that owns a direct factory result. */
|
|
876
|
-
function getFactoryHookBinding(call) {
|
|
877
|
-
let expression = call;
|
|
878
|
-
let parent = expression.parent;
|
|
879
|
-
while ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent) || ts.isSatisfiesExpression(parent)) && parent.expression === expression) {
|
|
880
|
-
expression = parent;
|
|
881
|
-
parent = expression.parent;
|
|
882
|
-
}
|
|
883
|
-
if (!ts.isVariableDeclaration(parent) || parent.initializer !== expression || !ts.isArrayBindingPattern(parent.name)) return;
|
|
884
|
-
const first = parent.name.elements[0];
|
|
885
|
-
return first && ts.isBindingElement(first) && ts.isIdentifier(first.name) ? first.name : void 0;
|
|
886
|
-
}
|
|
887
|
-
/** Collect real Kerros Store bindings from all user source files in one Program. */
|
|
888
|
-
function collectStoreNodes(program) {
|
|
889
|
-
const tools = createKerrosProgramTools(program);
|
|
890
|
-
const nodes = [];
|
|
891
|
-
for (const sourceFile of program.getSourceFiles()) {
|
|
892
|
-
if (sourceFile.isDeclarationFile || program.isSourceFileDefaultLibrary(sourceFile) || program.isSourceFileFromExternalLibrary(sourceFile)) continue;
|
|
893
|
-
forEachTsNode(sourceFile, (node) => {
|
|
894
|
-
if (!ts.isCallExpression(node)) return;
|
|
895
|
-
const kind = tools.getFactoryKind(node);
|
|
896
|
-
const binding = kind ? getFactoryHookBinding(node) : void 0;
|
|
897
|
-
const hook = binding ? tools.getTsSymbol(binding) : void 0;
|
|
898
|
-
if (!kind || !hook) return;
|
|
899
|
-
const modelInput = kind === "createStore" ? node.arguments[0] : void 0;
|
|
900
|
-
const model = modelInput ? tools.getModelFunction(modelInput) : void 0;
|
|
901
|
-
nodes.push({
|
|
902
|
-
call: node,
|
|
903
|
-
hook,
|
|
904
|
-
id: nodes.length,
|
|
905
|
-
kind,
|
|
906
|
-
model,
|
|
907
|
-
name: hook.getName()
|
|
908
|
-
});
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
return {
|
|
912
|
-
nodes,
|
|
913
|
-
tools
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
/** Collect Store Hook calls reached synchronously from one createStore model. */
|
|
917
|
-
function collectModelDependencies(source, storesByHook, tools) {
|
|
918
|
-
const dependencies = [];
|
|
919
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
920
|
-
const pendingFunctions = source.model ? [source.model] : [];
|
|
921
|
-
while (pendingFunctions.length > 0) {
|
|
922
|
-
const fn = pendingFunctions.pop();
|
|
923
|
-
if (!fn || visitedFunctions.has(fn)) continue;
|
|
924
|
-
visitedFunctions.add(fn);
|
|
925
|
-
const root = fn.body;
|
|
926
|
-
if (!root) continue;
|
|
927
|
-
const stack = [root];
|
|
928
|
-
while (stack.length > 0) {
|
|
929
|
-
const node = stack.pop();
|
|
930
|
-
if (!node) continue;
|
|
931
|
-
if (node !== root && ts.isFunctionLike(node)) continue;
|
|
932
|
-
if (ts.isCallExpression(node)) {
|
|
933
|
-
const hook = tools.getTsSymbol(node.expression);
|
|
934
|
-
const target = hook ? storesByHook.get(hook) : void 0;
|
|
935
|
-
if (target) dependencies.push({
|
|
936
|
-
site: node,
|
|
937
|
-
source,
|
|
938
|
-
target
|
|
939
|
-
});
|
|
940
|
-
else {
|
|
941
|
-
const calledFunction = tools.getModelFunction(node.expression);
|
|
942
|
-
if (calledFunction && !visitedFunctions.has(calledFunction)) pendingFunctions.push(calledFunction);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
const children = [];
|
|
946
|
-
ts.forEachChild(node, (child) => {
|
|
947
|
-
children.push(child);
|
|
948
|
-
});
|
|
949
|
-
for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return dependencies;
|
|
953
|
-
}
|
|
954
|
-
/** Find strongly connected components with iterative Kosaraju passes. */
|
|
955
|
-
function getStronglyConnectedComponents(adjacency) {
|
|
956
|
-
const visited = new Uint8Array(adjacency.length);
|
|
957
|
-
const finishOrder = [];
|
|
958
|
-
for (let start = 0; start < adjacency.length; start += 1) {
|
|
959
|
-
if (visited[start] === 1) continue;
|
|
960
|
-
visited[start] = 1;
|
|
961
|
-
const stack = [{
|
|
962
|
-
index: 0,
|
|
963
|
-
node: start
|
|
964
|
-
}];
|
|
965
|
-
while (stack.length > 0) {
|
|
966
|
-
const frame = stack.at(-1);
|
|
967
|
-
if (!frame) break;
|
|
968
|
-
const neighbor = adjacency[frame.node][frame.index];
|
|
969
|
-
if (neighbor !== void 0) {
|
|
970
|
-
frame.index += 1;
|
|
971
|
-
if (visited[neighbor] === 0) {
|
|
972
|
-
visited[neighbor] = 1;
|
|
973
|
-
stack.push({
|
|
974
|
-
index: 0,
|
|
975
|
-
node: neighbor
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
finishOrder.push(frame.node);
|
|
981
|
-
stack.pop();
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
const reverse = Array.from({ length: adjacency.length }, () => []);
|
|
985
|
-
for (let source = 0; source < adjacency.length; source += 1) for (const target of adjacency[source]) reverse[target].push(source);
|
|
986
|
-
visited.fill(0);
|
|
987
|
-
const components = [];
|
|
988
|
-
for (let index = finishOrder.length - 1; index >= 0; index -= 1) {
|
|
989
|
-
const start = finishOrder[index];
|
|
990
|
-
if (visited[start] === 1) continue;
|
|
991
|
-
const component = [];
|
|
992
|
-
const stack = [start];
|
|
993
|
-
visited[start] = 1;
|
|
994
|
-
while (stack.length > 0) {
|
|
995
|
-
const node = stack.pop();
|
|
996
|
-
if (node === void 0) continue;
|
|
997
|
-
component.push(node);
|
|
998
|
-
for (const neighbor of reverse[node]) if (visited[neighbor] === 0) {
|
|
999
|
-
visited[neighbor] = 1;
|
|
1000
|
-
stack.push(neighbor);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
components.push(component);
|
|
1004
|
-
}
|
|
1005
|
-
return components;
|
|
1006
|
-
}
|
|
1007
|
-
/** Compare dependency sites by file and source position for stable diagnostics. */
|
|
1008
|
-
function compareDependencies(left, right) {
|
|
1009
|
-
const leftFile = left.site.getSourceFile().fileName;
|
|
1010
|
-
const rightFile = right.site.getSourceFile().fileName;
|
|
1011
|
-
return leftFile.localeCompare(rightFile) || left.site.getStart() - right.site.getStart() || left.target.id - right.target.id;
|
|
1012
|
-
}
|
|
1013
|
-
/** Select bounded, deterministic diagnostics from cyclic graph components. */
|
|
1014
|
-
function collectCyclicDependencies(nodes, dependencies) {
|
|
1015
|
-
const adjacencySets = Array.from({ length: nodes.length }, () => /* @__PURE__ */ new Set());
|
|
1016
|
-
const dependenciesBySource = /* @__PURE__ */ new Map();
|
|
1017
|
-
for (const dependency of dependencies) {
|
|
1018
|
-
adjacencySets[dependency.source.id].add(dependency.target.id);
|
|
1019
|
-
const existing = dependenciesBySource.get(dependency.source.id) ?? [];
|
|
1020
|
-
existing.push(dependency);
|
|
1021
|
-
dependenciesBySource.set(dependency.source.id, existing);
|
|
1022
|
-
}
|
|
1023
|
-
const components = getStronglyConnectedComponents(adjacencySets.map((targets) => [...targets]));
|
|
1024
|
-
const cyclicDependencies = [];
|
|
1025
|
-
for (const component of components) {
|
|
1026
|
-
if (!(component.length > 1 || component[0] !== void 0 && adjacencySets[component[0]].has(component[0]))) continue;
|
|
1027
|
-
const members = new Set(component);
|
|
1028
|
-
component.sort((left, right) => left - right);
|
|
1029
|
-
for (const source of component) {
|
|
1030
|
-
const dependency = (dependenciesBySource.get(source) ?? []).filter((candidate) => members.has(candidate.target.id)).sort(compareDependencies)[0];
|
|
1031
|
-
if (dependency) cyclicDependencies.push(dependency);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
return cyclicDependencies.sort(compareDependencies);
|
|
1035
|
-
}
|
|
1036
|
-
/** Build and cache the complete Store graph once for a TypeScript Program. */
|
|
1037
|
-
function getProgramStoreGraph(program) {
|
|
1038
|
-
const cached = programGraphCache.get(program);
|
|
1039
|
-
if (cached) return cached;
|
|
1040
|
-
const { nodes, tools } = collectStoreNodes(program);
|
|
1041
|
-
const storesByHook = new Map(nodes.map((node) => [node.hook, node]));
|
|
1042
|
-
const graph = { cyclicDependencies: collectCyclicDependencies(nodes, nodes.flatMap((node) => {
|
|
1043
|
-
return node.kind === "createStore" ? collectModelDependencies(node, storesByHook, tools) : [];
|
|
1044
|
-
})) };
|
|
1045
|
-
programGraphCache.set(program, graph);
|
|
1046
|
-
return graph;
|
|
1047
|
-
}
|
|
1048
|
-
/** Read cached cyclic dependency sites belonging to one current source file. */
|
|
1049
|
-
function getCyclicStoreDependencies(program, sourceFile) {
|
|
1050
|
-
let sourceFiles = sourceDependencyCache.get(program);
|
|
1051
|
-
if (!sourceFiles) {
|
|
1052
|
-
sourceFiles = /* @__PURE__ */ new WeakMap();
|
|
1053
|
-
sourceDependencyCache.set(program, sourceFiles);
|
|
1054
|
-
}
|
|
1055
|
-
const cached = sourceFiles.get(sourceFile);
|
|
1056
|
-
if (cached) return cached;
|
|
1057
|
-
const dependencies = getProgramStoreGraph(program).cyclicDependencies.filter((dependency) => dependency.site.getSourceFile() === sourceFile).map((dependency) => ({
|
|
1058
|
-
site: dependency.site,
|
|
1059
|
-
source: dependency.source.name,
|
|
1060
|
-
target: dependency.target.name
|
|
1061
|
-
}));
|
|
1062
|
-
sourceFiles.set(sourceFile, dependencies);
|
|
1063
|
-
return dependencies;
|
|
1064
|
-
}
|
|
1065
|
-
//#endregion
|
|
1066
|
-
//#region src/rules/no-cyclic-store-dependency.ts
|
|
1067
|
-
const noCyclicStoreDependency = createRule({
|
|
1068
|
-
name: "no-cyclic-store-dependency",
|
|
1069
|
-
meta: {
|
|
1070
|
-
type: "problem",
|
|
1071
|
-
docs: { description: "Prevent createStore models from forming Store dependency cycles." },
|
|
1072
|
-
schema: [],
|
|
1073
|
-
messages: { cyclicDependency: "Store \"{{source}}\" depends on \"{{target}}\" in a dependency cycle." }
|
|
1074
|
-
},
|
|
1075
|
-
defaultOptions: [],
|
|
1076
|
-
create(context) {
|
|
1077
|
-
const services = getTypeServices(context);
|
|
1078
|
-
const program = services.program;
|
|
1079
|
-
const sourceFile = services.esTreeNodeToTSNodeMap.get(context.sourceCode.ast);
|
|
1080
|
-
return { "Program:exit"() {
|
|
1081
|
-
if (!ts.isSourceFile(sourceFile)) return;
|
|
1082
|
-
for (const dependency of getCyclicStoreDependencies(program, sourceFile)) {
|
|
1083
|
-
const node = services.tsNodeToESTreeNodeMap.get(dependency.site);
|
|
1084
|
-
if (!node) continue;
|
|
1085
|
-
context.report({
|
|
1086
|
-
node,
|
|
1087
|
-
messageId: "cyclicDependency",
|
|
1088
|
-
data: {
|
|
1089
|
-
source: dependency.source,
|
|
1090
|
-
target: dependency.target
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
} };
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
//#endregion
|
|
1098
|
-
//#region src/internal/typescript.ts
|
|
1099
|
-
/** Remove TypeScript expression wrappers that preserve runtime identity. */
|
|
1100
|
-
function unwrapTsExpression(input) {
|
|
1101
|
-
let node = input;
|
|
1102
|
-
while (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) node = node.expression;
|
|
1103
|
-
return node;
|
|
1104
|
-
}
|
|
1105
|
-
/** Collect direct function return values without entering nested callbacks. */
|
|
1106
|
-
function getTsReturnExpressions(fn) {
|
|
1107
|
-
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return [fn.body];
|
|
1108
|
-
const expressions = [];
|
|
1109
|
-
if (!fn.body) return expressions;
|
|
1110
|
-
/** Scan control-flow branches owned by the current function. */
|
|
1111
|
-
const visit = (node) => {
|
|
1112
|
-
if (node !== fn.body && ts.isFunctionLike(node)) return;
|
|
1113
|
-
if (ts.isReturnStatement(node) && node.expression) {
|
|
1114
|
-
expressions.push(node.expression);
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
ts.forEachChild(node, visit);
|
|
1118
|
-
};
|
|
1119
|
-
visit(fn.body);
|
|
1120
|
-
return expressions;
|
|
1121
|
-
}
|
|
1122
|
-
//#endregion
|
|
1123
|
-
//#region src/rules/no-effect-event-action.ts
|
|
1124
|
-
const noEffectEventAction = createRule({
|
|
1125
|
-
name: "no-effect-event-action",
|
|
1126
|
-
meta: {
|
|
1127
|
-
type: "problem",
|
|
1128
|
-
docs: { description: "Prevent React Effect Events from becoming public Store actions." },
|
|
1129
|
-
schema: [],
|
|
1130
|
-
messages: { effectEventAction: "A useEffectEvent function cannot be exposed as a Store action." }
|
|
1131
|
-
},
|
|
1132
|
-
defaultOptions: [],
|
|
1133
|
-
create(context) {
|
|
1134
|
-
const { getFactoryKind, getIdentifierSymbol, getModelFunction, getTsNode, getTsSymbol, isReactCall, services } = createKerrosTypeTools(context);
|
|
1135
|
-
const origins = createReferenceOriginTracker(context.sourceCode.ast);
|
|
1136
|
-
const candidates = [];
|
|
1137
|
-
/** Test whether one model exposes an Effect Event in its returned Store object. */
|
|
1138
|
-
const exposesEffectEvent = (model) => {
|
|
1139
|
-
const writes = /* @__PURE__ */ new Map();
|
|
1140
|
-
if (!model.body) return false;
|
|
1141
|
-
/** Record one local value source at its execution point. */
|
|
1142
|
-
const record = (symbol, source, write) => {
|
|
1143
|
-
let conditional = false;
|
|
1144
|
-
let current = write.parent;
|
|
1145
|
-
while (current && current !== model) {
|
|
1146
|
-
if (ts.isIfStatement(current) || ts.isConditionalExpression(current) || ts.isSwitchStatement(current) || ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isTryStatement(current)) {
|
|
1147
|
-
conditional = true;
|
|
1148
|
-
break;
|
|
1149
|
-
}
|
|
1150
|
-
current = current.parent;
|
|
1151
|
-
}
|
|
1152
|
-
writes.set(symbol, [...writes.get(symbol) ?? [], {
|
|
1153
|
-
conditional,
|
|
1154
|
-
source,
|
|
1155
|
-
write
|
|
1156
|
-
}]);
|
|
1157
|
-
};
|
|
1158
|
-
/** Resolve straight-line overwrites while retaining conditional alternatives. */
|
|
1159
|
-
const getReachingWrites = (symbol, reference) => {
|
|
1160
|
-
let reaching = [];
|
|
1161
|
-
for (const candidate of writes.get(symbol) ?? []) {
|
|
1162
|
-
if (candidate.write.end > reference) continue;
|
|
1163
|
-
reaching = candidate.conditional ? [...reaching, candidate] : [candidate];
|
|
1164
|
-
}
|
|
1165
|
-
return reaching;
|
|
1166
|
-
};
|
|
1167
|
-
/** Prefer the shared flow tracker for current-file references, with TS fallback cross-file. */
|
|
1168
|
-
const getSources = (symbol, reference) => {
|
|
1169
|
-
const estreeReference = services.tsNodeToESTreeNodeMap.get(reference);
|
|
1170
|
-
if (estreeReference) {
|
|
1171
|
-
const expressions = [];
|
|
1172
|
-
for (const source of origins.resolve(symbol, estreeReference)) {
|
|
1173
|
-
const tsSource = getTsNode(source);
|
|
1174
|
-
if (ts.isExpression(tsSource)) expressions.push(tsSource);
|
|
1175
|
-
}
|
|
1176
|
-
return expressions;
|
|
1177
|
-
}
|
|
1178
|
-
return getReachingWrites(symbol, reference.getStart()).map((source) => source.source);
|
|
1179
|
-
};
|
|
1180
|
-
/** Record local aliases and assignments owned by this model invocation. */
|
|
1181
|
-
const collectWrites = (node) => {
|
|
1182
|
-
if (node !== model.body && ts.isFunctionLike(node)) return;
|
|
1183
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
1184
|
-
const symbol = getTsSymbol(node.name);
|
|
1185
|
-
if (symbol) record(symbol, node.initializer, node);
|
|
1186
|
-
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left)) {
|
|
1187
|
-
const symbol = getTsSymbol(node.left);
|
|
1188
|
-
if (symbol) record(symbol, node.right, node);
|
|
1189
|
-
}
|
|
1190
|
-
ts.forEachChild(node, collectWrites);
|
|
1191
|
-
};
|
|
1192
|
-
collectWrites(model.body);
|
|
1193
|
-
/** Resolve local aliases and logical branches of returned Store objects. */
|
|
1194
|
-
function getStoreObjects(input, reference = input.getStart(), seen = /* @__PURE__ */ new Set()) {
|
|
1195
|
-
const node = unwrapTsExpression(input);
|
|
1196
|
-
if (ts.isObjectLiteralExpression(node)) return [node];
|
|
1197
|
-
if (ts.isConditionalExpression(node)) return [...getStoreObjects(node.whenTrue, reference, seen), ...getStoreObjects(node.whenFalse, reference, seen)];
|
|
1198
|
-
if (ts.isBinaryExpression(node) && (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken || node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) return [...getStoreObjects(node.left, reference, seen), ...getStoreObjects(node.right, reference, seen)];
|
|
1199
|
-
if (ts.isCommaListExpression(node)) {
|
|
1200
|
-
const value = node.elements.at(-1);
|
|
1201
|
-
return value ? getStoreObjects(value, reference, seen) : [];
|
|
1202
|
-
}
|
|
1203
|
-
if (!ts.isIdentifier(node)) return [];
|
|
1204
|
-
const symbol = getTsSymbol(node);
|
|
1205
|
-
if (!symbol || seen.has(symbol)) return [];
|
|
1206
|
-
seen.add(symbol);
|
|
1207
|
-
const objects = getSources(symbol, node).flatMap((source) => {
|
|
1208
|
-
return getStoreObjects(source, source.getStart(), seen);
|
|
1209
|
-
});
|
|
1210
|
-
seen.delete(symbol);
|
|
1211
|
-
return objects;
|
|
1212
|
-
}
|
|
1213
|
-
/** Read a statically named TypeScript object property. */
|
|
1214
|
-
function getPropertyName(node) {
|
|
1215
|
-
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
|
|
1216
|
-
}
|
|
1217
|
-
/** Test whether an expression originates from React useEffectEvent. */
|
|
1218
|
-
function isEffectEvent(input, reference = input.getStart(), seen = /* @__PURE__ */ new Set()) {
|
|
1219
|
-
const node = unwrapTsExpression(input);
|
|
1220
|
-
if (ts.isCallExpression(node)) return isReactCall(node, "useEffectEvent");
|
|
1221
|
-
if (ts.isConditionalExpression(node)) return isEffectEvent(node.whenTrue, reference, seen) || isEffectEvent(node.whenFalse, reference, seen);
|
|
1222
|
-
if (ts.isBinaryExpression(node) && (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken || node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) return isEffectEvent(node.left, reference, seen) || isEffectEvent(node.right, reference, seen);
|
|
1223
|
-
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
1224
|
-
const name = ts.isPropertyAccessExpression(node) ? node.name.text : node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : void 0;
|
|
1225
|
-
if (!name) return false;
|
|
1226
|
-
return getStoreObjects(node.expression, reference).some((object) => {
|
|
1227
|
-
return object.properties.some((property) => {
|
|
1228
|
-
if (ts.isShorthandPropertyAssignment(property)) return property.name.text === name && isEffectEvent(property.name);
|
|
1229
|
-
return ts.isPropertyAssignment(property) && getPropertyName(property.name) === name && isEffectEvent(property.initializer);
|
|
1230
|
-
});
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
if (!ts.isIdentifier(node)) return false;
|
|
1234
|
-
const symbol = getTsSymbol(node);
|
|
1235
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1236
|
-
seen.add(symbol);
|
|
1237
|
-
const effectEvent = getSources(symbol, node).some((source) => {
|
|
1238
|
-
return isEffectEvent(source, source.getStart(), seen);
|
|
1239
|
-
});
|
|
1240
|
-
seen.delete(symbol);
|
|
1241
|
-
return effectEvent;
|
|
1242
|
-
}
|
|
1243
|
-
/** Test Store properties and recursively expanded object spreads. */
|
|
1244
|
-
function objectContainsEffectEvent(object, seen = /* @__PURE__ */ new Set()) {
|
|
1245
|
-
if (seen.has(object)) return false;
|
|
1246
|
-
seen.add(object);
|
|
1247
|
-
for (const property of object.properties) {
|
|
1248
|
-
if (ts.isSpreadAssignment(property)) {
|
|
1249
|
-
if (getStoreObjects(property.expression, property.getStart()).some((spread) => {
|
|
1250
|
-
return objectContainsEffectEvent(spread, seen);
|
|
1251
|
-
})) return true;
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
const value = ts.isPropertyAssignment(property) ? property.initializer : ts.isShorthandPropertyAssignment(property) ? property.name : void 0;
|
|
1255
|
-
if (value && isEffectEvent(value)) return true;
|
|
1256
|
-
}
|
|
1257
|
-
return false;
|
|
1258
|
-
}
|
|
1259
|
-
for (const returned of getTsReturnExpressions(model)) if (getStoreObjects(returned).some((object) => objectContainsEffectEvent(object))) return true;
|
|
1260
|
-
return false;
|
|
1261
|
-
};
|
|
1262
|
-
return {
|
|
1263
|
-
VariableDeclarator(node) {
|
|
1264
|
-
if (node.id.type !== "Identifier" || !node.init) return;
|
|
1265
|
-
const symbol = getIdentifierSymbol(node.id);
|
|
1266
|
-
if (symbol) origins.record(symbol, node.init, node);
|
|
1267
|
-
},
|
|
1268
|
-
AssignmentExpression(node) {
|
|
1269
|
-
if (node.left.type !== "Identifier") return;
|
|
1270
|
-
const symbol = getIdentifierSymbol(node.left);
|
|
1271
|
-
if (symbol) origins.record(symbol, node.right, node);
|
|
1272
|
-
},
|
|
1273
|
-
CallExpression(node) {
|
|
1274
|
-
if (getFactoryKind(node) !== "createStore") return;
|
|
1275
|
-
const model = node.arguments[0];
|
|
1276
|
-
if (!model || model.type === "SpreadElement") return;
|
|
1277
|
-
const declaration = getModelFunction(model);
|
|
1278
|
-
if (declaration) candidates.push({
|
|
1279
|
-
declaration,
|
|
1280
|
-
node: model
|
|
1281
|
-
});
|
|
1282
|
-
},
|
|
1283
|
-
"Program:exit"() {
|
|
1284
|
-
for (const candidate of candidates) if (exposesEffectEvent(candidate.declaration)) context.report({
|
|
1285
|
-
node: candidate.node,
|
|
1286
|
-
messageId: "effectEventAction"
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
});
|
|
1292
|
-
//#endregion
|
|
1293
|
-
//#region src/rules/no-provider-key-prop.ts
|
|
1294
|
-
const noProviderKeyProp = createRule({
|
|
1295
|
-
name: "no-provider-key-prop",
|
|
1296
|
-
meta: {
|
|
1297
|
-
type: "problem",
|
|
1298
|
-
docs: { description: "Prevent createStore models from consuming React key as a Provider prop." },
|
|
1299
|
-
schema: [],
|
|
1300
|
-
messages: { keyProp: "React key is not a Provider prop and cannot be consumed by a model." }
|
|
1301
|
-
},
|
|
1302
|
-
defaultOptions: [],
|
|
1303
|
-
create(context) {
|
|
1304
|
-
const { checker, getFactoryKind, getType } = createKerrosTypeTools(context);
|
|
1305
|
-
return { CallExpression(node) {
|
|
1306
|
-
if (getFactoryKind(node) !== "createStore") return;
|
|
1307
|
-
const model = node.arguments[0];
|
|
1308
|
-
if (!model || model.type === "SpreadElement") return;
|
|
1309
|
-
const props = getType(model).getCallSignatures()[0]?.getParameters()[0];
|
|
1310
|
-
const declaration = props?.valueDeclaration ?? props?.declarations?.[0];
|
|
1311
|
-
if (!props || !declaration) return;
|
|
1312
|
-
const propsType = checker.getTypeOfSymbolAtLocation(props, declaration);
|
|
1313
|
-
if (getTypeProperty(checker, propsType, "key")) context.report({
|
|
1314
|
-
node: model,
|
|
1315
|
-
messageId: "keyProp"
|
|
1316
|
-
});
|
|
1317
|
-
} };
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
//#endregion
|
|
1321
|
-
//#region src/rules/no-render-instance-snapshot.ts
|
|
1322
|
-
const deferredReactHooks = /* @__PURE__ */ new Set([
|
|
1323
|
-
"useEffect",
|
|
1324
|
-
"useEffectEvent",
|
|
1325
|
-
"useInsertionEffect",
|
|
1326
|
-
"useLayoutEffect"
|
|
1327
|
-
]);
|
|
1328
|
-
const deferredGlobals = /* @__PURE__ */ new Set([
|
|
1329
|
-
"queueMicrotask",
|
|
1330
|
-
"setInterval",
|
|
1331
|
-
"setTimeout"
|
|
1332
|
-
]);
|
|
1333
|
-
const renderFunctionPattern$1 = /^(?:[A-Z]|use[A-Z])/u;
|
|
1334
|
-
/** Return the declaration identifier that owns a local function. */
|
|
1335
|
-
function getFunctionIdentifier$2(node) {
|
|
1336
|
-
if (node.type !== "ArrowFunctionExpression" && node.id) return node.id;
|
|
1337
|
-
const parent = node.parent;
|
|
1338
|
-
return parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" ? parent.id : void 0;
|
|
1339
|
-
}
|
|
1340
|
-
/** Test whether an attribute is an intrinsic element event callback. */
|
|
1341
|
-
function isIntrinsicEventAttribute(node) {
|
|
1342
|
-
if (node.name.type !== "JSXIdentifier" || !/^on[A-Z]/u.test(node.name.name)) return false;
|
|
1343
|
-
const opening = node.parent;
|
|
1344
|
-
return opening?.type === "JSXOpeningElement" && opening.name.type === "JSXIdentifier" && /^[a-z]/u.test(opening.name.name);
|
|
1345
|
-
}
|
|
1346
|
-
const noRenderInstanceSnapshot = createRule({
|
|
1347
|
-
name: "no-render-instance-snapshot",
|
|
1348
|
-
meta: {
|
|
1349
|
-
type: "problem",
|
|
1350
|
-
docs: { description: "Prevent direct external Store snapshot reads during render." },
|
|
1351
|
-
schema: [],
|
|
1352
|
-
messages: { renderSnapshot: "Subscribe with the Store Hook instead of reading getSnapshot during render." }
|
|
1353
|
-
},
|
|
1354
|
-
defaultOptions: [],
|
|
1355
|
-
create(context) {
|
|
1356
|
-
const { checker, getIdentifierSymbol, isStoreInstanceHookCall, services } = createKerrosTypeTools(context);
|
|
1357
|
-
const functions = /* @__PURE__ */ new Set();
|
|
1358
|
-
const deferredFunctions = /* @__PURE__ */ new Set();
|
|
1359
|
-
const deferredSymbols = /* @__PURE__ */ new Set();
|
|
1360
|
-
const origins = createReferenceOriginTracker(context.sourceCode.ast);
|
|
1361
|
-
const snapshotReaders = /* @__PURE__ */ new Map();
|
|
1362
|
-
const calls = [];
|
|
1363
|
-
const immediateJsxCallbacks = [];
|
|
1364
|
-
const snapshots = [];
|
|
1365
|
-
/** Find the function whose body contains a syntax node. */
|
|
1366
|
-
const getOwner = (input) => {
|
|
1367
|
-
let node = input;
|
|
1368
|
-
while (node) {
|
|
1369
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
|
|
1370
|
-
node = node.parent;
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
/** Resolve a TypeScript symbol through import and export aliases. */
|
|
1374
|
-
const getResolvedSymbol = (node) => {
|
|
1375
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1376
|
-
let symbol = checker.getSymbolAtLocation(tsNode);
|
|
1377
|
-
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol);
|
|
1378
|
-
return symbol;
|
|
1379
|
-
};
|
|
1380
|
-
/** Test whether a call targets one of React's deferred callback Hooks. */
|
|
1381
|
-
const isDeferredReactCall = (node) => {
|
|
1382
|
-
const symbol = getResolvedSymbol(node.callee);
|
|
1383
|
-
if (!symbol || !deferredReactHooks.has(symbol.getName())) return false;
|
|
1384
|
-
return symbol.declarations?.some((declaration) => {
|
|
1385
|
-
return declaration.getSourceFile().fileName.replaceAll("\\", "/").includes("/node_modules/@types/react/");
|
|
1386
|
-
}) === true;
|
|
1387
|
-
};
|
|
1388
|
-
/** Test whether a call targets a real global scheduling function. */
|
|
1389
|
-
const isDeferredGlobalCall = (node) => {
|
|
1390
|
-
const symbol = getResolvedSymbol(node.callee);
|
|
1391
|
-
if (!symbol || !deferredGlobals.has(symbol.getName())) return false;
|
|
1392
|
-
return symbol.declarations?.some((declaration) => {
|
|
1393
|
-
const sourceFile = declaration.getSourceFile();
|
|
1394
|
-
const filename = sourceFile.fileName.replaceAll("\\", "/");
|
|
1395
|
-
return services.program.isSourceFileDefaultLibrary(sourceFile) || filename.includes("/node_modules/@types/node/");
|
|
1396
|
-
}) === true;
|
|
1397
|
-
};
|
|
1398
|
-
/** Test whether a call defers its first callback beyond render. */
|
|
1399
|
-
const isDeferredCall = (node) => {
|
|
1400
|
-
return isDeferredReactCall(node) || isDeferredGlobalCall(node);
|
|
1401
|
-
};
|
|
1402
|
-
/** Mark a direct function or referenced local function as deferred. */
|
|
1403
|
-
const markDeferred = (node) => {
|
|
1404
|
-
if (!node) return;
|
|
1405
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
|
|
1406
|
-
deferredFunctions.add(node);
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (node.type === "Identifier") {
|
|
1410
|
-
const symbol = getIdentifierSymbol(node);
|
|
1411
|
-
if (symbol) deferredSymbols.add(symbol);
|
|
1412
|
-
}
|
|
1413
|
-
};
|
|
1414
|
-
/** Collect a function node for the final local call graph. */
|
|
1415
|
-
const collectFunction = (node) => {
|
|
1416
|
-
functions.add(node);
|
|
1417
|
-
};
|
|
1418
|
-
return {
|
|
1419
|
-
ArrowFunctionExpression: collectFunction,
|
|
1420
|
-
FunctionDeclaration: collectFunction,
|
|
1421
|
-
FunctionExpression: collectFunction,
|
|
1422
|
-
VariableDeclarator(node) {
|
|
1423
|
-
if (!node.init) return;
|
|
1424
|
-
if (node.id.type === "Identifier") {
|
|
1425
|
-
const symbol = getIdentifierSymbol(node.id);
|
|
1426
|
-
if (symbol) origins.record(symbol, node.init, node);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
if (node.id.type !== "ObjectPattern") return;
|
|
1430
|
-
for (const property of node.id.properties) {
|
|
1431
|
-
if (property.type !== "Property") continue;
|
|
1432
|
-
const key = property.key;
|
|
1433
|
-
const name = !property.computed && key.type === "Identifier" ? key.name : key.type === "Literal" && typeof key.value === "string" ? key.value : void 0;
|
|
1434
|
-
const value = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
1435
|
-
if (name !== "getSnapshot" || value.type !== "Identifier") continue;
|
|
1436
|
-
const symbol = getIdentifierSymbol(value);
|
|
1437
|
-
if (symbol) snapshotReaders.set(symbol, node.init);
|
|
1438
|
-
}
|
|
1439
|
-
},
|
|
1440
|
-
AssignmentExpression(node) {
|
|
1441
|
-
if (node.left.type !== "Identifier") return;
|
|
1442
|
-
const symbol = getIdentifierSymbol(node.left);
|
|
1443
|
-
if (symbol) origins.record(symbol, node.right, node);
|
|
1444
|
-
},
|
|
1445
|
-
JSXAttribute(node) {
|
|
1446
|
-
if (node.name.type !== "JSXIdentifier" || !/^on[A-Z]/u.test(node.name.name) || node.value?.type !== "JSXExpressionContainer") return;
|
|
1447
|
-
const callback = node.value.expression.type === "JSXEmptyExpression" ? void 0 : node.value.expression;
|
|
1448
|
-
if (!callback) return;
|
|
1449
|
-
if (isIntrinsicEventAttribute(node)) markDeferred(callback);
|
|
1450
|
-
else immediateJsxCallbacks.push({
|
|
1451
|
-
caller: getOwner(node.parent),
|
|
1452
|
-
node: callback,
|
|
1453
|
-
site: node
|
|
1454
|
-
});
|
|
1455
|
-
},
|
|
1456
|
-
CallExpression(node) {
|
|
1457
|
-
const caller = getOwner(node.parent);
|
|
1458
|
-
calls.push({
|
|
1459
|
-
caller,
|
|
1460
|
-
node
|
|
1461
|
-
});
|
|
1462
|
-
if (isDeferredCall(node)) {
|
|
1463
|
-
const callback = node.arguments[0];
|
|
1464
|
-
if (callback?.type !== "SpreadElement") markDeferred(callback);
|
|
1465
|
-
}
|
|
1466
|
-
const callee = unwrapExpression(node.callee);
|
|
1467
|
-
if (callee.type === "Identifier") {
|
|
1468
|
-
snapshots.push({
|
|
1469
|
-
kind: "reader",
|
|
1470
|
-
node,
|
|
1471
|
-
source: callee,
|
|
1472
|
-
owner: caller
|
|
1473
|
-
});
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (callee.type !== "MemberExpression" || getMemberName(callee) !== "getSnapshot") return;
|
|
1477
|
-
snapshots.push({
|
|
1478
|
-
kind: "instance",
|
|
1479
|
-
node,
|
|
1480
|
-
source: callee.object,
|
|
1481
|
-
owner: caller
|
|
1482
|
-
});
|
|
1483
|
-
},
|
|
1484
|
-
"Program:exit"() {
|
|
1485
|
-
const functionsBySymbol = /* @__PURE__ */ new Map();
|
|
1486
|
-
const rendered = /* @__PURE__ */ new Set();
|
|
1487
|
-
for (const fn of functions) {
|
|
1488
|
-
const identifier = getFunctionIdentifier$2(fn);
|
|
1489
|
-
if (!identifier) {
|
|
1490
|
-
if (fn.parent?.type === "ExportDefaultDeclaration") rendered.add(fn);
|
|
1491
|
-
continue;
|
|
1492
|
-
}
|
|
1493
|
-
const symbol = getIdentifierSymbol(identifier);
|
|
1494
|
-
if (symbol) {
|
|
1495
|
-
functionsBySymbol.set(symbol, fn);
|
|
1496
|
-
if (deferredSymbols.has(symbol)) deferredFunctions.add(fn);
|
|
1497
|
-
}
|
|
1498
|
-
if (renderFunctionPattern$1.test(identifier.name) && !deferredFunctions.has(fn)) rendered.add(fn);
|
|
1499
|
-
}
|
|
1500
|
-
/** Resolve a local function expression or identifier used as a callback. */
|
|
1501
|
-
const resolveFunction = (input) => {
|
|
1502
|
-
const node = unwrapExpression(input);
|
|
1503
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return node;
|
|
1504
|
-
if (node.type !== "Identifier") return void 0;
|
|
1505
|
-
const symbol = getIdentifierSymbol(node);
|
|
1506
|
-
return symbol ? functionsBySymbol.get(symbol) : void 0;
|
|
1507
|
-
};
|
|
1508
|
-
const localEdges = [];
|
|
1509
|
-
for (const { caller, node } of calls) {
|
|
1510
|
-
if (!caller) continue;
|
|
1511
|
-
const callee = resolveFunction(node.callee);
|
|
1512
|
-
if (callee) localEdges.push({
|
|
1513
|
-
callee,
|
|
1514
|
-
caller,
|
|
1515
|
-
site: node
|
|
1516
|
-
});
|
|
1517
|
-
if (isDeferredCall(node)) continue;
|
|
1518
|
-
for (const argument of node.arguments) {
|
|
1519
|
-
if (argument.type === "SpreadElement") continue;
|
|
1520
|
-
const callback = resolveFunction(argument);
|
|
1521
|
-
if (callback) localEdges.push({
|
|
1522
|
-
callee: callback,
|
|
1523
|
-
caller,
|
|
1524
|
-
site: node
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
for (const { caller, node, site } of immediateJsxCallbacks) {
|
|
1529
|
-
const callback = resolveFunction(node);
|
|
1530
|
-
if (caller && callback) localEdges.push({
|
|
1531
|
-
callee: callback,
|
|
1532
|
-
caller,
|
|
1533
|
-
site
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
let changed = true;
|
|
1537
|
-
while (changed) {
|
|
1538
|
-
changed = false;
|
|
1539
|
-
for (const edge of localEdges) {
|
|
1540
|
-
if (!rendered.has(edge.caller) || rendered.has(edge.callee)) continue;
|
|
1541
|
-
rendered.add(edge.callee);
|
|
1542
|
-
changed = true;
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
const callContexts = createFunctionCallSiteContexts(localEdges.filter((edge) => rendered.has(edge.caller)));
|
|
1546
|
-
/** Test whether an expression is a local alias of a Store instance Hook result. */
|
|
1547
|
-
const isInstanceDerived = (input, calls, seen = /* @__PURE__ */ new Set()) => {
|
|
1548
|
-
const node = unwrapExpression(input);
|
|
1549
|
-
if (node.type === "CallExpression") return isStoreInstanceHookCall(node);
|
|
1550
|
-
if (node.type === "AssignmentExpression") return isInstanceDerived(node.right, calls, seen);
|
|
1551
|
-
if (node.type !== "Identifier") return false;
|
|
1552
|
-
const symbol = getIdentifierSymbol(node);
|
|
1553
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1554
|
-
const sources = origins.resolve(symbol, node, calls);
|
|
1555
|
-
if (sources.length === 0) return false;
|
|
1556
|
-
seen.add(symbol);
|
|
1557
|
-
const derived = sources.some((source) => isInstanceDerived(source, calls, seen));
|
|
1558
|
-
seen.delete(symbol);
|
|
1559
|
-
return derived;
|
|
1560
|
-
};
|
|
1561
|
-
/** Test whether an identifier comes from destructuring an instance getSnapshot method. */
|
|
1562
|
-
const isSnapshotReaderDerived = (input, calls, seen = /* @__PURE__ */ new Set()) => {
|
|
1563
|
-
const node = unwrapExpression(input);
|
|
1564
|
-
if (node.type !== "Identifier") return false;
|
|
1565
|
-
const symbol = getIdentifierSymbol(node);
|
|
1566
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1567
|
-
const instance = snapshotReaders.get(symbol);
|
|
1568
|
-
if (instance) return isInstanceDerived(instance, calls);
|
|
1569
|
-
const sources = origins.resolve(symbol, node, calls);
|
|
1570
|
-
if (sources.length === 0) return false;
|
|
1571
|
-
seen.add(symbol);
|
|
1572
|
-
const derived = sources.some((source) => isSnapshotReaderDerived(source, calls, seen));
|
|
1573
|
-
seen.delete(symbol);
|
|
1574
|
-
return derived;
|
|
1575
|
-
};
|
|
1576
|
-
for (const snapshot of snapshots) {
|
|
1577
|
-
if (!snapshot.owner || !rendered.has(snapshot.owner)) continue;
|
|
1578
|
-
if (callContexts.some(snapshot.owner, (calls) => {
|
|
1579
|
-
return snapshot.kind === "instance" ? isInstanceDerived(snapshot.source, calls) : isSnapshotReaderDerived(snapshot.source, calls);
|
|
1580
|
-
})) context.report({
|
|
1581
|
-
node: snapshot.node,
|
|
1582
|
-
messageId: "renderSnapshot"
|
|
1583
|
-
});
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
});
|
|
1589
|
-
//#endregion
|
|
1590
|
-
//#region src/rules/no-store-mutation.ts
|
|
1591
|
-
/** Return the declaration identifier that owns a local function. */
|
|
1592
|
-
function getFunctionIdentifier$1(node) {
|
|
1593
|
-
if (node.type !== "ArrowFunctionExpression" && node.id) return node.id;
|
|
1594
|
-
const parent = node.parent;
|
|
1595
|
-
return parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" ? parent.id : void 0;
|
|
1596
|
-
}
|
|
1597
|
-
/** Collect every identifier introduced by one binding pattern. */
|
|
1598
|
-
function collectPatternIdentifiers$1(pattern, identifiers) {
|
|
1599
|
-
if (pattern.type === "Identifier") {
|
|
1600
|
-
identifiers.push(pattern);
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
if (pattern.type === "RestElement") {
|
|
1604
|
-
collectPatternIdentifiers$1(pattern.argument, identifiers);
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
if (pattern.type === "AssignmentPattern") {
|
|
1608
|
-
collectPatternIdentifiers$1(pattern.left, identifiers);
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
if (pattern.type !== "ObjectPattern" && pattern.type !== "ArrayPattern") return;
|
|
1612
|
-
for (const property of pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements) {
|
|
1613
|
-
if (!property) continue;
|
|
1614
|
-
if (property.type === "Property") collectPatternIdentifiers$1(property.value, identifiers);
|
|
1615
|
-
else collectPatternIdentifiers$1(property, identifiers);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
const noStoreMutation = createRule({
|
|
1619
|
-
name: "no-store-mutation",
|
|
1620
|
-
meta: {
|
|
1621
|
-
type: "problem",
|
|
1622
|
-
docs: { description: "Prevent mutation of selector-free Store snapshots." },
|
|
1623
|
-
schema: [{
|
|
1624
|
-
type: "object",
|
|
1625
|
-
additionalProperties: false,
|
|
1626
|
-
properties: { deepAliases: { type: "boolean" } }
|
|
1627
|
-
}],
|
|
1628
|
-
messages: { mutation: "Store snapshots are immutable." }
|
|
1629
|
-
},
|
|
1630
|
-
defaultOptions: [{ deepAliases: true }],
|
|
1631
|
-
create(context, [options]) {
|
|
1632
|
-
const { checker, getIdentifierSymbol, getType, isStoreHookCall, services } = createKerrosTypeTools(context);
|
|
1633
|
-
const origins = createReferenceOriginTracker(context.sourceCode.ast);
|
|
1634
|
-
const functions = /* @__PURE__ */ new Set();
|
|
1635
|
-
const calls = [];
|
|
1636
|
-
const candidates = [];
|
|
1637
|
-
const maxAliasDepth = options.deepAliases === false ? 1 : Number.POSITIVE_INFINITY;
|
|
1638
|
-
/** Find the function whose body contains a syntax node. */
|
|
1639
|
-
const getOwner = (input) => {
|
|
1640
|
-
let node = input;
|
|
1641
|
-
while (node) {
|
|
1642
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
|
|
1643
|
-
node = node.parent;
|
|
1644
|
-
}
|
|
1645
|
-
};
|
|
1646
|
-
/** Collect a function node for the final local call graph. */
|
|
1647
|
-
const collectFunction = (node) => {
|
|
1648
|
-
functions.add(node);
|
|
1649
|
-
};
|
|
1650
|
-
/** Test whether an expression comes from a selector-free Store Hook snapshot. */
|
|
1651
|
-
const isSnapshotDerived = (input, remainingAliases = maxAliasDepth, seen = /* @__PURE__ */ new Set(), calls) => {
|
|
1652
|
-
const node = unwrapExpression(input);
|
|
1653
|
-
if (node.type === "CallExpression") {
|
|
1654
|
-
const selector = node.arguments[0];
|
|
1655
|
-
return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & ts.TypeFlags.Undefined) !== 0) && isStoreHookCall(node);
|
|
1656
|
-
}
|
|
1657
|
-
if (node.type === "AssignmentExpression") return isSnapshotDerived(node.right, remainingAliases, seen, calls);
|
|
1658
|
-
if (node.type === "MemberExpression") return isSnapshotDerived(node.object, remainingAliases, seen, calls);
|
|
1659
|
-
if (node.type !== "Identifier" || remainingAliases <= 0) return false;
|
|
1660
|
-
const symbol = getIdentifierSymbol(node);
|
|
1661
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1662
|
-
const sources = origins.resolve(symbol, node, calls);
|
|
1663
|
-
if (sources.length === 0) return false;
|
|
1664
|
-
seen.add(symbol);
|
|
1665
|
-
const derived = sources.some((source) => {
|
|
1666
|
-
return isSnapshotDerived(source, remainingAliases - 1, seen, calls);
|
|
1667
|
-
});
|
|
1668
|
-
seen.delete(symbol);
|
|
1669
|
-
return derived;
|
|
1670
|
-
};
|
|
1671
|
-
return {
|
|
1672
|
-
ArrowFunctionExpression: collectFunction,
|
|
1673
|
-
FunctionDeclaration: collectFunction,
|
|
1674
|
-
FunctionExpression: collectFunction,
|
|
1675
|
-
VariableDeclarator(node) {
|
|
1676
|
-
if (!node.init) return;
|
|
1677
|
-
const identifiers = [];
|
|
1678
|
-
collectPatternIdentifiers$1(node.id, identifiers);
|
|
1679
|
-
for (const identifier of identifiers) {
|
|
1680
|
-
const symbol = getIdentifierSymbol(identifier);
|
|
1681
|
-
if (symbol) origins.record(symbol, node.init, node);
|
|
1682
|
-
}
|
|
1683
|
-
},
|
|
1684
|
-
AssignmentExpression(node) {
|
|
1685
|
-
if (node.left.type === "Identifier") {
|
|
1686
|
-
const symbol = getIdentifierSymbol(node.left);
|
|
1687
|
-
if (symbol) origins.record(symbol, node.right, node);
|
|
1688
|
-
} else if (node.left.type === "MemberExpression") candidates.push({
|
|
1689
|
-
expression: node.left,
|
|
1690
|
-
node,
|
|
1691
|
-
owner: getOwner(node.parent)
|
|
1692
|
-
});
|
|
1693
|
-
},
|
|
1694
|
-
UpdateExpression(node) {
|
|
1695
|
-
candidates.push({
|
|
1696
|
-
expression: node.argument,
|
|
1697
|
-
node,
|
|
1698
|
-
owner: getOwner(node.parent)
|
|
1699
|
-
});
|
|
1700
|
-
},
|
|
1701
|
-
UnaryExpression(node) {
|
|
1702
|
-
if (node.operator === "delete") candidates.push({
|
|
1703
|
-
expression: node.argument,
|
|
1704
|
-
node,
|
|
1705
|
-
owner: getOwner(node.parent)
|
|
1706
|
-
});
|
|
1707
|
-
},
|
|
1708
|
-
CallExpression(node) {
|
|
1709
|
-
calls.push({
|
|
1710
|
-
caller: getOwner(node.parent),
|
|
1711
|
-
node
|
|
1712
|
-
});
|
|
1713
|
-
if (!isMutableCollectionCall(node, checker, services.program, getType)) return;
|
|
1714
|
-
const callee = unwrapExpression(node.callee);
|
|
1715
|
-
if (callee.type === "MemberExpression") candidates.push({
|
|
1716
|
-
expression: callee.object,
|
|
1717
|
-
node,
|
|
1718
|
-
owner: getOwner(node.parent)
|
|
1719
|
-
});
|
|
1720
|
-
},
|
|
1721
|
-
"Program:exit"() {
|
|
1722
|
-
const functionsBySymbol = /* @__PURE__ */ new Map();
|
|
1723
|
-
for (const fn of functions) {
|
|
1724
|
-
const identifier = getFunctionIdentifier$1(fn);
|
|
1725
|
-
if (!identifier) continue;
|
|
1726
|
-
const symbol = getIdentifierSymbol(identifier);
|
|
1727
|
-
if (symbol) functionsBySymbol.set(symbol, fn);
|
|
1728
|
-
}
|
|
1729
|
-
/** Resolve a directly called local function. */
|
|
1730
|
-
const resolveFunction = (input) => {
|
|
1731
|
-
const node = unwrapExpression(input);
|
|
1732
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return node;
|
|
1733
|
-
if (node.type !== "Identifier") return void 0;
|
|
1734
|
-
const symbol = getIdentifierSymbol(node);
|
|
1735
|
-
return symbol ? functionsBySymbol.get(symbol) : void 0;
|
|
1736
|
-
};
|
|
1737
|
-
const localEdges = [];
|
|
1738
|
-
for (const { caller, node } of calls) {
|
|
1739
|
-
const callee = resolveFunction(node.callee);
|
|
1740
|
-
if (caller && callee) localEdges.push({
|
|
1741
|
-
callee,
|
|
1742
|
-
caller,
|
|
1743
|
-
site: node
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
const callContexts = createFunctionCallSiteContexts(localEdges);
|
|
1747
|
-
for (const candidate of candidates) if (candidate.owner ? callContexts.some(candidate.owner, (calls) => {
|
|
1748
|
-
return isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), calls);
|
|
1749
|
-
}) : isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map())) context.report({
|
|
1750
|
-
node: candidate.node,
|
|
1751
|
-
messageId: "mutation"
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
1757
|
-
//#endregion
|
|
1758
|
-
//#region src/rules/no-unstable-bound-store.ts
|
|
1759
|
-
const renderFunctionPattern = /^(?:[A-Z]|use[A-Z])/u;
|
|
1760
|
-
/** Find the nearest function that owns one JSX reference. */
|
|
1761
|
-
function getOwner(input) {
|
|
1762
|
-
let node = input.parent;
|
|
1763
|
-
while (node) {
|
|
1764
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
|
|
1765
|
-
node = node.parent;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
/** Read the declaration identifier for one local function. */
|
|
1769
|
-
function getFunctionIdentifier(node) {
|
|
1770
|
-
if (node.type !== "ArrowFunctionExpression" && node.id) return node.id;
|
|
1771
|
-
const parent = node.parent;
|
|
1772
|
-
return parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" ? parent.id : void 0;
|
|
1773
|
-
}
|
|
1774
|
-
/** Test whether a function is a component, Hook, or anonymous default render root. */
|
|
1775
|
-
function isRenderRoot(node) {
|
|
1776
|
-
if (!node) return false;
|
|
1777
|
-
const identifier = getFunctionIdentifier(node);
|
|
1778
|
-
if (identifier) return renderFunctionPattern.test(identifier.name);
|
|
1779
|
-
return node.parent?.type === "ExportDefaultDeclaration";
|
|
1780
|
-
}
|
|
1781
|
-
/** Collect every identifier introduced by one binding pattern. */
|
|
1782
|
-
function collectPatternIdentifiers(pattern, identifiers) {
|
|
1783
|
-
if (pattern.type === "Identifier") {
|
|
1784
|
-
identifiers.push(pattern);
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
if (pattern.type === "RestElement") {
|
|
1788
|
-
collectPatternIdentifiers(pattern.argument, identifiers);
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
if (pattern.type === "AssignmentPattern") {
|
|
1792
|
-
collectPatternIdentifiers(pattern.left, identifiers);
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
if (pattern.type !== "ObjectPattern" && pattern.type !== "ArrayPattern") return;
|
|
1796
|
-
for (const property of pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements) {
|
|
1797
|
-
if (!property) continue;
|
|
1798
|
-
if (property.type === "Property") collectPatternIdentifiers(property.value, identifiers);
|
|
1799
|
-
else collectPatternIdentifiers(property, identifiers);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
const noUnstableBoundStore = createRule({
|
|
1803
|
-
name: "no-unstable-bound-store",
|
|
1804
|
-
meta: {
|
|
1805
|
-
type: "problem",
|
|
1806
|
-
docs: { description: "Require stable Store instances for bindStore Providers." },
|
|
1807
|
-
schema: [],
|
|
1808
|
-
messages: { unstableStore: "The Provider store prop must have stable identity." }
|
|
1809
|
-
},
|
|
1810
|
-
defaultOptions: [],
|
|
1811
|
-
create(context) {
|
|
1812
|
-
const { checker, getIdentifierSymbol, getTsNode, getType, hasMarker, isReactCall, services } = createKerrosTypeTools(context);
|
|
1813
|
-
const origins = createReferenceOriginTracker(context.sourceCode.ast);
|
|
1814
|
-
const refCurrents = createReferenceOriginTracker(context.sourceCode.ast);
|
|
1815
|
-
/** Read a parameter or binding-element default; null means a parameter without a default. */
|
|
1816
|
-
const getParameterDefault = (input) => {
|
|
1817
|
-
let node = input;
|
|
1818
|
-
while (node) {
|
|
1819
|
-
if (ts.isBindingElement(node) && node.initializer) return node.initializer;
|
|
1820
|
-
if (ts.isParameter(node)) return node.initializer ?? null;
|
|
1821
|
-
if (ts.isVariableDeclaration(node) || ts.isFunctionLike(node.parent)) return void 0;
|
|
1822
|
-
node = node.parent;
|
|
1823
|
-
}
|
|
1824
|
-
};
|
|
1825
|
-
/** Test whether a destructured value is the lazy state owned by React useState. */
|
|
1826
|
-
const isLazyStateBinding = (declaration) => {
|
|
1827
|
-
if (!ts.isBindingElement(declaration) || !ts.isArrayBindingPattern(declaration.parent) || declaration.parent.elements[0] !== declaration) return false;
|
|
1828
|
-
const variable = declaration.parent.parent;
|
|
1829
|
-
if (!ts.isVariableDeclaration(variable) || !variable.initializer) return false;
|
|
1830
|
-
const initializer = variable.initializer;
|
|
1831
|
-
if (!ts.isCallExpression(initializer) || !isReactCall(initializer, "useState")) return false;
|
|
1832
|
-
const initialState = initializer.arguments[0];
|
|
1833
|
-
return initialState !== void 0 && checker.getTypeAtLocation(initialState).getCallSignatures().length > 0;
|
|
1834
|
-
};
|
|
1835
|
-
/** Test whether an expression is a ref object created by React useRef. */
|
|
1836
|
-
const isStableRef = (input, seen = /* @__PURE__ */ new Set()) => {
|
|
1837
|
-
const node = unwrapExpression(input);
|
|
1838
|
-
if (node.type === "CallExpression") {
|
|
1839
|
-
const tsNode = getTsNode(node);
|
|
1840
|
-
return ts.isCallExpression(tsNode) && isReactCall(tsNode, "useRef");
|
|
1841
|
-
}
|
|
1842
|
-
if (node.type !== "Identifier") return false;
|
|
1843
|
-
const symbol = getIdentifierSymbol(node);
|
|
1844
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1845
|
-
seen.add(symbol);
|
|
1846
|
-
const sources = origins.resolve(symbol, node);
|
|
1847
|
-
const stable = sources.length > 0 && sources.every((source) => isStableRef(source, seen));
|
|
1848
|
-
seen.delete(symbol);
|
|
1849
|
-
return stable;
|
|
1850
|
-
};
|
|
1851
|
-
/** Prove a Provider value is retained outside the current render at this reference point. */
|
|
1852
|
-
const isStable = (input, seen = /* @__PURE__ */ new Set()) => {
|
|
1853
|
-
const node = unwrapExpression(input);
|
|
1854
|
-
if (isPrimitiveType(checker, getType(node))) return true;
|
|
1855
|
-
if (node.type === "ThisExpression") return true;
|
|
1856
|
-
if (node.type === "ObjectExpression" || node.type === "ArrayExpression" || node.type === "NewExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "ClassExpression") return false;
|
|
1857
|
-
if (node.type === "CallExpression") {
|
|
1858
|
-
const tsNode = getTsNode(node);
|
|
1859
|
-
return ts.isCallExpression(tsNode) && isReactCall(tsNode, "useMemo");
|
|
1860
|
-
}
|
|
1861
|
-
if (node.type === "MemberExpression") {
|
|
1862
|
-
if (!node.computed && node.property.type === "Identifier" && node.property.name === "current" && node.object.type === "Identifier") {
|
|
1863
|
-
const symbol = getIdentifierSymbol(node.object);
|
|
1864
|
-
const sources = symbol ? refCurrents.resolve(symbol, node) : [];
|
|
1865
|
-
if (sources.length > 0) return sources.every((source) => isStable(source, seen));
|
|
1866
|
-
return isStableRef(node.object);
|
|
1867
|
-
}
|
|
1868
|
-
return isStable(node.object, seen);
|
|
1869
|
-
}
|
|
1870
|
-
if (node.type === "ConditionalExpression") return isStable(node.consequent, seen) && isStable(node.alternate, seen);
|
|
1871
|
-
if (node.type === "LogicalExpression") return isStable(node.left, seen) && isStable(node.right, seen);
|
|
1872
|
-
if (node.type === "SequenceExpression") {
|
|
1873
|
-
const value = node.expressions.at(-1);
|
|
1874
|
-
return value ? isStable(value, seen) : true;
|
|
1875
|
-
}
|
|
1876
|
-
if (node.type !== "Identifier") return false;
|
|
1877
|
-
const symbol = getIdentifierSymbol(node);
|
|
1878
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1879
|
-
if (symbol.declarations?.some(isModuleDeclaration)) return true;
|
|
1880
|
-
if (symbol.declarations?.some(isLazyStateBinding)) return true;
|
|
1881
|
-
seen.add(symbol);
|
|
1882
|
-
const sources = origins.resolve(symbol, node);
|
|
1883
|
-
if (sources.length > 0) {
|
|
1884
|
-
const stable = sources.every((source) => isStable(source, seen));
|
|
1885
|
-
seen.delete(symbol);
|
|
1886
|
-
return stable;
|
|
1887
|
-
}
|
|
1888
|
-
let stable = false;
|
|
1889
|
-
for (const declaration of symbol.declarations ?? []) {
|
|
1890
|
-
const defaultValue = getParameterDefault(declaration);
|
|
1891
|
-
if (defaultValue !== void 0) {
|
|
1892
|
-
const expression = defaultValue ? services.tsNodeToESTreeNodeMap.get(defaultValue) : void 0;
|
|
1893
|
-
stable = expression ? isStable(expression, seen) : true;
|
|
1894
|
-
break;
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
seen.delete(symbol);
|
|
1898
|
-
return stable;
|
|
1899
|
-
};
|
|
1900
|
-
return {
|
|
1901
|
-
VariableDeclarator(node) {
|
|
1902
|
-
if (!node.init) return;
|
|
1903
|
-
const identifiers = [];
|
|
1904
|
-
collectPatternIdentifiers(node.id, identifiers);
|
|
1905
|
-
for (const identifier of identifiers) {
|
|
1906
|
-
const symbol = getIdentifierSymbol(identifier);
|
|
1907
|
-
if (symbol) origins.record(symbol, node.init, node);
|
|
1908
|
-
}
|
|
1909
|
-
},
|
|
1910
|
-
AssignmentExpression(node) {
|
|
1911
|
-
if (node.left.type === "Identifier") {
|
|
1912
|
-
const symbol = getIdentifierSymbol(node.left);
|
|
1913
|
-
if (symbol) origins.record(symbol, node.right, node);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
if (node.left.type !== "MemberExpression" || node.left.object.type !== "Identifier" || node.left.computed || node.left.property.type !== "Identifier" || node.left.property.name !== "current") return;
|
|
1917
|
-
const symbol = getIdentifierSymbol(node.left.object);
|
|
1918
|
-
if (symbol) refCurrents.record(symbol, node.right, node);
|
|
1919
|
-
},
|
|
1920
|
-
JSXAttribute(node) {
|
|
1921
|
-
if (node.name.type !== "JSXIdentifier" || node.name.name !== "store") return;
|
|
1922
|
-
const opening = node.parent;
|
|
1923
|
-
if (opening?.type !== "JSXOpeningElement" || !hasMarker(getType(opening.name), "externalStoreProvider")) return;
|
|
1924
|
-
if (!isRenderRoot(getOwner(node))) return;
|
|
1925
|
-
if (node.value?.type !== "JSXExpressionContainer" || node.value.expression.type === "JSXEmptyExpression") return;
|
|
1926
|
-
if (!isStable(node.value.expression)) context.report({
|
|
1927
|
-
node: node.value.expression,
|
|
1928
|
-
messageId: "unstableStore"
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
});
|
|
1934
|
-
//#endregion
|
|
1935
|
-
//#region src/rules/no-unstable-selector-value.ts
|
|
1936
|
-
/** Collect object literals that directly form selector return branches. */
|
|
1937
|
-
function collectSelectionObjects(input, objects, getInitializer, seen = /* @__PURE__ */ new Set()) {
|
|
1938
|
-
const node = unwrapExpression(input);
|
|
1939
|
-
if (seen.has(node)) return;
|
|
1940
|
-
seen.add(node);
|
|
1941
|
-
if (node.type === "ObjectExpression") {
|
|
1942
|
-
objects.push(node);
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
if (node.type === "Identifier") {
|
|
1946
|
-
const initializer = getInitializer(node);
|
|
1947
|
-
if (initializer) collectSelectionObjects(initializer, objects, getInitializer, seen);
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
if (node.type === "ConditionalExpression") {
|
|
1951
|
-
collectSelectionObjects(node.consequent, objects, getInitializer, seen);
|
|
1952
|
-
collectSelectionObjects(node.alternate, objects, getInitializer, seen);
|
|
1953
|
-
} else if (node.type === "LogicalExpression") {
|
|
1954
|
-
collectSelectionObjects(node.left, objects, getInitializer, seen);
|
|
1955
|
-
collectSelectionObjects(node.right, objects, getInitializer, seen);
|
|
1956
|
-
} else if (node.type === "SequenceExpression") {
|
|
1957
|
-
const result = node.expressions.at(-1);
|
|
1958
|
-
if (result) collectSelectionObjects(result, objects, getInitializer, seen);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
const noUnstableSelectorValue = createRule({
|
|
1962
|
-
name: "no-unstable-selector-value",
|
|
1963
|
-
meta: {
|
|
1964
|
-
type: "problem",
|
|
1965
|
-
docs: { description: "Prevent selector fields from allocating unstable references." },
|
|
1966
|
-
schema: [],
|
|
1967
|
-
messages: { unstableValue: "Selector fields must not create a new reference on every call." }
|
|
1968
|
-
},
|
|
1969
|
-
defaultOptions: [],
|
|
1970
|
-
create(context) {
|
|
1971
|
-
const { checker, getIdentifierSymbol, getType, isStoreHookCall, services } = createKerrosTypeTools(context);
|
|
1972
|
-
/** Resolve a non-module variable to the expression assigned in its declaration. */
|
|
1973
|
-
const getLocalInitializer = (node) => {
|
|
1974
|
-
const declaration = getIdentifierSymbol(node)?.declarations?.find(ts.isVariableDeclaration);
|
|
1975
|
-
if (!declaration?.initializer || isModuleDeclaration(declaration)) return void 0;
|
|
1976
|
-
return services.tsNodeToESTreeNodeMap.get(declaration.initializer);
|
|
1977
|
-
};
|
|
1978
|
-
/** Test whether an expression preserves a primitive or previously cached reference. */
|
|
1979
|
-
const isStable = (input, seen = /* @__PURE__ */ new Set()) => {
|
|
1980
|
-
const node = unwrapExpression(input);
|
|
1981
|
-
if (node.type === "Literal") return !("regex" in node);
|
|
1982
|
-
if (node.type === "MemberExpression") return true;
|
|
1983
|
-
if (node.type === "ConditionalExpression") return isStable(node.consequent, seen) && isStable(node.alternate, seen);
|
|
1984
|
-
if (node.type === "LogicalExpression") return isStable(node.left, seen) && isStable(node.right, seen);
|
|
1985
|
-
if (node.type === "SequenceExpression") {
|
|
1986
|
-
const result = node.expressions.at(-1);
|
|
1987
|
-
return result ? isStable(result, seen) : true;
|
|
1988
|
-
}
|
|
1989
|
-
if (node.type === "Identifier") {
|
|
1990
|
-
const symbol = getIdentifierSymbol(node);
|
|
1991
|
-
if (symbol?.declarations?.some(isModuleDeclaration)) return true;
|
|
1992
|
-
if (isPrimitiveType(checker, getType(node))) return true;
|
|
1993
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
1994
|
-
seen.add(symbol);
|
|
1995
|
-
const declaration = symbol.declarations?.find(ts.isVariableDeclaration);
|
|
1996
|
-
const initializer = declaration?.initializer ? services.tsNodeToESTreeNodeMap.get(declaration.initializer) : void 0;
|
|
1997
|
-
const stable = initializer ? isStable(initializer, seen) : false;
|
|
1998
|
-
seen.delete(symbol);
|
|
1999
|
-
return stable;
|
|
2000
|
-
}
|
|
2001
|
-
if (node.type === "ObjectExpression" || node.type === "ArrayExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "ClassExpression" || node.type === "NewExpression" || node.type === "JSXElement" || node.type === "JSXFragment") return false;
|
|
2002
|
-
return isPrimitiveType(checker, getType(node));
|
|
2003
|
-
};
|
|
2004
|
-
return { CallExpression(node) {
|
|
2005
|
-
const selector = getInlineSelector(node, isStoreHookCall);
|
|
2006
|
-
if (!selector) return;
|
|
2007
|
-
const objects = [];
|
|
2008
|
-
for (const expression of getReturnedExpressions(selector)) collectSelectionObjects(expression, objects, getLocalInitializer);
|
|
2009
|
-
for (const object of objects) for (const property of object.properties) if (property.type === "Property" && !isStable(property.value)) context.report({
|
|
2010
|
-
node: property.value,
|
|
2011
|
-
messageId: "unstableValue"
|
|
2012
|
-
});
|
|
2013
|
-
} };
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
//#endregion
|
|
2017
|
-
//#region src/rules/no-whole-store-selector.ts
|
|
2018
|
-
/** Collect simple local aliases of a selector's complete Store parameter. */
|
|
2019
|
-
function getStoreAliases(selector, parameter, getIdentifierSymbol) {
|
|
2020
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
2021
|
-
const parameterSymbol = getIdentifierSymbol(parameter);
|
|
2022
|
-
if (parameterSymbol) aliases.add(parameterSymbol);
|
|
2023
|
-
if (selector.body.type !== "BlockStatement") return aliases;
|
|
2024
|
-
const declarations = [];
|
|
2025
|
-
const collectDeclarations = (node) => {
|
|
2026
|
-
if (node !== selector.body && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration")) return;
|
|
2027
|
-
if (node.type === "VariableDeclarator") declarations.push(node);
|
|
2028
|
-
for (const key of Object.keys(node)) {
|
|
2029
|
-
if (key === "parent" || key === "range" || key === "loc") continue;
|
|
2030
|
-
const value = node[key];
|
|
2031
|
-
if (Array.isArray(value)) {
|
|
2032
|
-
for (const child of value) if (child && typeof child === "object" && "type" in child) collectDeclarations(child);
|
|
2033
|
-
} else if (value && typeof value === "object" && "type" in value) collectDeclarations(value);
|
|
2034
|
-
}
|
|
2035
|
-
};
|
|
2036
|
-
collectDeclarations(selector.body);
|
|
2037
|
-
let changed = true;
|
|
2038
|
-
while (changed) {
|
|
2039
|
-
changed = false;
|
|
2040
|
-
for (const declaration of declarations) {
|
|
2041
|
-
if (declaration.id.type !== "Identifier" || !declaration.init) continue;
|
|
2042
|
-
const value = unwrapExpression(declaration.init);
|
|
2043
|
-
if (value.type !== "Identifier") continue;
|
|
2044
|
-
const valueSymbol = getIdentifierSymbol(value);
|
|
2045
|
-
const declarationSymbol = getIdentifierSymbol(declaration.id);
|
|
2046
|
-
if (valueSymbol && declarationSymbol && aliases.has(valueSymbol) && !aliases.has(declarationSymbol)) {
|
|
2047
|
-
aliases.add(declarationSymbol);
|
|
2048
|
-
changed = true;
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
return aliases;
|
|
2053
|
-
}
|
|
2054
|
-
/** Test whether an expression returns or embeds the complete Store value. */
|
|
2055
|
-
function containsWholeStore(node, aliases, getIdentifierSymbol) {
|
|
2056
|
-
const expression = unwrapExpression(node);
|
|
2057
|
-
if (expression.type === "Identifier") {
|
|
2058
|
-
const symbol = getIdentifierSymbol(expression);
|
|
2059
|
-
return symbol ? aliases.has(symbol) : false;
|
|
2060
|
-
}
|
|
2061
|
-
if (expression.type === "ObjectExpression") return expression.properties.some((property) => {
|
|
2062
|
-
return property.type === "SpreadElement" ? containsWholeStore(property.argument, aliases, getIdentifierSymbol) : containsWholeStore(property.value, aliases, getIdentifierSymbol);
|
|
2063
|
-
});
|
|
2064
|
-
if (expression.type === "ArrayExpression") return expression.elements.some((element) => {
|
|
2065
|
-
return element?.type === "SpreadElement" ? containsWholeStore(element.argument, aliases, getIdentifierSymbol) : element ? containsWholeStore(element, aliases, getIdentifierSymbol) : false;
|
|
2066
|
-
});
|
|
2067
|
-
if (expression.type === "ConditionalExpression") return containsWholeStore(expression.consequent, aliases, getIdentifierSymbol) || containsWholeStore(expression.alternate, aliases, getIdentifierSymbol);
|
|
2068
|
-
if (expression.type === "LogicalExpression") return containsWholeStore(expression.left, aliases, getIdentifierSymbol) || containsWholeStore(expression.right, aliases, getIdentifierSymbol);
|
|
2069
|
-
if (expression.type === "SequenceExpression") {
|
|
2070
|
-
const result = expression.expressions.at(-1);
|
|
2071
|
-
return result ? containsWholeStore(result, aliases, getIdentifierSymbol) : false;
|
|
2072
|
-
}
|
|
2073
|
-
if (expression.type === "AssignmentExpression") return containsWholeStore(expression.right, aliases, getIdentifierSymbol);
|
|
2074
|
-
if (expression.type === "AwaitExpression" || expression.type === "YieldExpression") return expression.argument ? containsWholeStore(expression.argument, aliases, getIdentifierSymbol) : false;
|
|
2075
|
-
return false;
|
|
2076
|
-
}
|
|
2077
|
-
const noWholeStoreSelector = createRule({
|
|
2078
|
-
name: "no-whole-store-selector",
|
|
2079
|
-
meta: {
|
|
2080
|
-
type: "problem",
|
|
2081
|
-
docs: { description: "Prevent selectors from returning the complete Store." },
|
|
351
|
+
docs: { description: "Prevent file-local Store selectors from returning the complete Store." },
|
|
2082
352
|
schema: [],
|
|
2083
353
|
messages: { wholeStore: "A selector cannot return or wrap the complete Store." }
|
|
2084
354
|
},
|
|
2085
355
|
defaultOptions: [],
|
|
2086
356
|
create(context) {
|
|
2087
|
-
const {
|
|
357
|
+
const { isStoreHookCall } = createLiteSyntaxTools(context);
|
|
2088
358
|
return { CallExpression(node) {
|
|
2089
359
|
if (!isStoreHookCall(node)) return;
|
|
2090
360
|
const selector = node.arguments[0];
|
|
2091
361
|
if (!selector || selector.type === "SpreadElement" || selector.type !== "ArrowFunctionExpression" && selector.type !== "FunctionExpression") return;
|
|
2092
362
|
const parameter = selector.params[0];
|
|
2093
363
|
if (!parameter || parameter.type !== "Identifier") return;
|
|
2094
|
-
const aliases =
|
|
2095
|
-
if (getReturnedExpressions(selector).some((expression) => {
|
|
2096
|
-
return containsWholeStore(expression, aliases, getIdentifierSymbol);
|
|
2097
|
-
})) context.report({
|
|
364
|
+
const aliases = getAliases(selector, parameter.name);
|
|
365
|
+
if (getReturnedExpressions(selector).some((expression) => containsWholeStore(expression, aliases))) context.report({
|
|
2098
366
|
node: selector,
|
|
2099
367
|
messageId: "wholeStore"
|
|
2100
368
|
});
|
|
2101
369
|
} };
|
|
2102
370
|
}
|
|
2103
371
|
});
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
messages: { bindExternalStore: "Use bindStore for an existing external Store." }
|
|
2113
|
-
},
|
|
2114
|
-
defaultOptions: [],
|
|
2115
|
-
create(context) {
|
|
2116
|
-
const { getFactoryKind, getModelFunction, isReactCall } = createKerrosTypeTools(context);
|
|
2117
|
-
/** Test only calls executed directly by the model body, excluding returned callbacks. */
|
|
2118
|
-
const readsExternalStore = (model) => {
|
|
2119
|
-
if (!model.body) return false;
|
|
2120
|
-
let found = false;
|
|
2121
|
-
/** Scan the model body without crossing into nested function lifecycles. */
|
|
2122
|
-
const visit = (node) => {
|
|
2123
|
-
if (found) return;
|
|
2124
|
-
if (node !== model && ts.isFunctionLike(node)) return;
|
|
2125
|
-
if (ts.isCallExpression(node) && isReactCall(node, "useSyncExternalStore")) {
|
|
2126
|
-
found = true;
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
ts.forEachChild(node, visit);
|
|
2130
|
-
};
|
|
2131
|
-
visit(model.body);
|
|
2132
|
-
return found;
|
|
2133
|
-
};
|
|
2134
|
-
return { CallExpression(node) {
|
|
2135
|
-
if (getFactoryKind(node) !== "createStore") return;
|
|
2136
|
-
const model = node.arguments[0];
|
|
2137
|
-
if (!model || model.type === "SpreadElement") return;
|
|
2138
|
-
const declaration = getModelFunction(model);
|
|
2139
|
-
if (declaration && readsExternalStore(declaration)) context.report({
|
|
2140
|
-
node: model,
|
|
2141
|
-
messageId: "bindExternalStore"
|
|
2142
|
-
});
|
|
2143
|
-
} };
|
|
2144
|
-
}
|
|
2145
|
-
});
|
|
2146
|
-
//#endregion
|
|
2147
|
-
//#region src/rules/pure-selector.ts
|
|
2148
|
-
const globalPureFunctions = /* @__PURE__ */ new Set([
|
|
2149
|
-
"BigInt",
|
|
2150
|
-
"Boolean",
|
|
2151
|
-
"Number",
|
|
2152
|
-
"String",
|
|
2153
|
-
"decodeURI",
|
|
2154
|
-
"decodeURIComponent",
|
|
2155
|
-
"encodeURI",
|
|
2156
|
-
"encodeURIComponent",
|
|
2157
|
-
"isFinite",
|
|
2158
|
-
"isNaN",
|
|
2159
|
-
"parseFloat",
|
|
2160
|
-
"parseInt"
|
|
2161
|
-
]);
|
|
2162
|
-
const pureStaticMethods = {
|
|
2163
|
-
Array: /* @__PURE__ */ new Set(["isArray"]),
|
|
2164
|
-
JSON: /* @__PURE__ */ new Set(["parse", "stringify"]),
|
|
2165
|
-
Number: /* @__PURE__ */ new Set([
|
|
2166
|
-
"isFinite",
|
|
2167
|
-
"isInteger",
|
|
2168
|
-
"isNaN",
|
|
2169
|
-
"isSafeInteger",
|
|
2170
|
-
"parseFloat",
|
|
2171
|
-
"parseInt"
|
|
2172
|
-
]),
|
|
2173
|
-
Object: /* @__PURE__ */ new Set([
|
|
2174
|
-
"entries",
|
|
2175
|
-
"getOwnPropertyDescriptor",
|
|
2176
|
-
"getOwnPropertyDescriptors",
|
|
2177
|
-
"getOwnPropertyNames",
|
|
2178
|
-
"getOwnPropertySymbols",
|
|
2179
|
-
"getPrototypeOf",
|
|
2180
|
-
"hasOwn",
|
|
2181
|
-
"is",
|
|
2182
|
-
"isExtensible",
|
|
2183
|
-
"isFrozen",
|
|
2184
|
-
"isSealed",
|
|
2185
|
-
"keys",
|
|
2186
|
-
"values"
|
|
2187
|
-
]),
|
|
2188
|
-
Promise: /* @__PURE__ */ new Set([
|
|
2189
|
-
"all",
|
|
2190
|
-
"allSettled",
|
|
2191
|
-
"any",
|
|
2192
|
-
"race",
|
|
2193
|
-
"reject",
|
|
2194
|
-
"resolve"
|
|
2195
|
-
]),
|
|
2196
|
-
String: /* @__PURE__ */ new Set([
|
|
2197
|
-
"fromCharCode",
|
|
2198
|
-
"fromCodePoint",
|
|
2199
|
-
"raw"
|
|
2200
|
-
])
|
|
2201
|
-
};
|
|
2202
|
-
const readonlyMethods = /* @__PURE__ */ new Set([
|
|
2203
|
-
"at",
|
|
2204
|
-
"concat",
|
|
2205
|
-
"endsWith",
|
|
2206
|
-
"entries",
|
|
2207
|
-
"every",
|
|
2208
|
-
"filter",
|
|
2209
|
-
"find",
|
|
2210
|
-
"findIndex",
|
|
2211
|
-
"findLast",
|
|
2212
|
-
"findLastIndex",
|
|
2213
|
-
"flat",
|
|
2214
|
-
"flatMap",
|
|
2215
|
-
"forEach",
|
|
2216
|
-
"get",
|
|
2217
|
-
"has",
|
|
2218
|
-
"includes",
|
|
2219
|
-
"indexOf",
|
|
2220
|
-
"join",
|
|
2221
|
-
"keys",
|
|
2222
|
-
"lastIndexOf",
|
|
2223
|
-
"map",
|
|
2224
|
-
"match",
|
|
2225
|
-
"matchAll",
|
|
2226
|
-
"reduce",
|
|
2227
|
-
"reduceRight",
|
|
2228
|
-
"replace",
|
|
2229
|
-
"replaceAll",
|
|
2230
|
-
"search",
|
|
2231
|
-
"slice",
|
|
2232
|
-
"some",
|
|
2233
|
-
"split",
|
|
2234
|
-
"startsWith",
|
|
2235
|
-
"substring",
|
|
2236
|
-
"substr",
|
|
2237
|
-
"toLocaleLowerCase",
|
|
2238
|
-
"toLocaleUpperCase",
|
|
2239
|
-
"toLowerCase",
|
|
2240
|
-
"toReversed",
|
|
2241
|
-
"toSorted",
|
|
2242
|
-
"toSpliced",
|
|
2243
|
-
"toString",
|
|
2244
|
-
"toUpperCase",
|
|
2245
|
-
"trim",
|
|
2246
|
-
"trimEnd",
|
|
2247
|
-
"trimStart",
|
|
2248
|
-
"valueOf",
|
|
2249
|
-
"values",
|
|
2250
|
-
"with"
|
|
2251
|
-
]);
|
|
2252
|
-
const pureSelector = createRule({
|
|
2253
|
-
name: "pure-selector",
|
|
2254
|
-
meta: {
|
|
2255
|
-
type: "problem",
|
|
2256
|
-
docs: { description: "Prevent side effects and mutable operations inside selectors." },
|
|
2257
|
-
schema: [],
|
|
2258
|
-
messages: { impureSelector: "Selectors must be pure." }
|
|
2259
|
-
},
|
|
2260
|
-
defaultOptions: [],
|
|
2261
|
-
create(context) {
|
|
2262
|
-
const { checker, getIdentifierSymbol, getType, isStoreHookCall, services } = createKerrosTypeTools(context);
|
|
2263
|
-
/** Test whether an identifier is the matching JavaScript global declaration. */
|
|
2264
|
-
const isGlobal = (node, names) => {
|
|
2265
|
-
if (!names.has(node.name)) return false;
|
|
2266
|
-
return getIdentifierSymbol(node)?.declarations?.some((declaration) => {
|
|
2267
|
-
return services.program.isSourceFileDefaultLibrary(declaration.getSourceFile());
|
|
2268
|
-
}) === true;
|
|
2269
|
-
};
|
|
2270
|
-
/** Resolve a callback through local declarations and symbol-safe aliases. */
|
|
2271
|
-
const resolveCallback = (input, seen = /* @__PURE__ */ new Set()) => {
|
|
2272
|
-
const node = unwrapExpression(input);
|
|
2273
|
-
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return node;
|
|
2274
|
-
if (node.type !== "Identifier") return void 0;
|
|
2275
|
-
const symbol = getIdentifierSymbol(node);
|
|
2276
|
-
if (!symbol || seen.has(symbol)) return void 0;
|
|
2277
|
-
seen.add(symbol);
|
|
2278
|
-
for (const declaration of symbol.declarations ?? []) {
|
|
2279
|
-
if (ts.isFunctionDeclaration(declaration)) {
|
|
2280
|
-
const callback = services.tsNodeToESTreeNodeMap.get(declaration);
|
|
2281
|
-
if (callback.type === "FunctionDeclaration") return callback;
|
|
2282
|
-
}
|
|
2283
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
2284
|
-
const initializer = services.tsNodeToESTreeNodeMap.get(declaration.initializer);
|
|
2285
|
-
const callback = resolveCallback(initializer, seen);
|
|
2286
|
-
if (callback) return callback;
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
};
|
|
2290
|
-
/** Test whether a call belongs to a small, side-effect-free built-in surface. */
|
|
2291
|
-
const isKnownPureCall = (node) => {
|
|
2292
|
-
const callee = unwrapExpression(node.callee);
|
|
2293
|
-
if (callee.type === "Identifier") return isGlobal(callee, globalPureFunctions);
|
|
2294
|
-
if (callee.type !== "MemberExpression") return false;
|
|
2295
|
-
const name = getMemberName(callee);
|
|
2296
|
-
if (!name) return false;
|
|
2297
|
-
const object = unwrapExpression(callee.object);
|
|
2298
|
-
if (object.type === "Identifier" && object.name in pureStaticMethods) {
|
|
2299
|
-
const objectName = object.name;
|
|
2300
|
-
return pureStaticMethods[objectName].has(name) && isGlobal(object, /* @__PURE__ */ new Set([objectName]));
|
|
2301
|
-
}
|
|
2302
|
-
if (object.type === "Identifier" && object.name === "Math") return name !== "random" && isGlobal(object, /* @__PURE__ */ new Set(["Math"]));
|
|
2303
|
-
const kind = getBuiltinTypeKind(checker, services.program, getType(callee.object));
|
|
2304
|
-
return readonlyMethods.has(name) && kind !== void 0;
|
|
2305
|
-
};
|
|
2306
|
-
return { CallExpression(node) {
|
|
2307
|
-
const selector = getInlineSelector(node, isStoreHookCall);
|
|
2308
|
-
if (!selector) return;
|
|
2309
|
-
if (selector.async || selector.generator) context.report({
|
|
2310
|
-
node: selector,
|
|
2311
|
-
messageId: "impureSelector"
|
|
2312
|
-
});
|
|
2313
|
-
const visitedCallbacks = /* @__PURE__ */ new Set();
|
|
2314
|
-
/** Scan one synchronous collection callback at most once. */
|
|
2315
|
-
function scanCallback(callback) {
|
|
2316
|
-
if (visitedCallbacks.has(callback)) return;
|
|
2317
|
-
visitedCallbacks.add(callback);
|
|
2318
|
-
visitSubtree(callback.body, checkNode, true);
|
|
2319
|
-
}
|
|
2320
|
-
/** Report one syntax node that executes as part of the selector call. */
|
|
2321
|
-
function checkNode(child) {
|
|
2322
|
-
if (child.type === "AssignmentExpression" || child.type === "UpdateExpression" || child.type === "AwaitExpression" || child.type === "NewExpression" && child.parent?.type !== "ThrowStatement" || child.type === "YieldExpression" || child.type === "ThrowStatement" || child.type === "UnaryExpression" && child.operator === "delete") {
|
|
2323
|
-
context.report({
|
|
2324
|
-
node: child,
|
|
2325
|
-
messageId: "impureSelector"
|
|
2326
|
-
});
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
if (child.type !== "CallExpression") return;
|
|
2330
|
-
if (!(!isMutableCollectionCall(child, checker, services.program, getType) && isKnownPureCall(child))) {
|
|
2331
|
-
context.report({
|
|
2332
|
-
node: child,
|
|
2333
|
-
messageId: "impureSelector"
|
|
2334
|
-
});
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
for (const argument of child.arguments) {
|
|
2338
|
-
if (argument.type === "SpreadElement") continue;
|
|
2339
|
-
const callback = resolveCallback(argument);
|
|
2340
|
-
if (callback) scanCallback(callback);
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
visitSubtree(selector.body, checkNode, true);
|
|
2344
|
-
} };
|
|
2345
|
-
}
|
|
2346
|
-
});
|
|
2347
|
-
//#endregion
|
|
2348
|
-
//#region src/rules/require-cached-snapshot.ts
|
|
2349
|
-
/** Test whether a declaration executes inside one snapshot reader invocation. */
|
|
2350
|
-
function isInsideFunction(node, owner) {
|
|
2351
|
-
let current = node;
|
|
2352
|
-
while (current) {
|
|
2353
|
-
if (current === owner) return true;
|
|
2354
|
-
current = current.parent;
|
|
2355
|
-
}
|
|
2356
|
-
return false;
|
|
372
|
+
/** Read the argument of a broad object operation. */
|
|
373
|
+
function getBroadArgument(node) {
|
|
374
|
+
const [argument] = node.arguments;
|
|
375
|
+
if (!argument || argument.type === "SpreadElement") return;
|
|
376
|
+
const callee = node.callee;
|
|
377
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.object.type !== "Identifier" || callee.property.type !== "Identifier") return;
|
|
378
|
+
if (callee.object.name === "Object" && objectEnumerationMethods.has(callee.property.name)) return argument;
|
|
379
|
+
if (callee.object.name === "JSON" && callee.property.name === "stringify") return argument;
|
|
2357
380
|
}
|
|
2358
381
|
//#endregion
|
|
2359
382
|
//#region src/index.ts
|
|
2360
383
|
const rules = {
|
|
2361
|
-
"binding-naming":
|
|
2362
|
-
"factory-at-module-scope":
|
|
2363
|
-
"model-convention":
|
|
2364
|
-
"no-broad-store-access":
|
|
2365
|
-
|
|
2366
|
-
"no-effect-event-action": noEffectEventAction,
|
|
2367
|
-
"no-provider-key-prop": noProviderKeyProp,
|
|
2368
|
-
"no-render-instance-snapshot": noRenderInstanceSnapshot,
|
|
2369
|
-
"no-store-mutation": noStoreMutation,
|
|
2370
|
-
"no-unstable-bound-store": noUnstableBoundStore,
|
|
2371
|
-
"no-unstable-selector-value": noUnstableSelectorValue,
|
|
2372
|
-
"no-whole-store-selector": noWholeStoreSelector,
|
|
2373
|
-
"prefer-bind-store": preferBindStore,
|
|
2374
|
-
"pure-selector": pureSelector,
|
|
2375
|
-
"require-cached-snapshot": createRule({
|
|
2376
|
-
name: "require-cached-snapshot",
|
|
384
|
+
"binding-naming": liteBindingNaming,
|
|
385
|
+
"factory-at-module-scope": liteFactoryAtModuleScope,
|
|
386
|
+
"model-convention": liteModelConvention,
|
|
387
|
+
"no-broad-store-access": createRule({
|
|
388
|
+
name: "no-broad-store-access",
|
|
2377
389
|
meta: {
|
|
2378
390
|
type: "problem",
|
|
2379
|
-
docs: { description: "
|
|
391
|
+
docs: { description: "Prevent broad access to file-local selector-free Store snapshots." },
|
|
2380
392
|
schema: [],
|
|
2381
|
-
messages: {
|
|
393
|
+
messages: { broadAccess: "Do not enumerate, serialize, or spread a complete selector-free Store snapshot." }
|
|
2382
394
|
},
|
|
2383
395
|
defaultOptions: [],
|
|
2384
396
|
create(context) {
|
|
2385
|
-
const {
|
|
2386
|
-
|
|
2387
|
-
const
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const resolveNode = (node) => {
|
|
2392
|
-
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
2393
|
-
if (node.body) implementations.add(node);
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
if ((ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node)) && node.initializer) {
|
|
2397
|
-
resolveNode(node.initializer);
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
if (ts.isPropertyAssignment(node)) {
|
|
2401
|
-
resolveNode(node.initializer);
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
if (ts.isShorthandPropertyAssignment(node)) {
|
|
2405
|
-
const value = checker.getShorthandAssignmentValueSymbol(node);
|
|
2406
|
-
if (value) resolveSymbol(value);
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
if (ts.isIdentifier(node)) {
|
|
2410
|
-
const value = getTsSymbol(node);
|
|
2411
|
-
if (value) resolveSymbol(value);
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2414
|
-
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) resolveNode(node.expression);
|
|
2415
|
-
};
|
|
2416
|
-
/** Follow a symbol's declarations once to avoid recursive aliases. */
|
|
2417
|
-
function resolveSymbol(candidate) {
|
|
2418
|
-
if (seen.has(candidate)) return;
|
|
2419
|
-
seen.add(candidate);
|
|
2420
|
-
for (const declaration of candidate.declarations ?? []) resolveNode(declaration);
|
|
2421
|
-
}
|
|
2422
|
-
resolveSymbol(symbol);
|
|
2423
|
-
return implementations;
|
|
2424
|
-
};
|
|
2425
|
-
/** Prove that a snapshot value is primitive or allocated outside the reader invocation. */
|
|
2426
|
-
const isCached = (input, owner, seen = /* @__PURE__ */ new Set()) => {
|
|
2427
|
-
const node = unwrapTsExpression(input);
|
|
2428
|
-
const type = checker.getTypeAtLocation(node);
|
|
2429
|
-
if (isPrimitiveType(checker, type)) return true;
|
|
2430
|
-
if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node) || ts.isNewExpression(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isClassExpression(node) || ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node) || ts.isRegularExpressionLiteral(node)) return false;
|
|
2431
|
-
if (node.kind === ts.SyntaxKind.ThisKeyword) return true;
|
|
2432
|
-
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
2433
|
-
const symbol = (ts.isPropertyAccessExpression(node) ? checker.getSymbolAtLocation(node.name) : checker.getSymbolAtLocation(node.argumentExpression)) ?? checker.getSymbolAtLocation(node);
|
|
2434
|
-
const getters = symbol?.declarations?.filter(ts.isGetAccessorDeclaration) ?? [];
|
|
2435
|
-
if (getters.length > 0) {
|
|
2436
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
2437
|
-
seen.add(symbol);
|
|
2438
|
-
const cached = getters.every((getter) => {
|
|
2439
|
-
const returns = getTsReturnExpressions(getter);
|
|
2440
|
-
return returns.length > 0 && returns.every((value) => isCached(value, getter, seen));
|
|
2441
|
-
});
|
|
2442
|
-
seen.delete(symbol);
|
|
2443
|
-
return cached;
|
|
2444
|
-
}
|
|
2445
|
-
if (symbol?.declarations?.some(ts.isPropertyDeclaration)) return true;
|
|
2446
|
-
return isCached(node.expression, owner, seen);
|
|
2447
|
-
}
|
|
2448
|
-
if (ts.isConditionalExpression(node)) return isCached(node.whenTrue, owner, seen) && isCached(node.whenFalse, owner, seen);
|
|
2449
|
-
if (ts.isBinaryExpression(node) && (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken || node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) return isCached(node.left, owner, seen) && isCached(node.right, owner, seen);
|
|
2450
|
-
if (!ts.isIdentifier(node)) return false;
|
|
2451
|
-
const symbol = getTsSymbol(node);
|
|
2452
|
-
if (!symbol || seen.has(symbol)) return false;
|
|
2453
|
-
seen.add(symbol);
|
|
2454
|
-
let cached = false;
|
|
2455
|
-
for (const declaration of symbol.declarations ?? []) {
|
|
2456
|
-
if (!isInsideFunction(declaration, owner)) {
|
|
2457
|
-
cached = true;
|
|
2458
|
-
break;
|
|
2459
|
-
}
|
|
2460
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
2461
|
-
cached = isCached(declaration.initializer, owner, seen);
|
|
2462
|
-
if (cached) break;
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
seen.delete(symbol);
|
|
2466
|
-
return cached;
|
|
397
|
+
const { isStoreHookCall } = createLiteSyntaxTools(context);
|
|
398
|
+
const snapshots = /* @__PURE__ */ new Set();
|
|
399
|
+
const isSnapshot = (node) => {
|
|
400
|
+
const expression = unwrapExpression(node);
|
|
401
|
+
if (expression.type === "Identifier") return snapshots.has(expression.name);
|
|
402
|
+
return expression.type === "CallExpression" && expression.arguments.length === 0 && isStoreHookCall(expression);
|
|
2467
403
|
};
|
|
2468
|
-
|
|
2469
|
-
if (
|
|
2470
|
-
const tsNode = getTsNode(node);
|
|
2471
|
-
if (!ts.isCallExpression(tsNode)) return;
|
|
2472
|
-
const signature = checker.getResolvedSignature(tsNode);
|
|
2473
|
-
if (!signature) return;
|
|
2474
|
-
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
2475
|
-
const providerProperty = checker.getPropertyOfType(returnType, "1");
|
|
2476
|
-
const providerType = providerProperty ? checker.getTypeOfSymbolAtLocation(providerProperty, tsNode) : void 0;
|
|
2477
|
-
const storeType = providerType ? getMarkerType(providerType, "externalStoreProvider", tsNode) : void 0;
|
|
2478
|
-
const snapshot = storeType ? getTypeProperty(checker, storeType, "getSnapshot") : void 0;
|
|
2479
|
-
if (!snapshot) return;
|
|
2480
|
-
if ([...getImplementations(snapshot)].some((implementation) => {
|
|
2481
|
-
return getTsReturnExpressions(implementation).some((value) => !isCached(value, implementation));
|
|
2482
|
-
})) context.report({
|
|
404
|
+
const report = (node) => {
|
|
405
|
+
if (isSnapshot(node)) context.report({
|
|
2483
406
|
node,
|
|
2484
|
-
messageId: "
|
|
407
|
+
messageId: "broadAccess"
|
|
2485
408
|
});
|
|
2486
|
-
}
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
VariableDeclarator(node) {
|
|
412
|
+
if (!node.init) return;
|
|
413
|
+
if (node.id.type === "Identifier" && isSnapshot(node.init)) snapshots.add(node.id.name);
|
|
414
|
+
if (node.id.type === "ObjectPattern" && node.id.properties.some((property) => property.type === "RestElement")) report(node.init);
|
|
415
|
+
},
|
|
416
|
+
AssignmentExpression(node) {
|
|
417
|
+
if (node.left.type === "Identifier" && isSnapshot(node.right)) snapshots.add(node.left.name);
|
|
418
|
+
},
|
|
419
|
+
CallExpression(node) {
|
|
420
|
+
const argument = getBroadArgument(node);
|
|
421
|
+
if (argument) report(argument);
|
|
422
|
+
},
|
|
423
|
+
SpreadElement(node) {
|
|
424
|
+
report(node.argument);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
2487
427
|
}
|
|
2488
428
|
}),
|
|
2489
|
-
"
|
|
2490
|
-
|
|
2491
|
-
meta: {
|
|
2492
|
-
type: "suggestion",
|
|
2493
|
-
docs: { description: "Use s as the conventional Kerros selector parameter name." },
|
|
2494
|
-
schema: [],
|
|
2495
|
-
messages: { parameterName: "Name the Store selector parameter s." }
|
|
2496
|
-
},
|
|
2497
|
-
defaultOptions: [],
|
|
2498
|
-
create(context) {
|
|
2499
|
-
const { isStoreHookCall } = createKerrosTypeTools(context);
|
|
2500
|
-
return { CallExpression(node) {
|
|
2501
|
-
if (!isStoreHookCall(node)) return;
|
|
2502
|
-
const selector = node.arguments[0];
|
|
2503
|
-
if (!selector || selector.type === "SpreadElement" || selector.type !== "ArrowFunctionExpression" && selector.type !== "FunctionExpression") return;
|
|
2504
|
-
const parameter = selector.params[0];
|
|
2505
|
-
if (parameter && (parameter.type !== "Identifier" || parameter.name !== "s")) context.report({
|
|
2506
|
-
node: parameter,
|
|
2507
|
-
messageId: "parameterName"
|
|
2508
|
-
});
|
|
2509
|
-
} };
|
|
2510
|
-
}
|
|
2511
|
-
})
|
|
429
|
+
"no-whole-store-selector": liteNoWholeStoreSelector,
|
|
430
|
+
"selector-parameter-name": liteSelectorParameterName
|
|
2512
431
|
};
|
|
2513
432
|
const plugin = {
|
|
2514
433
|
meta: {
|
|
2515
434
|
name: "@violetflux/eslint-plugin-kerros",
|
|
2516
|
-
version: "0.3.
|
|
435
|
+
version: "0.3.5"
|
|
2517
436
|
},
|
|
2518
437
|
rules
|
|
2519
438
|
};
|
|
2520
439
|
const recommendedRules = Object.fromEntries(Object.keys(rules).map((name) => [`kerros/${name}`, "error"]));
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
};
|
|
2528
|
-
const configs = {
|
|
2529
|
-
fastTypeChecked: {
|
|
2530
|
-
name: "kerros/fast-type-checked",
|
|
2531
|
-
files: ["**/*.{ts,tsx,mts,cts}"],
|
|
2532
|
-
languageOptions: {
|
|
2533
|
-
parser,
|
|
2534
|
-
parserOptions: { projectService: true }
|
|
2535
|
-
},
|
|
2536
|
-
plugins: { kerros: plugin },
|
|
2537
|
-
rules: fastRules
|
|
2538
|
-
},
|
|
2539
|
-
recommendedTypeChecked: {
|
|
2540
|
-
name: "kerros/recommended-type-checked",
|
|
2541
|
-
files: ["**/*.{ts,tsx,mts,cts}"],
|
|
2542
|
-
languageOptions: {
|
|
2543
|
-
parser,
|
|
2544
|
-
parserOptions: { projectService: true }
|
|
2545
|
-
},
|
|
2546
|
-
plugins: { kerros: plugin },
|
|
2547
|
-
rules: recommendedRules
|
|
2548
|
-
}
|
|
2549
|
-
};
|
|
440
|
+
const configs = { recommended: {
|
|
441
|
+
name: "kerros/recommended",
|
|
442
|
+
files: ["**/*.{ts,tsx,mts,cts}"],
|
|
443
|
+
languageOptions: { parser },
|
|
444
|
+
plugins: { kerros: plugin },
|
|
445
|
+
rules: recommendedRules
|
|
446
|
+
} };
|
|
2550
447
|
plugin.configs = configs;
|
|
2551
448
|
//#endregion
|
|
2552
449
|
export { configs, plugin as default, rules };
|