@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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.134',
226
+ version: '1.20.136',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- // Only flag simple template literals that are just `${id}`
812
- // Complex templates with additional text like `prefix_${id}_suffix` are allowed
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
- if (!isSimpleVarInterpolation) {
818
- // Complex template literals with additional text are fine
819
- return;
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/methods, unsupported key literals) or would produce nothing at
125
- // all. Bailing leaves the report in place so the developer flattens by hand
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
- entries.push([fullKey, sourceCode.getText(property.value)]);
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.property.type === utils_1.AST_NODE_TYPES.Identifier &&
359
- node.property.name === name) {
385
+ staticMemberName(node) === name) {
360
386
  unsafe = true;
361
387
  return;
362
388
  }