@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- 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-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- 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-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- 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-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- 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 +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- 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 +44 -18
- 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 +3818 -4644
- 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 +66 -23
- 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 +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -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 +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- 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 +527 -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 +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -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 +465 -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.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -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 +508 -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 +344 -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 +143 -59
- 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 +118 -8
- 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 +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- 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-usememo-over-useeffect-usestate.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 +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -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 +155 -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 +18 -10
- 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 +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- 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
|
@@ -15,23 +15,31 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
15
15
|
meta: {
|
|
16
16
|
type: 'suggestion',
|
|
17
17
|
docs: {
|
|
18
|
-
description: '
|
|
18
|
+
description: 'Enforces a top-to-bottom class layout so callers lead into the helpers they rely on.',
|
|
19
19
|
recommended: 'warn',
|
|
20
20
|
},
|
|
21
21
|
schema: [],
|
|
22
22
|
messages: {
|
|
23
|
-
classMethodsReadTopToBottom:
|
|
23
|
+
classMethodsReadTopToBottom: [
|
|
24
|
+
"What's wrong: In {{className}}, {{actualMember}} appears before {{expectedMember}}.",
|
|
25
|
+
'Why it matters: Top-down flow enables local reasoning: you can verify each caller without scrolling back. Upward jumps make code reviews harder (must verify call chains in reverse), obscure which fields a helper assumes are initialized, and increase the risk of calling helpers before state is ready (leading to null reference errors or accessing uninitialized fields).',
|
|
26
|
+
'How to fix: Move {{expectedMember}} above {{actualMember}} so the class reads top-to-bottom (fields to constructor to callers to helpers).',
|
|
27
|
+
].join('\n'),
|
|
24
28
|
},
|
|
25
29
|
fixable: 'code', // To allow ESLint to autofix issues.
|
|
26
30
|
},
|
|
27
31
|
defaultOptions: [],
|
|
28
32
|
create(context) {
|
|
29
|
-
|
|
33
|
+
const classNames = new WeakMap();
|
|
30
34
|
return {
|
|
31
35
|
ClassDeclaration(node) {
|
|
32
|
-
|
|
36
|
+
classNames.set(node.body, node.id?.name || '');
|
|
37
|
+
},
|
|
38
|
+
ClassExpression(node) {
|
|
39
|
+
classNames.set(node.body, node.id?.name || '');
|
|
33
40
|
},
|
|
34
41
|
'ClassBody:exit'(node) {
|
|
42
|
+
const className = classNames.get(node) || '';
|
|
35
43
|
const graphBuilder = new ClassGraphBuilder_1.ClassGraphBuilder(className, node);
|
|
36
44
|
const sortedOrder = graphBuilder.memberNamesSorted;
|
|
37
45
|
const actualOrder = node.body
|
|
@@ -51,7 +59,13 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
51
59
|
return; // Skip if there are actual duplicates
|
|
52
60
|
}
|
|
53
61
|
for (let i = 0; i < actualOrder.length; i++) {
|
|
54
|
-
|
|
62
|
+
const actualMember = actualOrder[i];
|
|
63
|
+
const expectedMember = sortedOrder[i];
|
|
64
|
+
if (!actualMember || !expectedMember) {
|
|
65
|
+
throw new Error(`class-methods-read-top-to-bottom invariant violated while comparing members in ${className || 'an unnamed class'} at position ${i}: actualMember=${String(actualMember)}, expectedMember=${String(expectedMember)}, actualOrder.length=${actualOrder.length}, sortedOrder.length=${sortedOrder.length}`);
|
|
66
|
+
}
|
|
67
|
+
if (actualMember !== expectedMember) {
|
|
68
|
+
const classNameReport = className || 'this class';
|
|
55
69
|
const sourceCode = context.getSourceCode();
|
|
56
70
|
const newClassBody = sortedOrder
|
|
57
71
|
.map((n) => {
|
|
@@ -75,6 +89,11 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
75
89
|
return context.report({
|
|
76
90
|
node,
|
|
77
91
|
messageId: 'classMethodsReadTopToBottom',
|
|
92
|
+
data: {
|
|
93
|
+
className: classNameReport,
|
|
94
|
+
actualMember,
|
|
95
|
+
expectedMember,
|
|
96
|
+
},
|
|
78
97
|
fix(fixer) {
|
|
79
98
|
return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], // Exclude the curly braces
|
|
80
99
|
newClassBody);
|
|
@@ -37,8 +37,12 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
37
37
|
fixable: 'code',
|
|
38
38
|
schema: [],
|
|
39
39
|
messages: {
|
|
40
|
-
callbackPropPrefix: 'Callback
|
|
41
|
-
|
|
40
|
+
callbackPropPrefix: 'Callback prop "{{propName}}" is a function but lacks the "on" prefix. ' +
|
|
41
|
+
'Consistent "on" prefixes signal event handlers to consumers and distinguish callbacks from data props. ' +
|
|
42
|
+
'Rename to "on{{eventName}}".',
|
|
43
|
+
callbackFunctionPrefix: 'Function "{{functionName}}" uses the "handle" prefix. ' +
|
|
44
|
+
'The "handle" prefix is redundant and less descriptive than action-oriented verb phrases. ' +
|
|
45
|
+
'Rename using a descriptive verb (e.g., click instead of handleClick).',
|
|
42
46
|
},
|
|
43
47
|
},
|
|
44
48
|
defaultOptions: [],
|
|
@@ -91,6 +95,28 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
91
95
|
const type = checker.getTypeAtLocation(tsNode);
|
|
92
96
|
return type.getCallSignatures().length > 0;
|
|
93
97
|
}
|
|
98
|
+
function isRenderFunction(node) {
|
|
99
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
100
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
101
|
+
const signatures = type.getCallSignatures();
|
|
102
|
+
if (signatures.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
const isReactType = (t) => {
|
|
105
|
+
const typeStr = checker.typeToString(t);
|
|
106
|
+
return (typeStr.includes('JSX.Element') ||
|
|
107
|
+
typeStr.includes('ReactElement') ||
|
|
108
|
+
typeStr.includes('ReactNode'));
|
|
109
|
+
};
|
|
110
|
+
return signatures.some((signature) => {
|
|
111
|
+
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
112
|
+
if (isReactType(returnType))
|
|
113
|
+
return true;
|
|
114
|
+
if (returnType.isUnion()) {
|
|
115
|
+
return returnType.types.some((t) => isReactType(t));
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
94
120
|
return {
|
|
95
121
|
// Check JSX attributes for callback props
|
|
96
122
|
JSXAttribute(node) {
|
|
@@ -137,13 +163,19 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
137
163
|
if (isFunctionType(node.value.expression) &&
|
|
138
164
|
propName &&
|
|
139
165
|
!propName.startsWith('on') &&
|
|
166
|
+
!propName.startsWith('render') &&
|
|
167
|
+
!isRenderFunction(node.value.expression) &&
|
|
140
168
|
!isReactComponentType(node.value.expression)) {
|
|
169
|
+
const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
141
170
|
context.report({
|
|
142
171
|
node,
|
|
143
172
|
messageId: 'callbackPropPrefix',
|
|
173
|
+
data: {
|
|
174
|
+
propName,
|
|
175
|
+
eventName,
|
|
176
|
+
},
|
|
144
177
|
fix(fixer) {
|
|
145
178
|
// Convert camelCase to PascalCase for the event name
|
|
146
|
-
const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
147
179
|
return fixer.replaceText(node.name, `on${eventName}`);
|
|
148
180
|
},
|
|
149
181
|
});
|
|
@@ -159,6 +191,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
159
191
|
context.report({
|
|
160
192
|
node,
|
|
161
193
|
messageId: 'callbackFunctionPrefix',
|
|
194
|
+
data: { functionName },
|
|
162
195
|
});
|
|
163
196
|
return;
|
|
164
197
|
}
|
|
@@ -169,6 +202,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
169
202
|
context.report({
|
|
170
203
|
node,
|
|
171
204
|
messageId: 'callbackFunctionPrefix',
|
|
205
|
+
data: { functionName },
|
|
172
206
|
});
|
|
173
207
|
return;
|
|
174
208
|
}
|
|
@@ -210,7 +244,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
// Get references from global scope
|
|
213
|
-
const sourceCode = context.
|
|
247
|
+
const sourceCode = context.sourceCode;
|
|
214
248
|
if (sourceCode.scopeManager?.globalScope) {
|
|
215
249
|
const globalVar = sourceCode.scopeManager.globalScope.variables.find((v) => v.name === functionName);
|
|
216
250
|
if (globalVar) {
|
|
@@ -220,6 +254,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
220
254
|
context.report({
|
|
221
255
|
node,
|
|
222
256
|
messageId: 'callbackFunctionPrefix',
|
|
257
|
+
data: { functionName },
|
|
223
258
|
fix(fixer) {
|
|
224
259
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
225
260
|
const newName = functionName.slice(6).charAt(0).toLowerCase() +
|
|
@@ -248,6 +283,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
248
283
|
context.report({
|
|
249
284
|
node: node.key,
|
|
250
285
|
messageId: 'callbackFunctionPrefix',
|
|
286
|
+
data: { functionName: name },
|
|
251
287
|
});
|
|
252
288
|
return;
|
|
253
289
|
}
|
|
@@ -256,12 +292,14 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
256
292
|
context.report({
|
|
257
293
|
node: node.key,
|
|
258
294
|
messageId: 'callbackFunctionPrefix',
|
|
295
|
+
data: { functionName: name },
|
|
259
296
|
});
|
|
260
297
|
return;
|
|
261
298
|
}
|
|
262
299
|
context.report({
|
|
263
300
|
node: node.key,
|
|
264
301
|
messageId: 'callbackFunctionPrefix',
|
|
302
|
+
data: { functionName: name },
|
|
265
303
|
fix(fixer) {
|
|
266
304
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
267
305
|
const newName = name.slice(6).charAt(0).toLowerCase() + name.slice(7);
|
|
@@ -277,6 +315,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
277
315
|
context.report({
|
|
278
316
|
node,
|
|
279
317
|
messageId: 'callbackFunctionPrefix',
|
|
318
|
+
data: { functionName: node.parameter.name },
|
|
280
319
|
});
|
|
281
320
|
}
|
|
282
321
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'dynamicHttpsErrors' | 'missingThirdArgument';
|
|
2
|
+
type MessageIds = 'dynamicHttpsErrors' | 'missingThirdArgument' | 'missingDetailsProperty' | 'missingDetailsDueToSpread' | 'unexpectedExtraArgumentForObjectCall';
|
|
3
3
|
export declare const dynamicHttpsErrors: TSESLint.RuleModule<MessageIds, never[]>;
|
|
4
4
|
export {};
|
|
@@ -4,81 +4,171 @@ exports.dynamicHttpsErrors = void 0;
|
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
6
|
const isHttpsErrorCall = (callee) => {
|
|
7
|
-
if (callee.type ===
|
|
8
|
-
return (callee.object.type ===
|
|
7
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
8
|
+
return (callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
9
9
|
callee.object.name === 'https' &&
|
|
10
|
-
callee.property.type ===
|
|
10
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
11
11
|
callee.property.name === 'HttpsError');
|
|
12
12
|
}
|
|
13
|
-
else if (callee.type ===
|
|
13
|
+
else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
14
14
|
return callee.name === 'HttpsError';
|
|
15
15
|
}
|
|
16
16
|
return false;
|
|
17
17
|
};
|
|
18
|
+
const findPropertyByName = (properties, name) => properties.find((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
|
|
19
|
+
!p.computed &&
|
|
20
|
+
((p.key.type === utils_1.AST_NODE_TYPES.Identifier && p.key.name === name) ||
|
|
21
|
+
(p.key.type === utils_1.AST_NODE_TYPES.Literal && p.key.value === name)));
|
|
18
22
|
exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
|
|
19
23
|
name: 'dynamic-https-errors',
|
|
20
24
|
meta: {
|
|
21
25
|
type: 'suggestion',
|
|
22
26
|
docs: {
|
|
23
|
-
description: '
|
|
27
|
+
description: 'Keep HttpsError messages static and move request-specific details to the third argument so error identifiers remain stable and debugging context is preserved.',
|
|
24
28
|
recommended: 'error',
|
|
25
29
|
},
|
|
26
30
|
schema: [],
|
|
27
31
|
messages: {
|
|
28
|
-
dynamicHttpsErrors: '
|
|
29
|
-
missingThirdArgument:
|
|
32
|
+
dynamicHttpsErrors: 'The HttpsError message (second argument) must stay static. Template expressions here change the hashed message and explode the number of error ids for the same failure. Keep this argument constant and move interpolated values into the third "details" argument so monitoring groups the error while still capturing request context.',
|
|
33
|
+
missingThirdArgument: 'HttpsError calls must include a third "details" argument. The message (second argument) is hashed into a stable identifier, so omitting details leaves errors hard to debug and encourages packing variables into the hashed message. Provide a third argument with the request-specific context (object or string) to keep identifiers stable and diagnostics useful.',
|
|
34
|
+
missingDetailsProperty: 'HttpsError calls must include a "details" property. The message is hashed into a stable identifier, so omitting details leaves errors hard to debug and encourages packing variables into the hashed message. Provide a details property with the request-specific context (object or string) to keep identifiers stable and diagnostics useful.',
|
|
35
|
+
missingDetailsDueToSpread: 'HttpsError calls must include a "details" property. This call uses an object spread, which prevents static verification that "details" is present. Ensure the spread object contains "details" or provide it explicitly to keep identifiers stable and diagnostics useful.',
|
|
36
|
+
unexpectedExtraArgumentForObjectCall: 'Object-based HttpsError calls must have exactly one argument containing code, message, and details properties. Remove extra arguments or use the positional signature (code, message, details).',
|
|
30
37
|
},
|
|
31
38
|
},
|
|
32
39
|
defaultOptions: [],
|
|
33
40
|
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
|
+
// Only string concatenation with "+" can be static; all other operators
|
|
52
|
+
// are treated as dynamic to avoid hashing non-literal message content.
|
|
53
|
+
const isDynamicBinaryExpression = (expression) => {
|
|
54
|
+
if (expression.operator !== '+')
|
|
55
|
+
return true;
|
|
56
|
+
const isStaticLiteral = (expr) => {
|
|
57
|
+
const inner = unwrapTSAssertions(expr);
|
|
58
|
+
return (inner.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
59
|
+
typeof inner.value === 'string');
|
|
60
|
+
};
|
|
61
|
+
const isSafe = (expr) => {
|
|
62
|
+
if (expr.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const inner = unwrapTSAssertions(expr);
|
|
66
|
+
if (inner.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
67
|
+
return !isDynamicBinaryExpression(inner);
|
|
68
|
+
}
|
|
69
|
+
return isStaticLiteral(inner);
|
|
70
|
+
};
|
|
71
|
+
return !(isSafe(expression.left) && isSafe(expression.right));
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Determines if a node should be validated for staticness.
|
|
75
|
+
*
|
|
76
|
+
* Pragmatic Exception: Identifier, MemberExpression, and ChainExpression nodes
|
|
77
|
+
* (e.g., `props.message`, `props?.message`, `ERROR_MSG`) are excluded from
|
|
78
|
+
* staticness validation. While they can be dynamic and may affect message
|
|
79
|
+
* stability, they are permitted to support common React/props patterns and
|
|
80
|
+
* constants, preserving developer ergonomics as an intentional trade-off.
|
|
81
|
+
*/
|
|
82
|
+
const shouldValidateForStaticness = (node) => {
|
|
83
|
+
return (node.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
84
|
+
node.type !== utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
85
|
+
node.type !== utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
86
|
+
node.type !== utils_1.AST_NODE_TYPES.SpreadElement &&
|
|
87
|
+
// Explicitly exclude patterns and other non-Expression nodes that can appear in Property.value
|
|
88
|
+
node.type !== utils_1.AST_NODE_TYPES.ArrayPattern &&
|
|
89
|
+
node.type !== utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
90
|
+
node.type !== utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
91
|
+
node.type !== utils_1.AST_NODE_TYPES.RestElement &&
|
|
92
|
+
node.type !== utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression);
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Checks if the message node is static.
|
|
96
|
+
*
|
|
97
|
+
* A message is considered static if it's a Literal (string), a TemplateLiteral with no expressions,
|
|
98
|
+
* or a BinaryExpression (string concatenation with '+') where all parts are static.
|
|
99
|
+
*
|
|
100
|
+
* This function reports `dynamicHttpsErrors` for all other expression types (CallExpression,
|
|
101
|
+
* ConditionalExpression, etc.) that reach it, except for those explicitly excluded by
|
|
102
|
+
* `shouldValidateForStaticness`.
|
|
103
|
+
*/
|
|
104
|
+
const checkMessageIsStatic = (messageNode) => {
|
|
105
|
+
const currentNode = unwrapTSAssertions(messageNode);
|
|
106
|
+
if (currentNode.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (currentNode.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
110
|
+
currentNode.expressions.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (currentNode.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
114
|
+
if (isDynamicBinaryExpression(currentNode)) {
|
|
115
|
+
context.report({
|
|
116
|
+
node: messageNode,
|
|
117
|
+
messageId: 'dynamicHttpsErrors',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Catch-all for other dynamic forms (CallExpression, ConditionalExpression, etc.)
|
|
123
|
+
context.report({
|
|
124
|
+
node: messageNode,
|
|
125
|
+
messageId: 'dynamicHttpsErrors',
|
|
126
|
+
});
|
|
127
|
+
};
|
|
34
128
|
const checkForHttpsError = (node) => {
|
|
35
129
|
const callee = node.callee;
|
|
36
|
-
if (isHttpsErrorCall(callee))
|
|
37
|
-
|
|
38
|
-
|
|
130
|
+
if (!isHttpsErrorCall(callee))
|
|
131
|
+
return;
|
|
132
|
+
// Signature 1: Object-based constructor (HttpsErrorProps)
|
|
133
|
+
if (node.arguments.length >= 1 &&
|
|
134
|
+
node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
135
|
+
if (node.arguments.length > 1) {
|
|
39
136
|
context.report({
|
|
40
137
|
node,
|
|
41
|
-
messageId: '
|
|
138
|
+
messageId: 'unexpectedExtraArgumentForObjectCall',
|
|
42
139
|
});
|
|
43
140
|
return;
|
|
44
141
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
secondArg.expressions.length > 0) {
|
|
142
|
+
const props = node.arguments[0];
|
|
143
|
+
const messageProperty = findPropertyByName(props.properties, 'message');
|
|
144
|
+
const detailsProperty = findPropertyByName(props.properties, 'details');
|
|
145
|
+
if (!detailsProperty) {
|
|
146
|
+
const hasSpread = props.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.SpreadElement);
|
|
51
147
|
context.report({
|
|
52
|
-
node
|
|
53
|
-
messageId:
|
|
148
|
+
node,
|
|
149
|
+
messageId: hasSpread
|
|
150
|
+
? 'missingDetailsDueToSpread'
|
|
151
|
+
: 'missingDetailsProperty',
|
|
54
152
|
});
|
|
55
|
-
return;
|
|
56
153
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (expression.operator !== '+')
|
|
61
|
-
return true;
|
|
62
|
-
const isStaticLiteral = (expr) => expr.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
63
|
-
typeof expr.value === 'string';
|
|
64
|
-
const isSafe = (expr) => {
|
|
65
|
-
if (expr.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
if (expr.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
69
|
-
return !isDynamicBinaryExpression(expr);
|
|
70
|
-
}
|
|
71
|
-
return isStaticLiteral(expr);
|
|
72
|
-
};
|
|
73
|
-
return !(isSafe(expression.left) && isSafe(expression.right));
|
|
74
|
-
};
|
|
75
|
-
if (secondArg.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
76
|
-
isDynamicBinaryExpression(secondArg)) {
|
|
77
|
-
context.report({
|
|
78
|
-
node: secondArg,
|
|
79
|
-
messageId: 'dynamicHttpsErrors',
|
|
80
|
-
});
|
|
154
|
+
if (messageProperty &&
|
|
155
|
+
shouldValidateForStaticness(messageProperty.value)) {
|
|
156
|
+
checkMessageIsStatic(messageProperty.value);
|
|
81
157
|
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Signature 2: Positional arguments (code, message, details)
|
|
161
|
+
// Check for missing third argument
|
|
162
|
+
if (node.arguments.length < 3) {
|
|
163
|
+
context.report({
|
|
164
|
+
node,
|
|
165
|
+
messageId: 'missingThirdArgument',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Check for dynamic content in second argument
|
|
169
|
+
const secondArg = node.arguments[1];
|
|
170
|
+
if (secondArg && shouldValidateForStaticness(secondArg)) {
|
|
171
|
+
checkMessageIsStatic(secondArg);
|
|
82
172
|
}
|
|
83
173
|
};
|
|
84
174
|
return {
|
|
@@ -34,7 +34,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
34
34
|
* Helper function to add assertSafe import if needed
|
|
35
35
|
*/
|
|
36
36
|
const addAssertSafeImport = (fixer) => {
|
|
37
|
-
const program = context.
|
|
37
|
+
const program = context.sourceCode.ast;
|
|
38
38
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
39
39
|
const importStatement = `import { assertSafe } from '${importPath}';\n`;
|
|
40
40
|
if (firstImport) {
|
|
@@ -83,7 +83,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
83
83
|
key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
84
84
|
key.callee.name === 'String') {
|
|
85
85
|
const arg = key.arguments[0];
|
|
86
|
-
const argText = context.
|
|
86
|
+
const argText = context.sourceCode.getText(arg);
|
|
87
87
|
reportUseAssertSafe(key, argText);
|
|
88
88
|
}
|
|
89
89
|
// Check for template literals like `${id}`
|
|
@@ -93,7 +93,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
93
93
|
key.quasis[0].value.raw === '' &&
|
|
94
94
|
key.quasis[1].value.raw === '') {
|
|
95
95
|
const expr = key.expressions[0];
|
|
96
|
-
const exprText = context.
|
|
96
|
+
const exprText = context.sourceCode.getText(expr);
|
|
97
97
|
reportUseAssertSafe(key, exprText);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -107,7 +107,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
107
107
|
left.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
108
108
|
left.callee.name === 'String') {
|
|
109
109
|
const arg = left.arguments[0];
|
|
110
|
-
const argText = context.
|
|
110
|
+
const argText = context.sourceCode.getText(arg);
|
|
111
111
|
reportUseAssertSafe(left, argText);
|
|
112
112
|
}
|
|
113
113
|
// Check for template literals like `${id}`
|
|
@@ -117,7 +117,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
117
117
|
left.quasis[0].value.raw === '' &&
|
|
118
118
|
left.quasis[1].value.raw === '') {
|
|
119
119
|
const expr = left.expressions[0];
|
|
120
|
-
const exprText = context.
|
|
120
|
+
const exprText = context.sourceCode.getText(expr);
|
|
121
121
|
reportUseAssertSafe(left, exprText);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -158,7 +158,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
158
158
|
property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
159
159
|
property.callee.name === 'String') {
|
|
160
160
|
const arg = property.arguments[0];
|
|
161
|
-
const argText = context.
|
|
161
|
+
const argText = context.sourceCode.getText(arg);
|
|
162
162
|
reportUseAssertSafe(property, argText);
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
@@ -179,7 +179,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
const expr = property.expressions[0];
|
|
182
|
-
const exprText = context.
|
|
182
|
+
const exprText = context.sourceCode.getText(expr);
|
|
183
183
|
reportUseAssertSafe(property, exprText);
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
@@ -193,7 +193,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
193
193
|
if (isLikelyArray) {
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
|
-
const propText = context.
|
|
196
|
+
const propText = context.sourceCode.getText(property);
|
|
197
197
|
reportUseAssertSafe(property, propText);
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
@@ -203,7 +203,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
203
203
|
if (isLikelyArray) {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
-
const propText = context.
|
|
206
|
+
const propText = context.sourceCode.getText(property);
|
|
207
207
|
reportUseAssertSafe(property, propText);
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
@@ -215,7 +215,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
215
215
|
if (isLikelyArray) {
|
|
216
216
|
return;
|
|
217
217
|
}
|
|
218
|
-
const propText = context.
|
|
218
|
+
const propText = context.sourceCode.getText(property);
|
|
219
219
|
reportUseAssertSafe(property, propText);
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
@@ -228,7 +228,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
228
228
|
if (isLikelyArray) {
|
|
229
229
|
return;
|
|
230
230
|
}
|
|
231
|
-
const propText = context.
|
|
231
|
+
const propText = context.sourceCode.getText(property);
|
|
232
232
|
reportUseAssertSafe(property, propText);
|
|
233
233
|
return;
|
|
234
234
|
}
|