@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
|
@@ -12,6 +12,47 @@ const DEFAULT_USE_DYNAMIC_SOURCES = [
|
|
|
12
12
|
function matchesAllowedSource(source, allowedSources) {
|
|
13
13
|
return allowedSources.some((pattern) => source === pattern || source.endsWith(`/${pattern}`));
|
|
14
14
|
}
|
|
15
|
+
function hasNodeStructure(value) {
|
|
16
|
+
return (!!value &&
|
|
17
|
+
typeof value === 'object' &&
|
|
18
|
+
'type' in value &&
|
|
19
|
+
typeof value.type === 'string');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Walks the AST and invokes the visitor on each node.
|
|
23
|
+
* Uses WeakSet to prevent infinite cycles.
|
|
24
|
+
*
|
|
25
|
+
* @param visitor - Callback invoked on each node. Return `false` to stop
|
|
26
|
+
* traversal; return `undefined` (or nothing) to continue walking.
|
|
27
|
+
* @returns `false` if traversal stopped early; `true` when traversal completes.
|
|
28
|
+
*/
|
|
29
|
+
function walkAst(node, visitor, visited = new WeakSet()) {
|
|
30
|
+
if (visited.has(node))
|
|
31
|
+
return true;
|
|
32
|
+
visited.add(node);
|
|
33
|
+
const shouldContinue = visitor(node);
|
|
34
|
+
if (shouldContinue === false)
|
|
35
|
+
return false;
|
|
36
|
+
const anyNode = node;
|
|
37
|
+
for (const key of Object.keys(anyNode)) {
|
|
38
|
+
if (key === 'parent')
|
|
39
|
+
continue;
|
|
40
|
+
const child = anyNode[key];
|
|
41
|
+
if (Array.isArray(child)) {
|
|
42
|
+
for (const c of child) {
|
|
43
|
+
if (hasNodeStructure(c)) {
|
|
44
|
+
if (!walkAst(c, visitor, visited))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (hasNodeStructure(child)) {
|
|
50
|
+
if (!walkAst(child, visitor, visited))
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
15
56
|
function isUseDynamicCall(node) {
|
|
16
57
|
const { callee, arguments: args } = node;
|
|
17
58
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -126,7 +167,7 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
126
167
|
docs: {
|
|
127
168
|
description: 'Prefer Next.js dynamic() over custom useDynamic() for component imports',
|
|
128
169
|
recommended: 'error',
|
|
129
|
-
requiresTypeChecking:
|
|
170
|
+
requiresTypeChecking: false,
|
|
130
171
|
},
|
|
131
172
|
fixable: 'code',
|
|
132
173
|
schema: [
|
|
@@ -143,9 +184,9 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
143
184
|
},
|
|
144
185
|
],
|
|
145
186
|
messages: {
|
|
146
|
-
preferNextDynamic: '
|
|
147
|
-
addNextDynamicImport: "
|
|
148
|
-
removeUseDynamicImport: 'Remove unused useDynamic import',
|
|
187
|
+
preferNextDynamic: 'Component "{{componentName}}" is created with useDynamic(import(...)), which bypasses Next.js dynamic() handling for client-only components and leaves SSR control to a custom wrapper. Wrap the import in dynamic(() => import(...), { ssr: false }) so Next.js manages code-splitting and disables server rendering safely.',
|
|
188
|
+
addNextDynamicImport: "The auto-fix will replace useDynamic(import(...)) with dynamic(() => import(...), { ssr: false }), which references Next.js's dynamic function. Without importing dynamic from 'next/dynamic', the fixed code will throw a ReferenceError at runtime when the module loads. Add `import dynamic from 'next/dynamic'` at the top of the file to make the dynamic identifier available.",
|
|
189
|
+
removeUseDynamicImport: 'Remove the unused useDynamic import after migrating to dynamic(); leaving the custom hook imported invites accidental reuse and keeps dead code in the bundle.',
|
|
149
190
|
},
|
|
150
191
|
},
|
|
151
192
|
defaultOptions: [{}],
|
|
@@ -154,7 +195,7 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
154
195
|
const allowedUseDynamicSources = options.useDynamicSources && options.useDynamicSources.length > 0
|
|
155
196
|
? options.useDynamicSources
|
|
156
197
|
: DEFAULT_USE_DYNAMIC_SOURCES;
|
|
157
|
-
const sourceCode = context.
|
|
198
|
+
const sourceCode = context.sourceCode;
|
|
158
199
|
return {
|
|
159
200
|
VariableDeclarator(node) {
|
|
160
201
|
const info = inferVariableInfo(node);
|
|
@@ -169,50 +210,18 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
169
210
|
if (!useDynamicImportInfo)
|
|
170
211
|
return;
|
|
171
212
|
const identifierName = info.idText;
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
// Walk AST to find JSX usage; track visited nodes to avoid cycles
|
|
175
|
-
// Note: We intentionally avoid relying on tokens here.
|
|
176
|
-
// Fallback AST traversal for JSX usage
|
|
177
|
-
const visited = new WeakSet();
|
|
178
|
-
const checkJsxUsage = (n) => {
|
|
179
|
-
if (visited.has(n))
|
|
180
|
-
return;
|
|
181
|
-
visited.add(n);
|
|
182
|
-
if (usedInJsx)
|
|
183
|
-
return;
|
|
213
|
+
// walkAst returns false when visitor returns false (early exit on match)
|
|
214
|
+
const usedInJsx = program.body.some((b) => walkAst(b, (n) => {
|
|
184
215
|
if (n.type === utils_1.AST_NODE_TYPES.JSXOpeningElement) {
|
|
185
216
|
const name = n.name;
|
|
186
217
|
if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
187
|
-
if (name.name === identifierName)
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
else if (name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
|
|
191
|
-
// Not matching identifier simple usage
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// recurse
|
|
195
|
-
const anyNode = n;
|
|
196
|
-
for (const key of Object.keys(anyNode)) {
|
|
197
|
-
if (key === 'parent')
|
|
198
|
-
continue;
|
|
199
|
-
const child = anyNode[key];
|
|
200
|
-
if (Array.isArray(child)) {
|
|
201
|
-
for (const c of child) {
|
|
202
|
-
if (c && typeof c.type === 'string') {
|
|
203
|
-
checkJsxUsage(c);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else if (child && typeof child === 'object') {
|
|
208
|
-
if (child.type &&
|
|
209
|
-
typeof child.type === 'string') {
|
|
210
|
-
checkJsxUsage(child);
|
|
218
|
+
if (name.name === identifierName) {
|
|
219
|
+
return false;
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
return undefined;
|
|
224
|
+
}) === false);
|
|
216
225
|
if (!usedInJsx) {
|
|
217
226
|
// Skip to avoid flagging non-component dynamic imports
|
|
218
227
|
return;
|
|
@@ -223,10 +232,11 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
223
232
|
context.report({
|
|
224
233
|
node: init,
|
|
225
234
|
messageId: 'preferNextDynamic',
|
|
235
|
+
data: { componentName: identifierName },
|
|
226
236
|
fix(fixer) {
|
|
227
237
|
const fixes = [];
|
|
228
238
|
// ensure dynamic import is present
|
|
229
|
-
const programNode =
|
|
239
|
+
const programNode = program;
|
|
230
240
|
let dynamicLocal = getNextDynamicLocalName(programNode);
|
|
231
241
|
const hasDynamic = !!dynamicLocal;
|
|
232
242
|
if (!hasDynamic) {
|
|
@@ -266,33 +276,14 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
|
|
|
266
276
|
const latestUseDynamicImport = findUseDynamicImport(programNode, allowedUseDynamicSources);
|
|
267
277
|
if (latestUseDynamicImport) {
|
|
268
278
|
// Abort removal if there are other useDynamic(import(...)) calls in the file
|
|
269
|
-
|
|
270
|
-
const visit = (n) => {
|
|
271
|
-
if (otherUseDynamicCalls)
|
|
272
|
-
return;
|
|
279
|
+
const otherUseDynamicCalls = programNode.body.some((b) => walkAst(b, (n) => {
|
|
273
280
|
if (n.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
274
|
-
isUseDynamicCall(n)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
const anyNode = n;
|
|
279
|
-
for (const key of Object.keys(anyNode)) {
|
|
280
|
-
if (key === 'parent')
|
|
281
|
-
continue;
|
|
282
|
-
const child = anyNode[key];
|
|
283
|
-
if (Array.isArray(child)) {
|
|
284
|
-
for (const c of child)
|
|
285
|
-
if (c && typeof c.type === 'string')
|
|
286
|
-
visit(c);
|
|
287
|
-
}
|
|
288
|
-
else if (child &&
|
|
289
|
-
typeof child === 'object' &&
|
|
290
|
-
child.type) {
|
|
291
|
-
visit(child);
|
|
292
|
-
}
|
|
281
|
+
isUseDynamicCall(n) &&
|
|
282
|
+
n !== init) {
|
|
283
|
+
return false;
|
|
293
284
|
}
|
|
294
|
-
|
|
295
|
-
|
|
285
|
+
return undefined;
|
|
286
|
+
}) === false);
|
|
296
287
|
if (otherUseDynamicCalls) {
|
|
297
288
|
return fixes; // keep the import; other occurrences still rely on it
|
|
298
289
|
}
|
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.preferNullishCoalescingBooleanProps = void 0;
|
|
4
27
|
const utils_1 = require("@typescript-eslint/utils");
|
|
28
|
+
const ts = __importStar(require("typescript"));
|
|
5
29
|
const createRule_1 = require("../utils/createRule");
|
|
6
30
|
const BOOLEAN_PROP_REGEX = /^(is|has|should|can|will|do|does|did|was|were|enable|disable)/;
|
|
31
|
+
function isBooleanType(type, checker) {
|
|
32
|
+
if (type.isUnion()) {
|
|
33
|
+
return type.types.every((t) => isBooleanType(t, checker));
|
|
34
|
+
}
|
|
35
|
+
// For type parameters, check if the constraint is a boolean type
|
|
36
|
+
if (type.getFlags() & ts.TypeFlags.TypeParameter) {
|
|
37
|
+
if (checker) {
|
|
38
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
39
|
+
if (constraint && constraint !== type) {
|
|
40
|
+
return isBooleanType(constraint, checker);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return ((type.getFlags() & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) !==
|
|
46
|
+
0);
|
|
47
|
+
}
|
|
48
|
+
function isPossiblyNullish(type, checker) {
|
|
49
|
+
if (type.isUnion()) {
|
|
50
|
+
return type.types.some((t) => isPossiblyNullish(t, checker));
|
|
51
|
+
}
|
|
52
|
+
// For type parameters, check if the constraint excludes nullish
|
|
53
|
+
if (type.getFlags() & ts.TypeFlags.TypeParameter) {
|
|
54
|
+
if (checker) {
|
|
55
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
56
|
+
if (constraint && constraint !== type) {
|
|
57
|
+
return isPossiblyNullish(constraint, checker);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// If we can't determine the constraint, assume it could be nullish
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return ((type.getFlags() &
|
|
64
|
+
(ts.TypeFlags.Null |
|
|
65
|
+
ts.TypeFlags.Undefined |
|
|
66
|
+
ts.TypeFlags.Void |
|
|
67
|
+
ts.TypeFlags.Any |
|
|
68
|
+
ts.TypeFlags.Unknown)) !==
|
|
69
|
+
0);
|
|
70
|
+
}
|
|
7
71
|
function isInJSXBooleanAttribute(node) {
|
|
8
72
|
const parent = node.parent;
|
|
9
73
|
if (parent?.type !== utils_1.AST_NODE_TYPES.JSXAttribute)
|
|
@@ -57,7 +121,34 @@ function isInConditionalContext(node) {
|
|
|
57
121
|
/**
|
|
58
122
|
* Determines if a node is within a boolean context in JSX props or other boolean contexts
|
|
59
123
|
*/
|
|
60
|
-
function isInBooleanContext(node) {
|
|
124
|
+
function isInBooleanContext(node, checker, parserServices) {
|
|
125
|
+
if (checker && parserServices) {
|
|
126
|
+
try {
|
|
127
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
128
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
129
|
+
if (contextualType && isBooleanType(contextualType, checker)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Also check if the expression itself is a boolean type
|
|
133
|
+
const actualType = checker.getTypeAtLocation(tsNode);
|
|
134
|
+
if (actualType && isBooleanType(actualType, checker)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
// If this is a logical expression, also check if the left operand is a boolean
|
|
138
|
+
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
139
|
+
const leftTSNode = parserServices.esTreeNodeToTSNodeMap.get(node.left);
|
|
140
|
+
const leftType = checker.getTypeAtLocation(leftTSNode);
|
|
141
|
+
if (leftType && isBooleanType(leftType, checker)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// esTreeNodeToTSNodeMap may fail for synthetic nodes or nodes without
|
|
148
|
+
// source positions; getContextualType may return undefined for non-expression
|
|
149
|
+
// contexts. Fall back to AST-based heuristics in these cases.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
61
152
|
let current = node;
|
|
62
153
|
// Traverse up the AST to find if we're in a boolean context
|
|
63
154
|
while (current && current.parent) {
|
|
@@ -365,7 +456,19 @@ function isInBooleanContext(node) {
|
|
|
365
456
|
/**
|
|
366
457
|
* Checks if the left operand could be nullish (null or undefined)
|
|
367
458
|
*/
|
|
368
|
-
function couldBeNullish(node) {
|
|
459
|
+
function couldBeNullish(node, checker, parserServices) {
|
|
460
|
+
if (checker && parserServices) {
|
|
461
|
+
try {
|
|
462
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
463
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
464
|
+
return isPossiblyNullish(type, checker);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// esTreeNodeToTSNodeMap may fail for synthetic nodes or nodes without
|
|
468
|
+
// source positions; getTypeAtLocation may throw for nodes without
|
|
469
|
+
// type information. Fall back to manual check.
|
|
470
|
+
}
|
|
471
|
+
}
|
|
369
472
|
// For literals, check the actual value
|
|
370
473
|
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
371
474
|
return node.value === null || node.value === undefined;
|
|
@@ -396,29 +499,36 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
396
499
|
},
|
|
397
500
|
fixable: 'code',
|
|
398
501
|
messages: {
|
|
399
|
-
preferNullishCoalescing: '
|
|
502
|
+
preferNullishCoalescing: 'Logical OR between "{{left}}" and "{{right}}" treats every falsy value (false, 0, "", NaN) as missing and will override intentional boolean or empty states. Use the nullish coalescing operator (??) so "{{right}}" only applies when "{{left}}" is null or undefined, preserving explicit false/0/"" values.',
|
|
400
503
|
},
|
|
401
504
|
schema: [],
|
|
402
505
|
},
|
|
403
506
|
defaultOptions: [],
|
|
404
507
|
create(context) {
|
|
508
|
+
const parserServices = utils_1.ESLintUtils.getParserServices(context, true);
|
|
509
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
405
510
|
return {
|
|
406
511
|
LogicalExpression(node) {
|
|
407
512
|
if (node.operator === '||') {
|
|
408
513
|
// If the node is in a boolean context, we allow logical OR
|
|
409
|
-
if (isInBooleanContext(node)) {
|
|
514
|
+
if (isInBooleanContext(node, checker, parserServices)) {
|
|
410
515
|
return;
|
|
411
516
|
}
|
|
412
517
|
// Check if this could benefit from nullish coalescing
|
|
413
518
|
// We only suggest nullish coalescing when the left operand could be nullish
|
|
414
|
-
if (couldBeNullish(node.left)) {
|
|
519
|
+
if (couldBeNullish(node.left, checker, parserServices)) {
|
|
520
|
+
const sourceCode = context.getSourceCode();
|
|
521
|
+
const leftText = sourceCode.getText(node.left);
|
|
522
|
+
const rightText = sourceCode.getText(node.right);
|
|
415
523
|
context.report({
|
|
416
524
|
node,
|
|
417
525
|
messageId: 'preferNullishCoalescing',
|
|
526
|
+
data: {
|
|
527
|
+
left: leftText,
|
|
528
|
+
right: rightText,
|
|
529
|
+
},
|
|
418
530
|
fix(fixer) {
|
|
419
|
-
return fixer.replaceText(node, `${
|
|
420
|
-
.getSourceCode()
|
|
421
|
-
.getText(node.right)}`);
|
|
531
|
+
return fixer.replaceText(node, `${leftText} ?? ${rightText}`);
|
|
422
532
|
},
|
|
423
533
|
});
|
|
424
534
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
/**
|
|
2
3
|
* This rule overrides the behavior of @typescript-eslint/prefer-nullish-coalescing
|
|
3
4
|
* to only suggest using the nullish coalescing operator when checking for null/undefined,
|
|
4
5
|
* not when intentionally checking for all falsy values.
|
|
5
6
|
*/
|
|
6
|
-
export declare const preferNullishCoalescingOverride:
|
|
7
|
+
export declare const preferNullishCoalescingOverride: TSESLint.RuleModule<"preferNullishCoalescing", [], TSESLint.RuleListener>;
|
|
@@ -19,11 +19,13 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
|
|
|
19
19
|
fixable: 'code',
|
|
20
20
|
schema: [],
|
|
21
21
|
messages: {
|
|
22
|
-
preferNullishCoalescing: '
|
|
22
|
+
preferNullishCoalescing: 'Replace "{{left}} || {{right}}" with nullish coalescing when you only want a fallback for null or undefined. The logical OR operator treats "", 0, and false as missing values and silently swaps in the fallback, which hides valid data. Use "{{left}} ?? {{right}}" so only nullish inputs trigger the fallback.',
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
defaultOptions: [],
|
|
26
26
|
create(context) {
|
|
27
|
+
const contextWithSource = context;
|
|
28
|
+
const sourceCode = contextWithSource.sourceCode ?? contextWithSource.getSourceCode();
|
|
27
29
|
/**
|
|
28
30
|
* Checks if a node is in a boolean context where truthiness matters
|
|
29
31
|
*/
|
|
@@ -154,7 +156,8 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
|
|
|
154
156
|
/**
|
|
155
157
|
* Checks if this logical OR should be converted to nullish coalescing
|
|
156
158
|
*/
|
|
157
|
-
function shouldConvertToNullishCoalescing(
|
|
159
|
+
function shouldConvertToNullishCoalescing(node) {
|
|
160
|
+
void node;
|
|
158
161
|
return false;
|
|
159
162
|
}
|
|
160
163
|
void isInBooleanContext;
|
|
@@ -164,12 +167,15 @@ exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
|
|
|
164
167
|
return {
|
|
165
168
|
LogicalExpression(node) {
|
|
166
169
|
if (shouldConvertToNullishCoalescing(node)) {
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const rightText = sc.getText(node.right);
|
|
170
|
+
const leftText = sourceCode.getText(node.left);
|
|
171
|
+
const rightText = sourceCode.getText(node.right);
|
|
170
172
|
context.report({
|
|
171
173
|
node,
|
|
172
174
|
messageId: 'preferNullishCoalescing',
|
|
175
|
+
data: {
|
|
176
|
+
left: leftText,
|
|
177
|
+
right: rightText,
|
|
178
|
+
},
|
|
173
179
|
fix(fixer) {
|
|
174
180
|
return fixer.replaceText(node, `${leftText} ?? ${rightText}`);
|
|
175
181
|
},
|