@blumintinc/eslint-plugin-blumint 1.19.5 → 1.19.7
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-redundant-this-params.js +105 -0
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -77,8 +77,105 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
77
77
|
}
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
|
+
function isBindingExportedInScope(scopeBody, bindingName) {
|
|
81
|
+
// A class bound at module (or namespace) scope can be exported by a
|
|
82
|
+
// statement separate from its declaration — `export { Foo }`,
|
|
83
|
+
// `export { Foo as Bar }`, `export default Foo`, or `export = Foo`. Any of
|
|
84
|
+
// these exposes it to subclassing from another file just as an inline
|
|
85
|
+
// export does.
|
|
86
|
+
for (const statement of scopeBody) {
|
|
87
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
88
|
+
!statement.source) {
|
|
89
|
+
for (const specifier of statement.specifiers) {
|
|
90
|
+
if (specifier.local.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
91
|
+
specifier.local.name === bindingName) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration &&
|
|
97
|
+
statement.declaration.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
98
|
+
statement.declaration.name === bindingName) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSExportAssignment &&
|
|
102
|
+
statement.expression.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
103
|
+
statement.expression.name === bindingName) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function getEnclosingScopeBody(node) {
|
|
110
|
+
if (!node) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === utils_1.AST_NODE_TYPES.Program) {
|
|
114
|
+
return node.body;
|
|
115
|
+
}
|
|
116
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
|
|
117
|
+
return node.body;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function isExportedClass(node) {
|
|
122
|
+
const parent = node.parent;
|
|
123
|
+
if (!parent) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
127
|
+
parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
// A `class Foo {}` declaration exported by a later `export { Foo }` /
|
|
131
|
+
// `export default Foo` in the same scope is still reachable cross-file.
|
|
132
|
+
if (node.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
133
|
+
node.id &&
|
|
134
|
+
(parent.type === utils_1.AST_NODE_TYPES.Program ||
|
|
135
|
+
parent.type === utils_1.AST_NODE_TYPES.TSModuleBlock)) {
|
|
136
|
+
const scopeBody = getEnclosingScopeBody(parent);
|
|
137
|
+
if (scopeBody && isBindingExportedInScope(scopeBody, node.id.name)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// `export const Foo = class { ... }`, or `const Foo = class {}` exported by
|
|
142
|
+
// a later statement, both expose the class expression like an exported
|
|
143
|
+
// declaration does.
|
|
144
|
+
if (node.type === utils_1.AST_NODE_TYPES.ClassExpression &&
|
|
145
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
146
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
147
|
+
const declaration = parent.parent;
|
|
148
|
+
const exportNode = declaration?.parent;
|
|
149
|
+
if (exportNode?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
150
|
+
exportNode?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const scopeBody = getEnclosingScopeBody(exportNode);
|
|
154
|
+
if (scopeBody && isBindingExportedInScope(scopeBody, parent.id.name)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
80
160
|
function collectClassInfo(node) {
|
|
81
161
|
const methods = new Map();
|
|
162
|
+
// A method's `this.method(...)` call sites are confined to this file only
|
|
163
|
+
// when the class cannot be extended from another file. An `abstract` class
|
|
164
|
+
// exists to be subclassed, and an exported class can be subclassed
|
|
165
|
+
// anywhere; either way subclasses may call an inherited non-private method
|
|
166
|
+
// with a different `this.<member>`, so those methods are externally
|
|
167
|
+
// reachable and must not be reported. See issue #1309.
|
|
168
|
+
const classIsExtensible = Boolean(node.abstract) ||
|
|
169
|
+
isExportedClass(node);
|
|
170
|
+
function computeExternallyReachable(methodName, accessibility) {
|
|
171
|
+
if (!classIsExtensible) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// `private` (TS) and `#name` (ECMAScript) methods are never inherited,
|
|
175
|
+
// so all of their call sites live in the declaring class body.
|
|
176
|
+
const isPrivate = accessibility === 'private' || methodName.startsWith('#');
|
|
177
|
+
return !isPrivate;
|
|
178
|
+
}
|
|
82
179
|
function setMethod(methodName, meta) {
|
|
83
180
|
/**
|
|
84
181
|
* Prefer instance methods over static methods when names collide.
|
|
@@ -120,6 +217,7 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
120
217
|
params: getMethodParams(member),
|
|
121
218
|
isStatic: Boolean(member.static),
|
|
122
219
|
isAbstract: member.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition,
|
|
220
|
+
externallyReachable: computeExternallyReachable(methodName, member.accessibility),
|
|
123
221
|
});
|
|
124
222
|
}
|
|
125
223
|
else if (member.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
@@ -137,6 +235,7 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
137
235
|
params,
|
|
138
236
|
isStatic: false,
|
|
139
237
|
isAbstract: false,
|
|
238
|
+
externallyReachable: computeExternallyReachable(methodName, member.accessibility),
|
|
140
239
|
});
|
|
141
240
|
}
|
|
142
241
|
}
|
|
@@ -432,6 +531,12 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
432
531
|
for (const [, propStats] of argProperties) {
|
|
433
532
|
if (propStats.callsWithProperty === methodStats.totalCalls) {
|
|
434
533
|
for (const violation of propStats.violations) {
|
|
534
|
+
// Skip methods whose call sites are not provably confined to
|
|
535
|
+
// this file: a subclass elsewhere may thread a different
|
|
536
|
+
// `this.<member>` through the same parameter (issue #1309).
|
|
537
|
+
if (violation.methodMeta.externallyReachable) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
435
540
|
reportAccess(methodName, violation.methodMeta, argIndex, violation.access);
|
|
436
541
|
}
|
|
437
542
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.7",
|
|
4
|
+
"date": "2026-07-16T01:36:53.555Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-redundant-this-params",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1309
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat trailing-export classes as reachable"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.6",
|
|
18
|
+
"date": "2026-07-16T01:27:07.130Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-redundant-this-params",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1309
|
|
25
|
+
],
|
|
26
|
+
"summary": "skip externally-reachable methods (closes #1309)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.5",
|
|
4
32
|
"date": "2026-07-15T15:24:16.132Z",
|