@blumintinc/eslint-plugin-blumint 1.20.181 → 1.20.182
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/enforce-assert-throws.js +27 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +55 -0
- package/lib/rules/enforce-positive-naming.js +56 -9
- package/lib/rules/enforce-verb-noun-naming.js +67 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +43 -0
- package/lib/rules/no-unnecessary-verb-suffix.js +37 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +236 -82
- package/lib/rules/semantic-function-prefixes.js +44 -0
- package/package.json +1 -1
- package/release-manifest.json +70 -0
package/lib/index.js
CHANGED
|
@@ -48,6 +48,30 @@ function resolveMemberName(key) {
|
|
|
48
48
|
function memberDisplayName(member) {
|
|
49
49
|
return member.isEcmaPrivate ? `#${member.name}` : member.name;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The member name a class field contributes, when the function under inspection is
|
|
53
|
+
* that field's own initializer. A field-declared helper is the same class member as
|
|
54
|
+
* its prototype-method spelling, so the assert- contract has to read the same name
|
|
55
|
+
* from both: deriving nothing here would let `assertFoo() {}` be rewritten to
|
|
56
|
+
* `assertFoo = () => {}` and escape the check without changing anything the check
|
|
57
|
+
* judges. The `value` identity test keeps a function nested somewhere inside a
|
|
58
|
+
* field's initializer — a callback, a wrapped factory — from borrowing the field's
|
|
59
|
+
* name.
|
|
60
|
+
*
|
|
61
|
+
* A computed key evaluates an expression at class-definition time instead of
|
|
62
|
+
* spelling a member name (`[assertKey] = ...` names a variable, not the member), so
|
|
63
|
+
* it contributes no name to a naming convention. A `declare` field states a type
|
|
64
|
+
* with no implementation, so it has no control flow that could throw.
|
|
65
|
+
*/
|
|
66
|
+
function propertyDefinitionMemberName(parent, node) {
|
|
67
|
+
if (parent.type !== utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (parent.value !== node || parent.computed || parent.declare) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return resolveMemberName(parent.key);
|
|
74
|
+
}
|
|
51
75
|
/**
|
|
52
76
|
* Whether a member expression's property names an assert helper. The `#` sigil is a
|
|
53
77
|
* privacy marker rather than part of the identifier the naming convention governs,
|
|
@@ -607,6 +631,9 @@ exports.enforceAssertThrows = (0, createRule_1.createRule)({
|
|
|
607
631
|
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
608
632
|
member = { name: parent.id.name, isEcmaPrivate: false };
|
|
609
633
|
}
|
|
634
|
+
else if (parent) {
|
|
635
|
+
member = propertyDefinitionMemberName(parent, node);
|
|
636
|
+
}
|
|
610
637
|
}
|
|
611
638
|
// The bare name drives the assert- convention; the report quotes the name as
|
|
612
639
|
// written so a `#assertFoo` finding is not read as its public namesake.
|
|
@@ -1505,6 +1505,29 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1505
1505
|
});
|
|
1506
1506
|
}
|
|
1507
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Whether a class field's value is a function whose DECLARED return type is
|
|
1510
|
+
* `boolean`.
|
|
1511
|
+
*
|
|
1512
|
+
* Booleanness is read from the return annotation alone, which is exactly
|
|
1513
|
+
* what the method arm requires. Routing the value through
|
|
1514
|
+
* `returnsBooleanValue` instead would additionally accept an un-annotated
|
|
1515
|
+
* arrow whose expression body merely looks boolean (`valid = () => x > 0`),
|
|
1516
|
+
* whose method counterpart (`valid() { return x > 0; }`) stays silent — so
|
|
1517
|
+
* the two spellings would disagree in the opposite direction. A type
|
|
1518
|
+
* predicate (`(v): v is Foo => …`) is excluded by the same keying, matching
|
|
1519
|
+
* the method arm's explicit predicate carve-out.
|
|
1520
|
+
*/
|
|
1521
|
+
function declaresBooleanReturningFunction(node) {
|
|
1522
|
+
const value = node.value;
|
|
1523
|
+
if (value?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
1524
|
+
value?.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
const returnAnnotation = value.returnType?.typeAnnotation;
|
|
1528
|
+
return (!!returnAnnotation &&
|
|
1529
|
+
returnAnnotation.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword);
|
|
1530
|
+
}
|
|
1508
1531
|
/**
|
|
1509
1532
|
* Check class property declarations for boolean values.
|
|
1510
1533
|
*
|
|
@@ -1518,6 +1541,38 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1518
1541
|
if (!key)
|
|
1519
1542
|
return;
|
|
1520
1543
|
const propertyName = key.name;
|
|
1544
|
+
// A field holding a boolean-returning function declares a member that is a
|
|
1545
|
+
// method in every respect its NAME is judged on: callers write
|
|
1546
|
+
// `instance.member()` and read a true/false answer from it, so writing `=`
|
|
1547
|
+
// in front of the member cannot discharge the naming obligation the method
|
|
1548
|
+
// spelling carries. The three real differences between the spellings —
|
|
1549
|
+
// lexical `this`, own-instance placement and initialization order — are all
|
|
1550
|
+
// orthogonal to the name, so the rename remedy is identical.
|
|
1551
|
+
//
|
|
1552
|
+
// Reported before the data-field paths and returned from, so a field that
|
|
1553
|
+
// somehow satisfies both cannot draw two reports on one key.
|
|
1554
|
+
//
|
|
1555
|
+
// Two fields declare a name this site cannot rename: a computed key's
|
|
1556
|
+
// static name belongs to the expression holding it, so renaming `k` in
|
|
1557
|
+
// `[k] = …` renames nothing on the class, and an ambient (`declare`) field
|
|
1558
|
+
// describes a shape provided elsewhere — a base class, a mixin, a
|
|
1559
|
+
// framework — which owns the name.
|
|
1560
|
+
const declaresRenameableName = !node.computed && !node.declare;
|
|
1561
|
+
if (declaresRenameableName &&
|
|
1562
|
+
declaresBooleanReturningFunction(node) &&
|
|
1563
|
+
!hasApprovedPrefix(propertyName)) {
|
|
1564
|
+
context.report({
|
|
1565
|
+
node: node.key,
|
|
1566
|
+
messageId: 'missingBooleanPrefix',
|
|
1567
|
+
data: {
|
|
1568
|
+
type: 'method',
|
|
1569
|
+
name: key.written,
|
|
1570
|
+
capitalizedName: capitalizeFirst(propertyName),
|
|
1571
|
+
prefixes: formatPrefixes(),
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1521
1576
|
// Check if it's a boolean property
|
|
1522
1577
|
let isBooleanProperty = false;
|
|
1523
1578
|
// Check if it has a boolean type annotation
|
|
@@ -1122,6 +1122,11 @@ function classifyFunctionReturn(fn) {
|
|
|
1122
1122
|
? 'boolean'
|
|
1123
1123
|
: 'nonBoolean';
|
|
1124
1124
|
}
|
|
1125
|
+
// A body-less function (`abstract isNotBlank(value?: string);`) with no
|
|
1126
|
+
// return annotation offers no syntactic verdict at all.
|
|
1127
|
+
if (!fn.body) {
|
|
1128
|
+
return 'indeterminate';
|
|
1129
|
+
}
|
|
1125
1130
|
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
1126
1131
|
return classifyExpression(fn.body);
|
|
1127
1132
|
}
|
|
@@ -1148,9 +1153,25 @@ function isExemptFromBooleanNaming(fn) {
|
|
|
1148
1153
|
function isExemptFunctionValue(node) {
|
|
1149
1154
|
return (!!node &&
|
|
1150
1155
|
(node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
1151
|
-
node.type === utils_1.AST_NODE_TYPES.FunctionExpression
|
|
1156
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
1157
|
+
// `abstract isNotBlank(value?: string): string | true;` declares the
|
|
1158
|
+
// validator without a body, and must be exempt on the same grounds as
|
|
1159
|
+
// the implementation that satisfies it.
|
|
1160
|
+
node.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) &&
|
|
1152
1161
|
isExemptFromBooleanNaming(node));
|
|
1153
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* A member declared with a function type but no initializer
|
|
1165
|
+
* (`isNotBlank!: (value?: string) => string | true`) carries its return shape
|
|
1166
|
+
* only in the annotation. Reading it keeps the #1692 validator carve-out from
|
|
1167
|
+
* depending on whether the predicate is declared or implemented in place.
|
|
1168
|
+
*/
|
|
1169
|
+
function isExemptFunctionTypeAnnotation(annotation) {
|
|
1170
|
+
const typeNode = annotation?.typeAnnotation;
|
|
1171
|
+
return (typeNode?.type === utils_1.AST_NODE_TYPES.TSFunctionType &&
|
|
1172
|
+
!!typeNode.returnType &&
|
|
1173
|
+
!isBooleanOnlyType(typeNode.returnType.typeAnnotation));
|
|
1174
|
+
}
|
|
1154
1175
|
exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
1155
1176
|
name: 'enforce-positive-naming',
|
|
1156
1177
|
meta: {
|
|
@@ -1446,25 +1467,45 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1446
1467
|
}
|
|
1447
1468
|
}
|
|
1448
1469
|
/**
|
|
1449
|
-
* Check
|
|
1470
|
+
* Check class members — methods, fields and their `abstract` forms — for
|
|
1471
|
+
* negative naming. The docs' subject is "class members", and a field is one:
|
|
1472
|
+
* `isNotReady = () => ...` and `isNotReady() { ... }` force a reader through
|
|
1473
|
+
* the same mental inversion, so writing `=` must not silence the rule.
|
|
1450
1474
|
*/
|
|
1451
|
-
function
|
|
1475
|
+
function checkClassMember(node) {
|
|
1452
1476
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1453
1477
|
return;
|
|
1454
|
-
//
|
|
1478
|
+
// A computed key references a name bound elsewhere, where the rule
|
|
1479
|
+
// already judges it; reporting here would blame the wrong declaration.
|
|
1480
|
+
if (node.computed)
|
|
1481
|
+
return;
|
|
1482
|
+
// A `declare` field restates the type of a member owned by a base class
|
|
1483
|
+
// or an ambient declaration, so its name is not this class's to choose.
|
|
1484
|
+
if ((node.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
1485
|
+
node.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) &&
|
|
1486
|
+
node.declare) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
// Only check boolean-returning members
|
|
1455
1490
|
if (!isBooleanLike(node.key))
|
|
1456
1491
|
return;
|
|
1457
|
-
// Skip validator predicates returning a non-boolean value
|
|
1492
|
+
// Skip validator predicates returning a non-boolean value, whether the
|
|
1493
|
+
// shape comes from the value or from a declaration-only annotation.
|
|
1458
1494
|
if (isExemptFunctionValue(node.value))
|
|
1459
1495
|
return;
|
|
1460
|
-
|
|
1461
|
-
|
|
1496
|
+
if ((node.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
1497
|
+
node.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) &&
|
|
1498
|
+
isExemptFunctionTypeAnnotation(node.typeAnnotation)) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const memberName = node.key.name;
|
|
1502
|
+
const { isNegative, alternatives } = hasBooleanNegativeNaming(memberName);
|
|
1462
1503
|
if (isNegative) {
|
|
1463
1504
|
context.report({
|
|
1464
1505
|
node: node.key,
|
|
1465
1506
|
messageId: 'avoidNegativeNaming',
|
|
1466
1507
|
data: {
|
|
1467
|
-
name:
|
|
1508
|
+
name: memberName,
|
|
1468
1509
|
alternatives: formatAlternatives(alternatives),
|
|
1469
1510
|
},
|
|
1470
1511
|
});
|
|
@@ -1562,7 +1603,13 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1562
1603
|
checkFunctionDeclaration(node);
|
|
1563
1604
|
}
|
|
1564
1605
|
},
|
|
1565
|
-
MethodDefinition:
|
|
1606
|
+
MethodDefinition: checkClassMember,
|
|
1607
|
+
// A class field is a class member: the property spelling of a method
|
|
1608
|
+
// (`isNotReady = () => ...`) and a plain boolean field
|
|
1609
|
+
// (`isNotReady = false`) are both what the docs promise to cover.
|
|
1610
|
+
PropertyDefinition: checkClassMember,
|
|
1611
|
+
TSAbstractMethodDefinition: checkClassMember,
|
|
1612
|
+
TSAbstractPropertyDefinition: checkClassMember,
|
|
1566
1613
|
Property: checkProperty,
|
|
1567
1614
|
TSPropertySignature: checkPropertySignature,
|
|
1568
1615
|
Identifier(node) {
|
|
@@ -4081,6 +4081,14 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4081
4081
|
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4082
4082
|
return parent.id.name;
|
|
4083
4083
|
}
|
|
4084
|
+
// A class field holds its name on the member key, so the component
|
|
4085
|
+
// evidence keyed on the name has to reach `Foo = () => <div />` the
|
|
4086
|
+
// same way it reaches `const Foo = () => <div />`.
|
|
4087
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
4088
|
+
!parent.computed &&
|
|
4089
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
4090
|
+
return parent.key.name;
|
|
4091
|
+
}
|
|
4084
4092
|
}
|
|
4085
4093
|
return '';
|
|
4086
4094
|
}
|
|
@@ -4100,6 +4108,18 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4100
4108
|
}
|
|
4101
4109
|
}
|
|
4102
4110
|
}
|
|
4111
|
+
// A class field carries its annotation on the member rather than on a
|
|
4112
|
+
// binding: `Foo: React.FC = () => ...` declares a component exactly as
|
|
4113
|
+
// the `const` spelling above does.
|
|
4114
|
+
const memberParent = node.parent;
|
|
4115
|
+
if (memberParent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
4116
|
+
memberParent.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
4117
|
+
const typeText = context.sourceCode.getText(memberParent.typeAnnotation.typeAnnotation);
|
|
4118
|
+
if (/\bReact\.(FC|FunctionComponent)\b/.test(typeText) ||
|
|
4119
|
+
/\b(FC|FunctionComponent)\b/.test(typeText)) {
|
|
4120
|
+
return true;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4103
4123
|
// Handle FunctionDeclaration/FunctionExpression/ArrowFunction return type: function Foo(): React.JSX.Element { ... }
|
|
4104
4124
|
if (node.returnType?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
4105
4125
|
const typeText = context.sourceCode.getText(node.returnType.typeAnnotation);
|
|
@@ -4114,6 +4134,14 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4114
4134
|
* scope analysis, which records JSX element names as references.
|
|
4115
4135
|
*/
|
|
4116
4136
|
function isUsedAsReactComponent(node, functionName) {
|
|
4137
|
+
// A class field's name is a member, not a lexical binding, so a variable
|
|
4138
|
+
// of the same name found in scope belongs to some other symbol entirely
|
|
4139
|
+
// and says nothing about the field. `<this.Foo />` is a member expression
|
|
4140
|
+
// and records no reference to resolve, so the field relies on the other
|
|
4141
|
+
// component evidence.
|
|
4142
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4117
4145
|
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
4118
4146
|
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, functionName);
|
|
4119
4147
|
if (!variable) {
|
|
@@ -4205,6 +4233,45 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4205
4233
|
}
|
|
4206
4234
|
}
|
|
4207
4235
|
},
|
|
4236
|
+
/**
|
|
4237
|
+
* A callable class field is the same member as a method with one token
|
|
4238
|
+
* changed — `this.data()` reads identically under either spelling — so
|
|
4239
|
+
* the name answers to the same rule. Writing `=` cannot be a way to opt
|
|
4240
|
+
* out of it. The value gate is what separates the two kinds of field:
|
|
4241
|
+
* only a function-valued one names an action, so `data = 42` stays a
|
|
4242
|
+
* noun-phrased datum, exactly as an assigned variable does.
|
|
4243
|
+
*/
|
|
4244
|
+
PropertyDefinition(node) {
|
|
4245
|
+
// A computed key is an expression rather than a name, so there is no
|
|
4246
|
+
// identifier to judge or to rename.
|
|
4247
|
+
if (node.computed)
|
|
4248
|
+
return;
|
|
4249
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
4250
|
+
return;
|
|
4251
|
+
// A `declare` field only restates the type of a member initialized
|
|
4252
|
+
// elsewhere; the declaration that carries the value owns the name.
|
|
4253
|
+
if (node.declare)
|
|
4254
|
+
return;
|
|
4255
|
+
const value = node.value;
|
|
4256
|
+
if (!value ||
|
|
4257
|
+
(value.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
4258
|
+
value.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
// A component is a noun by convention, and a field is a routine place
|
|
4262
|
+
// to hold one — the generic-bound render helpers a class exposes are
|
|
4263
|
+
// written this way precisely because they close over `this`.
|
|
4264
|
+
if (isReactComponent(value)) {
|
|
4265
|
+
return;
|
|
4266
|
+
}
|
|
4267
|
+
if (!isVerbPhrase(node.key.name)) {
|
|
4268
|
+
context.report({
|
|
4269
|
+
node: node.key,
|
|
4270
|
+
messageId: 'functionVerbPhrase',
|
|
4271
|
+
data: { name: node.key.name },
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
},
|
|
4208
4275
|
MethodDefinition(node) {
|
|
4209
4276
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
4210
4277
|
return;
|
|
@@ -302,6 +302,29 @@ exports.noMisleadingBooleanPrefixes = (0, createRule_1.createRule)({
|
|
|
302
302
|
}
|
|
303
303
|
// If we can't determine it's non-boolean, do not report to avoid false positives
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Judges a class field only when it holds a function literal.
|
|
307
|
+
*
|
|
308
|
+
* Each gate excludes a member that makes no return-value promise, so none of
|
|
309
|
+
* them is conservatism for its own sake: a data field (`isDone = false`,
|
|
310
|
+
* `hasItems = compute()`) is a value rather than a callable contract, a
|
|
311
|
+
* computed key names a variable instead of the member a caller writes, and a
|
|
312
|
+
* `declare`, definite-assignment or abstract field carries no initializer
|
|
313
|
+
* whose returns could be read.
|
|
314
|
+
*/
|
|
315
|
+
function checkClassProperty(node) {
|
|
316
|
+
if (node.computed || node.declare)
|
|
317
|
+
return;
|
|
318
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
319
|
+
return;
|
|
320
|
+
const value = node.value;
|
|
321
|
+
if (!value ||
|
|
322
|
+
(value.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
323
|
+
value.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
checkFunctionLike(value, node.key.name, node.key);
|
|
327
|
+
}
|
|
305
328
|
return {
|
|
306
329
|
FunctionDeclaration(node) {
|
|
307
330
|
if (!node.id)
|
|
@@ -320,6 +343,12 @@ exports.noMisleadingBooleanPrefixes = (0, createRule_1.createRule)({
|
|
|
320
343
|
return;
|
|
321
344
|
if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition)
|
|
322
345
|
return;
|
|
346
|
+
// A named function expression assigned to a class field carries two
|
|
347
|
+
// names — its own `id` and the field's key — and the field key is the
|
|
348
|
+
// one every call site writes. Without this bail-out the class-member
|
|
349
|
+
// arm below and the `node.id` fallback both fire on the same site.
|
|
350
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition)
|
|
351
|
+
return;
|
|
323
352
|
if (node.id) {
|
|
324
353
|
checkFunctionLike(node, node.id.name, node.id);
|
|
325
354
|
}
|
|
@@ -351,6 +380,20 @@ exports.noMisleadingBooleanPrefixes = (0, createRule_1.createRule)({
|
|
|
351
380
|
checkFunctionLike(node.value, node.key.name, node.key);
|
|
352
381
|
}
|
|
353
382
|
},
|
|
383
|
+
// A class field holding a function is a function everywhere it matters:
|
|
384
|
+
// `instance.isReady()` reads the same whether the member was written as a
|
|
385
|
+
// method or as `isReady = () => ...`, so the boolean prefix makes the same
|
|
386
|
+
// promise to the same call sites. Keying the class arm on `MethodDefinition`
|
|
387
|
+
// alone let a single `=` silence the rule (#2155), and the bound-property
|
|
388
|
+
// spelling is what an interface demanding a bound member forces.
|
|
389
|
+
//
|
|
390
|
+
// `TSAbstractPropertyDefinition` is registered beside it so the class arm
|
|
391
|
+
// subscribes to every key a field declaration can parse as, matching the
|
|
392
|
+
// inverse boolean-naming rule in the same recommended config. An abstract
|
|
393
|
+
// field parses with no initializer, so the value gate leaves that arm
|
|
394
|
+
// silent — the key is here to keep the two spellings from drifting apart.
|
|
395
|
+
PropertyDefinition: checkClassProperty,
|
|
396
|
+
TSAbstractPropertyDefinition: checkClassProperty,
|
|
354
397
|
};
|
|
355
398
|
},
|
|
356
399
|
});
|
|
@@ -815,6 +815,43 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
|
|
|
815
815
|
checkFunctionName(node.value, node.key.name, null, null, false);
|
|
816
816
|
}
|
|
817
817
|
},
|
|
818
|
+
PropertyDefinition(node) {
|
|
819
|
+
// A class field holding a function declares the same callable member a
|
|
820
|
+
// method does — `member = () => {}` and `member() {}` differ by one
|
|
821
|
+
// token and by nothing this rule judges, since it reads only the
|
|
822
|
+
// member's name. Without this arm the `=` spelling silences the rule
|
|
823
|
+
// (#2156), and it is the spelling a class picks whenever a member must
|
|
824
|
+
// stay bound to its instance.
|
|
825
|
+
if (node.computed ||
|
|
826
|
+
// An ambient member declares a type rather than code; its initializer
|
|
827
|
+
// is not valid TypeScript at all (TS1039), so nothing in it is an
|
|
828
|
+
// implementation whose name this rule can hold the author to.
|
|
829
|
+
node.declare ||
|
|
830
|
+
node.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// The value gate is what keeps data fields inert: `cachedFor = new
|
|
834
|
+
// Map()` names a value, not a function, and only a function member is
|
|
835
|
+
// this rule's subject. A field annotated with a function type but
|
|
836
|
+
// holding something else is inert for the same reason.
|
|
837
|
+
const { value } = node;
|
|
838
|
+
if (!value ||
|
|
839
|
+
(value.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
840
|
+
value.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// A member implementing a contract the class declares conformance to
|
|
844
|
+
// is named by that contract, so renaming it would break conformance.
|
|
845
|
+
if (isDictatedByHeritage(node, node.key.name)) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
// Report-only for the reason the method arm gives: a field arrow is
|
|
849
|
+
// invoked through `this.x()` / `instance.x()`, member accesses the
|
|
850
|
+
// scope manager does not track as references, so a single-file fixer
|
|
851
|
+
// cannot rename the call sites and must not rename the declaration
|
|
852
|
+
// alone (#1256).
|
|
853
|
+
checkFunctionName(value, node.key.name, null, null, false);
|
|
854
|
+
},
|
|
818
855
|
TSMethodSignature(node) {
|
|
819
856
|
// Interface method signatures have their implementations and call sites
|
|
820
857
|
// elsewhere (member accesses on implementers), unreachable from this
|
|
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferGetterOverParameterlessMethod = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
function isPropertyLike(node) {
|
|
7
|
+
return (node.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
8
|
+
node.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition);
|
|
9
|
+
}
|
|
6
10
|
const DEFAULT_PREFIXES = [
|
|
7
11
|
'build',
|
|
8
12
|
'get',
|
|
@@ -143,6 +147,50 @@ function isFunctionLikeNode(value) {
|
|
|
143
147
|
value.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
144
148
|
value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
145
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* The member function a class member carries, or null when it carries none. A
|
|
152
|
+
* method's value is always a function; a field's value is one only when it is
|
|
153
|
+
* written as an arrow or a function expression, which is what makes the field a
|
|
154
|
+
* member function rather than data.
|
|
155
|
+
*/
|
|
156
|
+
function memberFunctionOf(node) {
|
|
157
|
+
if (!isPropertyLike(node))
|
|
158
|
+
return node.value;
|
|
159
|
+
const value = node.value;
|
|
160
|
+
if (value &&
|
|
161
|
+
(value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
162
|
+
value.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The return type a FIELD's own annotation declares for the function it holds.
|
|
169
|
+
* `render: () => void = () => this.draw()` states the member's contract beside
|
|
170
|
+
* the field name rather than on the arrow, and that contract is what a caller
|
|
171
|
+
* reads — so it answers the void and promise questions the same way an arrow's
|
|
172
|
+
* own `(): void` annotation does.
|
|
173
|
+
*/
|
|
174
|
+
function declaredFunctionReturnType(node) {
|
|
175
|
+
if (!isPropertyLike(node))
|
|
176
|
+
return null;
|
|
177
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
178
|
+
if (!annotation || annotation.type !== utils_1.AST_NODE_TYPES.TSFunctionType) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return annotation.returnType?.typeAnnotation ?? null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The nodes a top-level body walk starts from. A concise arrow body is a single
|
|
185
|
+
* expression rather than a statement list, and every walk here is about what
|
|
186
|
+
* the member's OWN scope does, so the expression is that scope's whole content.
|
|
187
|
+
*/
|
|
188
|
+
function bodyWalkRoots(fn) {
|
|
189
|
+
const body = fn.body;
|
|
190
|
+
if (!body)
|
|
191
|
+
return [];
|
|
192
|
+
return body.type === utils_1.AST_NODE_TYPES.BlockStatement ? [...body.body] : [body];
|
|
193
|
+
}
|
|
146
194
|
function lowerFirst(text) {
|
|
147
195
|
if (!text)
|
|
148
196
|
return text;
|
|
@@ -167,7 +215,15 @@ function memberNameOf(key, computed) {
|
|
|
167
215
|
function isEcmaPrivateName(name) {
|
|
168
216
|
return name.startsWith('#');
|
|
169
217
|
}
|
|
170
|
-
function computeBodyLineCount(
|
|
218
|
+
function computeBodyLineCount(fn) {
|
|
219
|
+
const body = fn.body;
|
|
220
|
+
if (!body)
|
|
221
|
+
return 0;
|
|
222
|
+
if (body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
223
|
+
// A concise body has no braces of its own, so its span IS its content: a
|
|
224
|
+
// one-line body counts 0, exactly as the one-line block spelling does.
|
|
225
|
+
return Math.max(0, body.loc.end.line - body.loc.start.line);
|
|
226
|
+
}
|
|
171
227
|
return Math.max(0, body.loc.end.line - body.loc.start.line - 1);
|
|
172
228
|
}
|
|
173
229
|
function hasNameCollision(node, newName) {
|
|
@@ -624,8 +680,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
624
680
|
return (matchesAffirmative(/\bside effects?\b/) ||
|
|
625
681
|
matchesAffirmative(/\bmutat(?:e|es|ing|ion|ions|ed|ive|ively)?\b/));
|
|
626
682
|
}
|
|
627
|
-
function analyzeMutations(
|
|
628
|
-
const stack =
|
|
683
|
+
function analyzeMutations(fn) {
|
|
684
|
+
const stack = bodyWalkRoots(fn);
|
|
629
685
|
while (stack.length) {
|
|
630
686
|
const current = stack.pop();
|
|
631
687
|
if (isFunctionLikeNode(current)) {
|
|
@@ -656,17 +712,21 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
656
712
|
}
|
|
657
713
|
return null;
|
|
658
714
|
}
|
|
659
|
-
function returnsValue(node) {
|
|
660
|
-
const returnType =
|
|
715
|
+
function returnsValue(node, fn) {
|
|
716
|
+
const returnType = fn.returnType?.typeAnnotation ?? declaredFunctionReturnType(node);
|
|
661
717
|
if (returnType) {
|
|
662
718
|
if (isVoidishType(returnType)) {
|
|
663
719
|
return false;
|
|
664
720
|
}
|
|
665
721
|
return true;
|
|
666
722
|
}
|
|
667
|
-
const body =
|
|
723
|
+
const body = fn.body;
|
|
668
724
|
if (!body)
|
|
669
725
|
return false;
|
|
726
|
+
// A concise arrow body IS the value handed back; the subject visitor has
|
|
727
|
+
// already required it to be one that cannot be read as a command.
|
|
728
|
+
if (body.type !== utils_1.AST_NODE_TYPES.BlockStatement)
|
|
729
|
+
return true;
|
|
670
730
|
const stack = [...body.body];
|
|
671
731
|
while (stack.length) {
|
|
672
732
|
const current = stack.pop();
|
|
@@ -886,7 +946,18 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
886
946
|
* mentions promises.
|
|
887
947
|
*/
|
|
888
948
|
function returnsThenable(node) {
|
|
889
|
-
|
|
949
|
+
const fn = memberFunctionOf(node);
|
|
950
|
+
if (!fn)
|
|
951
|
+
return false;
|
|
952
|
+
// A field states its contract beside the name when the arrow carries no
|
|
953
|
+
// annotation of its own, and that contract settles the question exactly
|
|
954
|
+
// as an arrow's own annotation would.
|
|
955
|
+
if (!fn.returnType) {
|
|
956
|
+
const declared = declaredFunctionReturnType(node);
|
|
957
|
+
if (declared)
|
|
958
|
+
return isThenableTypeNode(declared);
|
|
959
|
+
}
|
|
960
|
+
return functionYieldsThenable(node, fn, new Set([fn]));
|
|
890
961
|
}
|
|
891
962
|
/**
|
|
892
963
|
* Returns true when the method body contains a ThrowStatement that is
|
|
@@ -894,8 +965,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
894
965
|
* A method that can throw at the top level is an imperative assertion, not
|
|
895
966
|
* a computed property, so it must not become a getter.
|
|
896
967
|
*/
|
|
897
|
-
function containsTopLevelThrow(
|
|
898
|
-
const stack =
|
|
968
|
+
function containsTopLevelThrow(fn) {
|
|
969
|
+
const stack = bodyWalkRoots(fn);
|
|
899
970
|
while (stack.length) {
|
|
900
971
|
const current = stack.pop();
|
|
901
972
|
// Do not descend into nested function-like scopes — a throw there is
|
|
@@ -937,8 +1008,8 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
937
1008
|
}
|
|
938
1009
|
return false;
|
|
939
1010
|
}
|
|
940
|
-
function bodyReferencesThisProperty(
|
|
941
|
-
const stack =
|
|
1011
|
+
function bodyReferencesThisProperty(fn, propName) {
|
|
1012
|
+
const stack = bodyWalkRoots(fn);
|
|
942
1013
|
while (stack.length) {
|
|
943
1014
|
const current = stack.pop();
|
|
944
1015
|
if (isFunctionLikeNode(current)) {
|
|
@@ -1019,7 +1090,7 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1019
1090
|
}
|
|
1020
1091
|
return true;
|
|
1021
1092
|
}
|
|
1022
|
-
function trackMemberReference(member, callUsedNamesByClass, callUsedNamesInFile) {
|
|
1093
|
+
function trackMemberReference(member, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile) {
|
|
1023
1094
|
const propName = memberNameOf(member.property, member.computed);
|
|
1024
1095
|
if (propName === null) {
|
|
1025
1096
|
return;
|
|
@@ -1028,22 +1099,29 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1028
1099
|
return;
|
|
1029
1100
|
}
|
|
1030
1101
|
addCallUseForMember(member, propName, callUsedNamesByClass, callUsedNamesInFile);
|
|
1102
|
+
addCallUseForMember(member, propName, detachedNamesByClass, detachedNamesInFile);
|
|
1031
1103
|
}
|
|
1032
|
-
function trackThisDestructuring(pattern, init, callUsedNamesByClass, callUsedNamesInFile) {
|
|
1104
|
+
function trackThisDestructuring(pattern, init, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile) {
|
|
1033
1105
|
if (!init || init.type !== utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
1034
1106
|
return;
|
|
1035
1107
|
}
|
|
1108
|
+
const record = (name) => {
|
|
1109
|
+
addCallUse(pattern, name, callUsedNamesByClass, callUsedNamesInFile);
|
|
1110
|
+
// Pulling a member off `this` without calling it takes the function
|
|
1111
|
+
// value itself, which is the detachment a getter cannot survive.
|
|
1112
|
+
addCallUse(pattern, name, detachedNamesByClass, detachedNamesInFile);
|
|
1113
|
+
};
|
|
1036
1114
|
for (const prop of pattern.properties) {
|
|
1037
1115
|
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
1038
1116
|
if (prop.computed)
|
|
1039
1117
|
continue;
|
|
1040
1118
|
const key = prop.key;
|
|
1041
1119
|
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1042
|
-
|
|
1120
|
+
record(key.name);
|
|
1043
1121
|
}
|
|
1044
1122
|
else if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1045
1123
|
typeof key.value === 'string') {
|
|
1046
|
-
|
|
1124
|
+
record(key.value);
|
|
1047
1125
|
}
|
|
1048
1126
|
}
|
|
1049
1127
|
}
|
|
@@ -1087,7 +1165,82 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1087
1165
|
}
|
|
1088
1166
|
const callUsedNamesByClass = new WeakMap();
|
|
1089
1167
|
const callUsedNamesInFile = new Set();
|
|
1168
|
+
/**
|
|
1169
|
+
* Names read WITHOUT being called — the strict subset of the call-use sets
|
|
1170
|
+
* that means "the function value itself was taken". A field arrow exists to
|
|
1171
|
+
* be handed around bound (`store.on('change', this.getSnapshot)`), and a
|
|
1172
|
+
* getter would run on that read instead of yielding the function, so the
|
|
1173
|
+
* field arm withholds its report there rather than prescribe a remedy that
|
|
1174
|
+
* breaks the site.
|
|
1175
|
+
*/
|
|
1176
|
+
const detachedNamesByClass = new WeakMap();
|
|
1177
|
+
const detachedNamesInFile = new Set();
|
|
1090
1178
|
const candidates = [];
|
|
1179
|
+
/**
|
|
1180
|
+
* The gates every member function answers, whichever way it is spelled.
|
|
1181
|
+
*/
|
|
1182
|
+
function considerMember(node, fn, isField) {
|
|
1183
|
+
if (fn.params.length > 0)
|
|
1184
|
+
return;
|
|
1185
|
+
if (node.optional)
|
|
1186
|
+
return;
|
|
1187
|
+
if (node.computed)
|
|
1188
|
+
return;
|
|
1189
|
+
if (fn.typeParameters)
|
|
1190
|
+
return;
|
|
1191
|
+
// An ECMA private name (`#foo`) is the same privacy as the TypeScript
|
|
1192
|
+
// `private` modifier — and mutually exclusive with it, since `private
|
|
1193
|
+
// #foo` is TS18010 — so it carries a plain, strippable name that the
|
|
1194
|
+
// rule analyzes exactly like an `Identifier` key. Literal, computed and
|
|
1195
|
+
// template keys stay out: their names are not always statically known
|
|
1196
|
+
// and the emitted getter would need quoting the fixer does not do.
|
|
1197
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
1198
|
+
node.key.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
// `ignoreAsync` means "ignore asynchronous members", not "ignore members
|
|
1202
|
+
// bearing the async keyword": TypeScript does not require the keyword to
|
|
1203
|
+
// return a promise, so `fetchToken(): Promise<string>` is asynchronous
|
|
1204
|
+
// with no keyword written at all (#2154).
|
|
1205
|
+
if (config.ignoreAsync && returnsThenable(node))
|
|
1206
|
+
return;
|
|
1207
|
+
if (node.override)
|
|
1208
|
+
return;
|
|
1209
|
+
if (!fn.body)
|
|
1210
|
+
return;
|
|
1211
|
+
if (isConstrainedByHeritage(node))
|
|
1212
|
+
return;
|
|
1213
|
+
const name = node.key.name;
|
|
1214
|
+
if (ignoredMethods.has(name))
|
|
1215
|
+
return;
|
|
1216
|
+
// Factory/builder terminal members are imperative actions (issue #990 #4).
|
|
1217
|
+
// Exemption takes precedence over stripPrefixes so names like "build"
|
|
1218
|
+
// are never prefix-stripped into a getter candidate.
|
|
1219
|
+
if (factoryMethods.has(name))
|
|
1220
|
+
return;
|
|
1221
|
+
if (computeBodyLineCount(fn) < config.minBodyLines) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (!returnsValue(node, fn))
|
|
1225
|
+
return;
|
|
1226
|
+
if (config.respectJsDocSideEffects && hasSideEffectTag(node))
|
|
1227
|
+
return;
|
|
1228
|
+
// A member that throws at the top level is an imperative assertion or
|
|
1229
|
+
// command, not a computed property — getters must be pure/non-throwing.
|
|
1230
|
+
if (containsTopLevelThrow(fn))
|
|
1231
|
+
return;
|
|
1232
|
+
const sideEffectReason = analyzeMutations(fn);
|
|
1233
|
+
const suggestedName = suggestName(name);
|
|
1234
|
+
const sigil = node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
|
|
1235
|
+
candidates.push({
|
|
1236
|
+
node,
|
|
1237
|
+
fn,
|
|
1238
|
+
sideEffectReason,
|
|
1239
|
+
memberName: `${sigil}${name}`,
|
|
1240
|
+
getterName: `${sigil}${suggestedName}`,
|
|
1241
|
+
isField,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1091
1244
|
return {
|
|
1092
1245
|
CallExpression(node) {
|
|
1093
1246
|
const callee = node.callee;
|
|
@@ -1102,21 +1255,31 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1102
1255
|
}
|
|
1103
1256
|
},
|
|
1104
1257
|
MemberExpression(node) {
|
|
1105
|
-
trackMemberReference(node, callUsedNamesByClass, callUsedNamesInFile);
|
|
1258
|
+
trackMemberReference(node, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile);
|
|
1106
1259
|
},
|
|
1107
1260
|
ChainExpression(node) {
|
|
1108
1261
|
if (node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1109
|
-
trackMemberReference(node.expression, callUsedNamesByClass, callUsedNamesInFile);
|
|
1262
|
+
trackMemberReference(node.expression, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile);
|
|
1110
1263
|
}
|
|
1111
1264
|
},
|
|
1112
1265
|
VariableDeclarator(node) {
|
|
1113
1266
|
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
1114
|
-
trackThisDestructuring(node.id, node.init ?? null, callUsedNamesByClass, callUsedNamesInFile);
|
|
1267
|
+
trackThisDestructuring(node.id, node.init ?? null, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile);
|
|
1115
1268
|
}
|
|
1116
1269
|
},
|
|
1117
1270
|
AssignmentExpression(node) {
|
|
1118
1271
|
if (node.left.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
1119
|
-
trackThisDestructuring(node.left, node.right, callUsedNamesByClass, callUsedNamesInFile);
|
|
1272
|
+
trackThisDestructuring(node.left, node.right, callUsedNamesByClass, callUsedNamesInFile, detachedNamesByClass, detachedNamesInFile);
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
JSXMemberExpression(node) {
|
|
1276
|
+
// `<this.Panel />` takes the member's function value — JSX invokes the
|
|
1277
|
+
// component itself — but it is a JSXMemberExpression, which the member
|
|
1278
|
+
// trackers never see. A component declared as a field is exactly the
|
|
1279
|
+
// shape a getter would break, so this counts as detachment.
|
|
1280
|
+
const property = node.property;
|
|
1281
|
+
if (property.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
1282
|
+
addCallUse(node, property.name, detachedNamesByClass, detachedNamesInFile);
|
|
1120
1283
|
}
|
|
1121
1284
|
},
|
|
1122
1285
|
BinaryExpression(node) {
|
|
@@ -1133,73 +1296,37 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1133
1296
|
'MethodDefinition, TSAbstractMethodDefinition'(node) {
|
|
1134
1297
|
if (node.kind !== 'method')
|
|
1135
1298
|
return;
|
|
1136
|
-
if (node.value.params.length > 0)
|
|
1137
|
-
return;
|
|
1138
1299
|
if (node.value.generator)
|
|
1139
1300
|
return;
|
|
1140
|
-
if (node.optional)
|
|
1141
|
-
return;
|
|
1142
|
-
if (node.computed)
|
|
1143
|
-
return;
|
|
1144
|
-
if (node.value.typeParameters)
|
|
1145
|
-
return;
|
|
1146
|
-
// An ECMA private name (`#foo`) is the same privacy as the TypeScript
|
|
1147
|
-
// `private` modifier — and mutually exclusive with it, since `private
|
|
1148
|
-
// #foo` is TS18010 — so it carries a plain, strippable name that the
|
|
1149
|
-
// rule analyzes exactly like an `Identifier` key. Literal, computed and
|
|
1150
|
-
// template keys stay out: their names are not always statically known
|
|
1151
|
-
// and the emitted getter would need quoting the fixer does not do.
|
|
1152
|
-
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
1153
|
-
node.key.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
// `ignoreAsync` means "ignore asynchronous methods", not "ignore
|
|
1157
|
-
// methods bearing the async keyword": TypeScript does not require the
|
|
1158
|
-
// keyword to return a promise, so `fetchToken(): Promise<string>` is
|
|
1159
|
-
// asynchronous with no keyword written at all (#2154).
|
|
1160
|
-
if (config.ignoreAsync && returnsThenable(node))
|
|
1161
|
-
return;
|
|
1162
1301
|
if (config.ignoreAbstract &&
|
|
1163
1302
|
node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
|
|
1164
1303
|
return;
|
|
1165
1304
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
if (isConstrainedByHeritage(node))
|
|
1171
|
-
return;
|
|
1172
|
-
const name = node.key.name;
|
|
1173
|
-
if (ignoredMethods.has(name))
|
|
1174
|
-
return;
|
|
1175
|
-
// Factory/builder terminal methods are imperative actions (issue #990 #4).
|
|
1176
|
-
// Exemption takes precedence over stripPrefixes so names like "build"
|
|
1177
|
-
// are never prefix-stripped into a getter candidate.
|
|
1178
|
-
if (factoryMethods.has(name))
|
|
1179
|
-
return;
|
|
1305
|
+
// Getters cannot carry overload declarations, so leaving the signatures
|
|
1306
|
+
// in place would produce invalid TypeScript. Only a method can have
|
|
1307
|
+
// them: a field and an overload signature of the same name is not a
|
|
1308
|
+
// legal class body.
|
|
1180
1309
|
if (hasOverloadSignatures(node))
|
|
1181
1310
|
return;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1311
|
+
considerMember(node, node.value, false);
|
|
1312
|
+
},
|
|
1313
|
+
'PropertyDefinition, TSAbstractPropertyDefinition'(node) {
|
|
1314
|
+
// A field is a member function only when it holds one. Data fields,
|
|
1315
|
+
// `declare` fields and `abstract` declarations hold no function at all,
|
|
1316
|
+
// so they leave here rather than needing a gate of their own.
|
|
1317
|
+
const fn = memberFunctionOf(node);
|
|
1318
|
+
if (!fn)
|
|
1187
1319
|
return;
|
|
1188
|
-
|
|
1320
|
+
// `foo = function* () {}` hands back an iterator through a protocol a
|
|
1321
|
+
// getter has no way to express.
|
|
1322
|
+
if (fn.type === utils_1.AST_NODE_TYPES.FunctionExpression && fn.generator) {
|
|
1189
1323
|
return;
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1324
|
+
}
|
|
1325
|
+
if (config.ignoreAbstract &&
|
|
1326
|
+
node.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) {
|
|
1193
1327
|
return;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const sigil = node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
|
|
1197
|
-
candidates.push({
|
|
1198
|
-
node,
|
|
1199
|
-
sideEffectReason,
|
|
1200
|
-
memberName: `${sigil}${name}`,
|
|
1201
|
-
getterName: `${sigil}${suggestedName}`,
|
|
1202
|
-
});
|
|
1328
|
+
}
|
|
1329
|
+
considerMember(node, fn, true);
|
|
1203
1330
|
},
|
|
1204
1331
|
'Program:exit'() {
|
|
1205
1332
|
const suggestedNameCounts = new WeakMap();
|
|
@@ -1212,7 +1339,28 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1212
1339
|
const side = node.static ?? false;
|
|
1213
1340
|
return `${side ? 'static' : 'instance'}:${getterName}`;
|
|
1214
1341
|
};
|
|
1215
|
-
|
|
1342
|
+
/**
|
|
1343
|
+
* A field handed around as a function value is the one shape whose
|
|
1344
|
+
* getter remedy is wrong rather than merely unfixable: the caller that
|
|
1345
|
+
* wrote `this.getSnapshot` wants the function, and a getter would run
|
|
1346
|
+
* the body and hand back its result instead. The method arm keeps
|
|
1347
|
+
* reporting such members — its remedy is to convert the member AND its
|
|
1348
|
+
* call sites, which stays available — so this withholding is specific
|
|
1349
|
+
* to the spelling whose purpose is detachment.
|
|
1350
|
+
*/
|
|
1351
|
+
const isDetachedField = (candidate) => {
|
|
1352
|
+
if (!candidate.isField)
|
|
1353
|
+
return false;
|
|
1354
|
+
const classBody = candidate.node.parent;
|
|
1355
|
+
const withinClass = classBody?.type === utils_1.AST_NODE_TYPES.ClassBody
|
|
1356
|
+
? detachedNamesByClass
|
|
1357
|
+
.get(classBody)
|
|
1358
|
+
?.has(candidate.memberName) ?? false
|
|
1359
|
+
: false;
|
|
1360
|
+
return withinClass || detachedNamesInFile.has(candidate.memberName);
|
|
1361
|
+
};
|
|
1362
|
+
const reportable = candidates.filter((candidate) => !isDetachedField(candidate));
|
|
1363
|
+
for (const { node, getterName } of reportable) {
|
|
1216
1364
|
const classBody = node.parent;
|
|
1217
1365
|
if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
1218
1366
|
continue;
|
|
@@ -1222,7 +1370,7 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1222
1370
|
existingCounts.set(scopeKey, (existingCounts.get(scopeKey) ?? 0) + 1);
|
|
1223
1371
|
suggestedNameCounts.set(classBody, existingCounts);
|
|
1224
1372
|
}
|
|
1225
|
-
for (const { node, sideEffectReason, memberName, getterName, } of
|
|
1373
|
+
for (const { node, fn, sideEffectReason, memberName, getterName, isField, } of reportable) {
|
|
1226
1374
|
const classBody = node.parent;
|
|
1227
1375
|
const scopeKey = scopeKeyOf(node, getterName);
|
|
1228
1376
|
const leftParen = sourceCode.getTokenAfter(node.key, {
|
|
@@ -1233,10 +1381,7 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1233
1381
|
filter: (token) => token.value === ')',
|
|
1234
1382
|
})
|
|
1235
1383
|
: null;
|
|
1236
|
-
const
|
|
1237
|
-
const referencesSuggestedName = body && body.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
1238
|
-
? bodyReferencesThisProperty(body, getterName)
|
|
1239
|
-
: false;
|
|
1384
|
+
const referencesSuggestedName = bodyReferencesThisProperty(fn, getterName);
|
|
1240
1385
|
const hasCollision = hasNameCollision(node, getterName);
|
|
1241
1386
|
const isCallUsed = classBody?.type === utils_1.AST_NODE_TYPES.ClassBody
|
|
1242
1387
|
? callUsedNamesByClass.get(classBody)?.has(memberName) ?? false
|
|
@@ -1281,7 +1426,16 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1281
1426
|
suggestedName: getterName,
|
|
1282
1427
|
reason: sideEffectReason ?? 'it returns a value',
|
|
1283
1428
|
},
|
|
1284
|
-
fix:
|
|
1429
|
+
fix:
|
|
1430
|
+
// The field spelling is report-only. Its rewrite must consume the
|
|
1431
|
+
// `= (`…`) =>` and the terminating `;`, reshape a concise body
|
|
1432
|
+
// into a block, and drop a `readonly` modifier a getter cannot
|
|
1433
|
+
// carry — and even done perfectly it turns an own enumerable
|
|
1434
|
+
// per-instance property into a prototype accessor, which changes
|
|
1435
|
+
// `Object.keys(instance)` and object spread. No gate here models
|
|
1436
|
+
// that, so the developer applies it.
|
|
1437
|
+
isField ||
|
|
1438
|
+
!isPrivate ||
|
|
1285
1439
|
sideEffectReason ||
|
|
1286
1440
|
isThenableReturning ||
|
|
1287
1441
|
!leftParen ||
|
|
@@ -171,6 +171,49 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
|
|
|
171
171
|
privateIdentifierPrefix: '#',
|
|
172
172
|
}));
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* A class member written as a function-valued field (`getUser = () => {}`)
|
|
176
|
+
* declares the same thing as `getUser() {}`: a named, callable member of the
|
|
177
|
+
* class. The two spellings differ only in binding and `this` semantics, not
|
|
178
|
+
* in what this rule judges, so a single `=` must not decide whether the name
|
|
179
|
+
* is read.
|
|
180
|
+
*/
|
|
181
|
+
function checkPropertyName(node) {
|
|
182
|
+
const value = node.value;
|
|
183
|
+
/**
|
|
184
|
+
* Only a function-valued field names an operation. `updateCount = 0` and
|
|
185
|
+
* `getters = {}` are data whose names describe a value, not a verb applied
|
|
186
|
+
* to one, and a field with no initializer (`declare getData: Fn`,
|
|
187
|
+
* `getData!: Fn`) declares no implementation to name.
|
|
188
|
+
*/
|
|
189
|
+
if (value?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
190
|
+
value?.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A named function expression carries its own binding, which the function
|
|
195
|
+
* arm reads and reports on. Deferring to it keeps exactly one report per
|
|
196
|
+
* site and matches the precedence the rule already applies to the variable
|
|
197
|
+
* spelling, where `const getUser = function getData() {}` is reported as
|
|
198
|
+
* `getData`.
|
|
199
|
+
*/
|
|
200
|
+
if (value.type === utils_1.AST_NODE_TYPES.FunctionExpression && value.id) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { key } = node;
|
|
204
|
+
// Same readable-key requirement as the method arm; see checkMethodName.
|
|
205
|
+
if (node.computed ||
|
|
206
|
+
(key.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
207
|
+
key.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const propertyName = key.name;
|
|
211
|
+
if (!propertyName)
|
|
212
|
+
return;
|
|
213
|
+
reportIfGenericPrefix(key, propertyName, (0, getMethodName_1.getMethodName)(node, context.getSourceCode(), {
|
|
214
|
+
privateIdentifierPrefix: '#',
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
174
217
|
function checkFunctionName(node) {
|
|
175
218
|
// Skip anonymous functions
|
|
176
219
|
if (!node.id && node.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
@@ -194,6 +237,7 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
|
|
|
194
237
|
FunctionExpression: checkFunctionName,
|
|
195
238
|
ArrowFunctionExpression: checkFunctionName,
|
|
196
239
|
MethodDefinition: checkMethodName,
|
|
240
|
+
PropertyDefinition: checkPropertyName,
|
|
197
241
|
};
|
|
198
242
|
},
|
|
199
243
|
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,74 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.182",
|
|
4
|
+
"date": "2026-08-27T12:12:45.593Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-assert-throws",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2162
|
|
11
|
+
],
|
|
12
|
+
"summary": "name a function-valued class field (closes #2162)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2159
|
|
19
|
+
],
|
|
20
|
+
"summary": "read a field's boolean-returning function value (closes #2159)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-positive-naming",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2157
|
|
27
|
+
],
|
|
28
|
+
"summary": "judge class fields and abstract members (closes #2157)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "enforce-verb-noun-naming",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
2160
|
|
35
|
+
],
|
|
36
|
+
"summary": "check a callable class field (closes #2160)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no-misleading-boolean-prefixes",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
2155
|
|
43
|
+
],
|
|
44
|
+
"summary": "judge a boolean-prefixed class field holding a function (closes #2155)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "no-unnecessary-verb-suffix",
|
|
48
|
+
"changeType": "fix",
|
|
49
|
+
"issues": [
|
|
50
|
+
2156
|
|
51
|
+
],
|
|
52
|
+
"summary": "check a class field holding a function (closes #2156)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "prefer-getter-over-parameterless-method",
|
|
56
|
+
"changeType": "fix",
|
|
57
|
+
"issues": [
|
|
58
|
+
2158
|
|
59
|
+
],
|
|
60
|
+
"summary": "consider a function-valued class field (closes #2158)"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "semantic-function-prefixes",
|
|
64
|
+
"changeType": "fix",
|
|
65
|
+
"issues": [
|
|
66
|
+
2161
|
|
67
|
+
],
|
|
68
|
+
"summary": "judge a function-valued class property (closes #2161)"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
2
72
|
{
|
|
3
73
|
"version": "1.20.181",
|
|
4
74
|
"date": "2026-08-27T06:29:43.247Z",
|