@blumintinc/eslint-plugin-blumint 1.20.1 → 1.20.2
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-unnecessary-verb-suffix.js +219 -0
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -206,6 +206,183 @@ function isNameBoundInSubtree(root, targetName) {
|
|
|
206
206
|
}
|
|
207
207
|
return false;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Annotations that impose no excess-property check. A value annotated `any` or
|
|
211
|
+
* `unknown` may carry members its annotation never declares, so the annotation
|
|
212
|
+
* proves nothing about where a member name came from (#1350).
|
|
213
|
+
*/
|
|
214
|
+
const UNCHECKED_ANNOTATION_TYPES = new Set([
|
|
215
|
+
utils_1.AST_NODE_TYPES.TSAnyKeyword,
|
|
216
|
+
utils_1.AST_NODE_TYPES.TSUnknownKeyword,
|
|
217
|
+
]);
|
|
218
|
+
/**
|
|
219
|
+
* The declared type of an annotation-bearing node (`const x: T`, `field: T`),
|
|
220
|
+
* or null when the node carries no annotation.
|
|
221
|
+
*/
|
|
222
|
+
function declaredTypeNode(node) {
|
|
223
|
+
const { typeAnnotation } = node;
|
|
224
|
+
return typeAnnotation?.typeAnnotation ?? null;
|
|
225
|
+
}
|
|
226
|
+
function checksExcessProperties(typeNode) {
|
|
227
|
+
return typeNode !== null && !UNCHECKED_ANNOTATION_TYPES.has(typeNode.type);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Reports whether `node` sits inside a value whose shape TypeScript checks
|
|
231
|
+
* against a declared type — a type-annotated variable or class field, or a
|
|
232
|
+
* `satisfies` clause. Excess-property checking makes such a literal unable to
|
|
233
|
+
* carry a member the target type does not declare, so a member name there is
|
|
234
|
+
* dictated by that type rather than chosen by the author, and renaming it would
|
|
235
|
+
* break conformance (#1350). No member resolution is needed: the signal alone
|
|
236
|
+
* is proof, because code carrying an undeclared member does not compile.
|
|
237
|
+
*
|
|
238
|
+
* The walk climbs object/array containers so an outer signal covers nested
|
|
239
|
+
* members, and stops at anything else — notably `as` assertions, which do not
|
|
240
|
+
* reject undeclared members the same way.
|
|
241
|
+
*/
|
|
242
|
+
function hasConformanceSignal(node) {
|
|
243
|
+
let current = node;
|
|
244
|
+
for (;;) {
|
|
245
|
+
const parent = current.parent;
|
|
246
|
+
if (!parent) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
switch (parent.type) {
|
|
250
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
251
|
+
return (parent.expression === current &&
|
|
252
|
+
checksExcessProperties(parent.typeAnnotation));
|
|
253
|
+
case utils_1.AST_NODE_TYPES.VariableDeclarator:
|
|
254
|
+
return (parent.init === current &&
|
|
255
|
+
checksExcessProperties(declaredTypeNode(parent.id)));
|
|
256
|
+
case utils_1.AST_NODE_TYPES.PropertyDefinition:
|
|
257
|
+
return (parent.value === current &&
|
|
258
|
+
checksExcessProperties(declaredTypeNode(parent)));
|
|
259
|
+
case utils_1.AST_NODE_TYPES.Property:
|
|
260
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
261
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
262
|
+
current = parent;
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function buildDeclarationIndex(sourceCode) {
|
|
270
|
+
const index = {
|
|
271
|
+
interfaces: new Map(),
|
|
272
|
+
typeAliases: new Map(),
|
|
273
|
+
classes: new Map(),
|
|
274
|
+
};
|
|
275
|
+
// Declarations are collected from the whole file rather than from
|
|
276
|
+
// `Program.body` alone so contracts declared inside modules, blocks or
|
|
277
|
+
// functions resolve as well as top-level ones.
|
|
278
|
+
const stack = [sourceCode.ast];
|
|
279
|
+
while (stack.length > 0) {
|
|
280
|
+
const current = stack.pop();
|
|
281
|
+
switch (current.type) {
|
|
282
|
+
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration: {
|
|
283
|
+
const merged = index.interfaces.get(current.id.name) ?? [];
|
|
284
|
+
merged.push(current);
|
|
285
|
+
index.interfaces.set(current.id.name, merged);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
|
|
289
|
+
index.typeAliases.set(current.id.name, current);
|
|
290
|
+
break;
|
|
291
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
292
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
293
|
+
if (current.id) {
|
|
294
|
+
index.classes.set(current.id.name, current);
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
for (const key of sourceCode.visitorKeys[current.type] ?? []) {
|
|
301
|
+
const value = current[key];
|
|
302
|
+
const children = Array.isArray(value) ? value : [value];
|
|
303
|
+
for (const child of children) {
|
|
304
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
305
|
+
stack.push(child);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return index;
|
|
311
|
+
}
|
|
312
|
+
function heritageTypeName(expression) {
|
|
313
|
+
return expression.type === utils_1.AST_NODE_TYPES.Identifier ? expression.name : null;
|
|
314
|
+
}
|
|
315
|
+
function membersDeclareName(members, memberName) {
|
|
316
|
+
return members.some((member) => (member.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
|
|
317
|
+
member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) &&
|
|
318
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
319
|
+
member.key.name === memberName);
|
|
320
|
+
}
|
|
321
|
+
function classBodyDeclaresName(members, memberName) {
|
|
322
|
+
return members.some((member) => (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
323
|
+
member.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
324
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
325
|
+
member.key.name === memberName);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reports whether the contract named `typeName` accounts for `memberName`,
|
|
329
|
+
* either by declaring it or by being unreadable from this file. A class may add
|
|
330
|
+
* members its contract never declares, so presence of a heritage clause alone
|
|
331
|
+
* cannot exempt a name — but an imported or otherwise unresolvable contract
|
|
332
|
+
* hides its members from a purely syntactic rule, and this plugin prefers a
|
|
333
|
+
* false negative over a false positive (#1350).
|
|
334
|
+
*/
|
|
335
|
+
function contractCoversName(typeName, memberName, index, visited) {
|
|
336
|
+
// A name already inspected on this path adds nothing and would loop on a
|
|
337
|
+
// circular heritage chain.
|
|
338
|
+
if (visited.has(typeName)) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
visited.add(typeName);
|
|
342
|
+
const interfaces = index.interfaces.get(typeName);
|
|
343
|
+
if (interfaces) {
|
|
344
|
+
return interfaces.some((declaration) => membersDeclareName(declaration.body.body, memberName) ||
|
|
345
|
+
heritageCoversName(declaration.extends ?? [], memberName, index, visited));
|
|
346
|
+
}
|
|
347
|
+
const alias = index.typeAliases.get(typeName);
|
|
348
|
+
if (alias) {
|
|
349
|
+
// An alias to anything but a type literal (an intersection, a mapped type,
|
|
350
|
+
// a reference to an imported type) hides its member list from a syntactic
|
|
351
|
+
// reader, so it is treated as unreadable.
|
|
352
|
+
return (alias.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral ||
|
|
353
|
+
membersDeclareName(alias.typeAnnotation.members, memberName));
|
|
354
|
+
}
|
|
355
|
+
const classDeclaration = index.classes.get(typeName);
|
|
356
|
+
if (classDeclaration) {
|
|
357
|
+
return (classBodyDeclaresName(classDeclaration.body.body, memberName) ||
|
|
358
|
+
classContractCoversName(classDeclaration, memberName, index, visited));
|
|
359
|
+
}
|
|
360
|
+
// Nothing under this name in the file: the contract lives in another module
|
|
361
|
+
// and its members are unreadable here.
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
function heritageCoversName(heritage, memberName, index, visited) {
|
|
365
|
+
return heritage.some((clause) => {
|
|
366
|
+
const typeName = heritageTypeName(clause.expression);
|
|
367
|
+
// A namespaced or computed heritage expression cannot be followed
|
|
368
|
+
// syntactically, so it counts as an unreadable contract.
|
|
369
|
+
return (typeName === null ||
|
|
370
|
+
contractCoversName(typeName, memberName, index, visited));
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function classContractCoversName(classNode, memberName, index, visited) {
|
|
374
|
+
if (heritageCoversName(classNode.implements ?? [], memberName, index, visited)) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
const { superClass } = classNode;
|
|
378
|
+
if (!superClass) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
const superName = heritageTypeName(superClass);
|
|
382
|
+
// A computed superclass (a mixin call) is unreadable, like an imported one.
|
|
383
|
+
return (superName === null ||
|
|
384
|
+
contractCoversName(superName, memberName, index, visited));
|
|
385
|
+
}
|
|
209
386
|
exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
210
387
|
name: 'no-unnecessary-verb-suffix',
|
|
211
388
|
meta: {
|
|
@@ -222,6 +399,37 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
222
399
|
},
|
|
223
400
|
defaultOptions: [],
|
|
224
401
|
create(context) {
|
|
402
|
+
// Built on first heritage question only, since most files never ask one.
|
|
403
|
+
let declarationIndex = null;
|
|
404
|
+
function getDeclarationIndex() {
|
|
405
|
+
if (declarationIndex === null) {
|
|
406
|
+
declarationIndex = buildDeclarationIndex(context.sourceCode);
|
|
407
|
+
}
|
|
408
|
+
return declarationIndex;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Reports whether a class member's name comes from a contract the class
|
|
412
|
+
* declares conformance to, rather than from its author (#1350).
|
|
413
|
+
*/
|
|
414
|
+
function isDictatedByHeritage(node, memberName) {
|
|
415
|
+
const classBody = node.parent;
|
|
416
|
+
if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const classNode = classBody.parent;
|
|
420
|
+
if (!classNode ||
|
|
421
|
+
(classNode.type !== utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
422
|
+
classNode.type !== utils_1.AST_NODE_TYPES.ClassExpression)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
// Indexing the file's declarations is only worth its cost once a class
|
|
426
|
+
// declares conformance to something.
|
|
427
|
+
if ((classNode.implements ?? []).length === 0 &&
|
|
428
|
+
classNode.superClass === null) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return classContractCoversName(classNode, memberName, getDeclarationIndex(), new Set());
|
|
432
|
+
}
|
|
225
433
|
/**
|
|
226
434
|
* Returns true when renaming the symbol to `suggestion` would collide with
|
|
227
435
|
* an existing binding in any scope the rename touches, making the autofix
|
|
@@ -387,6 +595,11 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
387
595
|
},
|
|
388
596
|
MethodDefinition(node) {
|
|
389
597
|
if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
598
|
+
// A member implementing a contract the class declares conformance to
|
|
599
|
+
// is named by that contract, so renaming it would break conformance.
|
|
600
|
+
if (isDictatedByHeritage(node, node.key.name)) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
390
603
|
// Class methods are called via member expressions (`this.method()`,
|
|
391
604
|
// `instance.method()`) that the scope manager does not track as
|
|
392
605
|
// references. A syntactic single-file fixer therefore cannot find and
|
|
@@ -408,6 +621,12 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
408
621
|
if (node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
409
622
|
(node.value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
410
623
|
node.value.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
624
|
+
// A literal checked against a declared type can only hold members
|
|
625
|
+
// that type declares, so its member names are not the author's to
|
|
626
|
+
// rename.
|
|
627
|
+
if (hasConformanceSignal(node)) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
411
630
|
// Object-literal method properties are accessed via member expressions
|
|
412
631
|
// (`obj.method()`) the scope manager does not track. As with class
|
|
413
632
|
// methods, the fix is suppressed to avoid orphaning call sites.
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.2",
|
|
4
|
+
"date": "2026-07-25T02:42:18.863Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-unnecessary-verb-suffix",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1350
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt member names dictated by a declared contract (closes #1350)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.1",
|
|
4
18
|
"date": "2026-07-24T22:02:37.444Z",
|