@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 CHANGED
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.5',
225
+ version: '1.19.7',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.5",
3
+ "version": "1.19.7",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",