@blumintinc/eslint-plugin-blumint 1.20.161 → 1.20.163
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/no-hungarian.js +131 -2
- package/lib/rules/prefer-clone-deep.js +33 -10
- package/lib/rules/require-memoize-jsx-returners.js +137 -0
- package/package.json +1 -1
- package/release-manifest.json +36 -0
package/lib/index.js
CHANGED
|
@@ -155,6 +155,53 @@ const DOMAIN_SYMBOL_HEAD_NOUNS = new Set([
|
|
|
155
155
|
'unicode',
|
|
156
156
|
'ascii',
|
|
157
157
|
]);
|
|
158
|
+
// Domain head nouns that legitimately precede a "Class" suffix. In
|
|
159
|
+
// <taxonomy>Class the trailing "Class" is the HEAD NOUN of the domain concept —
|
|
160
|
+
// the BUCKET a value falls into — not the JavaScript `class` construct bolted
|
|
161
|
+
// onto the name. "Window size class" is the Material Design 3 / UIKit term for
|
|
162
|
+
// a breakpoint bucket (compact/medium/expanded), a regex *character class* is
|
|
163
|
+
// `[a-z]`, an S3 *storage class* is "STANDARD" — in each case the value is a
|
|
164
|
+
// string or number map, so there is no type marker to strip: removing the
|
|
165
|
+
// suffix yields a wrong name (WINDOW_SIZE names a width, not a bucket). Same
|
|
166
|
+
// reasoning as the <entity>Number (#1277) and <domain>Symbol (#1835)
|
|
167
|
+
// carve-outs, applied to the taxonomy sense of "class" (#2030).
|
|
168
|
+
//
|
|
169
|
+
// Nouns whose <noun>Class reads as a tag on a JS class value — user, config,
|
|
170
|
+
// helper, base, model, controller, wrapper — are intentionally ABSENT, so
|
|
171
|
+
// UserClass / userClass / HELPER_CLASS stay flagged, as do all PREFIX uses
|
|
172
|
+
// (classRegistry, CLASS_MAP). The carve-out is additionally vetoed whenever the
|
|
173
|
+
// declaration syntactically proves a real class (see isClassValuedDeclaration).
|
|
174
|
+
const DOMAIN_CLASS_HEAD_NOUNS = new Set([
|
|
175
|
+
// Layout / responsive design: M3 and UIKit bucket a window's width into a
|
|
176
|
+
// "size class".
|
|
177
|
+
'size',
|
|
178
|
+
// Regex and linguistics: character class ([a-z]), word class (noun/verb).
|
|
179
|
+
'character',
|
|
180
|
+
'word',
|
|
181
|
+
// Mathematics / CS taxonomy: equivalence class, complexity class (P, NP).
|
|
182
|
+
'equivalence',
|
|
183
|
+
'complexity',
|
|
184
|
+
// Systems / infrastructure: C and S3 storage class, QoS traffic/service
|
|
185
|
+
// class, USB device class.
|
|
186
|
+
'storage',
|
|
187
|
+
'traffic',
|
|
188
|
+
'service',
|
|
189
|
+
'device',
|
|
190
|
+
// Finance: asset class, share class.
|
|
191
|
+
'asset',
|
|
192
|
+
'share',
|
|
193
|
+
// Travel: fare/cabin/booking class (airline RBD codes).
|
|
194
|
+
'fare',
|
|
195
|
+
'cabin',
|
|
196
|
+
'booking',
|
|
197
|
+
// Categorization by attribute: weight class (boxing), age class, hazard
|
|
198
|
+
// class (DOT), drug class (pharmacology), vehicle class (DMV).
|
|
199
|
+
'weight',
|
|
200
|
+
'age',
|
|
201
|
+
'hazard',
|
|
202
|
+
'drug',
|
|
203
|
+
'vehicle',
|
|
204
|
+
]);
|
|
158
205
|
// Common built-in JavaScript prototype methods
|
|
159
206
|
const BUILT_IN_METHODS = new Set([
|
|
160
207
|
// String methods
|
|
@@ -404,6 +451,24 @@ function isDomainSymbolCompound(name) {
|
|
|
404
451
|
const lastSegment = segments[segments.length - 1];
|
|
405
452
|
return (!!lastSegment && DOMAIN_SYMBOL_HEAD_NOUNS.has(lastSegment.toLowerCase()));
|
|
406
453
|
}
|
|
454
|
+
// Is `name` a domain compound of the form <taxonomy>Class, where the word
|
|
455
|
+
// directly before the trailing "Class" names a bucketing taxonomy
|
|
456
|
+
// (windowSizeClass, characterClass, storageClass)? Only the LAST head segment
|
|
457
|
+
// is consulted, so prefixed variants generalize (currentWindowSizeClass passes)
|
|
458
|
+
// while names whose value is a real JS class keep firing (userClass -> head
|
|
459
|
+
// segment "user", not a taxonomy).
|
|
460
|
+
function isDomainClassCompound(name) {
|
|
461
|
+
if (!name.endsWith('Class')) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
const head = name.slice(0, -'Class'.length);
|
|
465
|
+
if (head.length === 0) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
const segments = splitCamelSegments(head);
|
|
469
|
+
const lastSegment = segments[segments.length - 1];
|
|
470
|
+
return (!!lastSegment && DOMAIN_CLASS_HEAD_NOUNS.has(lastSegment.toLowerCase()));
|
|
471
|
+
}
|
|
407
472
|
// Does a type annotation denote the JS `symbol` primitive (`symbol` or the
|
|
408
473
|
// declaration-site form `unique symbol`)?
|
|
409
474
|
function isSymbolTypeAnnotation(node) {
|
|
@@ -476,6 +541,43 @@ function isSymbolTypedDeclaration(node) {
|
|
|
476
541
|
return false;
|
|
477
542
|
}
|
|
478
543
|
}
|
|
544
|
+
// Does a type annotation denote a class constructor (`new (...) => T`)? Such an
|
|
545
|
+
// annotation is the one syntactic spelling that proves the annotated value is a
|
|
546
|
+
// class without type information.
|
|
547
|
+
function isConstructorTypeAnnotation(node) {
|
|
548
|
+
return !!node && node.type === utils_1.AST_NODE_TYPES.TSConstructorType;
|
|
549
|
+
}
|
|
550
|
+
// Does the declaration site PROVE, syntactically, that the named value is a JS
|
|
551
|
+
// class? Only a `class` expression initializer, a class declaration's own name,
|
|
552
|
+
// or an explicit constructor-type annotation are conclusive without type
|
|
553
|
+
// information — an aliased constructor (`const sizeClass = User`) is invisible
|
|
554
|
+
// here, which is precisely why the taxonomy carve-out is keyed on the head noun
|
|
555
|
+
// rather than on the type. When this holds, the trailing "Class" genuinely
|
|
556
|
+
// encodes the value's type and the DOMAIN_CLASS_HEAD_NOUNS carve-out is vetoed,
|
|
557
|
+
// so `const SizeClass = class {}` still reports. Mirrors
|
|
558
|
+
// isSymbolTypedDeclaration (#1835).
|
|
559
|
+
function isClassValuedDeclaration(node) {
|
|
560
|
+
if (isConstructorTypeAnnotation(node.typeAnnotation?.typeAnnotation)) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
const parent = node.parent;
|
|
564
|
+
if (!parent) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
switch (parent.type) {
|
|
568
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
569
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
570
|
+
return parent.id === node;
|
|
571
|
+
case utils_1.AST_NODE_TYPES.VariableDeclarator:
|
|
572
|
+
return (parent.id === node &&
|
|
573
|
+
parent.init?.type === utils_1.AST_NODE_TYPES.ClassExpression);
|
|
574
|
+
case utils_1.AST_NODE_TYPES.PropertyDefinition:
|
|
575
|
+
return (parent.key === node &&
|
|
576
|
+
parent.value?.type === utils_1.AST_NODE_TYPES.ClassExpression);
|
|
577
|
+
default:
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
479
581
|
// Rebuild a SCREAMING_SNAKE_CASE identifier's segments into a PascalCase compound
|
|
480
582
|
// (["MATCH","NUMBER"] -> "MatchNumber") so the snake-case branch can reuse the
|
|
481
583
|
// camelCase isDomainNumberCompound / DOMAIN_NUMBER_HEAD_NOUNS exemption verbatim,
|
|
@@ -512,7 +614,9 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
512
614
|
// interfaces, classes), enabling the semantic-type-concept exemption.
|
|
513
615
|
// `isSymbolTyped` is true when the declaration syntactically proves a JS
|
|
514
616
|
// `symbol` value, which vetoes the <domain>Symbol glyph exemption.
|
|
515
|
-
|
|
617
|
+
// `isClassValued` is true when the declaration syntactically proves a JS
|
|
618
|
+
// class value, which vetoes the <taxonomy>Class exemption.
|
|
619
|
+
function hasTypeMarker(variableName, isTypeName = false, isSymbolTyped = false, isClassValued = false) {
|
|
516
620
|
// Type names whose type-word denotes a concept/relation (StringToNumber,
|
|
517
621
|
// CapitalizedString, FuncKeys, PromiseOrValue) are not Hungarian — the word
|
|
518
622
|
// is part of the type's meaning, like the allowed compound noun PhoneNumber.
|
|
@@ -621,6 +725,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
621
725
|
isDomainSymbolCompound(screamingSnakePartsToPascalCase(parts))) {
|
|
622
726
|
return false;
|
|
623
727
|
}
|
|
728
|
+
// A trailing "..._CLASS" whose preceding head noun names a
|
|
729
|
+
// bucketing taxonomy (WINDOW_SIZE_CLASS, STORAGE_CLASS) is a
|
|
730
|
+
// domain compound, not a type tag — same carve-out as camelCase
|
|
731
|
+
// windowSizeClass (#2030), routed through the shared PascalCase
|
|
732
|
+
// helper so the two casings cannot diverge (the #1294 asymmetry).
|
|
733
|
+
if (normalizedMarker === 'class' &&
|
|
734
|
+
index === lastIndex &&
|
|
735
|
+
!isClassValued &&
|
|
736
|
+
isDomainClassCompound(screamingSnakePartsToPascalCase(parts))) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
624
739
|
return true;
|
|
625
740
|
});
|
|
626
741
|
});
|
|
@@ -695,6 +810,20 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
695
810
|
isDomainSymbolCompound(variableName)) {
|
|
696
811
|
return false;
|
|
697
812
|
}
|
|
813
|
+
// A trailing "...Class" whose head noun names a bucketing taxonomy
|
|
814
|
+
// (windowSizeClass, characterClass, storageClass) is a domain
|
|
815
|
+
// compound: the suffix names WHAT the value is (the BUCKET a window
|
|
816
|
+
// width falls into — Material Design 3 / UIKit vocabulary), and
|
|
817
|
+
// stripping it yields a wrong name (windowSize is a width, not a
|
|
818
|
+
// bucket) (#2030). Scoped to the full-word `Class` marker in SUFFIX
|
|
819
|
+
// position only, and vetoed when the declaration proves a real
|
|
820
|
+
// class, so userClass / classRegistry / `const SizeClass = class {}`
|
|
821
|
+
// keep firing.
|
|
822
|
+
if (normalizedMarker === 'class' &&
|
|
823
|
+
!isClassValued &&
|
|
824
|
+
isDomainClassCompound(variableName)) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
698
827
|
return true;
|
|
699
828
|
}
|
|
700
829
|
// Full type-word markers (non-abbreviations: String, Number, Function,
|
|
@@ -770,7 +899,7 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
770
899
|
if (isExternalOrBuiltIn(node))
|
|
771
900
|
return;
|
|
772
901
|
// Check for type markers
|
|
773
|
-
if (hasTypeMarker(name, isTypeName, isSymbolTypedDeclaration(node))) {
|
|
902
|
+
if (hasTypeMarker(name, isTypeName, isSymbolTypedDeclaration(node), isClassValuedDeclaration(node))) {
|
|
774
903
|
context.report({
|
|
775
904
|
node,
|
|
776
905
|
messageId: 'noHungarian',
|
|
@@ -34,15 +34,13 @@ function isConstAssertion(node) {
|
|
|
34
34
|
/**
|
|
35
35
|
* A `const` assertion is legal only on a literal, so leaving one wrapped around
|
|
36
36
|
* the emitted `cloneDeep(...)` call yields TS1355 and turns a compiling file
|
|
37
|
-
* into a broken one (#2011).
|
|
38
|
-
* the assertion, which is the conservative reading of a `const` the author
|
|
39
|
-
* asked for on a value this rule replaces.
|
|
37
|
+
* into a broken one (#2011).
|
|
40
38
|
*
|
|
41
|
-
* The whole assertion chain is walked because each of its links
|
|
42
|
-
* to the emitted call: `as Foo as const`, `satisfies Foo as
|
|
43
|
-
* `! as const` are TS1355 just the same. The walk stops at the first
|
|
44
|
-
* that is not an assertion, which keeps `as const` on an ENCLOSING
|
|
45
|
-
* fixable — that assertion still has a literal to apply to.
|
|
39
|
+
* The whole assertion chain above `node` is walked because each of its links
|
|
40
|
+
* still applies to the emitted call: `as Foo as const`, `satisfies Foo as
|
|
41
|
+
* const` and `! as const` are TS1355 just the same. The walk stops at the first
|
|
42
|
+
* parent that is not an assertion, which keeps `as const` on an ENCLOSING
|
|
43
|
+
* literal fixable — that assertion still has a literal to apply to.
|
|
46
44
|
*
|
|
47
45
|
* Only a `const` assertion is disqualifying: `as Foo` and `satisfies Foo` are
|
|
48
46
|
* legal on a call expression and keep their fix.
|
|
@@ -60,6 +58,30 @@ function isConstAsserted(node) {
|
|
|
60
58
|
}
|
|
61
59
|
return false;
|
|
62
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The node the `cloneDeep(...)` call replaces for a rewritten literal, or null
|
|
63
|
+
* where the fix has to be declined.
|
|
64
|
+
*
|
|
65
|
+
* A `const` assertion applied DIRECTLY to the literal (`{ ... } as const`,
|
|
66
|
+
* optionally followed by `as Foo`, `satisfies Foo` or `!`) is absorbed: the
|
|
67
|
+
* replaced range covers the assertion, so the call takes its place with no
|
|
68
|
+
* `as const` left to wrap it. The emitted call already spells `as const` on its
|
|
69
|
+
* overrides literal, which is the only place a `const` assertion stays legal
|
|
70
|
+
* after the rewrite, so the author's literal typing lands there. Absorbing it
|
|
71
|
+
* is also what keeps the fix reachable under a composed `--fix`:
|
|
72
|
+
* `global-const-style` wins the range race on a module-scope constant and
|
|
73
|
+
* appends `as const` before this rule's turn, and declining on that assertion
|
|
74
|
+
* would report the hazard forever without ever fixing it (#2032).
|
|
75
|
+
*
|
|
76
|
+
* A `const` assertion behind another link (`as Foo as const`) cannot be
|
|
77
|
+
* absorbed without dropping the intervening assertion, and it would still wrap
|
|
78
|
+
* the emitted call — TS1355 either way — so the fix is declined there.
|
|
79
|
+
*/
|
|
80
|
+
function rewriteSiteOf(target) {
|
|
81
|
+
const parent = target.parent;
|
|
82
|
+
const site = parent && isConstAssertion(parent) ? parent : target;
|
|
83
|
+
return isConstAsserted(site) ? null : site;
|
|
84
|
+
}
|
|
63
85
|
exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
64
86
|
name: 'prefer-clone-deep',
|
|
65
87
|
meta: {
|
|
@@ -479,14 +501,15 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
|
479
501
|
return null;
|
|
480
502
|
}
|
|
481
503
|
for (const target of targets) {
|
|
482
|
-
|
|
504
|
+
const site = rewriteSiteOf(target);
|
|
505
|
+
if (site === null) {
|
|
483
506
|
return null;
|
|
484
507
|
}
|
|
485
508
|
const call = buildCloneDeepCall(target);
|
|
486
509
|
if (call === null) {
|
|
487
510
|
return null;
|
|
488
511
|
}
|
|
489
|
-
rewrites.push(fixer.replaceText(
|
|
512
|
+
rewrites.push(fixer.replaceText(site, call));
|
|
490
513
|
}
|
|
491
514
|
return rewrites;
|
|
492
515
|
},
|
|
@@ -12,6 +12,25 @@ const MEMOIZE_MODULES = new Set([
|
|
|
12
12
|
'typescript-memoize',
|
|
13
13
|
]);
|
|
14
14
|
const MEMOIZE_EXPORT_NAME = 'Memoize';
|
|
15
|
+
/**
|
|
16
|
+
* The base classes React ships for class components. A class extending one of
|
|
17
|
+
* them hands its `render()` to React, which re-invokes it on every state and
|
|
18
|
+
* props change by contract (see `isReactComponentClass`).
|
|
19
|
+
*/
|
|
20
|
+
const REACT_COMPONENT_BASE_NAMES = new Set(['Component', 'PureComponent']);
|
|
21
|
+
const RENDER_METHOD_NAME = 'render';
|
|
22
|
+
/**
|
|
23
|
+
* The wrappers `unwrapSuperClass` strips. `ChainExpression` is ESTree's
|
|
24
|
+
* envelope for `extends X?.Component`, which is grammatical and, whenever the
|
|
25
|
+
* receiver is defined, means exactly `X.Component`.
|
|
26
|
+
*/
|
|
27
|
+
const SUPERCLASS_WRAPPER_TYPES = new Set([
|
|
28
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
29
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
30
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
31
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
32
|
+
utils_1.AST_NODE_TYPES.ChainExpression,
|
|
33
|
+
]);
|
|
15
34
|
function isMemoizeDecorator(decorator, alias, namespaceAlias) {
|
|
16
35
|
const expression = decorator.expression;
|
|
17
36
|
const matchesAliasIdentifier = (node) => !!node && node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === alias;
|
|
@@ -46,6 +65,100 @@ function isMemoizeDecorator(decorator, alias, namespaceAlias) {
|
|
|
46
65
|
}
|
|
47
66
|
return false;
|
|
48
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The expression a class extends, with the wrappers an author can put around it
|
|
70
|
+
* stripped: `extends (React.Component)`, `extends (Component as any)`,
|
|
71
|
+
* `extends Base!`, `extends React?.Component`. The type arguments in
|
|
72
|
+
* `extends Component<Props, State>` live on `superTypeParameters` and never
|
|
73
|
+
* reach here.
|
|
74
|
+
*/
|
|
75
|
+
function unwrapSuperClass(expression) {
|
|
76
|
+
let current = expression;
|
|
77
|
+
for (;;) {
|
|
78
|
+
if (isParenthesizedExpression(current)) {
|
|
79
|
+
current = current.expression;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Compared as strings: `superClass` is typed as a LeftHandSideExpression,
|
|
83
|
+
// which excludes the assertion forms the parser nevertheless yields there.
|
|
84
|
+
if (SUPERCLASS_WRAPPER_TYPES.has(current.type)) {
|
|
85
|
+
current = current
|
|
86
|
+
.expression;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether the class hands its `render()` to React — that is, whether it extends
|
|
94
|
+
* React's `Component` or `PureComponent`.
|
|
95
|
+
*
|
|
96
|
+
* The match is keyed on React's VOCABULARY, not on the binding's provenance: a
|
|
97
|
+
* superclass spelled `Component` or `PureComponent` qualifies wherever the name
|
|
98
|
+
* is bound — an unaliased `import { Component } from 'react'`, an ambient
|
|
99
|
+
* global, a fixture that omits the import — and so does `X.Component` /
|
|
100
|
+
* `X.PureComponent` through any namespace object (`React.Component`,
|
|
101
|
+
* `Preact.PureComponent`, an aliased default import). Only where the spelling
|
|
102
|
+
* carries no vocabulary is the binding resolved through the scope chain: an
|
|
103
|
+
* import specifier renamed away from those names
|
|
104
|
+
* (`import { Component as ReactComponent } from 'react'`) and a same-file class
|
|
105
|
+
* that itself extends one of them (`class Base extends React.Component {}` …
|
|
106
|
+
* `class Boundary extends Base {}`). A superclass whose name is neither of
|
|
107
|
+
* those and resolves to nothing React-shaped in this file — `extends Base` from
|
|
108
|
+
* another module — is NOT treated as a component.
|
|
109
|
+
*
|
|
110
|
+
* Provenance is deliberately not verified: `class Foo extends Component` where
|
|
111
|
+
* `Component` is an unrelated local class is a corner case whose cost is one
|
|
112
|
+
* unreported factory named `render`, while treating a real component's `render`
|
|
113
|
+
* as a factory hands `--fix` a decorator that pins the component to its first
|
|
114
|
+
* output — a silent behavioural break (#2033). A false negative is the cheaper
|
|
115
|
+
* mistake, so the vocabulary wins.
|
|
116
|
+
*/
|
|
117
|
+
function isReactComponentClass(classNode, context, visited = new Set()) {
|
|
118
|
+
if (!classNode.superClass || visited.has(classNode)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
visited.add(classNode);
|
|
122
|
+
const superClass = unwrapSuperClass(classNode.superClass);
|
|
123
|
+
if (superClass.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
124
|
+
!superClass.computed &&
|
|
125
|
+
superClass.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
126
|
+
return REACT_COMPONENT_BASE_NAMES.has(superClass.property.name);
|
|
127
|
+
}
|
|
128
|
+
if (superClass.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (REACT_COMPONENT_BASE_NAMES.has(superClass.name)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, classNode), superClass.name);
|
|
135
|
+
if (!variable) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return variable.defs.some((def) => {
|
|
139
|
+
const declaration = def.node;
|
|
140
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
141
|
+
declaration.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
142
|
+
return REACT_COMPONENT_BASE_NAMES.has(declaration.imported.name);
|
|
143
|
+
}
|
|
144
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
145
|
+
declaration.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
146
|
+
return isReactComponentClass(declaration, context, visited);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether the member is the `render` React calls — the key is read literally,
|
|
153
|
+
* so a computed `[render]()` naming some other value does not qualify.
|
|
154
|
+
*/
|
|
155
|
+
function isRenderMember(node) {
|
|
156
|
+
const { key } = node;
|
|
157
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier && !node.computed) {
|
|
158
|
+
return key.name === RENDER_METHOD_NAME;
|
|
159
|
+
}
|
|
160
|
+
return (key.type === utils_1.AST_NODE_TYPES.Literal && key.value === RENDER_METHOD_NAME);
|
|
161
|
+
}
|
|
49
162
|
function getMemberName(node) {
|
|
50
163
|
const key = node.key;
|
|
51
164
|
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -548,6 +661,30 @@ exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
|
548
661
|
if (classBody?.parent?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
549
662
|
return;
|
|
550
663
|
}
|
|
664
|
+
// React re-invokes a class component's `render()` on every state and
|
|
665
|
+
// props change BY CONTRACT, so `@Memoize()` there is never a remedy:
|
|
666
|
+
// it pins the component to the output of its first render. An error
|
|
667
|
+
// boundary is the sharpest case — it catches, `getDerivedStateFromError`
|
|
668
|
+
// sets state, React re-renders, and the memoized `render()` hands back
|
|
669
|
+
// the cached pre-error children, so the fallback can never appear
|
|
670
|
+
// (#2033). Unlike this rule's compile-breaking autofix defects
|
|
671
|
+
// (#1414, #1434, #1950, #1951, #1955), the result compiles and lints
|
|
672
|
+
// clean, so nothing downstream catches it. Report and fix are both
|
|
673
|
+
// withheld — the message's only remedy is the very edit that breaks
|
|
674
|
+
// the component. `render` is the ONLY instance lifecycle method that
|
|
675
|
+
// returns an element (`shouldComponentUpdate` returns a boolean,
|
|
676
|
+
// `getSnapshotBeforeUpdate` an opaque snapshot, the rest `void`), and
|
|
677
|
+
// the statics React also calls — `getDerivedStateFromError`,
|
|
678
|
+
// `getDerivedStateFromProps` — return state and are out of scope above
|
|
679
|
+
// regardless, so the exemption is keyed on that one name. Other
|
|
680
|
+
// members of a class component are the author's own factories, called
|
|
681
|
+
// on the author's schedule, and stay under the rule. Withholding the
|
|
682
|
+
// report here also keeps `render` out of the import-carrier race below.
|
|
683
|
+
if (isRenderMember(node) &&
|
|
684
|
+
classBody?.parent?.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
685
|
+
isReactComponentClass(classBody.parent, context)) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
551
688
|
const hasDecorator = node.decorators?.some((decorator) => isMemoizeDecorator(decorator, memoizeAlias, memoizeNamespace));
|
|
552
689
|
if (hasDecorator) {
|
|
553
690
|
return;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.163",
|
|
4
|
+
"date": "2026-08-17T20:02:54.585Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-clone-deep",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2032
|
|
11
|
+
],
|
|
12
|
+
"summary": "absorb a direct `as const` so the composed --fix lands (closes #2032)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "require-memoize-jsx-returners",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2033
|
|
19
|
+
],
|
|
20
|
+
"summary": "exempt render() on a React class component (closes #2033)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.162",
|
|
26
|
+
"date": "2026-08-17T15:38:50.817Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "no-hungarian",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2030
|
|
33
|
+
],
|
|
34
|
+
"summary": "treat Class as a taxonomy head noun in suffix position (closes #2030)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.161",
|
|
4
40
|
"date": "2026-08-17T13:22:13.676Z",
|