@blumintinc/eslint-plugin-blumint 1.20.133 → 1.20.135
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/enforce-assert-safe-object-key.js +331 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +40 -4
- package/lib/rules/enforce-id-capitalization.js +18 -0
- package/lib/rules/enforce-unique-cursor-headers.js +18 -1
- 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-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 +100 -0
package/lib/index.js
CHANGED
|
@@ -346,6 +346,77 @@ function definesNumericBinding(def) {
|
|
|
346
346
|
return (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
347
347
|
isNumberAnnotated(def.name));
|
|
348
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Utility wrappers whose single type argument keeps a Record annotation's key
|
|
351
|
+
* domain intact: `Readonly<Record<K, V>>` and `Partial<Record<K, V>>` admit
|
|
352
|
+
* exactly the keys `Record<K, V>` admits.
|
|
353
|
+
*/
|
|
354
|
+
const RECORD_KEY_PRESERVING_WRAPPERS = new Set(['Readonly', 'Partial']);
|
|
355
|
+
/**
|
|
356
|
+
* The property names assertSafe exists to keep out of a lookup. A literal key
|
|
357
|
+
* union that names one of them proves nothing about safety, so the
|
|
358
|
+
* compiler-bounded carve-out refuses it and the key keeps being reported.
|
|
359
|
+
*/
|
|
360
|
+
const PROTOTYPE_SURFACE_NAMES = new Set([
|
|
361
|
+
'__proto__',
|
|
362
|
+
'constructor',
|
|
363
|
+
'prototype',
|
|
364
|
+
]);
|
|
365
|
+
/**
|
|
366
|
+
* Union semantics over domains: one open member opens the whole domain, an
|
|
367
|
+
* unclassifiable member leaves it unclassifiable (it could be hiding an open
|
|
368
|
+
* one), and a closed-but-unenumerable member keeps the union closed without a
|
|
369
|
+
* literal listing.
|
|
370
|
+
*/
|
|
371
|
+
function foldKeyDomains(domains) {
|
|
372
|
+
const values = new Set();
|
|
373
|
+
let closed = false;
|
|
374
|
+
let unknown = false;
|
|
375
|
+
for (const domain of domains) {
|
|
376
|
+
if (domain === 'open') {
|
|
377
|
+
return 'open';
|
|
378
|
+
}
|
|
379
|
+
if (domain === 'unknown') {
|
|
380
|
+
unknown = true;
|
|
381
|
+
}
|
|
382
|
+
else if (domain === 'closed') {
|
|
383
|
+
closed = true;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
for (const value of domain) {
|
|
387
|
+
values.add(value);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (unknown) {
|
|
392
|
+
return 'unknown';
|
|
393
|
+
}
|
|
394
|
+
if (closed) {
|
|
395
|
+
return 'closed';
|
|
396
|
+
}
|
|
397
|
+
return values;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* An enum is a compiler-checked finite set. Members with literal initializers
|
|
401
|
+
* enumerate their runtime key strings (which is what lets the forbidden-name
|
|
402
|
+
* screen and subset comparison see them); a computed or auto-numbered member
|
|
403
|
+
* leaves the set finite but unenumerable.
|
|
404
|
+
*/
|
|
405
|
+
function enumKeyDomain(node) {
|
|
406
|
+
const values = new Set();
|
|
407
|
+
for (const member of node.members) {
|
|
408
|
+
const { initializer } = member;
|
|
409
|
+
if (initializer?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
410
|
+
(typeof initializer.value === 'string' ||
|
|
411
|
+
typeof initializer.value === 'number')) {
|
|
412
|
+
values.add(String(initializer.value));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
return 'closed';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return values;
|
|
419
|
+
}
|
|
349
420
|
exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
350
421
|
name: 'enforce-assert-safe-object-key',
|
|
351
422
|
meta: {
|
|
@@ -620,6 +691,260 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
620
691
|
init.callee.name === 'assertSafe');
|
|
621
692
|
});
|
|
622
693
|
};
|
|
694
|
+
/**
|
|
695
|
+
* The declared key domain of a type annotation. Alias references resolve
|
|
696
|
+
* through the scope chain to the declaration in this file — a type alias
|
|
697
|
+
* recurses into what it aliases, an enum enumerates its members, a generic
|
|
698
|
+
* type parameter is judged by its constraint (an unconstrained one could be
|
|
699
|
+
* instantiated with anything, so it reads as open). `seen` terminates a
|
|
700
|
+
* recursive alias without conflating it with a sibling reference to the
|
|
701
|
+
* same name.
|
|
702
|
+
*/
|
|
703
|
+
const keyDomainOf = (typeNode, anchor, seen) => {
|
|
704
|
+
switch (typeNode.type) {
|
|
705
|
+
case utils_1.AST_NODE_TYPES.TSLiteralType: {
|
|
706
|
+
const { literal } = typeNode;
|
|
707
|
+
if (literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
708
|
+
(typeof literal.value === 'string' ||
|
|
709
|
+
typeof literal.value === 'number')) {
|
|
710
|
+
return new Set([String(literal.value)]);
|
|
711
|
+
}
|
|
712
|
+
return 'unknown';
|
|
713
|
+
}
|
|
714
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
715
|
+
return foldKeyDomains(typeNode.types.map((member) => keyDomainOf(member, anchor, seen)));
|
|
716
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
717
|
+
if (typeNode.typeParameters ||
|
|
718
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
719
|
+
return 'unknown';
|
|
720
|
+
}
|
|
721
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), typeNode.typeName.name);
|
|
722
|
+
if (!variable || variable.defs.length === 0 || seen.has(variable)) {
|
|
723
|
+
return 'unknown';
|
|
724
|
+
}
|
|
725
|
+
const nextSeen = new Set(seen).add(variable);
|
|
726
|
+
return foldKeyDomains(variable.defs.map((def) => {
|
|
727
|
+
switch (def.node.type) {
|
|
728
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
729
|
+
return keyDomainOf(def.node.typeAnnotation, anchor, nextSeen);
|
|
730
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration:
|
|
731
|
+
return enumKeyDomain(def.node);
|
|
732
|
+
case utils_1.AST_NODE_TYPES.TSTypeParameter:
|
|
733
|
+
return def.node.constraint
|
|
734
|
+
? keyDomainOf(def.node.constraint, anchor, nextSeen)
|
|
735
|
+
: 'open';
|
|
736
|
+
default:
|
|
737
|
+
return 'unknown';
|
|
738
|
+
}
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
// `(typeof KINDS)[number]` — the union derived from a values array,
|
|
742
|
+
// which prefer-union-from-const-array rewrites literal-union aliases
|
|
743
|
+
// into. The members live in a value rather than the type syntax, so
|
|
744
|
+
// they are read off the array's own literal elements — but only under
|
|
745
|
+
// `as const`: without it the array's type widens to `string[]` and the
|
|
746
|
+
// indexed access IS `string`, the open domain this rule exists to keep
|
|
747
|
+
// reported.
|
|
748
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType: {
|
|
749
|
+
if (typeNode.objectType.type !== utils_1.AST_NODE_TYPES.TSTypeQuery) {
|
|
750
|
+
return 'unknown';
|
|
751
|
+
}
|
|
752
|
+
const { exprName } = typeNode.objectType;
|
|
753
|
+
if (exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
754
|
+
return 'unknown';
|
|
755
|
+
}
|
|
756
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), exprName.name);
|
|
757
|
+
if (!variable || variable.defs.length !== 1) {
|
|
758
|
+
return 'unknown';
|
|
759
|
+
}
|
|
760
|
+
const def = variable.defs[0];
|
|
761
|
+
if (def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
762
|
+
return 'unknown';
|
|
763
|
+
}
|
|
764
|
+
const init = def.node.init;
|
|
765
|
+
const isAsConstArray = init?.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
766
|
+
init.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
767
|
+
init.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
768
|
+
init.typeAnnotation.typeName.name === 'const' &&
|
|
769
|
+
init.expression.type === utils_1.AST_NODE_TYPES.ArrayExpression;
|
|
770
|
+
if (!isAsConstArray) {
|
|
771
|
+
return 'unknown';
|
|
772
|
+
}
|
|
773
|
+
if (typeNode.indexType.type !== utils_1.AST_NODE_TYPES.TSNumberKeyword) {
|
|
774
|
+
return 'closed';
|
|
775
|
+
}
|
|
776
|
+
const values = new Set();
|
|
777
|
+
for (const element of init.expression
|
|
778
|
+
.elements) {
|
|
779
|
+
if (element?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
780
|
+
(typeof element.value === 'string' ||
|
|
781
|
+
typeof element.value === 'number')) {
|
|
782
|
+
values.add(String(element.value));
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
return 'closed';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return values;
|
|
789
|
+
}
|
|
790
|
+
case utils_1.AST_NODE_TYPES.TSStringKeyword:
|
|
791
|
+
case utils_1.AST_NODE_TYPES.TSNumberKeyword:
|
|
792
|
+
case utils_1.AST_NODE_TYPES.TSSymbolKeyword:
|
|
793
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
794
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
795
|
+
case utils_1.AST_NODE_TYPES.TSTemplateLiteralType:
|
|
796
|
+
return 'open';
|
|
797
|
+
default:
|
|
798
|
+
return 'unknown';
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* The key type parameter of a Record-shaped annotation, read through the
|
|
803
|
+
* wrappers that keep its key domain (`Readonly`, `Partial`) and through a
|
|
804
|
+
* bare in-file alias (`type Lookup = Record<K, V>`). Anything else — a type
|
|
805
|
+
* literal with an index signature, a Map, an imported alias — yields null:
|
|
806
|
+
* the annotation then makes no syntactically checkable claim about which
|
|
807
|
+
* keys exist.
|
|
808
|
+
*/
|
|
809
|
+
const recordKeyTypeOf = (typeNode, anchor, seen = new Set()) => {
|
|
810
|
+
// `Record<K, V> | undefined` — the natural annotation for a receiver
|
|
811
|
+
// reached through `?.` — keys exactly what `Record<K, V>` keys: a nullish
|
|
812
|
+
// receiver short-circuits (or throws), it never indexes anything else.
|
|
813
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
814
|
+
const substantive = typeNode.types.filter((member) => member.type !== utils_1.AST_NODE_TYPES.TSUndefinedKeyword &&
|
|
815
|
+
member.type !== utils_1.AST_NODE_TYPES.TSNullKeyword);
|
|
816
|
+
return substantive.length === 1
|
|
817
|
+
? recordKeyTypeOf(substantive[0], anchor, seen)
|
|
818
|
+
: null;
|
|
819
|
+
}
|
|
820
|
+
if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
|
|
821
|
+
typeNode.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const { name } = typeNode.typeName;
|
|
825
|
+
const args = typeNode.typeParameters?.params;
|
|
826
|
+
if (name === 'Record') {
|
|
827
|
+
return args?.length === 2 ? args[0] : null;
|
|
828
|
+
}
|
|
829
|
+
if (RECORD_KEY_PRESERVING_WRAPPERS.has(name) && args?.length === 1) {
|
|
830
|
+
return recordKeyTypeOf(args[0], anchor, seen);
|
|
831
|
+
}
|
|
832
|
+
if (args) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, anchor), name);
|
|
836
|
+
if (!variable || seen.has(variable) || variable.defs.length !== 1) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
const def = variable.defs[0];
|
|
840
|
+
if (def.node.type !== utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
return recordKeyTypeOf(def.node.typeAnnotation, anchor, new Set(seen).add(variable));
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Whether the Record's declared key domain covers the key's declared type,
|
|
847
|
+
* so that TypeScript itself rejects any key value outside the record's
|
|
848
|
+
* declared keys.
|
|
849
|
+
*
|
|
850
|
+
* Two spellings prove it:
|
|
851
|
+
*
|
|
852
|
+
* - **The same type reference on both sides** (`kind: Kind` into
|
|
853
|
+
* `Record<Kind, V>`). Name identity makes the domains equal whatever the
|
|
854
|
+
* alias holds — an imported alias included — so resolution is consulted
|
|
855
|
+
* only to refuse a domain that resolves to something open (`type K =
|
|
856
|
+
* string` re-opens the very surface this rule guards) or to a literal
|
|
857
|
+
* union naming a prototype field.
|
|
858
|
+
* - **Literal unions the syntax can compare** (`kind: 'live' | 'simulated'`
|
|
859
|
+
* into `Record<'live' | 'simulated', V>`, or a narrowing of it): every
|
|
860
|
+
* literal the key admits must be a declared record key, and none of them
|
|
861
|
+
* may name the prototype surface.
|
|
862
|
+
*/
|
|
863
|
+
const recordKeyCovers = (keyAnnotation, recordKeyType, anchor) => {
|
|
864
|
+
const namesNoPrototypeField = (domain) => ![...domain].some((value) => PROTOTYPE_SURFACE_NAMES.has(value));
|
|
865
|
+
if (keyAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
866
|
+
recordKeyType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
867
|
+
!keyAnnotation.typeParameters &&
|
|
868
|
+
!recordKeyType.typeParameters &&
|
|
869
|
+
context.sourceCode.getText(keyAnnotation.typeName) ===
|
|
870
|
+
context.sourceCode.getText(recordKeyType.typeName)) {
|
|
871
|
+
const domain = keyDomainOf(keyAnnotation, anchor, new Set());
|
|
872
|
+
if (domain === 'open') {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
return typeof domain === 'string' || namesNoPrototypeField(domain);
|
|
876
|
+
}
|
|
877
|
+
const keyDomain = keyDomainOf(keyAnnotation, anchor, new Set());
|
|
878
|
+
if (typeof keyDomain === 'string' ||
|
|
879
|
+
keyDomain.size === 0 ||
|
|
880
|
+
!namesNoPrototypeField(keyDomain)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
const recordDomain = keyDomainOf(recordKeyType, anchor, new Set());
|
|
884
|
+
if (typeof recordDomain === 'string') {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
return [...keyDomain].every((value) => recordDomain.has(value));
|
|
888
|
+
};
|
|
889
|
+
/**
|
|
890
|
+
* The type annotations declared on the binding an identifier resolves to.
|
|
891
|
+
* Every definition must carry one on the binding name itself — an
|
|
892
|
+
* annotation on a binding is what TypeScript checks every write against, so
|
|
893
|
+
* it holds for the lookup no matter which statement assigned last. A
|
|
894
|
+
* destructured binding, an unannotated declarator, an import: null, because
|
|
895
|
+
* nothing constrains what the identifier holds.
|
|
896
|
+
*/
|
|
897
|
+
const declaredAnnotationsOf = (identifier) => {
|
|
898
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
899
|
+
if (!variable || variable.defs.length === 0) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
const annotations = [];
|
|
903
|
+
for (const def of variable.defs) {
|
|
904
|
+
const bindingName = def.name;
|
|
905
|
+
if (bindingName.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
906
|
+
!bindingName.typeAnnotation) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
annotations.push(bindingName.typeAnnotation.typeAnnotation);
|
|
910
|
+
}
|
|
911
|
+
return annotations;
|
|
912
|
+
};
|
|
913
|
+
/**
|
|
914
|
+
* Whether `object[key]` is a lookup the compiler already bounds: the object
|
|
915
|
+
* is a binding annotated `Record<K, V>` and the key a binding whose
|
|
916
|
+
* declared type `K` covers (#1875). Such a lookup cannot reach the
|
|
917
|
+
* prototype surface without the code failing to compile, so `assertSafe`
|
|
918
|
+
* would validate nothing — and it is not identity on the values that DO
|
|
919
|
+
* slip past a declared type at runtime (data crossing a persistence or
|
|
920
|
+
* version boundary): the plain lookup degrades to `undefined` where the
|
|
921
|
+
* wrapped one throws, which is precisely the semantic change that turned a
|
|
922
|
+
* graceful render fallback into a render-time crash. Both sides must be
|
|
923
|
+
* annotated: an `any`-typed or unannotated key indexes into any Record
|
|
924
|
+
* without a compile error, so the record annotation alone proves nothing.
|
|
925
|
+
*/
|
|
926
|
+
const isCompilerBoundedLookup = (node, key) => {
|
|
927
|
+
if (node.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
const objectAnnotations = declaredAnnotationsOf(node.object);
|
|
931
|
+
if (!objectAnnotations) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
const recordKeyTypes = [];
|
|
935
|
+
for (const annotation of objectAnnotations) {
|
|
936
|
+
const recordKeyType = recordKeyTypeOf(annotation, node);
|
|
937
|
+
if (!recordKeyType) {
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
recordKeyTypes.push(recordKeyType);
|
|
941
|
+
}
|
|
942
|
+
const keyAnnotations = declaredAnnotationsOf(key);
|
|
943
|
+
if (!keyAnnotations) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
return keyAnnotations.every((keyAnnotation) => recordKeyTypes.every((recordKeyType) => recordKeyCovers(keyAnnotation, recordKeyType, node)));
|
|
947
|
+
};
|
|
623
948
|
/**
|
|
624
949
|
* Whether the syntax alone proves the key is a number. `__proto__`,
|
|
625
950
|
* `constructor` and `prototype` are never the string form of a number, so a
|
|
@@ -838,6 +1163,12 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
838
1163
|
if (isAssertSafeValidatedIdentifier(property)) {
|
|
839
1164
|
return;
|
|
840
1165
|
}
|
|
1166
|
+
// A typed discriminant indexing a Record whose declared keys cover
|
|
1167
|
+
// its type is compile-time bounded; wrapping it would turn a total
|
|
1168
|
+
// lookup into a throwing one (#1875).
|
|
1169
|
+
if (isCompilerBoundedLookup(node, property)) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
841
1172
|
const propText = context.sourceCode.getText(property);
|
|
842
1173
|
reportWrittenKey(written, property, propText);
|
|
843
1174
|
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 &&
|
|
@@ -41,6 +41,23 @@ const DEFAULT_OPTIONS = {
|
|
|
41
41
|
'@vitest-environment',
|
|
42
42
|
],
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* What the consumer's options are deep-merged over, with every null-valued
|
|
46
|
+
* default removed.
|
|
47
|
+
*
|
|
48
|
+
* `applyDefault` merges before `create` runs, and its `deepMerge` classifies
|
|
49
|
+
* `null` as an object (`typeof null === 'object'`), so a key that is null on
|
|
50
|
+
* both sides is recursed into and reaches `Object.keys(null)`. That throws
|
|
51
|
+
* while LOADING the rule, which aborts the lint for the whole file and takes
|
|
52
|
+
* every other rule with it. `headerTemplate` is exactly that shape: `null` is
|
|
53
|
+
* both its schema-legal value and its documented default, so a consumer who
|
|
54
|
+
* writes the documented default out explicitly crashes their own run.
|
|
55
|
+
*
|
|
56
|
+
* Omitting the key leaves the merge nothing to recurse into. Nothing is lost —
|
|
57
|
+
* `normalizeOptions` reads the default straight from `DEFAULT_OPTIONS`, so an
|
|
58
|
+
* absent key and a null one already resolve identically.
|
|
59
|
+
*/
|
|
60
|
+
const MERGEABLE_DEFAULT_OPTIONS = Object.fromEntries(Object.entries(DEFAULT_OPTIONS).filter(([, value]) => value !== null));
|
|
44
61
|
/**
|
|
45
62
|
* Maximum number of characters to scan at the beginning of a file to detect generated markers.
|
|
46
63
|
*/
|
|
@@ -381,7 +398,7 @@ exports.enforceUniqueCursorHeaders = (0, createRule_1.createRule)({
|
|
|
381
398
|
splitHeaderFragment: 'Cursor header metadata is split across adjacent comment blocks → Fragmented headers are easy to miss and let required tags drift out of sync → Merge the fragments into a single top-of-file header containing: {{tags}}.',
|
|
382
399
|
},
|
|
383
400
|
},
|
|
384
|
-
defaultOptions: [
|
|
401
|
+
defaultOptions: [MERGEABLE_DEFAULT_OPTIONS],
|
|
385
402
|
create(context, [userOptions]) {
|
|
386
403
|
const options = normalizeOptions(userOptions);
|
|
387
404
|
const fileName = context.getFilename();
|
|
@@ -414,17 +414,20 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
414
414
|
if (!definition)
|
|
415
415
|
return;
|
|
416
416
|
const defNode = definition.node;
|
|
417
|
-
if (!defNode ||
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
417
|
+
if (!defNode || defNode.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// `global-const-style` autofixes a module-scope object literal to
|
|
421
|
+
// `as const`, so reading `init` raw lets one fixer silence this rule.
|
|
422
|
+
const holder = unwrapExpression(defNode.init);
|
|
423
|
+
if (!holder || holder.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
421
424
|
return;
|
|
422
425
|
}
|
|
423
426
|
if (resolvedOptions.allowModuleScopeFactories &&
|
|
424
427
|
isStableForConsumer(defNode, consumerFunction)) {
|
|
425
428
|
return;
|
|
426
429
|
}
|
|
427
|
-
const fnNode = findObjectPropertyFunction(
|
|
430
|
+
const fnNode = findObjectPropertyFunction(holder, member.property.name);
|
|
428
431
|
if (fnNode &&
|
|
429
432
|
isComponentLikeFunction(fnNode, context, member.property.name)) {
|
|
430
433
|
report(member, propName, member.property.name);
|
|
@@ -68,18 +68,33 @@ exports.noObjectValuesOnStrings = (0, createRule_1.createRule)({
|
|
|
68
68
|
node.callee.property.name === 'values' &&
|
|
69
69
|
node.arguments.length > 0);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* `a?.b()` parses as a ChainExpression wrapping the call. The wrapper is
|
|
73
|
+
* ESTree-only, so it has no TypeScript node: leaving it in place defeats
|
|
74
|
+
* both the syntactic tests below and `getTypeAtLocation`, and a nullable
|
|
75
|
+
* receiver is exactly where `?.` gets written.
|
|
76
|
+
*/
|
|
77
|
+
function unwrapChain(node) {
|
|
78
|
+
let current = node;
|
|
79
|
+
while (current.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
80
|
+
current = current.expression;
|
|
81
|
+
}
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
71
84
|
/**
|
|
72
85
|
* Checks if a node is a string literal or template literal
|
|
73
86
|
*/
|
|
74
87
|
function isStringLiteral(node) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
const inner = unwrapChain(node);
|
|
89
|
+
return ((inner.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
90
|
+
typeof inner.value === 'string') ||
|
|
91
|
+
inner.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
|
|
78
92
|
}
|
|
79
93
|
/**
|
|
80
94
|
* Checks if a node is likely to produce a string value based on AST patterns
|
|
81
95
|
*/
|
|
82
|
-
function isLikelyStringExpression(
|
|
96
|
+
function isLikelyStringExpression(maybeChained) {
|
|
97
|
+
const node = unwrapChain(maybeChained);
|
|
83
98
|
// Check for string concatenation
|
|
84
99
|
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
85
100
|
node.operator === '+' &&
|
|
@@ -190,8 +205,10 @@ exports.noObjectValuesOnStrings = (0, createRule_1.createRule)({
|
|
|
190
205
|
CallExpression(node) {
|
|
191
206
|
// Check if the call is Object.values()
|
|
192
207
|
if (isObjectValuesCall(node)) {
|
|
193
|
-
|
|
194
|
-
|
|
208
|
+
// The message quotes what the author wrote, so text comes from the
|
|
209
|
+
// original node while every test below reads through the chain.
|
|
210
|
+
const argumentText = sourceCode.getText(node.arguments[0]);
|
|
211
|
+
const argument = unwrapChain(node.arguments[0]);
|
|
195
212
|
// Quick check for string literals and template literals
|
|
196
213
|
if (isStringLiteral(argument)) {
|
|
197
214
|
context.report({
|
|
@@ -421,8 +421,16 @@ exports.noPassthroughGetters = (0, createRule_1.createRule)({
|
|
|
421
421
|
if (node.type === 'ConditionalExpression') {
|
|
422
422
|
return true;
|
|
423
423
|
}
|
|
424
|
-
// Check for optional chaining like this.settings?.property
|
|
425
|
-
|
|
424
|
+
// Check for optional chaining like this.settings?.property. `a?.b` parses
|
|
425
|
+
// as a ChainExpression wrapping the member access, so the wrapper has to
|
|
426
|
+
// be stripped here or this arm never fires — the shape stays silent only
|
|
427
|
+
// because the member walker cannot follow it, which would reverse the
|
|
428
|
+
// moment that walker learns to.
|
|
429
|
+
let unchained = node;
|
|
430
|
+
while (unchained.type === 'ChainExpression') {
|
|
431
|
+
unchained = unchained.expression;
|
|
432
|
+
}
|
|
433
|
+
if (unchained.type === 'MemberExpression' && unchained.optional) {
|
|
426
434
|
return true;
|
|
427
435
|
}
|
|
428
436
|
// The nullish coalescing check is already covered by the LogicalExpression check above
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noUselessUsememoPrimitives = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const replacementSegments_1 = require("../utils/replacementSegments");
|
|
6
7
|
const tsTypeClassifier_1 = require("../utils/tsTypeClassifier");
|
|
7
8
|
const DEFAULT_OPTIONS = {
|
|
8
9
|
ignoreCallExpressions: true,
|
|
@@ -381,24 +382,44 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
381
382
|
valueKind,
|
|
382
383
|
},
|
|
383
384
|
fix(fixer) {
|
|
385
|
+
const expressionText = sourceCode.getText(returnedExpression);
|
|
384
386
|
// Inlining replaces the entire useMemo(...) call with the returned
|
|
385
387
|
// expression's text, so any comment inside the call but outside
|
|
386
388
|
// that expression — an eslint-disable-next-line directive on the
|
|
387
389
|
// return statement among them — has no representation in the
|
|
388
|
-
// replacement
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// the
|
|
390
|
+
// replacement. Dropping one changes which rules report on the file
|
|
391
|
+
// (#1591), and declining the fix whenever one is present makes a
|
|
392
|
+
// comment decide whether the rule rewrites at all (#1877). Both are
|
|
393
|
+
// avoided by carrying every such comment into the replacement,
|
|
394
|
+
// where the directives among them keep the line relationship they
|
|
395
|
+
// were written with.
|
|
393
396
|
const strandedComments = sourceCode
|
|
394
397
|
.getCommentsInside(node)
|
|
395
398
|
.filter((comment) => comment.range[0] < returnedExpression.range[0] ||
|
|
396
399
|
comment.range[1] > returnedExpression.range[1]);
|
|
397
|
-
if (strandedComments.length
|
|
398
|
-
return
|
|
400
|
+
if (strandedComments.length === 0) {
|
|
401
|
+
return fixer.replaceText(node, `(${expressionText})`);
|
|
399
402
|
}
|
|
400
|
-
|
|
401
|
-
|
|
403
|
+
// A comment inside the call lies wholly on one side of the
|
|
404
|
+
// expression, since a comment is a token and cannot straddle a
|
|
405
|
+
// node; keeping each on its own side preserves what it annotates.
|
|
406
|
+
const toSegment = (comment) => ({
|
|
407
|
+
text: sourceCode.text.slice(comment.range[0], comment.range[1]),
|
|
408
|
+
breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
|
|
409
|
+
});
|
|
410
|
+
const isBefore = (comment) => comment.range[0] < returnedExpression.range[0];
|
|
411
|
+
const segments = [
|
|
412
|
+
...strandedComments.filter(isBefore).map(toSegment),
|
|
413
|
+
{ text: expressionText, breakAfter: false },
|
|
414
|
+
...strandedComments
|
|
415
|
+
.filter((comment) => !isBefore(comment))
|
|
416
|
+
.map(toSegment),
|
|
417
|
+
];
|
|
418
|
+
// The call can start mid-line, so the indentation of the line it
|
|
419
|
+
// opens on is the only anchor the carried comments have.
|
|
420
|
+
const startLine = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
421
|
+
const indent = /^[\t ]*/.exec(startLine)?.[0] ?? '';
|
|
422
|
+
return fixer.replaceText(node, (0, replacementSegments_1.joinSegments)(segments, indent));
|
|
402
423
|
},
|
|
403
424
|
});
|
|
404
425
|
},
|