eslint-plugin-ts-data-forge 0.10.0 → 0.10.2
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/dist/plugin.d.mts +1 -1
- package/dist/rule-types.d.mts +1 -1
- package/dist/rules/ast-utils.d.mts +0 -16
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +0 -36
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +1 -8
- package/dist/rules/no-side-effect-import.d.mts.map +1 -1
- package/dist/rules/no-side-effect-import.mjs +7 -43
- package/dist/rules/no-side-effect-import.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.d.mts +1 -5
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +5 -83
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts +1 -5
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +4 -184
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -5
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +4 -219
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -15
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -123
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -13
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +6 -121
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -1
- package/package.json +6 -6
- package/src/rule-types.mts +1 -1
- package/src/rules/ast-utils.mts +0 -51
- package/src/rules/no-side-effect-import.mts +7 -61
- package/src/rules/prefer-arr-is-array.mts +5 -122
- package/src/rules/prefer-arr-is-array.test.mts +50 -7
- package/src/rules/prefer-is-non-null-object.mts +4 -274
- package/src/rules/prefer-is-record-and-has-key.mts +4 -318
- package/src/rules/prefer-num-safe-parse-float.mts +6 -193
- package/src/rules/prefer-num-safe-parse-float.test.mts +18 -0
- package/src/rules/prefer-num-safe-parse-int.mts +6 -192
- package/src/rules/prefer-num-safe-parse-int.test.mts +18 -0
package/src/rules/ast-utils.mts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
typeWrapperLayers,
|
|
11
11
|
} from 'eslint-rule-forge';
|
|
12
12
|
import type { DeepReadonly } from 'ts-type-forge';
|
|
13
|
-
import type * as ts from 'typescript';
|
|
14
13
|
|
|
15
14
|
const isIntegerLiteral = (node: DeepReadonly<TSESTree.Expression>): boolean =>
|
|
16
15
|
node.type === AST_NODE_TYPES.Literal &&
|
|
@@ -265,53 +264,3 @@ const isVariableDefinition = (
|
|
|
265
264
|
def: TSESLint.Scope.Definition,
|
|
266
265
|
): def is TSESLint.Scope.Definition & VariableDefinition =>
|
|
267
266
|
(def.type as string) === 'Variable';
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
271
|
-
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
272
|
-
*/
|
|
273
|
-
export const isIdentifierNamed = (
|
|
274
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
275
|
-
node: TSESTree.Node,
|
|
276
|
-
name: string,
|
|
277
|
-
): boolean => {
|
|
278
|
-
const unwrapped = skipTypeWrappers(node);
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name
|
|
282
|
-
);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Whether `holds` is true of the type of every layer of type wrappers around
|
|
287
|
-
* `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
|
|
288
|
-
* without type information. A widening cast (`s as string | number`) is the
|
|
289
|
-
* author saying the value may be more than it is, and is respected; a
|
|
290
|
-
* narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
|
|
291
|
-
* value's own type does not, and the fix, which keeps the wrapper, has to
|
|
292
|
-
* type-check at every layer too.
|
|
293
|
-
*/
|
|
294
|
-
export const typeHoldsAtEveryLayer = (
|
|
295
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
296
|
-
sourceCode: TSESLint.SourceCode,
|
|
297
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
298
|
-
node: TSESTree.Node,
|
|
299
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
300
|
-
holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
|
|
301
|
-
): boolean => {
|
|
302
|
-
const services = sourceCode.parserServices;
|
|
303
|
-
|
|
304
|
-
const checker = services?.program?.getTypeChecker();
|
|
305
|
-
|
|
306
|
-
if (checker === undefined) {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return typeWrapperLayers(node).every((layer) => {
|
|
311
|
-
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
};
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Arr } from 'ts-data-forge';
|
|
1
|
+
import { createNoSideEffectImport } from 'eslint-rule-forge';
|
|
3
2
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
4
3
|
|
|
5
|
-
type Options = readonly [];
|
|
6
|
-
|
|
7
|
-
type MessageIds = 'removeSideEffectImport';
|
|
8
|
-
|
|
9
4
|
/**
|
|
10
5
|
* `import 'ts-data-forge';` binds nothing and runs nothing: every export is a
|
|
11
6
|
* pure value or function, and the package declares `sideEffects: false`, which
|
|
@@ -13,59 +8,10 @@ type MessageIds = 'removeSideEffectImport';
|
|
|
13
8
|
* a side-effect import in the emitted JavaScript — that is the one kind it
|
|
14
9
|
* never elides — so what is left is a module loaded for no reason, and a
|
|
15
10
|
* bundler that trusts the flag drops it anyway.
|
|
16
|
-
*
|
|
17
|
-
* The rule matches the bare specifier exactly; an import of some other module
|
|
18
|
-
* is none of its business.
|
|
19
11
|
*/
|
|
20
|
-
export const noSideEffectImport
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fixable: 'code',
|
|
27
|
-
schema: [],
|
|
28
|
-
messages: {
|
|
29
|
-
removeSideEffectImport: `\`import '${TS_DATA_FORGE_MODULE}';\` binds no name, and \`${TS_DATA_FORGE_MODULE}\` declares \`sideEffects: false\`, so loading it does nothing. Remove it, and import what you need by name.`,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
create: (context) => ({
|
|
34
|
-
ImportDeclaration: (node) => {
|
|
35
|
-
if (
|
|
36
|
-
node.source.value !== TS_DATA_FORGE_MODULE ||
|
|
37
|
-
!Arr.isEmpty(node.specifiers)
|
|
38
|
-
) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
context.report({
|
|
43
|
-
node,
|
|
44
|
-
messageId: 'removeSideEffectImport',
|
|
45
|
-
fix: (fixer) => removeStatement(fixer, context.sourceCode, node),
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
}),
|
|
49
|
-
defaultOptions: [],
|
|
50
|
-
} as const;
|
|
51
|
-
|
|
52
|
-
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Removes a statement along with the rest of its line, so that deleting it does
|
|
56
|
-
* not leave a blank line where it was.
|
|
57
|
-
*/
|
|
58
|
-
const removeStatement = (
|
|
59
|
-
fixer: TSESLint.RuleFixer,
|
|
60
|
-
sourceCode: TSESLint.SourceCode,
|
|
61
|
-
node: TSESTree.Node,
|
|
62
|
-
): TSESLint.RuleFix => {
|
|
63
|
-
const restOfLine = /^[^\S\n]*\n/u.exec(
|
|
64
|
-
sourceCode.getText().slice(node.range[1]),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return fixer.removeRange([
|
|
68
|
-
node.range[0],
|
|
69
|
-
node.range[1] + (restOfLine?.[0].length ?? 0),
|
|
70
|
-
]);
|
|
71
|
-
};
|
|
12
|
+
export const noSideEffectImport = createNoSideEffectImport({
|
|
13
|
+
moduleName: TS_DATA_FORGE_MODULE,
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
description: `Remove a side-effect-only \`import '${TS_DATA_FORGE_MODULE}';\`, which binds no name and, the package being \`sideEffects: false\`, does nothing.`,
|
|
16
|
+
message: `\`import '${TS_DATA_FORGE_MODULE}';\` binds no name, and \`${TS_DATA_FORGE_MODULE}\` declares \`sideEffects: false\`, so loading it does nothing. Remove it, and import what you need by name.`,
|
|
17
|
+
});
|
|
@@ -1,124 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
type TSESLint,
|
|
4
|
-
type TSESTree,
|
|
5
|
-
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
|
-
getImportDeclarations,
|
|
9
|
-
getNamedImports,
|
|
10
|
-
skipTypeWrappers,
|
|
11
|
-
} from 'eslint-rule-forge';
|
|
1
|
+
import { createPreferArrayIsArray } from 'eslint-rule-forge';
|
|
12
2
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
13
3
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
19
|
-
meta: {
|
|
20
|
-
type: 'suggestion',
|
|
21
|
-
docs: {
|
|
22
|
-
description:
|
|
23
|
-
'Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.',
|
|
24
|
-
},
|
|
25
|
-
fixable: 'code',
|
|
26
|
-
schema: [],
|
|
27
|
-
messages: {
|
|
28
|
-
useArrIsArray:
|
|
29
|
-
'Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
create: (context) => {
|
|
34
|
-
const sourceCode = context.sourceCode;
|
|
35
|
-
|
|
36
|
-
const program = sourceCode.ast;
|
|
37
|
-
|
|
38
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
39
|
-
program,
|
|
40
|
-
TS_DATA_FORGE_MODULE,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const mut_nodesToFix: TSESTree.CallExpression[] = [];
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
CallExpression: (node) => {
|
|
47
|
-
if (!isArrayIsArrayCall(node)) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
mut_nodesToFix.push(node);
|
|
52
|
-
},
|
|
53
|
-
'Program:exit': () => {
|
|
54
|
-
// Check if Arr is already imported
|
|
55
|
-
const hasArrImport =
|
|
56
|
-
getNamedImports(tsDataForgeImports).includes('Arr');
|
|
57
|
-
|
|
58
|
-
// Note: We add import only for the first node to avoid conflicts when
|
|
59
|
-
// multiple fixes try to insert at the same position. This means if the
|
|
60
|
-
// first node is disabled via eslint-disable comment, no import will be
|
|
61
|
-
// added.
|
|
62
|
-
for (const [index, node] of mut_nodesToFix.entries()) {
|
|
63
|
-
context.report({
|
|
64
|
-
node,
|
|
65
|
-
messageId: 'useArrIsArray',
|
|
66
|
-
fix: (fixer) => {
|
|
67
|
-
const callee = node.callee;
|
|
68
|
-
|
|
69
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Only the callee is replaced: parentheses the source put around
|
|
74
|
-
// it are outside its range, and stay balanced.
|
|
75
|
-
// Add import only for the first node and only if not already imported
|
|
76
|
-
const importFixes =
|
|
77
|
-
index === 0 && !hasArrImport
|
|
78
|
-
? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
79
|
-
'Arr',
|
|
80
|
-
])
|
|
81
|
-
: [];
|
|
82
|
-
|
|
83
|
-
return [...importFixes, fixer.replaceText(callee, 'Arr.isArray')];
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
defaultOptions: [],
|
|
91
|
-
} as const;
|
|
92
|
-
|
|
93
|
-
const isArrayIsArrayCall = (
|
|
94
|
-
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
95
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
96
|
-
node: TSESTree.CallExpression,
|
|
97
|
-
): boolean => {
|
|
98
|
-
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const { property } = node.callee;
|
|
103
|
-
|
|
104
|
-
// `(Array as ArrayConstructor).isArray` is still `Array.isArray`.
|
|
105
|
-
const object = skipTypeWrappers(node.callee.object);
|
|
106
|
-
|
|
107
|
-
if (object.type !== AST_NODE_TYPES.Identifier) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (object.name !== 'Array') {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (property.type !== AST_NODE_TYPES.Identifier) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (property.name !== 'isArray') {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return true;
|
|
124
|
-
};
|
|
4
|
+
export const preferArrIsArray = createPreferArrayIsArray({
|
|
5
|
+
moduleName: TS_DATA_FORGE_MODULE,
|
|
6
|
+
namespace: 'Arr',
|
|
7
|
+
});
|
|
@@ -20,6 +20,13 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
20
20
|
const result = obj.isArray(value);
|
|
21
21
|
`,
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
name: 'A computed member named by a variable is not `isArray`',
|
|
25
|
+
code: dedent`
|
|
26
|
+
declare const isArray: 'from';
|
|
27
|
+
const result = Array[isArray](value);
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
23
30
|
],
|
|
24
31
|
invalid: [
|
|
25
32
|
{
|
|
@@ -31,7 +38,7 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
31
38
|
import { Arr } from 'ts-data-forge';
|
|
32
39
|
const result = Arr.isArray(value);
|
|
33
40
|
`,
|
|
34
|
-
errors: [{ messageId: '
|
|
41
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
35
42
|
},
|
|
36
43
|
{
|
|
37
44
|
name: 'Arr imported by a second ts-data-forge declaration is not imported again',
|
|
@@ -45,7 +52,7 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
45
52
|
import { Arr } from 'ts-data-forge';
|
|
46
53
|
const result = Arr.isArray(value);
|
|
47
54
|
`,
|
|
48
|
-
errors: [{ messageId: '
|
|
55
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
49
56
|
},
|
|
50
57
|
{
|
|
51
58
|
name: 'Replace Array.isArray with Arr.isArray when Arr is already imported',
|
|
@@ -57,7 +64,7 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
57
64
|
import { Arr } from 'ts-data-forge';
|
|
58
65
|
const result = Arr.isArray(value);
|
|
59
66
|
`,
|
|
60
|
-
errors: [{ messageId: '
|
|
67
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
61
68
|
},
|
|
62
69
|
{
|
|
63
70
|
name: 'Replace multiple Array.isArray calls',
|
|
@@ -70,7 +77,7 @@ ruleTester.run('prefer-arr-is-array', preferArrIsArray, {
|
|
|
70
77
|
const result1 = Arr.isArray(value1);
|
|
71
78
|
const result2 = Arr.isArray(value2);
|
|
72
79
|
`,
|
|
73
|
-
errors: [{ messageId: '
|
|
80
|
+
errors: [{ messageId: 'useIsArray' }, { messageId: 'useIsArray' }],
|
|
74
81
|
},
|
|
75
82
|
],
|
|
76
83
|
});
|
|
@@ -89,7 +96,31 @@ describe('prefer-arr-is-array through type wrappers', () => {
|
|
|
89
96
|
import { Arr } from 'ts-data-forge';
|
|
90
97
|
const result = Arr.isArray(value);
|
|
91
98
|
`,
|
|
92
|
-
errors: [{ messageId: '
|
|
99
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'Array reached through `!`',
|
|
103
|
+
code: dedent`
|
|
104
|
+
import { Arr } from 'ts-data-forge';
|
|
105
|
+
const result = Array!.isArray(value);
|
|
106
|
+
`,
|
|
107
|
+
output: dedent`
|
|
108
|
+
import { Arr } from 'ts-data-forge';
|
|
109
|
+
const result = Arr.isArray(value);
|
|
110
|
+
`,
|
|
111
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'the callee reached through `satisfies`',
|
|
115
|
+
code: dedent`
|
|
116
|
+
import { Arr } from 'ts-data-forge';
|
|
117
|
+
const result = (Array.isArray satisfies unknown)(value);
|
|
118
|
+
`,
|
|
119
|
+
output: dedent`
|
|
120
|
+
import { Arr } from 'ts-data-forge';
|
|
121
|
+
const result = Arr.isArray(value);
|
|
122
|
+
`,
|
|
123
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
93
124
|
},
|
|
94
125
|
],
|
|
95
126
|
});
|
|
@@ -107,9 +138,21 @@ describe('prefer-arr-is-array with parenthesized operands', () => {
|
|
|
107
138
|
`,
|
|
108
139
|
output: dedent`
|
|
109
140
|
import { Arr } from 'ts-data-forge';
|
|
110
|
-
const result =
|
|
141
|
+
const result = Arr.isArray(value);
|
|
142
|
+
`,
|
|
143
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'a doubly parenthesized callee of an optional call',
|
|
147
|
+
code: dedent`
|
|
148
|
+
import { Arr } from 'ts-data-forge';
|
|
149
|
+
const result = ((Array.isArray))?.(value);
|
|
150
|
+
`,
|
|
151
|
+
output: dedent`
|
|
152
|
+
import { Arr } from 'ts-data-forge';
|
|
153
|
+
const result = Arr.isArray?.(value);
|
|
111
154
|
`,
|
|
112
|
-
errors: [{ messageId: '
|
|
155
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
113
156
|
},
|
|
114
157
|
],
|
|
115
158
|
});
|
|
@@ -1,276 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
type TSESLint,
|
|
4
|
-
type TSESTree,
|
|
5
|
-
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
|
-
getImportDeclarations,
|
|
9
|
-
getNamedImports,
|
|
10
|
-
getRangeWithParens,
|
|
11
|
-
parenthesesDepth,
|
|
12
|
-
skipTypeWrappers,
|
|
13
|
-
} from 'eslint-rule-forge';
|
|
1
|
+
import { createPreferIsNonNullObject } from 'eslint-rule-forge';
|
|
14
2
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
15
3
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
21
|
-
meta: {
|
|
22
|
-
type: 'suggestion',
|
|
23
|
-
docs: {
|
|
24
|
-
description:
|
|
25
|
-
'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-data-forge.',
|
|
26
|
-
},
|
|
27
|
-
fixable: 'code',
|
|
28
|
-
schema: [],
|
|
29
|
-
messages: {
|
|
30
|
-
useIsNonNullObject:
|
|
31
|
-
'Replace the object/null check with `isNonNullObject()` from ts-data-forge.',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
create: (context) => {
|
|
36
|
-
const sourceCode = context.sourceCode;
|
|
37
|
-
|
|
38
|
-
const program = sourceCode.ast;
|
|
39
|
-
|
|
40
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
41
|
-
program,
|
|
42
|
-
TS_DATA_FORGE_MODULE,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
LogicalExpression: (node) => {
|
|
49
|
-
// A chain is read once, from its outermost `&&`.
|
|
50
|
-
if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
|
|
55
|
-
},
|
|
56
|
-
'Program:exit': () => {
|
|
57
|
-
const hasIsNonNullObjectImport =
|
|
58
|
-
getNamedImports(tsDataForgeImports).includes('isNonNullObject');
|
|
59
|
-
|
|
60
|
-
// The import fix lives on the first report only:
|
|
61
|
-
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
62
|
-
// otherwise and only one of them would survive a pass. A first report
|
|
63
|
-
// silenced by an `eslint-disable` comment therefore takes the import
|
|
64
|
-
// with it.
|
|
65
|
-
for (const [
|
|
66
|
-
index,
|
|
67
|
-
{ range, identifierName },
|
|
68
|
-
] of mut_pairsToFix.entries()) {
|
|
69
|
-
context.report({
|
|
70
|
-
loc: {
|
|
71
|
-
start: sourceCode.getLocFromIndex(range[0]),
|
|
72
|
-
end: sourceCode.getLocFromIndex(range[1]),
|
|
73
|
-
},
|
|
74
|
-
messageId: 'useIsNonNullObject',
|
|
75
|
-
fix: (fixer) => {
|
|
76
|
-
const replacement = `isNonNullObject(${identifierName})`;
|
|
77
|
-
|
|
78
|
-
const importFixes =
|
|
79
|
-
index === 0 && !hasIsNonNullObjectImport
|
|
80
|
-
? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
81
|
-
'isNonNullObject',
|
|
82
|
-
])
|
|
83
|
-
: [];
|
|
84
|
-
|
|
85
|
-
return [
|
|
86
|
-
...importFixes,
|
|
87
|
-
fixer.replaceTextRange(range, replacement),
|
|
88
|
-
];
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
defaultOptions: [],
|
|
96
|
-
} as const;
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* A `typeof u === 'object'` and a `u !== null` next to each other in an `&&`
|
|
100
|
-
* chain, as the source range from the first to the second (parentheses
|
|
101
|
-
* included) and the name of `u`.
|
|
102
|
-
*/
|
|
103
|
-
type NonNullObjectPair = Readonly<{
|
|
104
|
-
range: TSESTree.Range;
|
|
105
|
-
identifierName: string;
|
|
106
|
-
}>;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Whether `node` is an operand of an enclosing `&&` chain: a `&&` written
|
|
110
|
-
* directly under another one, without parentheses.
|
|
111
|
-
*/
|
|
112
|
-
const isChainOperand = (
|
|
113
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
114
|
-
sourceCode: TSESLint.SourceCode,
|
|
115
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
116
|
-
node: TSESTree.LogicalExpression,
|
|
117
|
-
): boolean =>
|
|
118
|
-
node.parent.type === AST_NODE_TYPES.LogicalExpression &&
|
|
119
|
-
node.parent.operator === '&&' &&
|
|
120
|
-
parenthesesDepth(sourceCode, node) === 0;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
|
|
124
|
-
* two halves may come in either order, and the pair may have operands before
|
|
125
|
-
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
126
|
-
* be next to each other, so that the pair is one range of the source.
|
|
127
|
-
*/
|
|
128
|
-
const findNonNullObjectPairs = (
|
|
129
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
130
|
-
sourceCode: TSESLint.SourceCode,
|
|
131
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
132
|
-
node: TSESTree.LogicalExpression,
|
|
133
|
-
): readonly NonNullObjectPair[] => {
|
|
134
|
-
const operands = chainOperands(sourceCode, node);
|
|
135
|
-
|
|
136
|
-
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
137
|
-
const first = operands[index];
|
|
138
|
-
|
|
139
|
-
const second = operands[index + 1];
|
|
140
|
-
|
|
141
|
-
if (first === undefined || second === undefined) {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const identifierName =
|
|
146
|
-
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
147
|
-
|
|
148
|
-
return identifierName === undefined
|
|
149
|
-
? pairsFrom(index + 1)
|
|
150
|
-
: [
|
|
151
|
-
{
|
|
152
|
-
range: [
|
|
153
|
-
getRangeWithParens(sourceCode, first)[0],
|
|
154
|
-
getRangeWithParens(sourceCode, second)[1],
|
|
155
|
-
],
|
|
156
|
-
identifierName,
|
|
157
|
-
},
|
|
158
|
-
...pairsFrom(index + 2),
|
|
159
|
-
];
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
return pairsFrom(0);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
|
|
167
|
-
* is one operand: its halves cannot be replaced without its parentheses.
|
|
168
|
-
*/
|
|
169
|
-
const chainOperands = (
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
171
|
-
sourceCode: TSESLint.SourceCode,
|
|
172
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
173
|
-
node: TSESTree.LogicalExpression,
|
|
174
|
-
): readonly TSESTree.Expression[] =>
|
|
175
|
-
[node.left, node.right].flatMap((operand) =>
|
|
176
|
-
operand.type === AST_NODE_TYPES.LogicalExpression &&
|
|
177
|
-
operand.operator === '&&' &&
|
|
178
|
-
parenthesesDepth(sourceCode, operand) === 0
|
|
179
|
-
? chainOperands(sourceCode, operand)
|
|
180
|
-
: [operand],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
184
|
-
// unknown) === 'object' && u! !== null` is the same check.
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and
|
|
188
|
-
* `nullCheck` is `u !== null` for the same `u`.
|
|
189
|
-
*/
|
|
190
|
-
const pairIdentifierName = (
|
|
191
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
192
|
-
typeofCheck: TSESTree.Expression,
|
|
193
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
194
|
-
nullCheck: TSESTree.Expression,
|
|
195
|
-
): string | undefined => {
|
|
196
|
-
const identifierName = getTypeofObjectIdentifierName(
|
|
197
|
-
skipTypeWrappers(typeofCheck),
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
return identifierName !== undefined &&
|
|
201
|
-
identifierName ===
|
|
202
|
-
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
203
|
-
? identifierName
|
|
204
|
-
: undefined;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
/** `u` of `typeof u === 'object'`, in either operand order. */
|
|
208
|
-
const getTypeofObjectIdentifierName = (
|
|
209
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
210
|
-
node: TSESTree.Node,
|
|
211
|
-
): string | undefined => {
|
|
212
|
-
if (
|
|
213
|
-
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
214
|
-
node.operator !== '==='
|
|
215
|
-
) {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const left = skipTypeWrappers(node.left);
|
|
220
|
-
|
|
221
|
-
const right = skipTypeWrappers(node.right);
|
|
222
|
-
|
|
223
|
-
const typeofSide = isLiteralOf(right, 'object')
|
|
224
|
-
? left
|
|
225
|
-
: isLiteralOf(left, 'object')
|
|
226
|
-
? right
|
|
227
|
-
: undefined;
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
231
|
-
typeofSide.operator !== 'typeof'
|
|
232
|
-
) {
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const argument = skipTypeWrappers(typeofSide.argument);
|
|
237
|
-
|
|
238
|
-
return argument.type === AST_NODE_TYPES.Identifier
|
|
239
|
-
? argument.name
|
|
240
|
-
: undefined;
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* `u` of `u !== null`, in either operand order. `u != null` also excludes
|
|
245
|
-
* `undefined`, which `typeof u === 'object'` has already excluded, so it is
|
|
246
|
-
* the same check here.
|
|
247
|
-
*/
|
|
248
|
-
const getNonNullCheckIdentifierName = (
|
|
249
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
250
|
-
node: TSESTree.Node,
|
|
251
|
-
): string | undefined => {
|
|
252
|
-
if (
|
|
253
|
-
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
254
|
-
(node.operator !== '!==' && node.operator !== '!=')
|
|
255
|
-
) {
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const left = skipTypeWrappers(node.left);
|
|
260
|
-
|
|
261
|
-
const right = skipTypeWrappers(node.right);
|
|
262
|
-
|
|
263
|
-
const checked = isLiteralOf(right, null)
|
|
264
|
-
? left
|
|
265
|
-
: isLiteralOf(left, null)
|
|
266
|
-
? right
|
|
267
|
-
: undefined;
|
|
268
|
-
|
|
269
|
-
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const isLiteralOf = (
|
|
273
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
274
|
-
node: TSESTree.Node,
|
|
275
|
-
value: string | null,
|
|
276
|
-
): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
4
|
+
export const preferIsNonNullObject = createPreferIsNonNullObject({
|
|
5
|
+
moduleName: TS_DATA_FORGE_MODULE,
|
|
6
|
+
});
|