@blumintinc/eslint-plugin-blumint 1.20.180 → 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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.180',
226
+ version: '1.20.182',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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 method definitions for negative naming
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 checkMethodDefinition(node) {
1475
+ function checkClassMember(node) {
1452
1476
  if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
1453
1477
  return;
1454
- // Only check boolean-returning methods
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
- const methodName = node.key.name;
1461
- const { isNegative, alternatives } = hasBooleanNegativeNaming(methodName);
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: methodName,
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: checkMethodDefinition,
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