@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.0

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.
Files changed (37) hide show
  1. package/README.md +28 -5
  2. package/lib/index.js +7 -1
  3. package/lib/rules/consistent-callback-naming.js +26 -0
  4. package/lib/rules/dynamic-https-errors.js +5 -26
  5. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  6. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
  7. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  8. package/lib/rules/enforce-dynamic-imports.js +42 -21
  9. package/lib/rules/enforce-memoize-async.js +1 -4
  10. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  11. package/lib/rules/enforce-verb-noun-naming.js +3 -0
  12. package/lib/rules/global-const-style.js +9 -0
  13. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  14. package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
  15. package/lib/rules/memo-nested-react-components.js +143 -8
  16. package/lib/rules/no-array-length-in-deps.js +74 -3
  17. package/lib/rules/no-circular-references.js +145 -482
  18. package/lib/rules/no-compositing-layer-props.js +31 -0
  19. package/lib/rules/no-entire-object-hook-deps.js +132 -97
  20. package/lib/rules/no-explicit-return-type.js +6 -0
  21. package/lib/rules/no-hungarian.js +119 -24
  22. package/lib/rules/no-margin-properties.js +7 -38
  23. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  24. package/lib/rules/no-unused-props.js +215 -37
  25. package/lib/rules/no-useless-fragment.js +10 -2
  26. package/lib/rules/parallelize-async-operations.js +1 -3
  27. package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
  28. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  29. package/lib/rules/react-memoize-literals.js +87 -1
  30. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  31. package/lib/rules/require-migration-script-metadata.js +206 -0
  32. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  33. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  34. package/lib/utils/ASTHelpers.d.ts +15 -0
  35. package/lib/utils/ASTHelpers.js +48 -0
  36. package/package.json +7 -6
  37. package/release-manifest.json +166 -0
package/README.md CHANGED
@@ -47,6 +47,27 @@ Or use the recommended config:
47
47
  }
48
48
  ```
49
49
 
50
+ ## Autonomous maintenance
51
+
52
+ This repo is maintained by an autonomous loop, not a human cutting releases on
53
+ demand. See [.claude/skills/repo-maintenance/SKILL.md](.claude/skills/repo-maintenance/SKILL.md)
54
+ for the operating model. Two contracts make it safe and self-healing:
55
+
56
+ - **The maintainer** (`/maintainer`, backed by `scripts/maintainer.ts`) is the
57
+ sole operator: it drains the open-issue queue (bugs before features, oldest
58
+ first), self-merges fixes to `develop`, and on an empty queue promotes
59
+ `develop → main` (firing the release) then fast-forwards `develop`.
60
+ - **`release-manifest.json`** is a strict, published `version → fixed-rules` map
61
+ (`scripts/generate-release-manifest.js`, a `@semantic-release/exec` prepareCmd)
62
+ so consumers re-enable disabled rules by exact rule name — never by parsing the
63
+ changelog. It's only reliable because **every `fix`/`feat` commit's scope must
64
+ be a real rule name**: enforced by the `blumint-rule-scope` commitlint rule and
65
+ the `validate-commit-scopes` CI gate. One rule per fix/feat commit.
66
+
67
+ On publish, `scripts/dispatch-agora-release.js` fires a `repository_dispatch` to
68
+ agora, which bumps its pin and re-enables the now-fixed rules (FP-audited). The
69
+ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.md`.
70
+
50
71
  ## Rules
51
72
 
52
73
  <!-- begin auto-generated rules list -->
@@ -74,10 +95,10 @@ Or use the recommended config:
74
95
  | [enforce-centralized-mock-firestore](docs/rules/enforce-centralized-mock-firestore.md) | Enforce usage of centralized mockFirestore from predefined location | ✅ | | | 🔧 | | |
75
96
  | [enforce-console-error](docs/rules/enforce-console-error.md) | Enforce proper logging for useAlertDialog based on severity. When severity is "error", console.error must be included. When severity is "warning", console.warn must be included. This ensures all user-facing errors and warnings are properly logged to observability systems. | ✅ | | | | | |
76
97
  | [enforce-css-media-queries](docs/rules/enforce-css-media-queries.md) | Enforce CSS media queries over JS breakpoints | ✅ | | | | | |
77
- | [enforce-date-ttime](docs/rules/enforce-date-ttime.md) | Enforce that any generic type parameter named TTime is explicitly set to Date in frontend code | ✅ | | | 🔧 | | 💭 |
98
+ | [enforce-date-ttime](docs/rules/enforce-date-ttime.md) | Enforce that any generic type parameter named TTime is explicitly set to Date in frontend code | ✅ | | | 🔧 | | |
78
99
  | [enforce-dynamic-file-naming](docs/rules/enforce-dynamic-file-naming.md) | Enforce .dynamic.ts(x) file naming when @blumintinc/blumint/enforce-dynamic-imports or @blumintinc/blumint/require-dynamic-firebase-imports rule is disabled | | | | | | |
79
100
  | [enforce-dynamic-firebase-imports](docs/rules/enforce-dynamic-firebase-imports.md) | Require firebaseCloud modules to be loaded via dynamic import so Firebase code stays out of the initial bundle and only loads when needed. | ✅ | | | 🔧 | 💡 | |
80
- | [enforce-dynamic-imports](docs/rules/enforce-dynamic-imports.md) | Enforce dynamic imports for specified libraries to optimize bundle size | ✅ | | | | | |
101
+ | [enforce-dynamic-imports](docs/rules/enforce-dynamic-imports.md) | Enforce dynamic imports for external libraries by default to optimize bundle size, unless explicitly ignored | ✅ | | | | | |
81
102
  | [enforce-early-destructuring](docs/rules/enforce-early-destructuring.md) | Hoist object destructuring out of React hooks so dependency arrays track the fields in use instead of the entire object. | ✅ | | | 🔧 | | |
82
103
  | [enforce-empty-object-check](docs/rules/enforce-empty-object-check.md) | Ensure object existence checks also guard against empty objects so that empty payloads are treated like missing data. | ✅ | | | 🔧 | | |
83
104
  | [enforce-exported-function-types](docs/rules/enforce-exported-function-types.md) | Enforce exporting types for function props and return values | ✅ | | | | | |
@@ -91,7 +112,7 @@ Or use the recommended config:
91
112
  | [enforce-global-constants](docs/rules/enforce-global-constants.md) | Enforce global static constants for React components/hooks | ✅ | | | 🔧 | | |
92
113
  | [enforce-id-capitalization](docs/rules/enforce-id-capitalization.md) | Enforce the use of "ID" instead of "id" in user-facing text | ✅ | | | 🔧 | | |
93
114
  | [enforce-identifiable-firestore-type](docs/rules/enforce-identifiable-firestore-type.md) | Enforce that Firestore type definitions extend Identifiable and match their folder name | ✅ | | | | | |
94
- | [enforce-memoize-async](docs/rules/enforce-memoize-async.md) | Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. This improves performance by reusing previous results when the same parameters are provided, particularly useful for data fetching methods. | ✅ | | | 🔧 | | |
115
+ | [enforce-memoize-async](docs/rules/enforce-memoize-async.md) | Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. Without memoization, repeated calls trigger redundant requests or expensive computations, increasing latency. @Memoize() caches results by parameter, ensuring subsequent calls with identical inputs return immediately. | ✅ | | | 🔧 | | |
95
116
  | [enforce-memoize-getters](docs/rules/enforce-memoize-getters.md) | Enforce @Memoize() decorator on private class getters to avoid re-instantiation and preserve state across accesses. | ✅ | | | 🔧 | | |
96
117
  | [enforce-microdiff](docs/rules/enforce-microdiff.md) | Enforce using microdiff for object and array comparison operations | ✅ | | | 🔧 | | |
97
118
  | [enforce-mock-firestore](docs/rules/enforce-mock-firestore.md) | Enforce using the standardized mockFirestore utility instead of manual Firestore mocking or third-party mocks. This ensures consistent test behavior across the codebase, reduces boilerplate, and provides type-safe mocking of Firestore operations. | ✅ | | | | | |
@@ -110,7 +131,7 @@ Or use the recommended config:
110
131
  | [enforce-stable-hash-spread-props](docs/rules/enforce-stable-hash-spread-props.md) | Require stableHash wrapping when spread props rest objects are used in React hook dependency arrays to avoid re-renders triggered by new object references on every render. | ✅ | | | 🔧 | | |
111
132
  | [enforce-storage-context](docs/rules/enforce-storage-context.md) | Require storage access to go through the LocalStorage and SessionStorage context providers instead of direct browser APIs | ✅ | | | | | |
112
133
  | [enforce-timestamp-now](docs/rules/enforce-timestamp-now.md) | Enforce the use of Timestamp.now() for getting the current timestamp in backend code. This rule prevents using alternatives like Timestamp.fromDate(new Date()) or other date creation patterns that could lead to inconsistency. | ✅ | | | 🔧 | | |
113
- | [enforce-transform-memoization](docs/rules/enforce-transform-memoization.md) | Enforce memoization of transformValue and transformOnChange in adaptValue | ✅ | | | | | |
134
+ | [enforce-transform-memoization](docs/rules/enforce-transform-memoization.md) | Enforce memoization of transformValue and transformOnChange in adaptValue | ✅ | | | | | |
114
135
  | [enforce-typescript-markdown-code-blocks](docs/rules/enforce-typescript-markdown-code-blocks.md) | Ensure Markdown fenced code blocks without a language specifier default to typescript for consistent highlighting. | ✅ | | | 🔧 | | |
115
136
  | [enforce-unique-cursor-headers](docs/rules/enforce-unique-cursor-headers.md) | Ensure files have exactly one cursor header containing required tags (e.g., @fileoverview) before any code | ✅ | | | 🔧 | | |
116
137
  | [enforce-verb-noun-naming](docs/rules/enforce-verb-noun-naming.md) | Enforce verb phrases for functions and methods | ✅ | | | | | |
@@ -204,7 +225,7 @@ Or use the recommended config:
204
225
  | [prefer-type-alias-over-typeof-constant](docs/rules/prefer-type-alias-over-typeof-constant.md) | Prefer named type aliases over `typeof` on same-file global constants; ensure types are declared before constants. | ✅ | | | | | |
205
226
  | [prefer-type-over-interface](docs/rules/prefer-type-over-interface.md) | Prefer using type alias over interface | ✅ | | | 🔧 | | |
206
227
  | [prefer-url-tostring-over-tojson](docs/rules/prefer-url-tostring-over-tojson.md) | Enforce the use of toString() over toJSON() on URL objects. | ✅ | | | 🔧 | | |
207
- | [prefer-use-deep-compare-memo](docs/rules/prefer-use-deep-compare-memo.md) | Enforce using useDeepCompareMemo when dependency array contains non-primitive values (objects, arrays, functions) that are not already memoized. This prevents unnecessary re-renders due to reference changes. | ✅ | | | 🔧 | | |
228
+ | [prefer-use-deep-compare-memo](docs/rules/prefer-use-deep-compare-memo.md) | Enforce using useDeepCompareMemo when dependency array contains non-primitive values (objects, arrays) that are not already memoized. This prevents unnecessary re-renders due to reference changes. | ✅ | | | 🔧 | | |
208
229
  | [prefer-usecallback-over-usememo-for-functions](docs/rules/prefer-usecallback-over-usememo-for-functions.md) | Enforce using useCallback instead of useMemo for memoizing functions | ✅ | | | 🔧 | | |
209
230
  | [prefer-usememo-over-useeffect-usestate](docs/rules/prefer-usememo-over-useeffect-usestate.md) | Prefer useMemo over useEffect + useState for pure computations to avoid extra render cycles and stale derived state. | ✅ | | | | | |
210
231
  | [prefer-utility-function-over-private-static](docs/rules/prefer-utility-function-over-private-static.md) | Enforce abstraction of private static methods into utility functions | ✅ | | | | | |
@@ -218,6 +239,7 @@ Or use the recommended config:
218
239
  | [require-image-optimized](docs/rules/require-image-optimized.md) | Enforce using ImageOptimized component instead of next/image or img tags | ✅ | | | 🔧 | | |
219
240
  | [require-memo](docs/rules/require-memo.md) | React components must be memoized | ✅ | | | 🔧 | | |
220
241
  | [require-memoize-jsx-returners](docs/rules/require-memoize-jsx-returners.md) | Require @Memoize() decorator on instance members that return JSX or JSX factories | ✅ | | | 🔧 | | |
242
+ | [require-migration-script-metadata](docs/rules/require-migration-script-metadata.md) | Enforce JSDoc migration metadata in callable scripts | ✅ | | | | | |
221
243
  | [semantic-function-prefixes](docs/rules/semantic-function-prefixes.md) | Require semantic function prefixes instead of generic verbs so callers know whether a function fetches data, transforms input, or mutates state | ✅ | | | | | |
222
244
  | [sync-onwrite-name-func](docs/rules/sync-onwrite-name-func.md) | Ensure that the name field matches the func field in onWrite handlers | ✅ | | | 🔧 | | |
223
245
  | [test-file-location-enforcement](docs/rules/test-file-location-enforcement.md) | Enforce colocating *.test.ts or *.test.tsx files with the code they cover. | ✅ | | | | | |
@@ -226,6 +248,7 @@ Or use the recommended config:
226
248
  | [use-custom-router](docs/rules/use-custom-router.md) | Enforce using src/hooks/routing/useRouter instead of next/router | ✅ | | | 🔧 | | |
227
249
  | [use-latest-callback](docs/rules/use-latest-callback.md) | Enforce using useLatestCallback from use-latest-callback instead of React useCallback | ✅ | | | 🔧 | | |
228
250
  | [vertically-group-related-functions](docs/rules/vertically-group-related-functions.md) | Keep top-level functions grouped vertically so callers, exports, and helpers read top-down. | ✅ | | | 🔧 | | |
251
+ | [warn-https-error-message-user-friendly](docs/rules/warn-https-error-message-user-friendly.md) | Warn when messageUserFriendly is used in HttpsError or toHttpsError to ensure it is truly a user-caused error. | | ✅ | | | | |
229
252
 
230
253
  <!-- end auto-generated rules list -->
231
254
 
package/lib/index.js CHANGED
@@ -168,6 +168,8 @@ const vertically_group_related_functions_1 = require("./rules/vertically-group-r
168
168
  const no_static_constants_in_dynamic_files_1 = __importDefault(require("./rules/no-static-constants-in-dynamic-files"));
169
169
  const test_file_location_enforcement_1 = require("./rules/test-file-location-enforcement");
170
170
  const prevent_children_clobber_1 = require("./rules/prevent-children-clobber");
171
+ const require_migration_script_metadata_1 = require("./rules/require-migration-script-metadata");
172
+ const warn_https_error_message_user_friendly_1 = require("./rules/warn-https-error-message-user-friendly");
171
173
  const NO_FRONTEND_IMPORTS_FROM_FUNCTIONS_MESSAGE = 'Backend Cloud Functions (.f.ts under functions/) must not import frontend modules from the repo root src/**. Frontend code can depend on browser-only APIs and bundling it into Cloud Functions breaks server execution; move shared logic into functions/src or a shared package.';
172
174
  function noFrontendImportsFromFunctionsPatterns(pattern) {
173
175
  return [
@@ -194,7 +196,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
194
196
  module.exports = {
195
197
  meta: {
196
198
  name: '@blumintinc/eslint-plugin-blumint',
197
- version: '1.15.0',
199
+ version: '1.16.0',
198
200
  },
199
201
  parseOptions: {
200
202
  ecmaVersion: 2020,
@@ -368,6 +370,8 @@ module.exports = {
368
370
  '@blumintinc/blumint/vertically-group-related-functions': 'error',
369
371
  '@blumintinc/blumint/no-static-constants-in-dynamic-files': 'error',
370
372
  '@blumintinc/blumint/test-file-location-enforcement': 'error',
373
+ '@blumintinc/blumint/require-migration-script-metadata': 'error',
374
+ '@blumintinc/blumint/warn-https-error-message-user-friendly': 'warn',
371
375
  },
372
376
  /**
373
377
  * Depth-specific overrides block only import strings that traverse to the
@@ -616,6 +620,8 @@ module.exports = {
616
620
  'vertically-group-related-functions': vertically_group_related_functions_1.verticallyGroupRelatedFunctions,
617
621
  'no-static-constants-in-dynamic-files': no_static_constants_in_dynamic_files_1.default,
618
622
  'test-file-location-enforcement': test_file_location_enforcement_1.testFileLocationEnforcement,
623
+ 'require-migration-script-metadata': require_migration_script_metadata_1.requireMigrationScriptMetadata,
624
+ 'warn-https-error-message-user-friendly': warn_https_error_message_user_friendly_1.warnHttpsErrorMessageUserFriendly,
619
625
  },
620
626
  };
621
627
  //# sourceMappingURL=index.js.map
@@ -95,6 +95,31 @@ module.exports = (0, createRule_1.createRule)({
95
95
  const type = checker.getTypeAtLocation(tsNode);
96
96
  return type.getCallSignatures().length > 0;
97
97
  }
98
+ // A union counts as "mixed" when it pairs a callable member with a
99
+ // non-callable one (e.g. `Validate<T> | readonly T[]`). `undefined`/`null`
100
+ // members are ignored so plain optional callbacks (`(() => void) | undefined`)
101
+ // remain pure functions.
102
+ function isMixedFunctionUnion(type) {
103
+ if (!type || !type.isUnion()) {
104
+ return false;
105
+ }
106
+ const members = type.types.filter((member) => !(member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)));
107
+ const hasFunctionMember = members.some((member) => member.getCallSignatures().length > 0);
108
+ const hasNonFunctionMember = members.some((member) => member.getCallSignatures().length === 0);
109
+ return hasFunctionMember && hasNonFunctionMember;
110
+ }
111
+ // The `on` prefix is only meaningful for props that are *exclusively*
112
+ // callbacks. A prop typed `Validate<T> | readonly T[]` is a configuration
113
+ // prop that merely accepts a function as one option, so it must be skipped.
114
+ // The passed value may be a plain function even when the prop legitimately
115
+ // accepts non-function values, so the prop's contextual (declared) type is
116
+ // inspected alongside the value's own type.
117
+ function acceptsNonFunctionValue(node) {
118
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
119
+ const valueType = checker.getTypeAtLocation(tsNode);
120
+ const contextualType = checker.getContextualType(tsNode);
121
+ return (isMixedFunctionUnion(valueType) || isMixedFunctionUnion(contextualType));
122
+ }
98
123
  function isRenderFunction(node) {
99
124
  const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
100
125
  const type = checker.getTypeAtLocation(tsNode);
@@ -164,6 +189,7 @@ module.exports = (0, createRule_1.createRule)({
164
189
  propName &&
165
190
  !propName.startsWith('on') &&
166
191
  !propName.startsWith('render') &&
192
+ !acceptsNonFunctionValue(node.value.expression) &&
167
193
  !isRenderFunction(node.value.expression) &&
168
194
  !isReactComponentType(node.value.expression)) {
169
195
  const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
@@ -3,18 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dynamicHttpsErrors = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const utils_1 = require("@typescript-eslint/utils");
6
- const isHttpsErrorCall = (callee) => {
7
- if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
8
- return (callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
9
- callee.object.name === 'https' &&
10
- callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
11
- callee.property.name === 'HttpsError');
12
- }
13
- else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
14
- return callee.name === 'HttpsError';
15
- }
16
- return false;
17
- };
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
18
7
  const findPropertyByName = (properties, name) => properties.find((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
19
8
  !p.computed &&
20
9
  ((p.key.type === utils_1.AST_NODE_TYPES.Identifier && p.key.name === name) ||
@@ -38,23 +27,13 @@ exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
38
27
  },
39
28
  defaultOptions: [],
40
29
  create(context) {
41
- const unwrapTSAssertions = (node) => {
42
- let inner = node;
43
- while (inner.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
44
- inner.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
45
- inner.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
46
- inner.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
47
- inner = inner.expression;
48
- }
49
- return inner;
50
- };
51
30
  // Only string concatenation with "+" can be static; all other operators
52
31
  // are treated as dynamic to avoid hashing non-literal message content.
53
32
  const isDynamicBinaryExpression = (expression) => {
54
33
  if (expression.operator !== '+')
55
34
  return true;
56
35
  const isStaticLiteral = (expr) => {
57
- const inner = unwrapTSAssertions(expr);
36
+ const inner = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(expr);
58
37
  return (inner.type === utils_1.AST_NODE_TYPES.Literal &&
59
38
  typeof inner.value === 'string');
60
39
  };
@@ -62,7 +41,7 @@ exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
62
41
  if (expr.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
63
42
  return false;
64
43
  }
65
- const inner = unwrapTSAssertions(expr);
44
+ const inner = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(expr);
66
45
  if (inner.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
67
46
  return !isDynamicBinaryExpression(inner);
68
47
  }
@@ -102,7 +81,7 @@ exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
102
81
  * `shouldValidateForStaticness`.
103
82
  */
104
83
  const checkMessageIsStatic = (messageNode) => {
105
- const currentNode = unwrapTSAssertions(messageNode);
84
+ const currentNode = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(messageNode);
106
85
  if (currentNode.type === utils_1.AST_NODE_TYPES.Literal) {
107
86
  return;
108
87
  }
@@ -127,7 +106,7 @@ exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
127
106
  };
128
107
  const checkForHttpsError = (node) => {
129
108
  const callee = node.callee;
130
- if (!isHttpsErrorCall(callee))
109
+ if (!ASTHelpers_1.ASTHelpers.isHttpsErrorCall(callee))
131
110
  return;
132
111
  // Signature 1: Object-based constructor (HttpsErrorProps)
133
112
  if (node.arguments.length >= 1 &&
@@ -3,6 +3,7 @@ type Options = [
3
3
  {
4
4
  prefixes?: string[];
5
5
  ignoreOverriddenGetters?: boolean;
6
+ enforceForPropertySignatures?: boolean;
6
7
  }
7
8
  ];
8
9
  export declare const enforceBooleanNamingPrefixes: TSESLint.RuleModule<"missingBooleanPrefix", Options, TSESLint.RuleListener>;
@@ -33,6 +33,14 @@ const DEFAULT_BOOLEAN_PREFIXES = [
33
33
  const DEFAULT_OPTIONS = {
34
34
  prefixes: DEFAULT_BOOLEAN_PREFIXES,
35
35
  ignoreOverriddenGetters: false,
36
+ // Property names declared in interfaces and type aliases are frequently
37
+ // dictated by contracts the author cannot rename: external API request/response
38
+ // shapes, third-party library interfaces, and persisted data-model schemas
39
+ // (e.g. Firestore document fields). Flagging those produces unavoidable false
40
+ // positives, so property-signature enforcement is opt-in. Names the author does
41
+ // choose freshly — variables, function returns, class fields, parameters — stay
42
+ // enforced by default.
43
+ enforceForPropertySignatures: false,
36
44
  };
37
45
  const BOOLEANISH_BINARY_OPERATORS = new Set(['===', '!==', '==', '!=', '>', '<', '>=', '<=', 'in', 'instanceof']);
38
46
  exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
@@ -58,6 +66,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
58
66
  type: 'boolean',
59
67
  default: false,
60
68
  },
69
+ enforceForPropertySignatures: {
70
+ type: 'boolean',
71
+ default: false,
72
+ },
61
73
  },
62
74
  additionalProperties: false,
63
75
  },
@@ -71,8 +83,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
71
83
  defaultOptions: [DEFAULT_OPTIONS],
72
84
  create(context, [options]) {
73
85
  const approvedPrefixes = options.prefixes || DEFAULT_OPTIONS.prefixes;
86
+ const approvedPrefixesWithoutAsserts = approvedPrefixes.filter((p) => p !== 'asserts');
74
87
  const ignoreOverriddenGetters = options.ignoreOverriddenGetters ??
75
88
  DEFAULT_OPTIONS.ignoreOverriddenGetters;
89
+ const enforceForPropertySignatures = options.enforceForPropertySignatures ??
90
+ DEFAULT_OPTIONS.enforceForPropertySignatures;
76
91
  function findVariableInScopes(name) {
77
92
  let currentScope = context.getScope();
78
93
  while (currentScope) {
@@ -83,32 +98,58 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
83
98
  }
84
99
  return undefined;
85
100
  }
101
+ /**
102
+ * Check if a name is prefixed by a boolean keyword with proper boundaries.
103
+ * Supports camelCase (isSomething), snake_case (is_something, IS_SOMETHING),
104
+ * and exact matches.
105
+ */
106
+ function isPrefixedByBooleanKeyword(name, prefixes) {
107
+ const normalizedName = name.startsWith('_') ? name.slice(1) : name;
108
+ const checkPrefix = (p) => {
109
+ if (!normalizedName.toLowerCase().startsWith(p.toLowerCase())) {
110
+ return false;
111
+ }
112
+ if (normalizedName.length === p.length) {
113
+ return true;
114
+ }
115
+ const nextChar = normalizedName.charAt(p.length);
116
+ // For SCREAMING_SNAKE_CASE or similar all-uppercase names,
117
+ // we require an underscore boundary.
118
+ const isAllUppercase = normalizedName === normalizedName.toUpperCase() &&
119
+ /[a-z]/i.test(normalizedName);
120
+ if (isAllUppercase) {
121
+ return nextChar === '_';
122
+ }
123
+ // For camelCase, the next char must be uppercase, a digit, or $
124
+ return (nextChar === '_' ||
125
+ nextChar === '$' ||
126
+ (nextChar >= '0' && nextChar <= '9') ||
127
+ (nextChar === nextChar.toUpperCase() &&
128
+ nextChar !== nextChar.toLowerCase()));
129
+ };
130
+ const checkPrefixWithPlural = (p) => {
131
+ if (checkPrefix(p))
132
+ return true;
133
+ if (['is', 'has', 'does', 'was', 'had', 'did'].includes(p)) {
134
+ const pluralPrefix = pluralize_1.default.plural(p);
135
+ if (checkPrefix(pluralPrefix))
136
+ return true;
137
+ }
138
+ return false;
139
+ };
140
+ return prefixes.some((prefix) => checkPrefixWithPlural(prefix));
141
+ }
86
142
  /**
87
143
  * Check if a name starts with any of the approved prefixes, their plural forms,
88
144
  * or if it starts with an underscore (which indicates a private/internal property)
89
145
  */
90
146
  function hasApprovedPrefix(name, options) {
91
147
  const treatLeadingUnderscoreAsApproved = options?.treatLeadingUnderscoreAsApproved ?? true;
92
- const normalizedName = name.startsWith('_') ? name.slice(1) : name;
93
148
  // Skip checking properties that start with an underscore (private/internal properties)
94
149
  if (treatLeadingUnderscoreAsApproved && name.startsWith('_')) {
95
150
  return true;
96
151
  }
97
- return approvedPrefixes.some((prefix) => {
98
- // Check for exact prefix match
99
- if (normalizedName.toLowerCase().startsWith(prefix.toLowerCase())) {
100
- return true;
101
- }
102
- // Check for plural form of the prefix
103
- // Only apply pluralization to certain prefixes that have meaningful plural forms
104
- if (['is', 'has', 'does', 'was', 'had', 'did'].includes(prefix)) {
105
- const pluralPrefix = pluralize_1.default.plural(prefix);
106
- return normalizedName
107
- .toLowerCase()
108
- .startsWith(pluralPrefix.toLowerCase());
109
- }
110
- return false;
111
- });
152
+ return isPrefixedByBooleanKeyword(name, approvedPrefixes);
112
153
  }
113
154
  function nameSuggestsBoolean(name) {
114
155
  const normalizedName = name.startsWith('_') ? name.slice(1) : name;
@@ -125,9 +166,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
125
166
  'authenticated',
126
167
  'authorized',
127
168
  ];
128
- return (hasApprovedPrefix(normalizedName, {
129
- treatLeadingUnderscoreAsApproved: false,
130
- }) || suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
169
+ return (isPrefixedByBooleanKeyword(normalizedName, approvedPrefixes) ||
170
+ suffixKeywords.some((keyword) => lowerName.endsWith(keyword)));
131
171
  }
132
172
  /**
133
173
  * Capitalize the first letter of a name for use in suggested alternatives
@@ -205,94 +245,20 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
205
245
  // Check for logical expressions (&&)
206
246
  if (node.init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
207
247
  node.init.operator === '&&') {
208
- // Check if the right side is a method call that might return a non-boolean value
209
- const rightSide = node.init.right;
210
- if (rightSide.type === utils_1.AST_NODE_TYPES.CallExpression) {
211
- // If right side is a simple identifier call
212
- if (rightSide.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
213
- const calleeName = rightSide.callee.name;
214
- const lowerCallee = calleeName.toLowerCase();
215
- // For assert*-style utilities, only treat as boolean if we can confirm boolean return type
216
- if (lowerCallee.startsWith('assert')) {
217
- return identifierReturnsBoolean(calleeName);
218
- }
219
- // Otherwise, infer based on naming heuristics
220
- const isBooleanCall = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
221
- lowerCallee.startsWith(prefix.toLowerCase()));
222
- if (isBooleanCall ||
223
- lowerCallee.includes('boolean') ||
224
- lowerCallee.includes('enabled') ||
225
- lowerCallee.includes('auth') ||
226
- lowerCallee.includes('valid') ||
227
- lowerCallee.includes('check')) {
228
- return true;
229
- }
230
- if (lowerCallee.startsWith('get') ||
231
- lowerCallee.startsWith('fetch') ||
232
- lowerCallee.startsWith('retrieve') ||
233
- lowerCallee.startsWith('load') ||
234
- lowerCallee.startsWith('read')) {
235
- return false;
236
- }
237
- }
238
- // If the method name doesn't suggest it returns a boolean, don't flag it
239
- if (rightSide.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
240
- rightSide.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
241
- const methodName = rightSide.callee.property.name;
242
- const lowerMethodName = methodName.toLowerCase();
243
- // Ignore assert*-style methods which often return the input value
244
- if (lowerMethodName.startsWith('assert')) {
245
- return false;
246
- }
247
- // Check if the method name suggests it returns a boolean
248
- const isBooleanMethod = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
249
- lowerMethodName.startsWith(prefix.toLowerCase()));
250
- // If the method name suggests it returns a boolean (starts with a boolean prefix or contains 'boolean' or 'enabled'),
251
- // then the variable should be treated as a boolean
252
- if (isBooleanMethod ||
253
- lowerMethodName.includes('boolean') ||
254
- lowerMethodName.includes('enabled') ||
255
- lowerMethodName.includes('auth') ||
256
- lowerMethodName.includes('valid') ||
257
- lowerMethodName.includes('check')) {
258
- return true;
259
- }
260
- // For methods like getVolume(), getData(), etc., assume they return non-boolean values
261
- if (lowerMethodName.startsWith('get') ||
262
- lowerMethodName.startsWith('fetch') ||
263
- lowerMethodName.startsWith('retrieve') ||
264
- lowerMethodName.startsWith('load') ||
265
- lowerMethodName.startsWith('read')) {
266
- return false;
267
- }
268
- }
248
+ const left = evaluateBooleanishExpression(node.init.left);
249
+ const right = evaluateBooleanishExpression(node.init.right);
250
+ // If both sides are boolean, the result is boolean.
251
+ if (left === 'boolean' && right === 'boolean') {
252
+ return true;
269
253
  }
270
- // Check if the right side is a property access that might return a non-boolean value
271
- if (rightSide.type === utils_1.AST_NODE_TYPES.MemberExpression &&
272
- rightSide.property.type === utils_1.AST_NODE_TYPES.Identifier) {
273
- const propertyName = rightSide.property.name;
274
- const lowerPropertyName = propertyName.toLowerCase();
275
- // If the property name is 'parentElement', 'parentNode', etc., it's likely not a boolean
276
- if (lowerPropertyName.includes('parent') ||
277
- lowerPropertyName.includes('element') ||
278
- lowerPropertyName.includes('node') ||
279
- lowerPropertyName.includes('child') ||
280
- lowerPropertyName.includes('sibling')) {
281
- return false;
282
- }
283
- // Ignore assert*-style properties which often return the input value
284
- if (lowerPropertyName.startsWith('assert')) {
285
- return false;
286
- }
287
- // For property access like user.isAuthenticated, treat as boolean
288
- const isBooleanProperty = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
289
- lowerPropertyName.startsWith(prefix.toLowerCase()));
290
- if (isBooleanProperty) {
291
- return true;
292
- }
254
+ // If the right side is boolean, and the left side is unknown (but not non-boolean),
255
+ // we treat it as boolean to avoid false negatives for common patterns like `user && user.isActive`.
256
+ // This is a trade-off: it might cause some false positives for non-boolean variables
257
+ // used as guards, but those are less common than the `user && user.isActive` pattern.
258
+ if (right === 'boolean' && left === 'unknown') {
259
+ return true;
293
260
  }
294
- // Default to true for other cases with && to avoid false negatives
295
- return true;
261
+ return false;
296
262
  }
297
263
  // Special case for logical OR (||) - only consider it boolean if:
298
264
  // 1. It's used with boolean literals or
@@ -330,8 +296,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
330
296
  if (lowerCallee.startsWith('assert')) {
331
297
  return identifierReturnsBoolean(calleeName);
332
298
  }
333
- return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
334
- lowerCallee.startsWith(prefix.toLowerCase()));
299
+ return isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
335
300
  }
336
301
  // Default to false for other cases with || to avoid false positives
337
302
  return false;
@@ -351,8 +316,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
351
316
  return identifierReturnsBoolean(calleeName);
352
317
  }
353
318
  // Check if the function name suggests it returns a boolean
354
- return approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
355
- lowerCallee.startsWith(prefix.toLowerCase()));
319
+ return isPrefixedByBooleanKeyword(calleeName, approvedPrefixes.filter((p) => p !== 'asserts'));
356
320
  }
357
321
  }
358
322
  return false;
@@ -435,8 +399,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
435
399
  if (lowerCallee.startsWith('assert')) {
436
400
  return identifierReturnsBoolean(calleeName) ? 'boolean' : 'unknown';
437
401
  }
438
- const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
439
- lowerCallee.startsWith(prefix.toLowerCase()));
402
+ const matchesPrefix = isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
440
403
  if (matchesPrefix ||
441
404
  lowerCallee.includes('boolean') ||
442
405
  lowerCallee.includes('enabled') ||
@@ -460,8 +423,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
460
423
  if (lowerMethodName.startsWith('assert')) {
461
424
  return 'unknown';
462
425
  }
463
- const matchesPrefix = approvedPrefixes.some((prefix) => prefix !== 'asserts' &&
464
- lowerMethodName.startsWith(prefix.toLowerCase()));
426
+ const matchesPrefix = isPrefixedByBooleanKeyword(methodName, approvedPrefixesWithoutAsserts);
465
427
  if (matchesPrefix ||
466
428
  lowerMethodName.includes('boolean') ||
467
429
  lowerMethodName.includes('enabled') ||
@@ -475,7 +437,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
475
437
  }
476
438
  function evaluateBooleanishExpression(expression) {
477
439
  if (!expression)
478
- return 'nonBoolean';
440
+ return 'unknown';
479
441
  let currentExpression = expression;
480
442
  if (currentExpression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
481
443
  currentExpression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
@@ -825,10 +787,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
825
787
  });
826
788
  }
827
789
  /**
828
- * Check if a variable is used in a while loop condition and is likely a boolean
790
+ * Check if a variable is initialized with a boolean-suggesting member expression
829
791
  * This helps identify variables that should be flagged as needing a boolean prefix
830
792
  */
831
- function isLikelyBooleanInWhileLoop(node) {
793
+ function isLikelyBooleanByMemberExpression(node) {
832
794
  // Check if the variable is initialized with a boolean-related value
833
795
  const variableDeclarator = node.parent;
834
796
  if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
@@ -844,7 +806,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
844
806
  init.property.type === utils_1.AST_NODE_TYPES.Identifier) {
845
807
  const propertyName = init.property.name;
846
808
  // If the property name suggests it's a boolean (starts with a boolean prefix)
847
- const isBooleanProperty = approvedPrefixes.some((prefix) => propertyName.toLowerCase().startsWith(prefix.toLowerCase()));
809
+ const isBooleanProperty = isPrefixedByBooleanKeyword(propertyName, approvedPrefixes);
848
810
  if (isBooleanProperty) {
849
811
  return true;
850
812
  }
@@ -895,8 +857,8 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
895
857
  return;
896
858
  // Check if it's a boolean variable
897
859
  let isBooleanVar = hasBooleanTypeAnnotation(node.id) || hasInitialBooleanValue(node);
898
- // Check if it's a boolean variable used in a while loop
899
- if (!isBooleanVar && isLikelyBooleanInWhileLoop(node.id)) {
860
+ // Check if it's a boolean variable used in a while loop or initialized with a boolean property
861
+ if (!isBooleanVar && isLikelyBooleanByMemberExpression(node.id)) {
900
862
  isBooleanVar = true;
901
863
  }
902
864
  // Check if it's an arrow function with boolean return type
@@ -1074,6 +1036,11 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
1074
1036
  * Check property signatures in interfaces/types for boolean types
1075
1037
  */
1076
1038
  function checkPropertySignature(node) {
1039
+ // Interface/type-alias property names are commonly imposed by contracts the
1040
+ // author cannot rename (external APIs, data-model schemas), so they are only
1041
+ // enforced when explicitly opted in.
1042
+ if (!enforceForPropertySignatures)
1043
+ return;
1077
1044
  if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
1078
1045
  return;
1079
1046
  const propertyName = node.key.name;
@@ -1,9 +1,10 @@
1
1
  export declare const RULE_NAME = "enforce-dynamic-imports";
2
2
  type Options = [
3
3
  {
4
- libraries: string[];
4
+ ignoredLibraries?: string[];
5
5
  allowImportType?: boolean;
6
6
  }
7
7
  ];
8
+ export declare const DEFAULT_IGNORED_LIBRARIES: string[];
8
9
  declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
9
10
  export default _default;