@blumintinc/eslint-plugin-blumint 1.20.154 → 1.20.155
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-explicit-return-type.js +157 -21
- package/lib/rules/prefer-union-from-const-array.js +43 -0
- package/lib/rules/require-image-optimized.js +79 -1
- package/lib/utils/ASTHelpers.d.ts +6 -0
- package/lib/utils/ASTHelpers.js +41 -7
- package/package.json +1 -1
- package/release-manifest.json +38 -0
package/lib/index.js
CHANGED
|
@@ -569,32 +569,164 @@ function isOverloadedFunction(node) {
|
|
|
569
569
|
}
|
|
570
570
|
return false;
|
|
571
571
|
}
|
|
572
|
-
|
|
572
|
+
/**
|
|
573
|
+
* The statement list that directly holds `node`, looking through `export`.
|
|
574
|
+
*
|
|
575
|
+
* Overload signatures and their implementation are siblings of one statement
|
|
576
|
+
* list — an overload set cannot span containers — so this is the only list worth
|
|
577
|
+
* reading. Walking outward instead would let a same-named function in an
|
|
578
|
+
* enclosing scope answer for one it cannot overload.
|
|
579
|
+
*/
|
|
580
|
+
function siblingStatementsOf(node) {
|
|
581
|
+
const parent = node.parent;
|
|
582
|
+
if (!parent)
|
|
583
|
+
return undefined;
|
|
584
|
+
const container = parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
585
|
+
parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
586
|
+
? parent.parent
|
|
587
|
+
: parent;
|
|
588
|
+
return container ? (0, lexicalScope_1.statementsOf)(container) : undefined;
|
|
589
|
+
}
|
|
590
|
+
const EMPTY_OVERLOAD_SET = { signatures: 0, implementations: 0 };
|
|
591
|
+
/**
|
|
592
|
+
* How many declaration-only signatures and how many implementations the
|
|
593
|
+
* container holding `node` declares under `node`'s own name, `node` included.
|
|
594
|
+
*
|
|
595
|
+
* Every statement container is read, not just `Program` and `TSModuleBlock`: a
|
|
596
|
+
* function body, a bare block and a `switch` case each bind a name just as
|
|
597
|
+
* effectively, so reading only the top level makes the DEPTH of an overload set
|
|
598
|
+
* decide whether it exists (the same defect as #1771).
|
|
599
|
+
*/
|
|
600
|
+
function overloadSetOf(node) {
|
|
573
601
|
const functionName = node.id?.name;
|
|
574
602
|
if (!functionName)
|
|
603
|
+
return EMPTY_OVERLOAD_SET;
|
|
604
|
+
const statements = siblingStatementsOf(node);
|
|
605
|
+
if (!statements)
|
|
606
|
+
return EMPTY_OVERLOAD_SET;
|
|
607
|
+
let signatures = 0;
|
|
608
|
+
let implementations = 0;
|
|
609
|
+
for (const statement of statements) {
|
|
610
|
+
// `export function f(...)` is the same declaration one AST node deeper, and
|
|
611
|
+
// an overload set may export some of its members and not others.
|
|
612
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
613
|
+
if (declaration.type !== utils_1.AST_NODE_TYPES.TSDeclareFunction &&
|
|
614
|
+
declaration.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (declaration.id?.name !== functionName)
|
|
618
|
+
continue;
|
|
619
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.TSDeclareFunction ||
|
|
620
|
+
!declaration.body) {
|
|
621
|
+
signatures += 1;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
implementations += 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { signatures, implementations };
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* True when `node` is the IMPLEMENTATION of an overload set — a function with a
|
|
631
|
+
* body whose container also declares the same name as one or more
|
|
632
|
+
* declaration-only signatures.
|
|
633
|
+
*
|
|
634
|
+
* Its annotation is not a restatement of what the body returns: TypeScript
|
|
635
|
+
* checks each overload signature against the IMPLEMENTATION SIGNATURE, so the
|
|
636
|
+
* annotation is what the overloads are measured against. Inference yields the
|
|
637
|
+
* body's own type, which need not accept them — stripping `: void | string`
|
|
638
|
+
* from `function get(param?: string): void | string {}` infers `void` and makes
|
|
639
|
+
* the `: string` overload above it TS2394 (#2019).
|
|
640
|
+
*
|
|
641
|
+
* This carve-out ignores `allowOverloadedFunctions`. That option governs the
|
|
642
|
+
* declaration-only signatures, whose annotations are mandatory but carry no
|
|
643
|
+
* fixer, so reporting them costs nothing but a message. Here the report ships a
|
|
644
|
+
* fix that does not compile, which no option may ask for.
|
|
645
|
+
*/
|
|
646
|
+
function isOverloadImplementation(node) {
|
|
647
|
+
if (!node.body)
|
|
575
648
|
return false;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
649
|
+
return overloadSetOf(node).signatures > 0;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* True when `node` is a declaration-only signature that belongs to an overload
|
|
653
|
+
* set: another signature declares the same name, or an implementation below it
|
|
654
|
+
* does. A lone `declare function f(): number;` overloads nothing, so it stays
|
|
655
|
+
* reportable.
|
|
656
|
+
*/
|
|
657
|
+
function isOverloadedTsDeclareFunction(node) {
|
|
658
|
+
const { signatures, implementations } = overloadSetOf(node);
|
|
659
|
+
return signatures > 1 || implementations > 0;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* A method's identity inside its class body. Overloads agree on all three
|
|
663
|
+
* components; `static f` and `f` merely spell the same name, and a computed key
|
|
664
|
+
* names nothing resolvable, so it yields nothing. The separator keeps a private
|
|
665
|
+
* `#log` from colliding with a string key `'#log'`.
|
|
666
|
+
*/
|
|
667
|
+
function methodIdentityOf(node) {
|
|
668
|
+
if (node.computed)
|
|
669
|
+
return undefined;
|
|
670
|
+
if (node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
671
|
+
return `${node.static}\u0000private\u0000${node.key.name}`;
|
|
672
|
+
}
|
|
673
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
674
|
+
node.key.type !== utils_1.AST_NODE_TYPES.Literal) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
const name = getNameFromIdentifierOrLiteral(node.key);
|
|
678
|
+
return name === undefined
|
|
679
|
+
? undefined
|
|
680
|
+
: `${node.static}\u0000public\u0000${name}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The overload set a class method belongs to, counted over the members of its
|
|
684
|
+
* own class body. A member without a body is an overload signature
|
|
685
|
+
* (`TSEmptyBodyFunctionExpression`); the one member with a body is the
|
|
686
|
+
* implementation.
|
|
687
|
+
*/
|
|
688
|
+
function classOverloadSetOf(node) {
|
|
689
|
+
const identity = methodIdentityOf(node);
|
|
690
|
+
const classBody = node.parent;
|
|
691
|
+
if (!identity || classBody?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
692
|
+
return EMPTY_OVERLOAD_SET;
|
|
693
|
+
}
|
|
694
|
+
let signatures = 0;
|
|
695
|
+
let implementations = 0;
|
|
696
|
+
for (const member of classBody.body) {
|
|
697
|
+
if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition)
|
|
698
|
+
continue;
|
|
699
|
+
if (methodIdentityOf(member) !== identity)
|
|
700
|
+
continue;
|
|
701
|
+
if (member.value.body) {
|
|
702
|
+
implementations += 1;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
signatures += 1;
|
|
594
706
|
}
|
|
595
|
-
container = container.parent;
|
|
596
707
|
}
|
|
597
|
-
return
|
|
708
|
+
return { signatures, implementations };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* True when the method is the implementation of an overloaded class method, for
|
|
712
|
+
* the reason {@link isOverloadImplementation} gives — a class overload set is
|
|
713
|
+
* checked exactly as a function one is (#2019).
|
|
714
|
+
*/
|
|
715
|
+
function isOverloadImplementationMethod(node) {
|
|
716
|
+
if (!node.value.body)
|
|
717
|
+
return false;
|
|
718
|
+
return classOverloadSetOf(node).signatures > 0;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* True when the method is a declaration-only overload signature. A body-less
|
|
722
|
+
* method is legal only as part of an overload set, so it is exempt whenever the
|
|
723
|
+
* set holds anything else at all.
|
|
724
|
+
*/
|
|
725
|
+
function isOverloadedClassMethodSignature(node) {
|
|
726
|
+
if (node.value.body)
|
|
727
|
+
return false;
|
|
728
|
+
const { signatures, implementations } = classOverloadSetOf(node);
|
|
729
|
+
return signatures > 1 || implementations > 0;
|
|
598
730
|
}
|
|
599
731
|
function isInterfaceOrAbstractMethodSignature(node) {
|
|
600
732
|
if (node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition)
|
|
@@ -1233,6 +1365,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1233
1365
|
isReadonlyWideningReturnType(returnType) ||
|
|
1234
1366
|
isAllowedVoidReturnType(returnType) ||
|
|
1235
1367
|
isDecoratorFactory(node, returnType) ||
|
|
1368
|
+
isOverloadImplementation(node) ||
|
|
1236
1369
|
(mergedOptions.allowRecursiveFunctions &&
|
|
1237
1370
|
isRecursiveFunction(node)) ||
|
|
1238
1371
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -1292,6 +1425,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1292
1425
|
isReadonlyWideningReturnType(returnType) ||
|
|
1293
1426
|
isAllowedVoidReturnType(returnType) ||
|
|
1294
1427
|
isDecoratorFactory(node, returnType) ||
|
|
1428
|
+
isOverloadImplementationMethod(node) ||
|
|
1429
|
+
(mergedOptions.allowOverloadedFunctions &&
|
|
1430
|
+
isOverloadedClassMethodSignature(node)) ||
|
|
1295
1431
|
(mergedOptions.allowAbstractMethodSignatures &&
|
|
1296
1432
|
isInterfaceOrAbstractMethodSignature(node)) ||
|
|
1297
1433
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -34,6 +34,46 @@ function isStringLiteralType(member) {
|
|
|
34
34
|
const { literal } = member;
|
|
35
35
|
return (literal.type === utils_1.AST_NODE_TYPES.Literal && typeof literal.value === 'string');
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A declaration file is ambient in its entirety, so a declaration in one is
|
|
39
|
+
* subject to the ambient restriction even without an explicit `declare`.
|
|
40
|
+
*/
|
|
41
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/i;
|
|
42
|
+
/**
|
|
43
|
+
* An ambient context accepts only a string, numeric or literal-enum `const`
|
|
44
|
+
* initializer (TS1254), so the derived `as const` array cannot be emitted there
|
|
45
|
+
* at all — `as const` or not, an array literal is rejected. Declining is the
|
|
46
|
+
* remedy rather than a different rewrite, because no legal rewrite exists in
|
|
47
|
+
* that position: the rule asks for importable runtime values and an ambient
|
|
48
|
+
* declaration is precisely the promise that no runtime value is emitted.
|
|
49
|
+
*
|
|
50
|
+
* `declare` on the OUTERMOST module declaration makes every level nested below
|
|
51
|
+
* it ambient too, so the whole ancestor chain is walked rather than just the
|
|
52
|
+
* enclosing block. A module named by a string literal (`module 'x' {}`) and a
|
|
53
|
+
* `global {}` augmentation are ambient by construction, with or without the
|
|
54
|
+
* modifier.
|
|
55
|
+
*/
|
|
56
|
+
function isInAmbientContext(node, filename) {
|
|
57
|
+
if (DECLARATION_FILE.test(filename)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
61
|
+
node.declare === true) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
let current = node.parent;
|
|
65
|
+
while (current) {
|
|
66
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration) {
|
|
67
|
+
if (current.declare === true ||
|
|
68
|
+
current.global === true ||
|
|
69
|
+
current.id.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
current = current.parent;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
37
77
|
/**
|
|
38
78
|
* Walk the scope chain upward collecting declared variable names. Used to skip
|
|
39
79
|
* the autofix (report-only) when the derived `{TYPE}_VALUES` name is already
|
|
@@ -154,6 +194,9 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
154
194
|
if (!members.every(isStringLiteralType)) {
|
|
155
195
|
return;
|
|
156
196
|
}
|
|
197
|
+
if (isInAmbientContext(node, context.getFilename())) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
157
200
|
const typeName = node.id.name;
|
|
158
201
|
const constName = `${toUpperSnake(typeName)}_VALUES`;
|
|
159
202
|
// The type alias is exported when wrapped in `export type X = ...`
|
|
@@ -62,6 +62,49 @@ const isInsideComponentMock = (node, componentModule) => {
|
|
|
62
62
|
}
|
|
63
63
|
return false;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Name a declaration binds, for the forms a component is declared under:
|
|
67
|
+
* `const ImageOptimized = ...` (including `memo(...)`/`forwardRef(...)` around
|
|
68
|
+
* the body), `function ImageOptimized()` and `class ImageOptimized`. Anything
|
|
69
|
+
* else — an object property, a parameter — binds no declaration name the
|
|
70
|
+
* wrapper can be identified by, and treating it as one would exempt a mock
|
|
71
|
+
* factory keyed by the component name whatever module it stands in for.
|
|
72
|
+
*/
|
|
73
|
+
const declaredNameOf = (node) => {
|
|
74
|
+
switch (node.type) {
|
|
75
|
+
case utils_1.AST_NODE_TYPES.VariableDeclarator:
|
|
76
|
+
return node.id.type === utils_1.AST_NODE_TYPES.Identifier ? node.id.name : null;
|
|
77
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
78
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
79
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
80
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
81
|
+
return node.id ? node.id.name : null;
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Names each local binding is exported under, so a wrapper declared as
|
|
88
|
+
* `Picture` and shipped as `export { Picture as ImageOptimized }` is still
|
|
89
|
+
* recognized as the component's own definition. A specifier carrying a `from`
|
|
90
|
+
* clause re-exports another module's binding and declares nothing here.
|
|
91
|
+
*/
|
|
92
|
+
const exportedNamesByLocal = (program) => {
|
|
93
|
+
const exported = new Map();
|
|
94
|
+
for (const statement of program.body) {
|
|
95
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
96
|
+
statement.source ||
|
|
97
|
+
statement.exportKind === 'type') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const specifier of statement.specifiers) {
|
|
101
|
+
const names = exported.get(specifier.local.name) ?? new Set();
|
|
102
|
+
names.add(specifier.exported.name);
|
|
103
|
+
exported.set(specifier.local.name, names);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return exported;
|
|
107
|
+
};
|
|
65
108
|
/**
|
|
66
109
|
* A type-only specifier binds no value: it renders nothing, so it neither
|
|
67
110
|
* bypasses the optimization pipeline nor can back a fix. The modifier lives
|
|
@@ -219,6 +262,40 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
219
262
|
* rule exists to centralize, not a violation of it.
|
|
220
263
|
*/
|
|
221
264
|
const isComponentImplementationFile = moduleNameOf(context.getFilename()) === componentModule;
|
|
265
|
+
/**
|
|
266
|
+
* The wrapper is identified by the name the fixer would emit and by its
|
|
267
|
+
* module's name, which a component module's export shares by convention.
|
|
268
|
+
* Matching is exact so a distinct component whose name merely starts with
|
|
269
|
+
* it (`ImageOptimizedGallery`) stays reportable.
|
|
270
|
+
*/
|
|
271
|
+
const isComponentName = (name) => name === COMPONENT_NAME || name === componentModule;
|
|
272
|
+
const exportedNames = exportedNamesByLocal(sourceCode.ast);
|
|
273
|
+
const definesComponent = (name) => {
|
|
274
|
+
if (isComponentName(name)) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
const aliases = exportedNames.get(name);
|
|
278
|
+
return !!aliases && [...aliases].some(isComponentName);
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Whether the element sits inside the declaration of the component the fix
|
|
282
|
+
* points at. That declaration renders the image primitive by definition, so
|
|
283
|
+
* swapping it for the component makes the wrapper render itself: unbounded
|
|
284
|
+
* recursion, and a type error too, since the wrapper forwards only the
|
|
285
|
+
* props it destructured. The whole ancestry is walked because a helper
|
|
286
|
+
* nested inside the declaration is part of that implementation as well.
|
|
287
|
+
*/
|
|
288
|
+
const isInsideComponentDefinition = (node) => {
|
|
289
|
+
let current = node.parent;
|
|
290
|
+
while (current) {
|
|
291
|
+
const declared = declaredNameOf(current);
|
|
292
|
+
if (declared && definesComponent(declared)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
current = current.parent;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
};
|
|
222
299
|
return {
|
|
223
300
|
// Handle JSX img elements
|
|
224
301
|
JSXElement(node) {
|
|
@@ -228,7 +305,8 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
228
305
|
return;
|
|
229
306
|
}
|
|
230
307
|
if (isComponentImplementationFile ||
|
|
231
|
-
isInsideComponentMock(node, componentModule)
|
|
308
|
+
isInsideComponentMock(node, componentModule) ||
|
|
309
|
+
isInsideComponentDefinition(node)) {
|
|
232
310
|
return;
|
|
233
311
|
}
|
|
234
312
|
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
@@ -85,6 +85,12 @@ export declare class ASTHelpers {
|
|
|
85
85
|
* expression reads some other object.
|
|
86
86
|
*/
|
|
87
87
|
private static classMemberNameReferencedBy;
|
|
88
|
+
/**
|
|
89
|
+
* The ECMA private names a class body declares, spelled as the graph spells
|
|
90
|
+
* them. A private name is scoped to the body that declares it, so this set
|
|
91
|
+
* is what an enclosing class must not mistake for its own members.
|
|
92
|
+
*/
|
|
93
|
+
private static privateNamesDeclaredBy;
|
|
88
94
|
private static walkChildNodes;
|
|
89
95
|
static isNode(value: unknown): value is TSESTree.Node;
|
|
90
96
|
static hasReturnStatement(node: TSESTree.Node): boolean;
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -423,7 +423,16 @@ class ASTHelpers {
|
|
|
423
423
|
case 'ClassExpression': {
|
|
424
424
|
// Traversal continues so `<ClassName>.<member>` statics stay visible,
|
|
425
425
|
// but `this` no longer denotes the graphed instance.
|
|
426
|
-
|
|
426
|
+
const nested = [];
|
|
427
|
+
this.walkChildNodes(node, className, false, nested);
|
|
428
|
+
// A nested class body SHADOWS the private names it declares: its `#q`
|
|
429
|
+
// is a member of that class, distinct from an enclosing class's `#q`,
|
|
430
|
+
// so reads of it constrain the nested layout rather than this one.
|
|
431
|
+
// Private names the nested class does not declare still resolve
|
|
432
|
+
// outward, so only the declared ones are dropped. Filtering as the
|
|
433
|
+
// recursion unwinds composes across any depth of nesting.
|
|
434
|
+
const shadowed = this.privateNamesDeclaredBy(node);
|
|
435
|
+
dependencies.push(...nested.filter((name) => !shadowed.has(name)));
|
|
427
436
|
return;
|
|
428
437
|
}
|
|
429
438
|
case 'MemberExpression': {
|
|
@@ -447,6 +456,18 @@ class ASTHelpers {
|
|
|
447
456
|
*/
|
|
448
457
|
static classMemberNameReferencedBy(node, className, isThisTheInstance) {
|
|
449
458
|
const { object, property, computed } = node;
|
|
459
|
+
// A `#name` resolves LEXICALLY: it is a syntax error unless a class body
|
|
460
|
+
// enclosing the reference declares it, so the receiver it is read through
|
|
461
|
+
// cannot change which member it names. `other.#helper` and `this.#helper`
|
|
462
|
+
// reach the same member, which is why this branch precedes the receiver
|
|
463
|
+
// test that the dotted spellings need. Requiring `this`/<ClassName> here
|
|
464
|
+
// dropped the read a static initializer makes through another value of
|
|
465
|
+
// the same class, and with it the constraint that keeps the field's
|
|
466
|
+
// declaration above its reader (#2022). The `#` is part of the name so
|
|
467
|
+
// `#helper` and `helper` stay distinct members.
|
|
468
|
+
if (!computed && property?.type === 'PrivateIdentifier') {
|
|
469
|
+
return `#${property.name}`;
|
|
470
|
+
}
|
|
450
471
|
const readsInstance = object?.type === 'ThisExpression' && isThisTheInstance;
|
|
451
472
|
// An anonymous class expression has an empty name, which no identifier
|
|
452
473
|
// can match.
|
|
@@ -457,12 +478,6 @@ class ASTHelpers {
|
|
|
457
478
|
if (!computed && property?.type === 'Identifier') {
|
|
458
479
|
return property.name;
|
|
459
480
|
}
|
|
460
|
-
// `this.#helper` names a member as precisely as `this.helper` does, and it
|
|
461
|
-
// is the only spelling available for an ECMA private member. The `#` is
|
|
462
|
-
// part of the name so `#helper` and `helper` stay distinct members.
|
|
463
|
-
if (!computed && property?.type === 'PrivateIdentifier') {
|
|
464
|
-
return `#${property.name}`;
|
|
465
|
-
}
|
|
466
481
|
// `this['helper']` names the member as precisely as `this.helper` does,
|
|
467
482
|
// whereas `this[key]` names one only at runtime.
|
|
468
483
|
if (computed &&
|
|
@@ -472,6 +487,25 @@ class ASTHelpers {
|
|
|
472
487
|
}
|
|
473
488
|
return null;
|
|
474
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* The ECMA private names a class body declares, spelled as the graph spells
|
|
492
|
+
* them. A private name is scoped to the body that declares it, so this set
|
|
493
|
+
* is what an enclosing class must not mistake for its own members.
|
|
494
|
+
*/
|
|
495
|
+
static privateNamesDeclaredBy(node) {
|
|
496
|
+
const names = new Set();
|
|
497
|
+
const members = node.body?.body;
|
|
498
|
+
if (!Array.isArray(members)) {
|
|
499
|
+
return names;
|
|
500
|
+
}
|
|
501
|
+
for (const member of members) {
|
|
502
|
+
const key = member?.key;
|
|
503
|
+
if (key?.type === 'PrivateIdentifier') {
|
|
504
|
+
names.add(`#${key.name}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return names;
|
|
508
|
+
}
|
|
475
509
|
static walkChildNodes(node, className, isThisTheInstance, dependencies) {
|
|
476
510
|
for (const [key, value] of Object.entries(node)) {
|
|
477
511
|
if (ASTHelpers.NON_TRAVERSABLE_NODE_KEYS.has(key)) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.155",
|
|
4
|
+
"date": "2026-08-15T14:21:55.491Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "class-methods-read-top-to-bottom",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2022
|
|
11
|
+
],
|
|
12
|
+
"summary": "pin a # field by every read of it, not just this.#x (closes #2022)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-explicit-return-type",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2019
|
|
19
|
+
],
|
|
20
|
+
"summary": "spare the implementation signature of an overload set (closes #2019)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer-union-from-const-array",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2020
|
|
27
|
+
],
|
|
28
|
+
"summary": "decline in an ambient context, where no const array is legal (closes #2020)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "require-image-optimized",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
2021
|
|
35
|
+
],
|
|
36
|
+
"summary": "exempt the img inside ImageOptimized's own definition (closes #2021)"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
2
40
|
{
|
|
3
41
|
"version": "1.20.154",
|
|
4
42
|
"date": "2026-08-15T03:47:51.570Z",
|