@blumintinc/eslint-plugin-blumint 1.20.145 → 1.20.147

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.
@@ -84,31 +84,315 @@ function isNonPrimitiveWithoutTypes(expr) {
84
84
  return false;
85
85
  }
86
86
  }
87
- // TypeScript-aware check. Defensive and conservative.
88
- function isNonPrimitiveWithTypes(context, expr) {
87
+ /**
88
+ * Layer A: what the type checker proves about a dependency expression.
89
+ *
90
+ * Reading the type at the *reference* rather than at the declaration is
91
+ * deliberate: control-flow narrowing is what makes an optional `a?: string`
92
+ * answer `string` at the site React actually compares.
93
+ */
94
+ function primitivenessByType(context, expr) {
89
95
  const services = context.parserServices;
90
96
  if (!services?.program || !services?.esTreeNodeToTSNodeMap) {
91
- return false;
97
+ return 'unproven';
92
98
  }
93
99
  try {
100
+ // Dereferenced inside the function rather than at module scope: reading
101
+ // `ts.TypeFlags` while the module is being loaded takes the whole plugin
102
+ // down wherever `typescript` is not yet resolvable (#1354).
94
103
  // eslint-disable-next-line @typescript-eslint/no-var-requires
95
104
  const ts = require('typescript');
96
105
  const checker = services.program.getTypeChecker();
97
106
  const tsNode = services.esTreeNodeToTSNodeMap.get(expr);
98
107
  if (!tsNode)
99
- return false;
100
- const type = checker.getTypeAtLocation(tsNode);
101
- // Explicitly ignore functions even though they are non-primitive
102
- if (type.getCallSignatures().length > 0)
103
- return false;
104
- // Avoid guessing for unknown/any or non-function types
105
- if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
106
- return false;
107
- return false;
108
+ return 'unproven';
109
+ const primitiveFlags = ts.TypeFlags.StringLike |
110
+ ts.TypeFlags.NumberLike |
111
+ ts.TypeFlags.BooleanLike |
112
+ ts.TypeFlags.BigIntLike |
113
+ ts.TypeFlags.ESSymbolLike |
114
+ ts.TypeFlags.Null |
115
+ ts.TypeFlags.Undefined |
116
+ ts.TypeFlags.Void;
117
+ // `any` and `unknown` carry no shape, and an unresolved type parameter is
118
+ // whatever its caller supplies, so none of the three is an answer.
119
+ const uninformativeFlags = ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter;
120
+ const classify = (candidate) => {
121
+ if (candidate.flags & uninformativeFlags)
122
+ return 'unproven';
123
+ if (candidate.isUnion()) {
124
+ const verdicts = candidate.types.map(classify);
125
+ if (verdicts.includes('unproven'))
126
+ return 'unproven';
127
+ return verdicts.every((verdict) => verdict === 'primitive')
128
+ ? 'primitive'
129
+ : 'nonPrimitive';
130
+ }
131
+ return candidate.flags & primitiveFlags ? 'primitive' : 'nonPrimitive';
132
+ };
133
+ return classify(checker.getTypeAtLocation(tsNode));
108
134
  }
109
135
  catch {
136
+ return 'unproven';
137
+ }
138
+ }
139
+ /**
140
+ * Type nodes naming a primitive outright. A `TSTypeReference` is absent by
141
+ * design: an alias resolves only through the checker, which is Layer A's job.
142
+ */
143
+ const PRIMITIVE_TYPE_KEYWORDS = new Set([
144
+ utils_1.AST_NODE_TYPES.TSStringKeyword,
145
+ utils_1.AST_NODE_TYPES.TSNumberKeyword,
146
+ utils_1.AST_NODE_TYPES.TSBooleanKeyword,
147
+ utils_1.AST_NODE_TYPES.TSBigIntKeyword,
148
+ utils_1.AST_NODE_TYPES.TSSymbolKeyword,
149
+ utils_1.AST_NODE_TYPES.TSNullKeyword,
150
+ utils_1.AST_NODE_TYPES.TSUndefinedKeyword,
151
+ utils_1.AST_NODE_TYPES.TSVoidKeyword,
152
+ ]);
153
+ function isPrimitiveLiteralNode(node) {
154
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral)
155
+ return true;
156
+ if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
157
+ // `-1` and `+1` are literal types spelled across two nodes.
158
+ return ((node.operator === '-' || node.operator === '+') &&
159
+ isPrimitiveLiteralNode(node.argument));
160
+ }
161
+ if (node.type !== utils_1.AST_NODE_TYPES.Literal)
162
+ return false;
163
+ // A regular expression literal is an object, and its `value` is null in
164
+ // hosts that cannot construct it — the same shape a `null` literal has.
165
+ if ('regex' in node && node.regex)
166
+ return false;
167
+ const { value } = node;
168
+ return (value === null ||
169
+ typeof value === 'string' ||
170
+ typeof value === 'number' ||
171
+ typeof value === 'boolean' ||
172
+ typeof value === 'bigint');
173
+ }
174
+ function isPrimitiveTypeNode(node) {
175
+ if (PRIMITIVE_TYPE_KEYWORDS.has(node.type))
176
+ return true;
177
+ if (node.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
178
+ return isPrimitiveLiteralNode(node.literal);
179
+ }
180
+ // A union is primitive only if nothing in it can carry identity, which is
181
+ // what makes `string | undefined` — the type an optional parameter has —
182
+ // answer the same as `string`.
183
+ if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
184
+ return node.types.length > 0 && node.types.every(isPrimitiveTypeNode);
185
+ }
186
+ return false;
187
+ }
188
+ function isUseStateCallee(callee) {
189
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
190
+ return callee.name === 'useState';
191
+ }
192
+ return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
193
+ !callee.computed &&
194
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
195
+ callee.property.name === 'useState');
196
+ }
197
+ /**
198
+ * Whether a `const [x] = useState(<primitive>)` binding holds a primitive.
199
+ *
200
+ * Only element 0 qualifies: element 1 is the setter, a function. The setter is
201
+ * also the only other writer of element 0, and the initial value types what it
202
+ * accepts, so the state stays whatever kind the initializer made it.
203
+ */
204
+ function isPrimitiveUseStateElement(declarator, bound) {
205
+ const { id, init } = declarator;
206
+ if (id.type !== utils_1.AST_NODE_TYPES.ArrayPattern)
110
207
  return false;
208
+ if (id.elements[0] !== bound)
209
+ return false;
210
+ if (!init || init.type !== utils_1.AST_NODE_TYPES.CallExpression)
211
+ return false;
212
+ if (!isUseStateCallee(init.callee))
213
+ return false;
214
+ // An explicit type argument overrides whatever the initial value would have
215
+ // inferred, so it answers instead of the initializer.
216
+ const typeArguments = init.typeParameters?.params;
217
+ if (typeArguments && typeArguments.length > 0) {
218
+ return typeArguments.every(isPrimitiveTypeNode);
111
219
  }
220
+ const [initial] = init.arguments;
221
+ return initial !== undefined && isPrimitiveLiteralNode(initial);
222
+ }
223
+ /**
224
+ * Layer B: primitiveness the parser alone can see.
225
+ *
226
+ * This layer exists because Layer A goes blind exactly where the shared rule
227
+ * testers run it — with no `project`, `useState` imported from react resolves
228
+ * to `any`, and so does every `lib` type. Syntax the file spells out does not
229
+ * degrade that way.
230
+ */
231
+ function isProvablyPrimitiveBinding(definition) {
232
+ const bound = definition.name;
233
+ if (bound.type !== utils_1.AST_NODE_TYPES.Identifier)
234
+ return false;
235
+ // An annotation constrains every assignment to the binding, so it answers for
236
+ // a `let` and a reassigned parameter just as it does for a `const`.
237
+ const annotation = bound.typeAnnotation?.typeAnnotation;
238
+ if (annotation)
239
+ return isPrimitiveTypeNode(annotation);
240
+ const declarator = definition.node;
241
+ const declaration = definition.parent;
242
+ if (declarator.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
243
+ return false;
244
+ // Without an annotation the initializer is the only evidence, and it only
245
+ // describes the binding for as long as nothing rebinds it.
246
+ if (!declaration ||
247
+ declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
248
+ declaration.kind !== 'const') {
249
+ return false;
250
+ }
251
+ if (declarator.id === bound) {
252
+ return declarator.init !== null && isPrimitiveLiteralNode(declarator.init);
253
+ }
254
+ return isPrimitiveUseStateElement(declarator, bound);
255
+ }
256
+ /**
257
+ * Members that exist on primitives and on nothing else in ordinary code.
258
+ *
259
+ * `length`, `slice`, `includes`, `indexOf`, `concat`, `at`, `toString` and
260
+ * `valueOf` are deliberately absent: arrays and objects carry them too, so
261
+ * vetoing on one would stop the rule seeing the array dependencies it exists
262
+ * to catch.
263
+ */
264
+ const PRIMITIVE_ONLY_MEMBERS = new Set([
265
+ 'toUpperCase',
266
+ 'toLowerCase',
267
+ 'toFixed',
268
+ 'toPrecision',
269
+ 'trim',
270
+ 'trimStart',
271
+ 'trimEnd',
272
+ 'padStart',
273
+ 'padEnd',
274
+ 'charAt',
275
+ 'charCodeAt',
276
+ 'codePointAt',
277
+ 'normalize',
278
+ 'localeCompare',
279
+ 'startsWith',
280
+ 'endsWith',
281
+ 'repeat',
282
+ 'toExponential',
283
+ ]);
284
+ /**
285
+ * The children of a node an identifier can be *referenced* from.
286
+ *
287
+ * A non-computed member's property and a non-computed key spell a name rather
288
+ * than read a binding, so counting them as occurrences would let an unrelated
289
+ * `other.trim` decide what `trim` denotes.
290
+ */
291
+ function referencedChildren(node) {
292
+ const children = [];
293
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
+ const record = node;
295
+ for (const key of Object.keys(record)) {
296
+ if (key === 'parent')
297
+ continue;
298
+ if (key === 'property' &&
299
+ node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
300
+ !node.computed) {
301
+ continue;
302
+ }
303
+ if (key === 'key' &&
304
+ (node.type === utils_1.AST_NODE_TYPES.Property ||
305
+ node.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
306
+ node.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
307
+ !node.computed) {
308
+ continue;
309
+ }
310
+ const child = record[key];
311
+ if (!child)
312
+ continue;
313
+ if (Array.isArray(child)) {
314
+ for (const element of child) {
315
+ if (element && typeof element === 'object' && 'type' in element) {
316
+ children.push(element);
317
+ }
318
+ }
319
+ }
320
+ else if (typeof child === 'object' && 'type' in child) {
321
+ children.push(child);
322
+ }
323
+ }
324
+ return children;
325
+ }
326
+ /**
327
+ * Layer C: every read of the name inside the callback is a member that only a
328
+ * primitive has.
329
+ *
330
+ * The weakest of the three layers, and the only one that reaches a binding the
331
+ * file gives no annotation and no initializer for — `const { asPath } =
332
+ * useRouter()` is the shape the consumer's hooks are written in. It answers
333
+ * only when *every* occurrence agrees: a computed access, a spread, or the bare
334
+ * name passed along says nothing about the value's shape, and one such
335
+ * occurrence withdraws the guess.
336
+ */
337
+ function readsOnlyPrimitiveMembers(callback, name) {
338
+ let accesses = 0;
339
+ let proven = true;
340
+ const visit = (node) => {
341
+ if (!proven)
342
+ return;
343
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
344
+ node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
345
+ node.object.name === name) {
346
+ if (node.computed ||
347
+ node.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
348
+ !PRIMITIVE_ONLY_MEMBERS.has(node.property.name)) {
349
+ proven = false;
350
+ return;
351
+ }
352
+ accesses += 1;
353
+ return;
354
+ }
355
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === name) {
356
+ proven = false;
357
+ return;
358
+ }
359
+ for (const child of referencedChildren(node)) {
360
+ visit(child);
361
+ }
362
+ };
363
+ visit(callback.body);
364
+ return proven && accesses > 0;
365
+ }
366
+ /**
367
+ * The one binding a name denotes at a call site, or null when the answer is not
368
+ * a single declaration. A name with several definitions — a redeclared `var`, a
369
+ * merged declaration — describes more than one thing, and no one of them speaks
370
+ * for the reference.
371
+ */
372
+ function soleDefinitionOf(context, node, name) {
373
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, node), name);
374
+ if (!variable || variable.defs.length !== 1)
375
+ return null;
376
+ return variable.defs[0];
377
+ }
378
+ /**
379
+ * Whether a dependency is *provably* a primitive, across all three layers.
380
+ *
381
+ * The direction is asymmetric on purpose. A missed deep comparison costs a
382
+ * recomputation; a deep comparison wrapped around a `string` costs an injected
383
+ * dependency, a new import and a hook that cannot help, so nothing short of a
384
+ * proof promotes a dependency here (#1979).
385
+ */
386
+ function isProvablyPrimitiveDependency(context, call, callback, dep) {
387
+ const verdict = primitivenessByType(context, dep);
388
+ if (verdict === 'primitive')
389
+ return true;
390
+ const definition = soleDefinitionOf(context, call, dep.name);
391
+ if (definition && isProvablyPrimitiveBinding(definition))
392
+ return true;
393
+ // Method names are evidence only where nothing better exists: a checker that
394
+ // resolved the type has already answered, and answered from more than a name.
395
+ return (verdict === 'unproven' && readsOnlyPrimitiveMembers(callback, dep.name));
112
396
  }
113
397
  function collectMemoizedIdentifiers(context) {
114
398
  const memoized = new Set();
@@ -401,12 +685,8 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
401
685
  if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
402
686
  continue;
403
687
  const expr = el;
404
- // TS-aware check first
405
- let isNonPrimitive = isNonPrimitiveWithTypes(context, expr);
406
- // Fallback heuristic without type info for literals/arrays/objects/functions
407
- if (!isNonPrimitive) {
408
- isNonPrimitive = isNonPrimitiveWithoutTypes(expr);
409
- }
688
+ // Syntactic classification for literals/arrays/objects/functions
689
+ let isNonPrimitive = isNonPrimitiveWithoutTypes(expr);
410
690
  // Identifier-specific heuristic: consider non-primitive only if used as object or function in callback
411
691
  if (!isNonPrimitive && expr.type === utils_1.AST_NODE_TYPES.Identifier) {
412
692
  // Imported identifiers are treated as stable
@@ -414,7 +694,15 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
414
694
  isNonPrimitive = false;
415
695
  }
416
696
  else if (identifierUsedAsObjectOrArray(callback, expr.name) &&
417
- !isIdentifierMemoizedAbove(expr.name, memoizedIds)) {
697
+ !isIdentifierMemoizedAbove(expr.name, memoizedIds) &&
698
+ // Reading a member off a name proves the receiver has members,
699
+ // which every primitive also has: `slug.toUpperCase()` and
700
+ // `cfg.a` are the same shape. The promotion therefore stands only
701
+ // while the receiver is not provably a primitive — a bare
702
+ // Identifier reaches this branch through no other signal, so the
703
+ // veto can withhold nothing the rule established some other way
704
+ // (#1979).
705
+ !isProvablyPrimitiveDependency(context, node, callback, expr)) {
418
706
  isNonPrimitive = true;
419
707
  }
420
708
  }