@blumintinc/eslint-plugin-blumint 1.13.0 → 1.14.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.
- package/README.md +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +1 -1
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +2 -2
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +5 -4
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.js +9 -3
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -1
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +67 -12
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
- package/lib/rules/no-passthrough-getters.js +38 -2
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.js +6 -4
- package/lib/rules/parallelize-async-operations.js +24 -5
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.js +88 -13
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +1 -1
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -13,6 +13,7 @@ const PRIMITIVE_TYPES = new Set([
|
|
|
13
13
|
'undefined',
|
|
14
14
|
'GeoPoint',
|
|
15
15
|
]);
|
|
16
|
+
const UNKNOWN_FIELD_LABEL = '<unknown field>';
|
|
16
17
|
const isInFirestoreTypesDirectory = (filename) => {
|
|
17
18
|
if (!filename || filename === '<input>')
|
|
18
19
|
return false;
|
|
@@ -74,6 +75,56 @@ const unwrapArrayElementType = (node) => {
|
|
|
74
75
|
}
|
|
75
76
|
return current;
|
|
76
77
|
};
|
|
78
|
+
const literalToString = (node) => {
|
|
79
|
+
if (!node || node.type !== utils_1.AST_NODE_TYPES.Literal)
|
|
80
|
+
return null;
|
|
81
|
+
const value = node.value;
|
|
82
|
+
if (typeof value === 'string' ||
|
|
83
|
+
typeof value === 'number' ||
|
|
84
|
+
typeof value === 'bigint') {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Derives a field label for diagnostics by walking ancestors.
|
|
91
|
+
* Prefers TSPropertySignature keys and returns identifier or string/number/bigint literal keys when present.
|
|
92
|
+
* Falls back to the surrounding TSTypeAliasDeclaration or TSInterfaceDeclaration name when no property key is available.
|
|
93
|
+
* This ensures declaration-level array aliases (for example, `type Foo = Object[]`) surface a meaningful label and avoid unlabeled diagnostics.
|
|
94
|
+
* Returns a placeholder when resolution fails so downstream messaging remains readable.
|
|
95
|
+
*/
|
|
96
|
+
const getFieldName = (node) => {
|
|
97
|
+
let current = node;
|
|
98
|
+
while (current) {
|
|
99
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
100
|
+
const key = current.key;
|
|
101
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
102
|
+
return key.name;
|
|
103
|
+
const literalKey = literalToString(key);
|
|
104
|
+
if (literalKey !== null)
|
|
105
|
+
return literalKey;
|
|
106
|
+
const computedKey = key;
|
|
107
|
+
if (computedKey.type === 'TSComputedPropertyName' &&
|
|
108
|
+
computedKey.expression) {
|
|
109
|
+
const expr = computedKey.expression;
|
|
110
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
111
|
+
return expr.name;
|
|
112
|
+
const literalExpr = literalToString(expr);
|
|
113
|
+
if (literalExpr !== null)
|
|
114
|
+
return literalExpr;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
118
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
119
|
+
const id = current.id;
|
|
120
|
+
if (id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
121
|
+
return id.name;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
current = current.parent;
|
|
125
|
+
}
|
|
126
|
+
return UNKNOWN_FIELD_LABEL;
|
|
127
|
+
};
|
|
77
128
|
// Determine whether this type node appears in a Firestore model type context
|
|
78
129
|
// i.e., within an interface or type alias declaration, and not within variable/function annotations
|
|
79
130
|
const isInModelTypeContext = (node) => {
|
|
@@ -110,15 +161,14 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
110
161
|
},
|
|
111
162
|
schema: [],
|
|
112
163
|
messages: {
|
|
113
|
-
noObjectArrays:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
- Arrays of IDs where appropriate`,
|
|
164
|
+
noObjectArrays: [
|
|
165
|
+
// What's wrong
|
|
166
|
+
"What's wrong: {{fieldName}} stores an array of objects in a Firestore document.",
|
|
167
|
+
// Why it matters
|
|
168
|
+
'Why it matters: Firestore cannot query inside object arrays, and updating one item rewrites the whole array; concurrent writes can overwrite each other and lose data.',
|
|
169
|
+
// How to fix
|
|
170
|
+
'How to fix: Store items as Record<string, T> keyed by id with an index field for ordering (use toMap/toArr helpers), or move items into a subcollection or store only an array of IDs.',
|
|
171
|
+
].join('\n'),
|
|
122
172
|
},
|
|
123
173
|
},
|
|
124
174
|
defaultOptions: [],
|
|
@@ -126,7 +176,7 @@ Prefer:
|
|
|
126
176
|
if (!isInFirestoreTypesDirectory(context.getFilename())) {
|
|
127
177
|
return {};
|
|
128
178
|
}
|
|
129
|
-
const sourceCode = context.
|
|
179
|
+
const sourceCode = context.sourceCode;
|
|
130
180
|
// Collect alias/interface/enum information within this file to refine object vs primitive classification
|
|
131
181
|
const aliasNameToType = new Map();
|
|
132
182
|
const interfaceNames = new Set();
|
|
@@ -331,8 +381,7 @@ Prefer:
|
|
|
331
381
|
while (currentParent) {
|
|
332
382
|
if (currentParent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
333
383
|
currentParent.operator === 'readonly') {
|
|
334
|
-
currentParent = currentParent
|
|
335
|
-
.parent;
|
|
384
|
+
currentParent = currentParent.parent;
|
|
336
385
|
continue;
|
|
337
386
|
}
|
|
338
387
|
if (isParenthesizedType(currentParent)) {
|
|
@@ -369,6 +418,9 @@ Prefer:
|
|
|
369
418
|
context.report({
|
|
370
419
|
node,
|
|
371
420
|
messageId: 'noObjectArrays',
|
|
421
|
+
data: {
|
|
422
|
+
fieldName: getFieldName(node),
|
|
423
|
+
},
|
|
372
424
|
});
|
|
373
425
|
}
|
|
374
426
|
},
|
|
@@ -392,6 +444,9 @@ Prefer:
|
|
|
392
444
|
context.report({
|
|
393
445
|
node,
|
|
394
446
|
messageId: 'noObjectArrays',
|
|
447
|
+
data: {
|
|
448
|
+
fieldName: getFieldName(node),
|
|
449
|
+
},
|
|
395
450
|
});
|
|
396
451
|
}
|
|
397
452
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
ignoreClassMethods?: boolean;
|
|
4
|
+
ignoreInterfaceImplementations?: boolean;
|
|
5
|
+
interfaceAllowlist?: string[];
|
|
6
|
+
allowNames?: string[];
|
|
7
|
+
allowPatterns?: string[];
|
|
8
|
+
allowFilePatterns?: string[];
|
|
9
|
+
}
|
|
10
|
+
];
|
|
11
|
+
export declare const noHandlerSuffix: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"handlerSuffix", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noHandlerSuffix = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const DEFAULT_OPTIONS = {
|
|
8
|
+
ignoreClassMethods: false,
|
|
9
|
+
ignoreInterfaceImplementations: false,
|
|
10
|
+
interfaceAllowlist: [],
|
|
11
|
+
allowNames: [],
|
|
12
|
+
allowPatterns: [],
|
|
13
|
+
allowFilePatterns: [],
|
|
14
|
+
};
|
|
15
|
+
function toEntityName(entity) {
|
|
16
|
+
if (entity.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
17
|
+
return entity.name;
|
|
18
|
+
}
|
|
19
|
+
if (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
20
|
+
return `${toEntityName(entity.left)}.${entity.right.name}`;
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
function isUnsafeAllowPattern(pattern) {
|
|
25
|
+
// Flag nested quantifiers that commonly lead to catastrophic backtracking
|
|
26
|
+
const nestedQuantifierPattern = /\((?:[^()\\]|\\.)*[+*{][^)]*\)\s*[+*{]/;
|
|
27
|
+
return nestedQuantifierPattern.test(pattern);
|
|
28
|
+
}
|
|
29
|
+
function getStaticKeyName(key) {
|
|
30
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
31
|
+
return key.name;
|
|
32
|
+
}
|
|
33
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
|
|
34
|
+
return key.value;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function getHandlerSuffix(name) {
|
|
39
|
+
const match = name.match(/^(.*?)(Handlers?)$/i);
|
|
40
|
+
if (!match)
|
|
41
|
+
return null;
|
|
42
|
+
const [, baseName, suffix] = match;
|
|
43
|
+
if (!suffix)
|
|
44
|
+
return null;
|
|
45
|
+
const normalizedSuffix = suffix.toLowerCase() === 'handlers' ? 'Handlers' : 'Handler';
|
|
46
|
+
return {
|
|
47
|
+
baseName,
|
|
48
|
+
suffix: normalizedSuffix,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function isInAllowedFile(filename, allowFilePatterns) {
|
|
52
|
+
return allowFilePatterns.some((pattern) => (0, minimatch_1.minimatch)(filename, pattern));
|
|
53
|
+
}
|
|
54
|
+
function toMemberExpressionName(expr) {
|
|
55
|
+
if (expr.computed || expr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const objectName = expr.object.type === utils_1.AST_NODE_TYPES.Identifier
|
|
59
|
+
? expr.object.name
|
|
60
|
+
: expr.object.type === utils_1.AST_NODE_TYPES.MemberExpression
|
|
61
|
+
? toMemberExpressionName(expr.object)
|
|
62
|
+
: null;
|
|
63
|
+
return objectName ? `${objectName}.${expr.property.name}` : null;
|
|
64
|
+
}
|
|
65
|
+
function getImplementedInterfaces(classNode) {
|
|
66
|
+
return (classNode.implements?.flatMap((impl) => {
|
|
67
|
+
const expr = impl.expression;
|
|
68
|
+
const name = expr.type === utils_1.AST_NODE_TYPES.Identifier
|
|
69
|
+
? expr.name
|
|
70
|
+
: expr.type === utils_1.AST_NODE_TYPES.MemberExpression
|
|
71
|
+
? toMemberExpressionName(expr)
|
|
72
|
+
: null;
|
|
73
|
+
return name ? [name] : [];
|
|
74
|
+
}) ?? []);
|
|
75
|
+
}
|
|
76
|
+
function getClassFromMember(node) {
|
|
77
|
+
const classBody = node.parent;
|
|
78
|
+
if (classBody &&
|
|
79
|
+
classBody.type === utils_1.AST_NODE_TYPES.ClassBody &&
|
|
80
|
+
classBody.parent &&
|
|
81
|
+
(classBody.parent.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
82
|
+
classBody.parent.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
|
|
83
|
+
return classBody.parent;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function getTypeAnnotationName(annotation) {
|
|
88
|
+
if (!annotation)
|
|
89
|
+
return null;
|
|
90
|
+
if (annotation.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
91
|
+
const typeName = annotation.typeAnnotation.typeName;
|
|
92
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
93
|
+
typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
94
|
+
return toEntityName(typeName);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
exports.noHandlerSuffix = (0, createRule_1.createRule)({
|
|
100
|
+
name: 'no-handler-suffix',
|
|
101
|
+
meta: {
|
|
102
|
+
type: 'suggestion',
|
|
103
|
+
docs: {
|
|
104
|
+
description: 'Disallow the generic "handler" suffix in callback names so names explain the action they perform',
|
|
105
|
+
recommended: 'error',
|
|
106
|
+
},
|
|
107
|
+
schema: [
|
|
108
|
+
{
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
ignoreClassMethods: { type: 'boolean' },
|
|
112
|
+
ignoreInterfaceImplementations: { type: 'boolean' },
|
|
113
|
+
interfaceAllowlist: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
allowNames: { type: 'array', items: { type: 'string' } },
|
|
118
|
+
allowPatterns: { type: 'array', items: { type: 'string' } },
|
|
119
|
+
allowFilePatterns: { type: 'array', items: { type: 'string' } },
|
|
120
|
+
},
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
messages: {
|
|
125
|
+
handlerSuffix: 'Function "{{name}}" ends with the generic {{suffix}} suffix. Handler names hide the outcome of the callback and make call sites indistinguishable. Rename it to describe the effect (e.g., "{{suggestedName}}" or another action phrase) so readers know what the function actually does.',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
129
|
+
create(context, [options]) {
|
|
130
|
+
const filename = context.getFilename();
|
|
131
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...(options ?? {}) };
|
|
132
|
+
const allowNames = new Set(resolvedOptions.allowNames);
|
|
133
|
+
const interfaceAllowlist = new Set(resolvedOptions.interfaceAllowlist);
|
|
134
|
+
const invalidAllowPatterns = [];
|
|
135
|
+
const unsafeAllowPatterns = [];
|
|
136
|
+
const allowPatterns = (resolvedOptions.allowPatterns ?? []).flatMap((pattern) => {
|
|
137
|
+
try {
|
|
138
|
+
if (isUnsafeAllowPattern(pattern)) {
|
|
139
|
+
unsafeAllowPatterns.push(pattern);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
return [new RegExp(pattern)];
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const reason = error && typeof error === 'object' && 'message' in error
|
|
146
|
+
? ` (${String(error.message)})`
|
|
147
|
+
: '';
|
|
148
|
+
invalidAllowPatterns.push(`${pattern}${reason}`);
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (invalidAllowPatterns.length > 0 || unsafeAllowPatterns.length > 0) {
|
|
153
|
+
const errorParts = [];
|
|
154
|
+
if (invalidAllowPatterns.length > 0) {
|
|
155
|
+
errorParts.push(`invalid allowPatterns: ${invalidAllowPatterns.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
if (unsafeAllowPatterns.length > 0) {
|
|
158
|
+
errorParts.push(`unsafe allowPatterns (avoid nested quantifiers that risk catastrophic backtracking): ${unsafeAllowPatterns.join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`no-handler-suffix: ${errorParts.join('; ')}`);
|
|
161
|
+
}
|
|
162
|
+
if (isInAllowedFile(filename, resolvedOptions.allowFilePatterns ?? [])) {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
function isAllowedName(name) {
|
|
166
|
+
if (allowNames.has(name))
|
|
167
|
+
return true;
|
|
168
|
+
return allowPatterns.some((regex) => regex.test(name));
|
|
169
|
+
}
|
|
170
|
+
function shouldIgnoreForInterfaces(node) {
|
|
171
|
+
const { ignoreInterfaceImplementations } = resolvedOptions;
|
|
172
|
+
if ((node.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
173
|
+
node.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
174
|
+
!node.computed &&
|
|
175
|
+
(node.key.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
176
|
+
node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier ||
|
|
177
|
+
(node.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
178
|
+
typeof node.key.value === 'string'))) {
|
|
179
|
+
const classNode = getClassFromMember(node);
|
|
180
|
+
if (!classNode)
|
|
181
|
+
return false;
|
|
182
|
+
const implemented = getImplementedInterfaces(classNode);
|
|
183
|
+
if (implemented.length === 0)
|
|
184
|
+
return false;
|
|
185
|
+
if (ignoreInterfaceImplementations) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (interfaceAllowlist.size > 0) {
|
|
189
|
+
return implemented.some((iface) => interfaceAllowlist.has(iface));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
193
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
194
|
+
const annotationName = getTypeAnnotationName(node.id.typeAnnotation);
|
|
195
|
+
if (annotationName &&
|
|
196
|
+
(interfaceAllowlist.has(annotationName) ||
|
|
197
|
+
resolvedOptions.ignoreInterfaceImplementations)) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
function getParentName(parent) {
|
|
204
|
+
if (!parent)
|
|
205
|
+
return null;
|
|
206
|
+
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
207
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
208
|
+
return parent.id.name;
|
|
209
|
+
}
|
|
210
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
211
|
+
parent.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
212
|
+
parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
213
|
+
!parent.computed &&
|
|
214
|
+
parent.key.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
215
|
+
return getStaticKeyName(parent.key);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function reportIfHandlerName(name, identifierNode, owner) {
|
|
220
|
+
if (!name || !identifierNode)
|
|
221
|
+
return;
|
|
222
|
+
const suffixInfo = getHandlerSuffix(name);
|
|
223
|
+
if (!suffixInfo)
|
|
224
|
+
return;
|
|
225
|
+
if (isAllowedName(name))
|
|
226
|
+
return;
|
|
227
|
+
if ((owner.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
228
|
+
owner.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
229
|
+
resolvedOptions.ignoreClassMethods) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (owner.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
233
|
+
owner.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
234
|
+
owner.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
235
|
+
owner.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
236
|
+
if (shouldIgnoreForInterfaces(owner)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const suggestedName = suffixInfo.baseName || 'a verb phrase that states the outcome';
|
|
241
|
+
context.report({
|
|
242
|
+
node: identifierNode,
|
|
243
|
+
messageId: 'handlerSuffix',
|
|
244
|
+
data: {
|
|
245
|
+
name,
|
|
246
|
+
suffix: suffixInfo.suffix,
|
|
247
|
+
suggestedName,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
FunctionDeclaration(node) {
|
|
253
|
+
if (node.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
254
|
+
reportIfHandlerName(node.id.name, node.id, node);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
VariableDeclarator(node) {
|
|
258
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
259
|
+
(node.init?.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
260
|
+
node.init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
261
|
+
reportIfHandlerName(node.id.name, node.id, node);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
MethodDefinition(node) {
|
|
265
|
+
if (!node.computed) {
|
|
266
|
+
const name = getStaticKeyName(node.key);
|
|
267
|
+
if (name) {
|
|
268
|
+
reportIfHandlerName(name, node.key, node);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
PropertyDefinition(node) {
|
|
273
|
+
if (!node.computed &&
|
|
274
|
+
(node.value?.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
275
|
+
node.value?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
276
|
+
const name = getStaticKeyName(node.key);
|
|
277
|
+
if (name) {
|
|
278
|
+
reportIfHandlerName(name, node.key, node);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
Property(node) {
|
|
283
|
+
if (!node.computed &&
|
|
284
|
+
(node.value.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
285
|
+
node.value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
286
|
+
const name = getStaticKeyName(node.key);
|
|
287
|
+
if (name) {
|
|
288
|
+
reportIfHandlerName(name, node.key, node);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
FunctionExpression(node) {
|
|
293
|
+
if (!node.id)
|
|
294
|
+
return;
|
|
295
|
+
// Avoid double-reporting when the parent already names the function
|
|
296
|
+
const parentName = getParentName(node.parent);
|
|
297
|
+
if (parentName && parentName === node.id.name) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
reportIfHandlerName(node.id.name, node.id, node);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
//# sourceMappingURL=no-handler-suffix.js.map
|
|
@@ -201,7 +201,7 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
201
201
|
fixable: undefined,
|
|
202
202
|
schema: [],
|
|
203
203
|
messages: {
|
|
204
|
-
noHungarian: '
|
|
204
|
+
noHungarian: 'Identifier "{{name}}" encodes its type through a prefix or suffix (Hungarian notation). Type-coded names hide the domain concept and become misleading when the underlying type changes. Rename it to a domain-focused name without the type marker and rely on TypeScript for type information.',
|
|
205
205
|
},
|
|
206
206
|
},
|
|
207
207
|
defaultOptions: [],
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
props?: string[];
|
|
5
|
+
allowRenderProps?: boolean;
|
|
6
|
+
allowModuleScopeFactories?: boolean;
|
|
7
|
+
}
|
|
8
|
+
];
|
|
9
|
+
export declare const noInlineComponentProp: TSESLint.RuleModule<"inlineComponentProp", Options, TSESLint.RuleListener>;
|
|
10
|
+
export {};
|