@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.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((name) => `https://github.com/violetflux/kerros/tree/main/packages/eslint-plugin-kerros/src/rules/${name}.ts`);
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/internal/kerros-types.ts
53
- const markerNames = {
54
- externalStoreProvider: "externalStoreProviderMarker",
55
- storeGetter: "storeGetterMarker",
56
- storeHook: "storeHookMarker",
57
- storeInstanceHook: "storeInstanceHookMarker"
58
- };
59
- /** Test whether a declaration belongs to the Kerros runtime package. */
60
- function isKerrosSourceFile(sourceFile) {
61
- const filename = sourceFile.fileName.replaceAll("\\", "/");
62
- if (filename.includes("/node_modules/@violetflux/kerros/")) return true;
63
- return filename.endsWith("/src/index.tsx") && sourceFile.text.includes("declare const storeHookMarker: unique symbol") && sourceFile.text.includes("declare const storeInstanceHookMarker: unique symbol") && sourceFile.text.includes("declare const externalStoreProviderMarker: unique symbol");
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
- /** Create type-backed Kerros identity checks shared across one TypeScript Program. */
66
- function createKerrosProgramTools(program) {
67
- const checker = program.getTypeChecker();
68
- /** Resolve aliases until the declaration that owns the type identity. */
69
- const resolveSymbol = (input) => {
70
- let symbol = input;
71
- while ((symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol);
72
- return symbol;
73
- };
74
- /** Read and de-alias the symbol referenced by a TypeScript node. */
75
- const getTsSymbol = (node) => {
76
- const symbol = (ts.isIdentifier(node) && ts.isShorthandPropertyAssignment(node.parent) ? checker.getShorthandAssignmentValueSymbol(node.parent) : void 0) ?? checker.getSymbolAtLocation(node);
77
- return symbol ? resolveSymbol(symbol) : void 0;
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
- return resolve(input);
127
- };
128
- /** Test a call against an API declaration owned by React's type package. */
129
- const isReactCall = (node, name) => {
130
- const symbol = getTsSymbol(node.expression);
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 tsNode = getTsNode(node);
197
- return ts.isCallExpression(tsNode) ? programTools.getFactoryKind(tsNode) : void 0;
198
- };
199
- /** Resolve an ESTree model reference to its TypeScript implementation. */
200
- const getModelFunction = (node) => {
201
- return programTools.getModelFunction(getTsNode(node));
202
- };
203
- /** Test whether an ESTree call invokes a nominal Kerros Store Hook. */
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 tsNode = getTsNode(node);
206
- return ts.isCallExpression(tsNode) && programTools.isStoreHookCall(tsNode);
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
- return {
220
- checker,
110
+ const tools = {
221
111
  getFactoryKind,
222
- getIdentifierSymbol,
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/factory-at-module-scope.ts
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
- const factoryAtModuleScope = createRule({
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 } = createKerrosTypeTools(context);
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
- //#endregion
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, getIdentifierSymbol } = createKerrosTypeTools(context);
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
- const symbol = getIdentifierSymbol(model);
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
- //#endregion
425
- //#region src/internal/semantic.ts
426
- const arrayMutationMethods = /* @__PURE__ */ new Set([
427
- "copyWithin",
428
- "fill",
429
- "pop",
430
- "push",
431
- "reverse",
432
- "shift",
433
- "sort",
434
- "splice",
435
- "unshift"
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
- functions.reverse();
543
- if (calls && calls.size > 0) {
544
- const dynamicFunctions = [];
545
- const seenFunctions = /* @__PURE__ */ new Set();
546
- let fn = getOwner(reference);
547
- while (fn && !seenFunctions.has(fn)) {
548
- seenFunctions.add(fn);
549
- dynamicFunctions.unshift(fn);
550
- const site = calls.get(fn);
551
- fn = site ? getOwner(site) : void 0;
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
- if (dynamicFunctions.length > 0) functions = dynamicFunctions;
554
- }
555
- /** Apply straight-line writes in runtime order up to an optional point. */
556
- const applyEvents = (container, owner, state, limit = container.range[1]) => {
557
- const applicable = symbolEvents.filter((event) => {
558
- return event.owner === owner && contains(container, event.write) && event.write.range[1] <= limit;
559
- }).sort((left, right) => {
560
- return left.write.range[1] - right.write.range[1] || right.write.range[0] - left.write.range[0];
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
- for (const event of applicable) state = [event];
563
- return state;
564
- };
565
- /** Evaluate one statement completely, merging simple conditional branches. */
566
- const flowFull = (node, owner, state) => {
567
- if (node.type === "BlockStatement") return flowSequence(node.body, owner, state);
568
- if (node.type !== "IfStatement") return applyEvents(node, owner, state);
569
- const tested = applyEvents(node.test, owner, state);
570
- const consequent = flowFull(node.consequent, owner, tested);
571
- const alternate = node.alternate ? flowFull(node.alternate, owner, tested) : tested;
572
- const consequentContinues = !terminates(node.consequent);
573
- const alternateContinues = !node.alternate || !terminates(node.alternate);
574
- if (!consequentContinues) return alternateContinues ? alternate : [];
575
- if (!alternateContinues) return consequent;
576
- return merge(consequent, alternate);
577
- };
578
- /** Evaluate one statement only until the requested reference point. */
579
- const flowUntil = (node, owner, state, target) => {
580
- if (node.type === "BlockStatement") return flowSequence(node.body, owner, state, target);
581
- if (node.type !== "IfStatement") return applyEvents(node, owner, state, target.range[0]);
582
- if (contains(node.test, target)) return applyEvents(node.test, owner, state, target.range[0]);
583
- const tested = applyEvents(node.test, owner, state);
584
- if (contains(node.consequent, target)) return flowUntil(node.consequent, owner, tested, target);
585
- if (node.alternate && contains(node.alternate, target)) return flowUntil(node.alternate, owner, tested, target);
586
- return tested;
587
- };
588
- /** Evaluate a lexical statement sequence, stopping before one nested target. */
589
- function flowSequence(nodes, owner, input, target) {
590
- let state = input;
591
- for (const node of nodes) {
592
- if (target && contains(node, target)) return flowUntil(node, owner, state, target);
593
- if (target && node.range[0] >= target.range[0]) return state;
594
- state = flowFull(node, owner, state);
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 a call invokes a known mutable collection method. */
667
- function isMutableCollectionCall(node, checker, program, getType) {
668
- const callee = unwrapExpression(node.callee);
669
- if (callee.type !== "MemberExpression") return false;
670
- const name = getMemberName(callee);
671
- if (!name) return false;
672
- const kind = getBuiltinTypeKind(checker, program, getType(callee.object));
673
- if (kind === "array") return arrayMutationMethods.has(name);
674
- if (kind === "map") return mapMutationMethods.has(name);
675
- if (kind === "set") return setMutationMethods.has(name);
676
- return false;
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 noBroadStoreAccess = createRule({
705
- name: "no-broad-store-access",
347
+ const liteNoWholeStoreSelector = createRule({
348
+ name: "no-whole-store-selector",
706
349
  meta: {
707
350
  type: "problem",
708
- docs: { description: "Prevent broad enumeration and serialization of complete Store snapshots." },
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 { getIdentifierSymbol, isStoreHookCall } = createKerrosTypeTools(context);
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 = getStoreAliases(selector, parameter, getIdentifierSymbol);
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
- //#endregion
2105
- //#region src/rules/prefer-bind-store.ts
2106
- const preferBindStore = createRule({
2107
- name: "prefer-bind-store",
2108
- meta: {
2109
- type: "suggestion",
2110
- docs: { description: "Prefer bindStore when a model delegates to React useSyncExternalStore." },
2111
- schema: [],
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": bindingNaming,
2362
- "factory-at-module-scope": factoryAtModuleScope,
2363
- "model-convention": modelConvention,
2364
- "no-broad-store-access": noBroadStoreAccess,
2365
- "no-cyclic-store-dependency": noCyclicStoreDependency,
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: "Require bindStore snapshots to preserve reference identity between updates." },
391
+ docs: { description: "Prevent broad access to file-local selector-free Store snapshots." },
2380
392
  schema: [],
2381
- messages: { uncachedSnapshot: "getSnapshot must return a cached snapshot reference." }
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 { checker, getFactoryKind, getMarkerType, getTsNode, getTsSymbol } = createKerrosTypeTools(context);
2386
- /** Resolve property functions through methods, arrow properties, and shorthand identifiers. */
2387
- const getImplementations = (symbol) => {
2388
- const implementations = /* @__PURE__ */ new Set();
2389
- const seen = /* @__PURE__ */ new Set();
2390
- /** Follow one syntax node to a concrete function body or referenced symbol. */
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
- return { CallExpression(node) {
2469
- if (getFactoryKind(node) !== "bindStore") return;
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: "uncachedSnapshot"
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
- "selector-parameter-name": createRule({
2490
- name: "selector-parameter-name",
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.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 fastRules = {
2522
- ...recommendedRules,
2523
- "kerros/no-cyclic-store-dependency": "off",
2524
- "kerros/no-store-mutation": ["error", { deepAliases: false }],
2525
- "kerros/no-unstable-selector-value": "off",
2526
- "kerros/require-cached-snapshot": "off"
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 };