@blumintinc/eslint-plugin-blumint 1.20.134 → 1.20.136
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/lib/index.js +1 -1
- package/lib/rules/consistent-callback-naming.js +16 -0
- package/lib/rules/enforce-assert-safe-object-key.js +403 -9
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +40 -4
- package/lib/rules/enforce-id-capitalization.js +18 -0
- package/lib/rules/enforce-props-argument-name.js +28 -2
- package/lib/rules/enforce-props-naming-consistency.js +28 -2
- package/lib/rules/no-inline-component-prop.js +8 -5
- package/lib/rules/no-object-values-on-strings.js +23 -6
- package/lib/rules/no-passthrough-getters.js +10 -2
- package/lib/rules/no-redundant-annotation-assertion.js +611 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +46 -4
- package/lib/rules/no-useless-usememo-primitives.js +30 -9
- package/lib/rules/no-usememo-for-pass-by-value.js +185 -31
- package/lib/rules/parallelize-async-operations.js +10 -1
- package/lib/rules/prefer-map-over-conditional-dispatch.js +36 -8
- package/lib/utils/fixtureCorpus.d.ts +148 -8
- package/lib/utils/fixtureCorpus.js +207 -10
- package/lib/utils/harvestRuleTesterCases.d.ts +15 -0
- package/lib/utils/harvestRuleTesterCases.js +30 -3
- package/lib/utils/importRemoval.js +35 -6
- package/lib/utils/replacementSegments.d.ts +35 -0
- package/lib/utils/replacementSegments.js +55 -0
- package/package.json +1 -1
- package/release-manifest.json +143 -0
package/lib/index.js
CHANGED
|
@@ -607,11 +607,27 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
607
607
|
globalVar.references.forEach((ref) => references.add(ref));
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
|
+
// A binding that leaves the module is one end of a cross-file
|
|
611
|
+
// contract. Renaming `export const handleClick` to `click` strands
|
|
612
|
+
// every `import { handleClick }` with TS2724, and a single-file fixer
|
|
613
|
+
// cannot reach those importers — the same reasoning that already
|
|
614
|
+
// withholds the JSX prop rename and the destructured one, which is
|
|
615
|
+
// where `isExportedBinding` was first needed. The violation still
|
|
616
|
+
// reports; only the rename is withheld.
|
|
617
|
+
const declaredVariable = context
|
|
618
|
+
.getDeclaredVariables(node)
|
|
619
|
+
.find((v) => v.identifiers.includes(node.id));
|
|
620
|
+
const leavesModule = declaredVariable
|
|
621
|
+
? isExportedBinding(declaredVariable)
|
|
622
|
+
: isExportedDeclaration(node);
|
|
610
623
|
context.report({
|
|
611
624
|
node,
|
|
612
625
|
messageId: 'callbackFunctionPrefix',
|
|
613
626
|
data: { functionName },
|
|
614
627
|
fix(fixer) {
|
|
628
|
+
if (leavesModule) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
615
631
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
616
632
|
const newName = stripHandlePrefix(functionName);
|
|
617
633
|
// `const handleDelete = fn` would become `const delete = fn`,
|
|
@@ -256,6 +256,54 @@ function isNumericCall(node) {
|
|
|
256
256
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
257
257
|
callee.object.name === 'Math');
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The property names `assertSafe` exists to reject. A key that provably cannot
|
|
261
|
+
* spell one of these cannot reach the prototype surface, which is the entire
|
|
262
|
+
* hazard — so proving it is what earns an exemption, the same standard the
|
|
263
|
+
* numeric analysis already meets.
|
|
264
|
+
*/
|
|
265
|
+
const PROTOTYPE_REACHING_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
266
|
+
/**
|
|
267
|
+
* Whether a template's FIXED text still leaves room to spell `target`.
|
|
268
|
+
*
|
|
269
|
+
* The producible set is `q0 + * + q1 + * + … + * + qN`, each `*` an arbitrary
|
|
270
|
+
* substitution. `target` is producible iff it starts with `q0`, ends with `qN`,
|
|
271
|
+
* and the interior quasis occur in order in between without overlapping. So
|
|
272
|
+
* `` `user-${id}` `` can never be `__proto__` (no such prefix) while
|
|
273
|
+
* `` `__pro${x}` `` can — with `x` = `'to__'`, which resolves to
|
|
274
|
+
* `Object.prototype` at runtime.
|
|
275
|
+
*
|
|
276
|
+
* Interior quasis are matched greedily from the left. That is sufficient
|
|
277
|
+
* because they are fixed strings: taking the earliest occurrence never consumes
|
|
278
|
+
* a character a later quasi needed, so no backtracking can succeed where the
|
|
279
|
+
* greedy pass fails.
|
|
280
|
+
*
|
|
281
|
+
* A template with no substitutions produces exactly one string and is a static
|
|
282
|
+
* key like any other string literal, so it is never treated as reaching.
|
|
283
|
+
*/
|
|
284
|
+
function templateCanSpell(quasis, target) {
|
|
285
|
+
if (quasis.length < 2) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const first = quasis[0];
|
|
289
|
+
const last = quasis[quasis.length - 1];
|
|
290
|
+
if (!target.startsWith(first) || !target.endsWith(last)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const limit = target.length - last.length;
|
|
294
|
+
let cursor = first.length;
|
|
295
|
+
if (cursor > limit) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
for (const middle of quasis.slice(1, -1)) {
|
|
299
|
+
const at = target.indexOf(middle, cursor);
|
|
300
|
+
if (at < 0 || at + middle.length > limit) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
cursor = at + middle.length;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
259
307
|
/**
|
|
260
308
|
* A `: number` annotation on a binding name. Parameters and variable
|
|
261
309
|
* declarators are the bindings that carry one, and TypeScript checks every
|
|
@@ -346,6 +394,77 @@ function definesNumericBinding(def) {
|
|
|
346
394
|
return (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
347
395
|
isNumberAnnotated(def.name));
|
|
348
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Utility wrappers whose single type argument keeps a Record annotation's key
|
|
399
|
+
* domain intact: `Readonly<Record<K, V>>` and `Partial<Record<K, V>>` admit
|
|
400
|
+
* exactly the keys `Record<K, V>` admits.
|
|
401
|
+
*/
|
|
402
|
+
const RECORD_KEY_PRESERVING_WRAPPERS = new Set(['Readonly', 'Partial']);
|
|
403
|
+
/**
|
|
404
|
+
* The property names assertSafe exists to keep out of a lookup. A literal key
|
|
405
|
+
* union that names one of them proves nothing about safety, so the
|
|
406
|
+
* compiler-bounded carve-out refuses it and the key keeps being reported.
|
|
407
|
+
*/
|
|
408
|
+
const PROTOTYPE_SURFACE_NAMES = new Set([
|
|
409
|
+
'__proto__',
|
|
410
|
+
'constructor',
|
|
411
|
+
'prototype',
|
|
412
|
+
]);
|
|
413
|
+
/**
|
|
414
|
+
* Union semantics over domains: one open member opens the whole domain, an
|
|
415
|
+
* unclassifiable member leaves it unclassifiable (it could be hiding an open
|
|
416
|
+
* one), and a closed-but-unenumerable member keeps the union closed without a
|
|
417
|
+
* literal listing.
|
|
418
|
+
*/
|
|
419
|
+
function foldKeyDomains(domains) {
|
|
420
|
+
const values = new Set();
|
|
421
|
+
let closed = false;
|
|
422
|
+
let unknown = false;
|
|
423
|
+
for (const domain of domains) {
|
|
424
|
+
if (domain === 'open') {
|
|
425
|
+
return 'open';
|
|
426
|
+
}
|
|
427
|
+
if (domain === 'unknown') {
|
|
428
|
+
unknown = true;
|
|
429
|
+
}
|
|
430
|
+
else if (domain === 'closed') {
|
|
431
|
+
closed = true;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
for (const value of domain) {
|
|
435
|
+
values.add(value);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (unknown) {
|
|
440
|
+
return 'unknown';
|
|
441
|
+
}
|
|
442
|
+
if (closed) {
|
|
443
|
+
return 'closed';
|
|
444
|
+
}
|
|
445
|
+
return values;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* An enum is a compiler-checked finite set. Members with literal initializers
|
|
449
|
+
* enumerate their runtime key strings (which is what lets the forbidden-name
|
|
450
|
+
* screen and subset comparison see them); a computed or auto-numbered member
|
|
451
|
+
* leaves the set finite but unenumerable.
|
|
452
|
+
*/
|
|
453
|
+
function enumKeyDomain(node) {
|
|
454
|
+
const values = new Set();
|
|
455
|
+
for (const member of node.members) {
|
|
456
|
+
const { initializer } = member;
|
|
457
|
+
if (initializer?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
458
|
+
(typeof initializer.value === 'string' ||
|
|
459
|
+
typeof initializer.value === 'number')) {
|
|
460
|
+
values.add(String(initializer.value));
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
return 'closed';
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return values;
|
|
467
|
+
}
|
|
349
468
|
exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
350
469
|
name: 'enforce-assert-safe-object-key',
|
|
351
470
|
meta: {
|
|
@@ -620,6 +739,260 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
620
739
|
init.callee.name === 'assertSafe');
|
|
621
740
|
});
|
|
622
741
|
};
|
|
742
|
+
/**
|
|
743
|
+
* The declared key domain of a type annotation. Alias references resolve
|
|
744
|
+
* through the scope chain to the declaration in this file — a type alias
|
|
745
|
+
* recurses into what it aliases, an enum enumerates its members, a generic
|
|
746
|
+
* type parameter is judged by its constraint (an unconstrained one could be
|
|
747
|
+
* instantiated with anything, so it reads as open). `seen` terminates a
|
|
748
|
+
* recursive alias without conflating it with a sibling reference to the
|
|
749
|
+
* same name.
|
|
750
|
+
*/
|
|
751
|
+
const keyDomainOf = (typeNode, anchor, seen) => {
|
|
752
|
+
switch (typeNode.type) {
|
|
753
|
+
case utils_1.AST_NODE_TYPES.TSLiteralType: {
|
|
754
|
+
const { literal } = typeNode;
|
|
755
|
+
if (literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
756
|
+
(typeof literal.value === 'string' ||
|
|
757
|
+
typeof literal.value === 'number')) {
|
|
758
|
+
return new Set([String(literal.value)]);
|
|
759
|
+
}
|
|
760
|
+
return 'unknown';
|
|
761
|
+
}
|
|
762
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
763
|
+
return foldKeyDomains(typeNode.types.map((member) => keyDomainOf(member, anchor, seen)));
|
|
764
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
765
|
+
if (typeNode.typeParameters ||
|
|
766
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
767
|
+
return 'unknown';
|
|
768
|
+
}
|
|
769
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), typeNode.typeName.name);
|
|
770
|
+
if (!variable || variable.defs.length === 0 || seen.has(variable)) {
|
|
771
|
+
return 'unknown';
|
|
772
|
+
}
|
|
773
|
+
const nextSeen = new Set(seen).add(variable);
|
|
774
|
+
return foldKeyDomains(variable.defs.map((def) => {
|
|
775
|
+
switch (def.node.type) {
|
|
776
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
777
|
+
return keyDomainOf(def.node.typeAnnotation, anchor, nextSeen);
|
|
778
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration:
|
|
779
|
+
return enumKeyDomain(def.node);
|
|
780
|
+
case utils_1.AST_NODE_TYPES.TSTypeParameter:
|
|
781
|
+
return def.node.constraint
|
|
782
|
+
? keyDomainOf(def.node.constraint, anchor, nextSeen)
|
|
783
|
+
: 'open';
|
|
784
|
+
default:
|
|
785
|
+
return 'unknown';
|
|
786
|
+
}
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
// `(typeof KINDS)[number]` — the union derived from a values array,
|
|
790
|
+
// which prefer-union-from-const-array rewrites literal-union aliases
|
|
791
|
+
// into. The members live in a value rather than the type syntax, so
|
|
792
|
+
// they are read off the array's own literal elements — but only under
|
|
793
|
+
// `as const`: without it the array's type widens to `string[]` and the
|
|
794
|
+
// indexed access IS `string`, the open domain this rule exists to keep
|
|
795
|
+
// reported.
|
|
796
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType: {
|
|
797
|
+
if (typeNode.objectType.type !== utils_1.AST_NODE_TYPES.TSTypeQuery) {
|
|
798
|
+
return 'unknown';
|
|
799
|
+
}
|
|
800
|
+
const { exprName } = typeNode.objectType;
|
|
801
|
+
if (exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
802
|
+
return 'unknown';
|
|
803
|
+
}
|
|
804
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), exprName.name);
|
|
805
|
+
if (!variable || variable.defs.length !== 1) {
|
|
806
|
+
return 'unknown';
|
|
807
|
+
}
|
|
808
|
+
const def = variable.defs[0];
|
|
809
|
+
if (def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
810
|
+
return 'unknown';
|
|
811
|
+
}
|
|
812
|
+
const init = def.node.init;
|
|
813
|
+
const isAsConstArray = init?.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
814
|
+
init.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
815
|
+
init.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
816
|
+
init.typeAnnotation.typeName.name === 'const' &&
|
|
817
|
+
init.expression.type === utils_1.AST_NODE_TYPES.ArrayExpression;
|
|
818
|
+
if (!isAsConstArray) {
|
|
819
|
+
return 'unknown';
|
|
820
|
+
}
|
|
821
|
+
if (typeNode.indexType.type !== utils_1.AST_NODE_TYPES.TSNumberKeyword) {
|
|
822
|
+
return 'closed';
|
|
823
|
+
}
|
|
824
|
+
const values = new Set();
|
|
825
|
+
for (const element of init.expression
|
|
826
|
+
.elements) {
|
|
827
|
+
if (element?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
828
|
+
(typeof element.value === 'string' ||
|
|
829
|
+
typeof element.value === 'number')) {
|
|
830
|
+
values.add(String(element.value));
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
return 'closed';
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return values;
|
|
837
|
+
}
|
|
838
|
+
case utils_1.AST_NODE_TYPES.TSStringKeyword:
|
|
839
|
+
case utils_1.AST_NODE_TYPES.TSNumberKeyword:
|
|
840
|
+
case utils_1.AST_NODE_TYPES.TSSymbolKeyword:
|
|
841
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
842
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
843
|
+
case utils_1.AST_NODE_TYPES.TSTemplateLiteralType:
|
|
844
|
+
return 'open';
|
|
845
|
+
default:
|
|
846
|
+
return 'unknown';
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
/**
|
|
850
|
+
* The key type parameter of a Record-shaped annotation, read through the
|
|
851
|
+
* wrappers that keep its key domain (`Readonly`, `Partial`) and through a
|
|
852
|
+
* bare in-file alias (`type Lookup = Record<K, V>`). Anything else — a type
|
|
853
|
+
* literal with an index signature, a Map, an imported alias — yields null:
|
|
854
|
+
* the annotation then makes no syntactically checkable claim about which
|
|
855
|
+
* keys exist.
|
|
856
|
+
*/
|
|
857
|
+
const recordKeyTypeOf = (typeNode, anchor, seen = new Set()) => {
|
|
858
|
+
// `Record<K, V> | undefined` — the natural annotation for a receiver
|
|
859
|
+
// reached through `?.` — keys exactly what `Record<K, V>` keys: a nullish
|
|
860
|
+
// receiver short-circuits (or throws), it never indexes anything else.
|
|
861
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
862
|
+
const substantive = typeNode.types.filter((member) => member.type !== utils_1.AST_NODE_TYPES.TSUndefinedKeyword &&
|
|
863
|
+
member.type !== utils_1.AST_NODE_TYPES.TSNullKeyword);
|
|
864
|
+
return substantive.length === 1
|
|
865
|
+
? recordKeyTypeOf(substantive[0], anchor, seen)
|
|
866
|
+
: null;
|
|
867
|
+
}
|
|
868
|
+
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
869
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const { name } = typeNode.typeName;
|
|
873
|
+
const args = typeNode.typeParameters?.params;
|
|
874
|
+
if (name === 'Record') {
|
|
875
|
+
return args?.length === 2 ? args[0] : null;
|
|
876
|
+
}
|
|
877
|
+
if (RECORD_KEY_PRESERVING_WRAPPERS.has(name) && args?.length === 1) {
|
|
878
|
+
return recordKeyTypeOf(args[0], anchor, seen);
|
|
879
|
+
}
|
|
880
|
+
if (args) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), name);
|
|
884
|
+
if (!variable || seen.has(variable) || variable.defs.length !== 1) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const def = variable.defs[0];
|
|
888
|
+
if (def.node.type !== utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
return recordKeyTypeOf(def.node.typeAnnotation, anchor, new Set(seen).add(variable));
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* Whether the Record's declared key domain covers the key's declared type,
|
|
895
|
+
* so that TypeScript itself rejects any key value outside the record's
|
|
896
|
+
* declared keys.
|
|
897
|
+
*
|
|
898
|
+
* Two spellings prove it:
|
|
899
|
+
*
|
|
900
|
+
* - **The same type reference on both sides** (`kind: Kind` into
|
|
901
|
+
* `Record<Kind, V>`). Name identity makes the domains equal whatever the
|
|
902
|
+
* alias holds — an imported alias included — so resolution is consulted
|
|
903
|
+
* only to refuse a domain that resolves to something open (`type K =
|
|
904
|
+
* string` re-opens the very surface this rule guards) or to a literal
|
|
905
|
+
* union naming a prototype field.
|
|
906
|
+
* - **Literal unions the syntax can compare** (`kind: 'live' | 'simulated'`
|
|
907
|
+
* into `Record<'live' | 'simulated', V>`, or a narrowing of it): every
|
|
908
|
+
* literal the key admits must be a declared record key, and none of them
|
|
909
|
+
* may name the prototype surface.
|
|
910
|
+
*/
|
|
911
|
+
const recordKeyCovers = (keyAnnotation, recordKeyType, anchor) => {
|
|
912
|
+
const namesNoPrototypeField = (domain) => ![...domain].some((value) => PROTOTYPE_SURFACE_NAMES.has(value));
|
|
913
|
+
if (keyAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
914
|
+
recordKeyType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
915
|
+
!keyAnnotation.typeParameters &&
|
|
916
|
+
!recordKeyType.typeParameters &&
|
|
917
|
+
context.sourceCode.getText(keyAnnotation.typeName) ===
|
|
918
|
+
context.sourceCode.getText(recordKeyType.typeName)) {
|
|
919
|
+
const domain = keyDomainOf(keyAnnotation, anchor, new Set());
|
|
920
|
+
if (domain === 'open') {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
return typeof domain === 'string' || namesNoPrototypeField(domain);
|
|
924
|
+
}
|
|
925
|
+
const keyDomain = keyDomainOf(keyAnnotation, anchor, new Set());
|
|
926
|
+
if (typeof keyDomain === 'string' ||
|
|
927
|
+
keyDomain.size === 0 ||
|
|
928
|
+
!namesNoPrototypeField(keyDomain)) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
const recordDomain = keyDomainOf(recordKeyType, anchor, new Set());
|
|
932
|
+
if (typeof recordDomain === 'string') {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
return [...keyDomain].every((value) => recordDomain.has(value));
|
|
936
|
+
};
|
|
937
|
+
/**
|
|
938
|
+
* The type annotations declared on the binding an identifier resolves to.
|
|
939
|
+
* Every definition must carry one on the binding name itself — an
|
|
940
|
+
* annotation on a binding is what TypeScript checks every write against, so
|
|
941
|
+
* it holds for the lookup no matter which statement assigned last. A
|
|
942
|
+
* destructured binding, an unannotated declarator, an import: null, because
|
|
943
|
+
* nothing constrains what the identifier holds.
|
|
944
|
+
*/
|
|
945
|
+
const declaredAnnotationsOf = (identifier) => {
|
|
946
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
947
|
+
if (!variable || variable.defs.length === 0) {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
const annotations = [];
|
|
951
|
+
for (const def of variable.defs) {
|
|
952
|
+
const bindingName = def.name;
|
|
953
|
+
if (bindingName.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
954
|
+
!bindingName.typeAnnotation) {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
annotations.push(bindingName.typeAnnotation.typeAnnotation);
|
|
958
|
+
}
|
|
959
|
+
return annotations;
|
|
960
|
+
};
|
|
961
|
+
/**
|
|
962
|
+
* Whether `object[key]` is a lookup the compiler already bounds: the object
|
|
963
|
+
* is a binding annotated `Record<K, V>` and the key a binding whose
|
|
964
|
+
* declared type `K` covers (#1875). Such a lookup cannot reach the
|
|
965
|
+
* prototype surface without the code failing to compile, so `assertSafe`
|
|
966
|
+
* would validate nothing — and it is not identity on the values that DO
|
|
967
|
+
* slip past a declared type at runtime (data crossing a persistence or
|
|
968
|
+
* version boundary): the plain lookup degrades to `undefined` where the
|
|
969
|
+
* wrapped one throws, which is precisely the semantic change that turned a
|
|
970
|
+
* graceful render fallback into a render-time crash. Both sides must be
|
|
971
|
+
* annotated: an `any`-typed or unannotated key indexes into any Record
|
|
972
|
+
* without a compile error, so the record annotation alone proves nothing.
|
|
973
|
+
*/
|
|
974
|
+
const isCompilerBoundedLookup = (node, key) => {
|
|
975
|
+
if (node.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
const objectAnnotations = declaredAnnotationsOf(node.object);
|
|
979
|
+
if (!objectAnnotations) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
const recordKeyTypes = [];
|
|
983
|
+
for (const annotation of objectAnnotations) {
|
|
984
|
+
const recordKeyType = recordKeyTypeOf(annotation, node);
|
|
985
|
+
if (!recordKeyType) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
recordKeyTypes.push(recordKeyType);
|
|
989
|
+
}
|
|
990
|
+
const keyAnnotations = declaredAnnotationsOf(key);
|
|
991
|
+
if (!keyAnnotations) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
return keyAnnotations.every((keyAnnotation) => recordKeyTypes.every((recordKeyType) => recordKeyCovers(keyAnnotation, recordKeyType, node)));
|
|
995
|
+
};
|
|
623
996
|
/**
|
|
624
997
|
* Whether the syntax alone proves the key is a number. `__proto__`,
|
|
625
998
|
* `constructor` and `prototype` are never the string form of a number, so a
|
|
@@ -808,19 +1181,34 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
808
1181
|
if (isLikelyArray) {
|
|
809
1182
|
return;
|
|
810
1183
|
}
|
|
811
|
-
//
|
|
812
|
-
//
|
|
1184
|
+
// A template whose every substitution is provably numeric can only
|
|
1185
|
+
// widen into digits, and no dangerous property name is the string
|
|
1186
|
+
// form of a number — the same proof the identifier path accepts.
|
|
1187
|
+
const canWidenToText = property.expressions.some((expr) => !isStaticallyNumeric(expr));
|
|
1188
|
+
const quasis = property.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
1189
|
+
const reachesPrototype = canWidenToText &&
|
|
1190
|
+
PROTOTYPE_REACHING_KEYS.some((key) => templateCanSpell(quasis, key));
|
|
1191
|
+
// Fixed text on either side of the substitution can rule a property
|
|
1192
|
+
// name out — `user-${id}` is never `__proto__` — and the rule skips
|
|
1193
|
+
// a key it can prove harmless. What it must NOT do is assume that:
|
|
1194
|
+
// `__pro${x}` carries fixed text too and still reaches the
|
|
1195
|
+
// prototype (#1880).
|
|
1196
|
+
if (!reachesPrototype) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// `${id}` alone is the whole key, so the remedy names the inner
|
|
1200
|
+
// expression and the fix wraps it directly. A template carrying
|
|
1201
|
+
// fixed text has no such inner key — the string it builds is the
|
|
1202
|
+
// key — so that whole template is what gets wrapped, which is the
|
|
1203
|
+
// shape the docs show for `assertSafe(`${id}_suffix`)`.
|
|
813
1204
|
const isSimpleVarInterpolation = property.expressions.length === 1 &&
|
|
814
1205
|
property.quasis.length === 2 &&
|
|
815
1206
|
property.quasis[0].value.raw === '' &&
|
|
816
1207
|
property.quasis[1].value.raw === '';
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const expr = property.expressions[0];
|
|
822
|
-
const exprText = context.sourceCode.getText(expr);
|
|
823
|
-
reportWrittenKey(written, property, exprText);
|
|
1208
|
+
const unwrapped = isSimpleVarInterpolation
|
|
1209
|
+
? property.expressions[0]
|
|
1210
|
+
: property;
|
|
1211
|
+
reportWrittenKey(written, property, context.sourceCode.getText(unwrapped));
|
|
824
1212
|
return;
|
|
825
1213
|
}
|
|
826
1214
|
// Check for direct variable usage (identifiers)
|
|
@@ -838,6 +1226,12 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
838
1226
|
if (isAssertSafeValidatedIdentifier(property)) {
|
|
839
1227
|
return;
|
|
840
1228
|
}
|
|
1229
|
+
// A typed discriminant indexing a Record whose declared keys cover
|
|
1230
|
+
// its type is compile-time bounded; wrapping it would turn a total
|
|
1231
|
+
// lookup into a throwing one (#1875).
|
|
1232
|
+
if (isCompilerBoundedLookup(node, property)) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
841
1235
|
const propText = context.sourceCode.getText(property);
|
|
842
1236
|
reportWrittenKey(written, property, propText);
|
|
843
1237
|
return;
|
|
@@ -119,17 +119,49 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
119
119
|
function needsQuoting(key) {
|
|
120
120
|
return key.includes('.') || !/^(?:[$_A-Za-z][$\w]*)$/u.test(key);
|
|
121
121
|
}
|
|
122
|
+
// A method shorthand elides the `function` keyword, and its
|
|
123
|
+
// FunctionExpression range starts at the parameter list, so copying the
|
|
124
|
+
// value's text verbatim emits `() { … }`, which is not an expression. The
|
|
125
|
+
// member is re-emitted with the keyword the shorthand leaves out, matching
|
|
126
|
+
// what the `key: function () {}` spelling already produces.
|
|
127
|
+
//
|
|
128
|
+
// `super` is the one binding the two spellings do not share: it resolves
|
|
129
|
+
// through the enclosing object literal's home object, which a function
|
|
130
|
+
// expression has none of, so such a method is declined rather than
|
|
131
|
+
// rewritten into code that cannot resolve it. Any nested `super` counts,
|
|
132
|
+
// because narrowing the scan to the method's own body would have to model
|
|
133
|
+
// which inner forms rebind it.
|
|
134
|
+
function getMethodValueText(value, sourceCode) {
|
|
135
|
+
const referencesSuper = sourceCode
|
|
136
|
+
.getTokens(value)
|
|
137
|
+
.some((token) => token.type === utils_1.AST_TOKEN_TYPES.Keyword && token.value === 'super');
|
|
138
|
+
if (referencesSuper) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return `${value.async ? 'async ' : ''}function${value.generator ? '*' : ''} ${sourceCode.getText(value)}`;
|
|
142
|
+
}
|
|
143
|
+
// Text a nested leaf contributes to its FieldPath entry, or undefined when
|
|
144
|
+
// the value has no expression-position equivalent.
|
|
145
|
+
function getFlattenedValueText(property, sourceCode) {
|
|
146
|
+
if (property.method &&
|
|
147
|
+
property.value.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
148
|
+
return getMethodValueText(property.value, sourceCode);
|
|
149
|
+
}
|
|
150
|
+
return sourceCode.getText(property.value);
|
|
151
|
+
}
|
|
122
152
|
// Collect the FieldPath entries a nested property flattens into, or bail out
|
|
123
153
|
// when flattening would silently drop payload data (spreads, computed keys,
|
|
124
|
-
// accessors
|
|
125
|
-
//
|
|
154
|
+
// accessors, unsupported key literals) or would produce nothing at all.
|
|
155
|
+
// Bailing leaves the report in place so the developer flattens by hand
|
|
126
156
|
// instead of receiving a fix that deletes fields or emits invalid syntax.
|
|
127
157
|
function collectFieldPathEntries(obj, prefix, sourceCode) {
|
|
128
158
|
const entries = [];
|
|
129
159
|
for (const property of obj.properties) {
|
|
160
|
+
// A getter or setter is declined even though it is spelled like a
|
|
161
|
+
// method: its body runs on access rather than holding a value, so no
|
|
162
|
+
// FieldPath entry can carry it
|
|
130
163
|
if (property.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
131
164
|
property.computed ||
|
|
132
|
-
property.method ||
|
|
133
165
|
property.kind !== 'init') {
|
|
134
166
|
return null;
|
|
135
167
|
}
|
|
@@ -146,7 +178,11 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
|
146
178
|
entries.push(...nestedEntries);
|
|
147
179
|
continue;
|
|
148
180
|
}
|
|
149
|
-
|
|
181
|
+
const valueText = getFlattenedValueText(property, sourceCode);
|
|
182
|
+
if (valueText === undefined) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
entries.push([fullKey, valueText]);
|
|
150
186
|
}
|
|
151
187
|
return entries.length > 0 ? entries : null;
|
|
152
188
|
}
|
|
@@ -43,6 +43,9 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
43
43
|
'removeAttributeNS',
|
|
44
44
|
'toHaveAttribute',
|
|
45
45
|
]);
|
|
46
|
+
// A single identifier token — no whitespace, no punctuation. Prose is a
|
|
47
|
+
// phrase, so anything matching this is a name rather than displayed text.
|
|
48
|
+
const IDENTIFIER_TOKEN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
46
49
|
/**
|
|
47
50
|
* Check if a node is in a context that should be excluded from the rule
|
|
48
51
|
* (e.g., parameter names, property names, type definitions)
|
|
@@ -116,6 +119,21 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
116
119
|
return true;
|
|
117
120
|
}
|
|
118
121
|
}
|
|
122
|
+
// Check if the node is a lone identifier token listed in an array
|
|
123
|
+
// literal, e.g. ['id', 'broadcastTest'] as const. An array of bare
|
|
124
|
+
// identifiers is a key/field-name list — the array spelling of the object
|
|
125
|
+
// keys this rule already leaves alone — so 'ID' would name a key that
|
|
126
|
+
// does not exist. The carve-out requires the *whole* element to be one
|
|
127
|
+
// identifier, which keeps a phrase such as ['Enter your id', 'Name']
|
|
128
|
+
// reported: prose carries whitespace or punctuation, a key name does not.
|
|
129
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
130
|
+
typeof node.value === 'string' &&
|
|
131
|
+
node.parent &&
|
|
132
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
133
|
+
node.parent.elements.includes(node) &&
|
|
134
|
+
IDENTIFIER_TOKEN.test(node.value)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
119
137
|
// Check if the node is a string literal used for property access
|
|
120
138
|
// This handles cases like obj['id'] or OverwolfGame['id']
|
|
121
139
|
if (node.parent &&
|
|
@@ -340,6 +340,33 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
340
340
|
}
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* The member name a `this.<x>` access reads, whatever its spelling.
|
|
345
|
+
*
|
|
346
|
+
* Keying the check on the dot spelling alone left `this['settings']`
|
|
347
|
+
* invisible, so the rename shipped and stranded it — the class no longer had
|
|
348
|
+
* the member the getter reads (#1881). A computed access with a static
|
|
349
|
+
* string is the SAME member as the dot form, and the fixer cannot rewrite it
|
|
350
|
+
* either, so it has to count. `null` marks a genuinely dynamic key, which
|
|
351
|
+
* names no member statically.
|
|
352
|
+
*/
|
|
353
|
+
function staticMemberName(node) {
|
|
354
|
+
if (!node.computed) {
|
|
355
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
356
|
+
? node.property.name
|
|
357
|
+
: null;
|
|
358
|
+
}
|
|
359
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
360
|
+
typeof node.property.value === 'string') {
|
|
361
|
+
return node.property.value;
|
|
362
|
+
}
|
|
363
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
364
|
+
node.property.expressions.length === 0 &&
|
|
365
|
+
node.property.quasis.length === 1) {
|
|
366
|
+
return node.property.quasis[0].value.cooked;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
343
370
|
// Determine whether renaming a constructor parameter property is unsafe to
|
|
344
371
|
// autofix. A parameter property (`private readonly foo: T`) creates BOTH a
|
|
345
372
|
// constructor-local binding and a `this.foo` class field, so a
|
|
@@ -355,8 +382,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
355
382
|
}
|
|
356
383
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
357
384
|
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
358
|
-
node
|
|
359
|
-
node.property.name === name) {
|
|
385
|
+
staticMemberName(node) === name) {
|
|
360
386
|
unsafe = true;
|
|
361
387
|
return;
|
|
362
388
|
}
|