@blumintinc/eslint-plugin-blumint 1.12.5 → 1.13.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 +140 -99
- package/lib/index.js +111 -20
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
- package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
- package/lib/rules/no-restricted-imports-dynamic.js +0 -213
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceQueryKeyTs = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Rule to enforce the use of centralized router state key constants imported from
|
|
8
|
+
* `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
|
|
9
|
+
* router methods that accept key parameters.
|
|
10
|
+
*/
|
|
11
|
+
exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
12
|
+
name: 'enforce-querykey-ts',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce using centralized router state key constants from queryKeys.ts for useRouterState key parameter',
|
|
17
|
+
recommended: 'error',
|
|
18
|
+
},
|
|
19
|
+
fixable: 'code',
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
enforceQueryKeyImport: 'Router state key must come from queryKeys.ts (e.g., "@/util/routing/queryKeys", "src/util/routing/queryKeys", or a relative path ending in "/util/routing/queryKeys"). Use a QUERY_KEY_* constant instead of string literals.',
|
|
23
|
+
enforceQueryKeyConstant: 'Router state key must use a QUERY_KEY_* constant from queryKeys.ts. Variable "{{variableName}}" is not imported from the correct source.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
// Track imports from queryKeys.ts
|
|
29
|
+
const queryKeyImports = new Map();
|
|
30
|
+
const localUseRouterStateNames = new Set(['useRouterState']);
|
|
31
|
+
const validQueryKeySources = new Set([
|
|
32
|
+
'@/util/routing/queryKeys',
|
|
33
|
+
'src/util/routing/queryKeys',
|
|
34
|
+
]);
|
|
35
|
+
const allowedQueryKeyFactories = new Set(['makeQueryKey', 'getQueryKey']);
|
|
36
|
+
/**
|
|
37
|
+
* Ensure a suggested constant is imported from queryKeys.ts and return combined fixes
|
|
38
|
+
*/
|
|
39
|
+
function buildImportFixes(fixer, keyNode, suggestedConstant) {
|
|
40
|
+
return [fixer.replaceText(keyNode, suggestedConstant)];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a source path refers to queryKeys.ts
|
|
44
|
+
*/
|
|
45
|
+
function isQueryKeysSource(source) {
|
|
46
|
+
return (validQueryKeySources.has(source) ||
|
|
47
|
+
source.endsWith('/util/routing/queryKeys'));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if an identifier is a valid QUERY_KEY constant
|
|
51
|
+
*/
|
|
52
|
+
function isValidQueryKeyConstant(name) {
|
|
53
|
+
return name.startsWith('QUERY_KEY_');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Track variable assignments to detect variables derived from query key constants
|
|
57
|
+
*/
|
|
58
|
+
const variableAssignments = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* Check if a node represents a valid query key usage
|
|
61
|
+
*/
|
|
62
|
+
function isValidQueryKeyUsage(node) {
|
|
63
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
64
|
+
const importInfo = queryKeyImports.get(node.name);
|
|
65
|
+
if (importInfo && isQueryKeysSource(importInfo.source)) {
|
|
66
|
+
return isValidQueryKeyConstant(importInfo.imported);
|
|
67
|
+
}
|
|
68
|
+
// Check if it's a variable derived from a query key constant
|
|
69
|
+
const assignment = variableAssignments.get(node.name);
|
|
70
|
+
if (assignment) {
|
|
71
|
+
return isValidQueryKeyUsage(assignment);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Allow member expressions accessing query key constants
|
|
75
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
76
|
+
const member = node;
|
|
77
|
+
if (member.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
78
|
+
!member.computed &&
|
|
79
|
+
member.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
80
|
+
const importInfo = queryKeyImports.get(member.object.name);
|
|
81
|
+
if (importInfo && isQueryKeysSource(importInfo.source)) {
|
|
82
|
+
if (importInfo.imported === '*') {
|
|
83
|
+
return isValidQueryKeyConstant(member.property.name);
|
|
84
|
+
}
|
|
85
|
+
return isValidQueryKeyConstant(member.property.name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Allow template literals only when they contain no static content and all expressions are valid
|
|
90
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
91
|
+
const hasSignificantStaticPart = node.quasis.some((quasi) => {
|
|
92
|
+
const content = quasi.value.raw.trim();
|
|
93
|
+
return content.length > 0 && !/^[-_:/.]+$/.test(content);
|
|
94
|
+
});
|
|
95
|
+
if (node.expressions.length === 0) {
|
|
96
|
+
// Pure static template acts like a string literal
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (hasSignificantStaticPart) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return node.expressions.some((expr) => isValidQueryKeyUsage(expr));
|
|
103
|
+
}
|
|
104
|
+
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
105
|
+
node.operator === '+') {
|
|
106
|
+
return (isValidQueryKeyUsage(node.left) || isValidQueryKeyUsage(node.right));
|
|
107
|
+
}
|
|
108
|
+
// Allow conditional expressions if both branches use valid query keys
|
|
109
|
+
if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
110
|
+
return (isValidQueryKeyUsage(node.consequent) &&
|
|
111
|
+
isValidQueryKeyUsage(node.alternate));
|
|
112
|
+
}
|
|
113
|
+
// Allow function calls that might return query keys
|
|
114
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
115
|
+
const callee = node.callee;
|
|
116
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
117
|
+
return allowedQueryKeyFactories.has(callee.name);
|
|
118
|
+
}
|
|
119
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
120
|
+
!callee.computed &&
|
|
121
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
122
|
+
return allowedQueryKeyFactories.has(callee.property.name);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if a node contains string literals that should be reported
|
|
130
|
+
*/
|
|
131
|
+
function containsInvalidStringLiteral(node) {
|
|
132
|
+
// Direct string literal
|
|
133
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
134
|
+
typeof node.value === 'string') {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
// String concatenation with + operator containing literals
|
|
138
|
+
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
139
|
+
node.operator === '+') {
|
|
140
|
+
return (containsInvalidStringLiteral(node.left) ||
|
|
141
|
+
containsInvalidStringLiteral(node.right));
|
|
142
|
+
}
|
|
143
|
+
// Conditional (ternary) expression with string literals
|
|
144
|
+
if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
145
|
+
return (containsInvalidStringLiteral(node.consequent) ||
|
|
146
|
+
containsInvalidStringLiteral(node.alternate));
|
|
147
|
+
}
|
|
148
|
+
// Template literal handling
|
|
149
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
150
|
+
const hasSignificantStaticPart = node.quasis.some((quasi) => {
|
|
151
|
+
const content = quasi.value.raw.trim();
|
|
152
|
+
return content.length > 0 && !/^[-_:/.]+$/.test(content);
|
|
153
|
+
});
|
|
154
|
+
if (node.expressions.length === 0) {
|
|
155
|
+
// Pure static template behaves like a string literal
|
|
156
|
+
return hasSignificantStaticPart;
|
|
157
|
+
}
|
|
158
|
+
// Any meaningful static content makes this invalid regardless of expressions
|
|
159
|
+
if (hasSignificantStaticPart) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
// Only dynamic parts remain; all expressions must be valid query key usages
|
|
163
|
+
return !node.expressions.every((expr) => isValidQueryKeyUsage(expr));
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generate auto-fix suggestion for string literals
|
|
169
|
+
*/
|
|
170
|
+
function generateAutoFix(keyValue) {
|
|
171
|
+
// Simple heuristic to suggest query key constant names
|
|
172
|
+
const normalizedKey = keyValue
|
|
173
|
+
.toUpperCase()
|
|
174
|
+
.replace(/[^A-Z0-9]/g, '_')
|
|
175
|
+
.replace(/_+/g, '_')
|
|
176
|
+
.replace(/^_|_$/g, '');
|
|
177
|
+
return `QUERY_KEY_${normalizedKey}`;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
// Track imports from queryKeys.ts
|
|
181
|
+
ImportDeclaration(node) {
|
|
182
|
+
if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
183
|
+
typeof node.source.value === 'string') {
|
|
184
|
+
const source = node.source.value;
|
|
185
|
+
if (isQueryKeysSource(source)) {
|
|
186
|
+
node.specifiers.forEach((spec) => {
|
|
187
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
188
|
+
const imported = spec.imported.name;
|
|
189
|
+
const local = spec.local.name;
|
|
190
|
+
queryKeyImports.set(local, { source, imported });
|
|
191
|
+
}
|
|
192
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
193
|
+
const local = spec.local.name;
|
|
194
|
+
queryKeyImports.set(local, { source, imported: '*' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
node.specifiers.forEach((spec) => {
|
|
199
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
200
|
+
spec.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
201
|
+
spec.imported.name === 'useRouterState') {
|
|
202
|
+
localUseRouterStateNames.add(spec.local.name);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
// Track variable declarations that might derive from query key constants
|
|
208
|
+
VariableDeclarator(node) {
|
|
209
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier && node.init) {
|
|
210
|
+
variableAssignments.set(node.id.name, node.init);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
AssignmentExpression(node) {
|
|
214
|
+
if (node.left.type === utils_1.AST_NODE_TYPES.Identifier && node.right) {
|
|
215
|
+
variableAssignments.set(node.left.name, node.right);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
// Check useRouterState calls
|
|
219
|
+
CallExpression(node) {
|
|
220
|
+
// Check if this is a call to useRouterState
|
|
221
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
222
|
+
localUseRouterStateNames.has(node.callee.name)) {
|
|
223
|
+
// Check if there are arguments
|
|
224
|
+
if (node.arguments.length > 0) {
|
|
225
|
+
const firstArg = node.arguments[0];
|
|
226
|
+
// Check if the first argument is an object expression
|
|
227
|
+
if (firstArg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
228
|
+
// Find the key property in the object
|
|
229
|
+
const keyProperty = firstArg.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
230
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
231
|
+
prop.key.name === 'key');
|
|
232
|
+
// If key property exists, check its value
|
|
233
|
+
if (keyProperty && keyProperty.value) {
|
|
234
|
+
const keyValue = keyProperty.value;
|
|
235
|
+
// Check if it's a valid query key usage
|
|
236
|
+
if (!isValidQueryKeyUsage(keyValue)) {
|
|
237
|
+
// Check if it contains invalid string literals
|
|
238
|
+
if (containsInvalidStringLiteral(keyValue)) {
|
|
239
|
+
context.report({
|
|
240
|
+
node: keyValue,
|
|
241
|
+
messageId: 'enforceQueryKeyImport',
|
|
242
|
+
fix(fixer) {
|
|
243
|
+
// Only provide auto-fix for simple string literals
|
|
244
|
+
if (keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
245
|
+
typeof keyValue.value === 'string') {
|
|
246
|
+
const suggestedConstant = generateAutoFix(keyValue.value);
|
|
247
|
+
if (suggestedConstant) {
|
|
248
|
+
return buildImportFixes(fixer, keyValue, suggestedConstant);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
256
|
+
// Report variables that aren't from the correct source
|
|
257
|
+
context.report({
|
|
258
|
+
node: keyValue,
|
|
259
|
+
messageId: 'enforceQueryKeyConstant',
|
|
260
|
+
data: {
|
|
261
|
+
variableName: keyValue.name,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
//# sourceMappingURL=enforce-querykey-ts.js.map
|
|
@@ -26,6 +26,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.enforceSingularTypeNames = void 0;
|
|
27
27
|
const createRule_1 = require("../utils/createRule");
|
|
28
28
|
const pluralize = __importStar(require("pluralize"));
|
|
29
|
+
const NON_PLURALIZABLE_SUFFIXES = [
|
|
30
|
+
'Props',
|
|
31
|
+
'Params',
|
|
32
|
+
'Options',
|
|
33
|
+
'Settings',
|
|
34
|
+
'Data',
|
|
35
|
+
];
|
|
29
36
|
exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
30
37
|
create(context) {
|
|
31
38
|
/**
|
|
@@ -37,11 +44,8 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
|
37
44
|
// Skip checking if name is too short (less than 3 characters)
|
|
38
45
|
if (name.length < 3)
|
|
39
46
|
return false;
|
|
40
|
-
// Skip checking if name ends with 'Props'
|
|
41
|
-
if (name.endsWith(
|
|
42
|
-
name.endsWith('Params') ||
|
|
43
|
-
name.endsWith('Options') ||
|
|
44
|
-
name.endsWith('Settings'))
|
|
47
|
+
// Skip checking if name ends with 'Props', 'Params', 'Data', etc.
|
|
48
|
+
if (NON_PLURALIZABLE_SUFFIXES.some((suffix) => name.toLowerCase().endsWith(suffix.toLowerCase())))
|
|
45
49
|
return false;
|
|
46
50
|
// Skip checking if name is already singular according to pluralize
|
|
47
51
|
if (pluralize.isSingular(name))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const enforceTypescriptMarkdownCodeBlocks: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"missingLanguageSpecifier", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceTypescriptMarkdownCodeBlocks = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const FENCE = '```';
|
|
6
|
+
function isFenceLine(text, fenceIndex, indent) {
|
|
7
|
+
const lineEnd = text.indexOf('\n', fenceIndex);
|
|
8
|
+
const afterFence = lineEnd === -1 ? text.slice(fenceIndex + FENCE.length) : text.slice(fenceIndex + FENCE.length, lineEnd);
|
|
9
|
+
return afterFence.trim().length === 0 && text.slice(text.lastIndexOf('\n', fenceIndex - 1) + 1, fenceIndex) === indent;
|
|
10
|
+
}
|
|
11
|
+
function findClosingFence(text, startIndex, indent) {
|
|
12
|
+
let searchIndex = startIndex;
|
|
13
|
+
while (searchIndex < text.length) {
|
|
14
|
+
const candidate = text.indexOf(FENCE, searchIndex);
|
|
15
|
+
if (candidate === -1) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (isFenceLine(text, candidate, indent)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
searchIndex = candidate + FENCE.length;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function isIndentOnly(value) {
|
|
26
|
+
return /^[\t ]*$/.test(value);
|
|
27
|
+
}
|
|
28
|
+
exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
|
|
29
|
+
name: 'enforce-typescript-markdown-code-blocks',
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'suggestion',
|
|
32
|
+
docs: {
|
|
33
|
+
description: 'Ensure Markdown fenced code blocks without a language specifier default to typescript for consistent highlighting.',
|
|
34
|
+
recommended: 'error',
|
|
35
|
+
},
|
|
36
|
+
fixable: 'code',
|
|
37
|
+
schema: [],
|
|
38
|
+
messages: {
|
|
39
|
+
missingLanguageSpecifier: 'Code block starting on line {{line}} has no language label. Unlabeled fences render as plain text, hiding TypeScript syntax and type cues. Add "typescript" after the opening backticks (```typescript) to keep documentation readable and consistent.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [],
|
|
43
|
+
create(context) {
|
|
44
|
+
const filename = context.getFilename();
|
|
45
|
+
if (!filename.toLowerCase().endsWith('.md')) {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
Program() {
|
|
50
|
+
const sourceCode = context.getSourceCode();
|
|
51
|
+
const text = sourceCode.getText();
|
|
52
|
+
let index = 0;
|
|
53
|
+
while (index < text.length) {
|
|
54
|
+
const openingFence = text.indexOf(FENCE, index);
|
|
55
|
+
if (openingFence === -1) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const lineStart = text.lastIndexOf('\n', openingFence - 1) + 1;
|
|
59
|
+
const indent = text.slice(lineStart, openingFence);
|
|
60
|
+
if (!isIndentOnly(indent)) {
|
|
61
|
+
index = openingFence + FENCE.length;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const lineEnd = text.indexOf('\n', openingFence + FENCE.length);
|
|
65
|
+
if (lineEnd === -1) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const infoString = text.slice(openingFence + FENCE.length, lineEnd);
|
|
69
|
+
const closingFence = findClosingFence(text, lineEnd + 1, indent);
|
|
70
|
+
if (closingFence === null) {
|
|
71
|
+
index = lineEnd + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const content = text.slice(lineEnd + 1, closingFence);
|
|
75
|
+
const hasContent = content.trim().length > 0;
|
|
76
|
+
const hasLanguage = infoString.trim().length > 0;
|
|
77
|
+
if (!hasLanguage && hasContent) {
|
|
78
|
+
const locStart = sourceCode.getLocFromIndex(openingFence);
|
|
79
|
+
const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
|
|
80
|
+
context.report({
|
|
81
|
+
loc: {
|
|
82
|
+
start: locStart,
|
|
83
|
+
end: sourceCode.getLocFromIndex(lineEnd),
|
|
84
|
+
},
|
|
85
|
+
messageId: 'missingLanguageSpecifier',
|
|
86
|
+
data: { line: locStart.line },
|
|
87
|
+
fix: (fixer) => fixer.replaceTextRange([openingFence + FENCE.length, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
index = closingFence + FENCE.length;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=enforce-typescript-markdown-code-blocks.js.map
|
|
@@ -4600,18 +4600,86 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4600
4600
|
}
|
|
4601
4601
|
return false;
|
|
4602
4602
|
}
|
|
4603
|
+
function bodyContainsJsx(node) {
|
|
4604
|
+
const stack = [node];
|
|
4605
|
+
while (stack.length) {
|
|
4606
|
+
const current = stack.pop();
|
|
4607
|
+
if (current.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
4608
|
+
current.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
4609
|
+
return true;
|
|
4610
|
+
}
|
|
4611
|
+
for (const key of Object.keys(current)) {
|
|
4612
|
+
if (key === 'parent')
|
|
4613
|
+
continue;
|
|
4614
|
+
const value = current[key];
|
|
4615
|
+
if (Array.isArray(value)) {
|
|
4616
|
+
for (const child of value) {
|
|
4617
|
+
if (child && typeof child.type === 'string') {
|
|
4618
|
+
stack.push(child);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
else if (value && typeof value.type === 'string') {
|
|
4623
|
+
stack.push(value);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
return false;
|
|
4628
|
+
}
|
|
4603
4629
|
function hasJsxReturn(node) {
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4630
|
+
/**
|
|
4631
|
+
* Recursively checks if an expression looks like JSX
|
|
4632
|
+
*/
|
|
4633
|
+
function isJsxExpression(expr) {
|
|
4634
|
+
if (!expr)
|
|
4635
|
+
return false;
|
|
4636
|
+
// Handle parenthesized expressions
|
|
4637
|
+
if (expr.type === 'ParenthesizedExpression') {
|
|
4638
|
+
return isJsxExpression(expr.expression);
|
|
4639
|
+
}
|
|
4640
|
+
switch (expr.type) {
|
|
4641
|
+
case utils_1.AST_NODE_TYPES.JSXElement:
|
|
4642
|
+
case utils_1.AST_NODE_TYPES.JSXFragment:
|
|
4643
|
+
return true;
|
|
4644
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
4645
|
+
return (isJsxExpression(expr.consequent) ||
|
|
4646
|
+
isJsxExpression(expr.alternate));
|
|
4647
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
4648
|
+
// e.g. condition && <div />
|
|
4649
|
+
return isJsxExpression(expr.right);
|
|
4650
|
+
default:
|
|
4651
|
+
return false;
|
|
4652
|
+
}
|
|
4609
4653
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4654
|
+
/**
|
|
4655
|
+
* Recursively checks if a statement/body returns JSX
|
|
4656
|
+
*/
|
|
4657
|
+
function checkBodyForJsx(body) {
|
|
4658
|
+
if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
4659
|
+
for (const stmt of body.body) {
|
|
4660
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
4661
|
+
if (isJsxExpression(stmt.argument))
|
|
4662
|
+
return true;
|
|
4663
|
+
}
|
|
4664
|
+
else if (stmt.type === utils_1.AST_NODE_TYPES.IfStatement) {
|
|
4665
|
+
if (checkBodyForJsx(stmt.consequent))
|
|
4666
|
+
return true;
|
|
4667
|
+
if (stmt.alternate && checkBodyForJsx(stmt.alternate))
|
|
4668
|
+
return true;
|
|
4669
|
+
}
|
|
4670
|
+
// We could traverse more (try/catch, switch), but this covers most idiomatic React
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
else {
|
|
4674
|
+
// Implicit return (arrow function expression body)
|
|
4675
|
+
return isJsxExpression(body);
|
|
4676
|
+
}
|
|
4677
|
+
return false;
|
|
4678
|
+
}
|
|
4679
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
4680
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
4681
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
4682
|
+
return checkBodyForJsx(node.body);
|
|
4615
4683
|
}
|
|
4616
4684
|
return false;
|
|
4617
4685
|
}
|
|
@@ -4664,6 +4732,13 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4664
4732
|
// 5. Component name ends with "Unmemoized" (common React pattern)
|
|
4665
4733
|
functionName && functionName.endsWith('Unmemoized'),
|
|
4666
4734
|
];
|
|
4735
|
+
// If the function has a PascalCase name AND returns JSX, it's definitely a React component
|
|
4736
|
+
if (functionName && isPascalCase(functionName) && hasJsxReturn(node)) {
|
|
4737
|
+
return true;
|
|
4738
|
+
}
|
|
4739
|
+
if (functionName && isPascalCase(functionName) && bodyContainsJsx(node)) {
|
|
4740
|
+
return true;
|
|
4741
|
+
}
|
|
4667
4742
|
// Consider it a React component if it matches at least 2 indicators
|
|
4668
4743
|
// OR if it has the Unmemoized suffix (strong indicator of a React component)
|
|
4669
4744
|
return (indicators.filter(Boolean).length >= 2 ||
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.extractGlobalConstants = void 0;
|
|
4
4
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
7
|
function isInsideFunction(node) {
|
|
7
8
|
let current = node;
|
|
8
9
|
while (current) {
|
|
@@ -19,53 +20,61 @@ function isFunctionDefinition(node) {
|
|
|
19
20
|
return (node?.type === 'FunctionExpression' ||
|
|
20
21
|
node?.type === 'ArrowFunctionExpression');
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
const unwrapOnce = (node) => {
|
|
23
24
|
if (!node)
|
|
24
|
-
return
|
|
25
|
+
return null;
|
|
26
|
+
const maybeParen = node;
|
|
27
|
+
if (maybeParen.type === 'ParenthesizedExpression') {
|
|
28
|
+
return maybeParen.expression ?? null;
|
|
29
|
+
}
|
|
25
30
|
switch (node.type) {
|
|
26
|
-
case
|
|
27
|
-
|
|
28
|
-
case
|
|
29
|
-
|
|
30
|
-
case
|
|
31
|
-
return
|
|
32
|
-
case 'BinaryExpression':
|
|
33
|
-
if (node.left.type === 'PrivateIdentifier')
|
|
34
|
-
return false;
|
|
35
|
-
return isImmutableValue(node.left) && isImmutableValue(node.right);
|
|
31
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
32
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
33
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
34
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
35
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
36
|
+
return node.expression;
|
|
36
37
|
default:
|
|
37
|
-
return
|
|
38
|
+
return null;
|
|
38
39
|
}
|
|
40
|
+
};
|
|
41
|
+
function unwrapExpression(node) {
|
|
42
|
+
let current = node;
|
|
43
|
+
while (current) {
|
|
44
|
+
const inner = unwrapOnce(current);
|
|
45
|
+
if (!inner)
|
|
46
|
+
break;
|
|
47
|
+
current = inner;
|
|
48
|
+
}
|
|
49
|
+
return current;
|
|
39
50
|
}
|
|
40
51
|
function isMutableValue(node) {
|
|
41
|
-
|
|
52
|
+
const unwrapped = unwrapExpression(node);
|
|
53
|
+
if (!unwrapped)
|
|
54
|
+
return false;
|
|
55
|
+
// If explicitly marked readonly with `as const`, treat as immutable
|
|
56
|
+
if (node && isAsConstExpression(node)) {
|
|
42
57
|
return false;
|
|
58
|
+
}
|
|
43
59
|
// Check for JSX elements (always mutable due to props/context)
|
|
44
|
-
if (
|
|
60
|
+
if (unwrapped.type === 'JSXElement' || unwrapped.type === 'JSXFragment') {
|
|
45
61
|
return true;
|
|
46
62
|
}
|
|
47
63
|
// Check for object expressions (always mutable)
|
|
48
|
-
if (
|
|
64
|
+
if (unwrapped.type === 'ObjectExpression') {
|
|
49
65
|
return true;
|
|
50
66
|
}
|
|
51
|
-
//
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
if (node.elements.length === 0)
|
|
55
|
-
return true;
|
|
56
|
-
// Arrays with spread elements are mutable
|
|
57
|
-
if (node.elements.some((element) => !element || element.type === 'SpreadElement'))
|
|
58
|
-
return true;
|
|
59
|
-
// Arrays with non-immutable values are mutable
|
|
60
|
-
return node.elements.some((element) => !isImmutableValue(element));
|
|
67
|
+
// Arrays are mutable objects unless explicitly narrowed with `as const`.
|
|
68
|
+
if (unwrapped.type === 'ArrayExpression') {
|
|
69
|
+
return true;
|
|
61
70
|
}
|
|
62
71
|
// Check for new expressions (e.g., new Map(), new Set())
|
|
63
|
-
if (
|
|
72
|
+
if (unwrapped.type === 'NewExpression') {
|
|
64
73
|
return true;
|
|
65
74
|
}
|
|
66
75
|
// Check for array/object methods that return mutable values
|
|
67
|
-
if (
|
|
68
|
-
const callee =
|
|
76
|
+
if (unwrapped.type === 'CallExpression') {
|
|
77
|
+
const callee = unwrapped.callee;
|
|
69
78
|
if (callee.type === 'MemberExpression') {
|
|
70
79
|
// Handle both Identifier and non-Identifier property nodes
|
|
71
80
|
if (callee.property.type !== 'Identifier') {
|
|
@@ -103,14 +112,15 @@ function isZeroOrOne(node) {
|
|
|
103
112
|
return value === 0 || value === 1;
|
|
104
113
|
}
|
|
105
114
|
function isAsConstExpression(node) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
let current = node;
|
|
116
|
+
while (current) {
|
|
117
|
+
if (current.type === 'TSAsExpression' &&
|
|
118
|
+
current.typeAnnotation.type === 'TSTypeReference' &&
|
|
119
|
+
current.typeAnnotation.typeName.type === 'Identifier' &&
|
|
120
|
+
current.typeAnnotation.typeName.name === 'const') {
|
|
112
121
|
return true;
|
|
113
122
|
}
|
|
123
|
+
current = unwrapOnce(current);
|
|
114
124
|
}
|
|
115
125
|
return false;
|
|
116
126
|
}
|
|
@@ -134,16 +144,18 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
134
144
|
if (!hasDependencies &&
|
|
135
145
|
!hasAsConstAssertion &&
|
|
136
146
|
(scope.type === 'function' || scope.type === 'block') &&
|
|
137
|
-
isInsideFunction(node)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
isInsideFunction(node) &&
|
|
148
|
+
node.declarations.some((d) => d.id.type === 'Identifier')) {
|
|
149
|
+
for (const d of node.declarations) {
|
|
150
|
+
if (d.id.type !== 'Identifier')
|
|
151
|
+
continue;
|
|
152
|
+
const constName = d.id.name;
|
|
153
|
+
context.report({
|
|
154
|
+
node: d,
|
|
155
|
+
messageId: 'extractGlobalConstants',
|
|
156
|
+
data: { declarationName: constName },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
},
|
|
149
161
|
FunctionDeclaration(node) {
|