eslint-plugin-ts-data-forge 0.1.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/LICENSE +201 -0
- package/README.md +57 -0
- package/package.json +74 -0
- package/src/index.mts +7 -0
- package/src/plugin.mts +9 -0
- package/src/rule-types.mts +326 -0
- package/src/rules/ast-utils.mts +169 -0
- package/src/rules/branded-number-types.mts +47 -0
- package/src/rules/constants.mts +1 -0
- package/src/rules/import-utils.mts +152 -0
- package/src/rules/index.mts +1 -0
- package/src/rules/no-unnecessary-type-guard.mts +515 -0
- package/src/rules/no-unnecessary-type-guard.test.mts +368 -0
- package/src/rules/prefer-arr-is-array.mts +98 -0
- package/src/rules/prefer-arr-is-array.test.mts +62 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +143 -0
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +154 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +149 -0
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +214 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +188 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +141 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +202 -0
- package/src/rules/prefer-arr-is-non-empty.mts +131 -0
- package/src/rules/prefer-arr-is-non-empty.test.mts +90 -0
- package/src/rules/prefer-arr-sum.mts +269 -0
- package/src/rules/prefer-arr-sum.test.mts +171 -0
- package/src/rules/prefer-as-int.mts +131 -0
- package/src/rules/prefer-as-int.test.mts +267 -0
- package/src/rules/prefer-canonical-array-slicing.mts +800 -0
- package/src/rules/prefer-canonical-array-slicing.test.mts +588 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +141 -0
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +171 -0
- package/src/rules/prefer-is-non-null-object.mts +145 -0
- package/src/rules/prefer-is-non-null-object.test.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.mts +156 -0
- package/src/rules/prefer-is-record-and-has-key.test.mts +217 -0
- package/src/rules/prefer-num-safe-parse-float.mts +166 -0
- package/src/rules/prefer-num-safe-parse-float.test.mts +163 -0
- package/src/rules/prefer-num-safe-parse-int.mts +166 -0
- package/src/rules/prefer-num-safe-parse-int.test.mts +137 -0
- package/src/rules/prefer-range-for-loop.mts +179 -0
- package/src/rules/prefer-range-for-loop.test.mts +156 -0
- package/src/rules/rules.mts +36 -0
- package/src/types.mts +4 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import { buildCalleeResolver, getTsDataForgeImport } from './import-utils.mjs';
|
|
8
|
+
|
|
9
|
+
type Options = readonly [];
|
|
10
|
+
|
|
11
|
+
type MessageIds = 'preferComparison';
|
|
12
|
+
|
|
13
|
+
type ComparisonSpec = Readonly<{
|
|
14
|
+
op: '!==' | '===';
|
|
15
|
+
literal: 'null' | 'undefined';
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The nullish guards that are meant for point-free use (e.g.
|
|
20
|
+
* `xs.filter(isNotUndefined)`). When called with an explicit argument a direct
|
|
21
|
+
* comparison is clearer, so map each to its equivalent operator and literal.
|
|
22
|
+
*/
|
|
23
|
+
const COMPARISON_GUARDS: DeepReadonly<Record<string, ComparisonSpec>> = {
|
|
24
|
+
isUndefined: { op: '===', literal: 'undefined' },
|
|
25
|
+
isNotUndefined: { op: '!==', literal: 'undefined' },
|
|
26
|
+
isNull: { op: '===', literal: 'null' },
|
|
27
|
+
isNotNull: { op: '!==', literal: 'null' },
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Argument expression kinds whose precedence is lower than equality, so they
|
|
32
|
+
* must be parenthesized when placed on the left of `===` / `!==`.
|
|
33
|
+
*/
|
|
34
|
+
const ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([
|
|
35
|
+
AST_NODE_TYPES.LogicalExpression,
|
|
36
|
+
AST_NODE_TYPES.ConditionalExpression,
|
|
37
|
+
AST_NODE_TYPES.AssignmentExpression,
|
|
38
|
+
AST_NODE_TYPES.SequenceExpression,
|
|
39
|
+
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
40
|
+
AST_NODE_TYPES.YieldExpression,
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns `true` when the comparison must be wrapped in parentheses to preserve
|
|
45
|
+
* the original grouping given the call expression's parent context.
|
|
46
|
+
*/
|
|
47
|
+
const needsWrappingInParent = (
|
|
48
|
+
node: DeepReadonly<TSESTree.CallExpression>,
|
|
49
|
+
): boolean => {
|
|
50
|
+
const { parent } = node;
|
|
51
|
+
|
|
52
|
+
// `parent.type` is one of 130+ AST kinds; only a handful need wrapping.
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
54
|
+
switch (parent.type) {
|
|
55
|
+
case AST_NODE_TYPES.UnaryExpression:
|
|
56
|
+
case AST_NODE_TYPES.AwaitExpression:
|
|
57
|
+
case AST_NODE_TYPES.BinaryExpression:
|
|
58
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
59
|
+
return true;
|
|
60
|
+
|
|
61
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
62
|
+
return parent.object === node;
|
|
63
|
+
|
|
64
|
+
case AST_NODE_TYPES.CallExpression:
|
|
65
|
+
case AST_NODE_TYPES.NewExpression:
|
|
66
|
+
return parent.callee === node;
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
|
|
74
|
+
MessageIds,
|
|
75
|
+
Options
|
|
76
|
+
> = {
|
|
77
|
+
meta: {
|
|
78
|
+
type: 'suggestion',
|
|
79
|
+
docs: {
|
|
80
|
+
description:
|
|
81
|
+
'Prefer a direct `=== null` / `!== undefined` comparison over calling `isNull`, `isNotNull`, `isUndefined`, or `isNotUndefined` with an explicit argument (those guards are intended for point-free use such as `xs.filter(isNotUndefined)`).',
|
|
82
|
+
},
|
|
83
|
+
fixable: 'code',
|
|
84
|
+
schema: [],
|
|
85
|
+
messages: {
|
|
86
|
+
preferComparison:
|
|
87
|
+
'Prefer `{{comparison}}` over calling `{{name}}` with an explicit argument.',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
create: (context) => {
|
|
92
|
+
const sourceCode = context.sourceCode;
|
|
93
|
+
|
|
94
|
+
const resolveGuard = buildCalleeResolver(
|
|
95
|
+
getTsDataForgeImport(sourceCode.ast),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
CallExpression: (node) => {
|
|
100
|
+
if (node.arguments.length !== 1) return;
|
|
101
|
+
|
|
102
|
+
const argument = node.arguments[0];
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
argument === undefined ||
|
|
106
|
+
argument.type === AST_NODE_TYPES.SpreadElement
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolved = resolveGuard(node.callee);
|
|
112
|
+
|
|
113
|
+
if (resolved === undefined) return;
|
|
114
|
+
|
|
115
|
+
const spec = COMPARISON_GUARDS[resolved.canonicalName];
|
|
116
|
+
|
|
117
|
+
if (spec === undefined) return;
|
|
118
|
+
|
|
119
|
+
const argRaw = sourceCode.getText(argument);
|
|
120
|
+
|
|
121
|
+
const argText = ARG_NEEDS_PARENS.has(argument.type)
|
|
122
|
+
? `(${argRaw})`
|
|
123
|
+
: argRaw;
|
|
124
|
+
|
|
125
|
+
const comparison = `${argText} ${spec.op} ${spec.literal}`;
|
|
126
|
+
|
|
127
|
+
const replacement = needsWrappingInParent(node)
|
|
128
|
+
? `(${comparison})`
|
|
129
|
+
: comparison;
|
|
130
|
+
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: 'preferComparison',
|
|
134
|
+
data: { comparison, name: resolved.canonicalName },
|
|
135
|
+
fix: (fixer) => fixer.replaceText(node, replacement),
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
defaultOptions: [],
|
|
141
|
+
} as const;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferComparisonOverNullishGuard } from './prefer-comparison-over-nullish-guard.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
projectService: {
|
|
13
|
+
allowDefaultProject: ['*.ts*'],
|
|
14
|
+
},
|
|
15
|
+
tsconfigRootDir: `${import.meta.dirname}/../..`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('prefer-comparison-over-nullish-guard', () => {
|
|
21
|
+
tester.run(
|
|
22
|
+
'prefer-comparison-over-nullish-guard',
|
|
23
|
+
preferComparisonOverNullishGuard,
|
|
24
|
+
{
|
|
25
|
+
valid: [
|
|
26
|
+
{
|
|
27
|
+
name: 'point-free usage is allowed',
|
|
28
|
+
code: dedent`
|
|
29
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
30
|
+
declare const xs: readonly (string | undefined)[];
|
|
31
|
+
const ys = xs.filter(isNotUndefined);
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'other ts-data-forge guards are not affected',
|
|
36
|
+
code: dedent`
|
|
37
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
38
|
+
declare const x: string | null | undefined;
|
|
39
|
+
const y = isNonNullish(x);
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'same-named function not imported from ts-data-forge',
|
|
44
|
+
code: dedent`
|
|
45
|
+
const isNotUndefined = (u: unknown): boolean => u !== undefined;
|
|
46
|
+
declare const x: string | undefined;
|
|
47
|
+
const y = isNotUndefined(x);
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
invalid: [
|
|
52
|
+
{
|
|
53
|
+
name: 'isNotUndefined with explicit argument',
|
|
54
|
+
code: dedent`
|
|
55
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
56
|
+
declare const x: string | undefined;
|
|
57
|
+
const y = isNotUndefined(x);
|
|
58
|
+
`,
|
|
59
|
+
output: dedent`
|
|
60
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
61
|
+
declare const x: string | undefined;
|
|
62
|
+
const y = x !== undefined;
|
|
63
|
+
`,
|
|
64
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'isUndefined with explicit argument',
|
|
68
|
+
code: dedent`
|
|
69
|
+
import { isUndefined } from 'ts-data-forge';
|
|
70
|
+
declare const x: string | undefined;
|
|
71
|
+
const y = isUndefined(x);
|
|
72
|
+
`,
|
|
73
|
+
output: dedent`
|
|
74
|
+
import { isUndefined } from 'ts-data-forge';
|
|
75
|
+
declare const x: string | undefined;
|
|
76
|
+
const y = x === undefined;
|
|
77
|
+
`,
|
|
78
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'isNull with explicit argument',
|
|
82
|
+
code: dedent`
|
|
83
|
+
import { isNull } from 'ts-data-forge';
|
|
84
|
+
declare const x: string | null;
|
|
85
|
+
const y = isNull(x);
|
|
86
|
+
`,
|
|
87
|
+
output: dedent`
|
|
88
|
+
import { isNull } from 'ts-data-forge';
|
|
89
|
+
declare const x: string | null;
|
|
90
|
+
const y = x === null;
|
|
91
|
+
`,
|
|
92
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'isNotNull inside a logical expression (no extra parens)',
|
|
96
|
+
code: dedent`
|
|
97
|
+
import { isNotNull } from 'ts-data-forge';
|
|
98
|
+
declare const x: string | null;
|
|
99
|
+
const y = isNotNull(x) && x.length > 0;
|
|
100
|
+
`,
|
|
101
|
+
output: dedent`
|
|
102
|
+
import { isNotNull } from 'ts-data-forge';
|
|
103
|
+
declare const x: string | null;
|
|
104
|
+
const y = x !== null && x.length > 0;
|
|
105
|
+
`,
|
|
106
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'negated call is wrapped in parentheses',
|
|
110
|
+
code: dedent`
|
|
111
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
112
|
+
declare const x: string | undefined;
|
|
113
|
+
const y = !isNotUndefined(x);
|
|
114
|
+
`,
|
|
115
|
+
output: dedent`
|
|
116
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
117
|
+
declare const x: string | undefined;
|
|
118
|
+
const y = !(x !== undefined);
|
|
119
|
+
`,
|
|
120
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'member-access argument',
|
|
124
|
+
code: dedent`
|
|
125
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
126
|
+
declare const obj: { value: string | undefined };
|
|
127
|
+
const y = isNotUndefined(obj.value);
|
|
128
|
+
`,
|
|
129
|
+
output: dedent`
|
|
130
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
131
|
+
declare const obj: { value: string | undefined };
|
|
132
|
+
const y = obj.value !== undefined;
|
|
133
|
+
`,
|
|
134
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'low-precedence argument is parenthesized',
|
|
138
|
+
code: dedent`
|
|
139
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
140
|
+
declare const a: string | undefined;
|
|
141
|
+
declare const b: string | undefined;
|
|
142
|
+
declare const cond: boolean;
|
|
143
|
+
const y = isNotUndefined(cond ? a : b);
|
|
144
|
+
`,
|
|
145
|
+
output: dedent`
|
|
146
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
147
|
+
declare const a: string | undefined;
|
|
148
|
+
declare const b: string | undefined;
|
|
149
|
+
declare const cond: boolean;
|
|
150
|
+
const y = (cond ? a : b) !== undefined;
|
|
151
|
+
`,
|
|
152
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'namespace import member call',
|
|
156
|
+
code: dedent`
|
|
157
|
+
import * as tf from 'ts-data-forge';
|
|
158
|
+
declare const x: string | undefined;
|
|
159
|
+
const y = tf.isNotUndefined(x);
|
|
160
|
+
`,
|
|
161
|
+
output: dedent`
|
|
162
|
+
import * as tf from 'ts-data-forge';
|
|
163
|
+
declare const x: string | undefined;
|
|
164
|
+
const y = x !== undefined;
|
|
165
|
+
`,
|
|
166
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsDataForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useIsNonNullObject';
|
|
16
|
+
|
|
17
|
+
export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'suggestion',
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
'Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-data-forge.',
|
|
23
|
+
},
|
|
24
|
+
fixable: 'code',
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
useIsNonNullObject:
|
|
28
|
+
'Replace the object/null check with `isNonNullObject()` from ts-data-forge.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create: (context) => {
|
|
33
|
+
const sourceCode = context.sourceCode;
|
|
34
|
+
|
|
35
|
+
const program = sourceCode.ast;
|
|
36
|
+
|
|
37
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
38
|
+
|
|
39
|
+
const mut_nodesToFix: {
|
|
40
|
+
node: TSESTree.LogicalExpression;
|
|
41
|
+
identifierName: string;
|
|
42
|
+
}[] = [];
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
LogicalExpression: (node) => {
|
|
46
|
+
const identifierName = getNonNullObjectIdentifierName(node);
|
|
47
|
+
|
|
48
|
+
if (identifierName === undefined) return;
|
|
49
|
+
|
|
50
|
+
mut_nodesToFix.push({ node, identifierName });
|
|
51
|
+
},
|
|
52
|
+
'Program:exit': () => {
|
|
53
|
+
// Check if isNonNullObject is already imported
|
|
54
|
+
const hasIsNonNullObjectImport =
|
|
55
|
+
getNamedImports(tsDataForgeImport).includes('isNonNullObject');
|
|
56
|
+
|
|
57
|
+
// Note: We add import only for the first node to avoid conflicts when
|
|
58
|
+
// multiple fixes try to insert at the same position. This means if the
|
|
59
|
+
// first node is disabled via eslint-disable comment, no import will be
|
|
60
|
+
// added.
|
|
61
|
+
for (const [
|
|
62
|
+
index,
|
|
63
|
+
{ node, identifierName },
|
|
64
|
+
] of mut_nodesToFix.entries()) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: 'useIsNonNullObject',
|
|
68
|
+
fix: (fixer) => {
|
|
69
|
+
const replacement = `isNonNullObject(${identifierName})`;
|
|
70
|
+
|
|
71
|
+
// Add import only for the first node and only if not already imported
|
|
72
|
+
const importFixes =
|
|
73
|
+
index === 0 && !hasIsNonNullObjectImport
|
|
74
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, [
|
|
75
|
+
'isNonNullObject',
|
|
76
|
+
])
|
|
77
|
+
: [];
|
|
78
|
+
|
|
79
|
+
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
defaultOptions: [],
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
const getNonNullObjectIdentifierName = (
|
|
90
|
+
node: DeepReadonly<TSESTree.LogicalExpression>,
|
|
91
|
+
): string | undefined => {
|
|
92
|
+
if (node.operator !== '&&') return undefined;
|
|
93
|
+
|
|
94
|
+
const leftIdentifierName = getTypeofObjectIdentifierName(node.left);
|
|
95
|
+
|
|
96
|
+
const rightIdentifierName = getNonNullCheckIdentifierName(node.right);
|
|
97
|
+
|
|
98
|
+
if (leftIdentifierName === undefined) return undefined;
|
|
99
|
+
|
|
100
|
+
if (rightIdentifierName === undefined) return undefined;
|
|
101
|
+
|
|
102
|
+
if (leftIdentifierName !== rightIdentifierName) return undefined;
|
|
103
|
+
|
|
104
|
+
return leftIdentifierName;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getTypeofObjectIdentifierName = (
|
|
108
|
+
node: DeepReadonly<TSESTree.Expression>,
|
|
109
|
+
): string | undefined => {
|
|
110
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) return undefined;
|
|
111
|
+
|
|
112
|
+
if (node.operator !== '===') return undefined;
|
|
113
|
+
|
|
114
|
+
const { left, right } = node;
|
|
115
|
+
|
|
116
|
+
if (left.type !== AST_NODE_TYPES.UnaryExpression) return undefined;
|
|
117
|
+
|
|
118
|
+
if (left.operator !== 'typeof') return undefined;
|
|
119
|
+
|
|
120
|
+
if (right.type !== AST_NODE_TYPES.Literal) return undefined;
|
|
121
|
+
|
|
122
|
+
if (right.value !== 'object') return undefined;
|
|
123
|
+
|
|
124
|
+
if (left.argument.type !== AST_NODE_TYPES.Identifier) return undefined;
|
|
125
|
+
|
|
126
|
+
return left.argument.name;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const getNonNullCheckIdentifierName = (
|
|
130
|
+
node: DeepReadonly<TSESTree.Expression>,
|
|
131
|
+
): string | undefined => {
|
|
132
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) return undefined;
|
|
133
|
+
|
|
134
|
+
if (node.operator !== '!==') return undefined;
|
|
135
|
+
|
|
136
|
+
const { left, right } = node;
|
|
137
|
+
|
|
138
|
+
if (left.type !== AST_NODE_TYPES.Identifier) return undefined;
|
|
139
|
+
|
|
140
|
+
if (right.type !== AST_NODE_TYPES.Literal) return undefined;
|
|
141
|
+
|
|
142
|
+
if (right.value !== null) return undefined;
|
|
143
|
+
|
|
144
|
+
return left.name;
|
|
145
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
projectService: {
|
|
13
|
+
allowDefaultProject: ['*.ts*'],
|
|
14
|
+
},
|
|
15
|
+
tsconfigRootDir: `${import.meta.dirname}/../..`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('prefer-is-non-null-object', () => {
|
|
21
|
+
tester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'ignores non-matching typeof checks',
|
|
25
|
+
code: dedent`
|
|
26
|
+
const u = {};
|
|
27
|
+
const ok = typeof u === "object" && v !== null;
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
...([
|
|
33
|
+
{
|
|
34
|
+
name: 'replaces typeof object check with isNonNullObject import',
|
|
35
|
+
code: dedent`
|
|
36
|
+
const u = {};
|
|
37
|
+
const ok = typeof u === "object" && u !== null;
|
|
38
|
+
`,
|
|
39
|
+
output: dedent`
|
|
40
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
41
|
+
const u = {};
|
|
42
|
+
const ok = isNonNullObject(u);
|
|
43
|
+
`,
|
|
44
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'replaces typeof object check with isNonNullObject import when other imports exist',
|
|
48
|
+
code: dedent`
|
|
49
|
+
import { noop } from './noop.mjs';
|
|
50
|
+
|
|
51
|
+
const u = {};
|
|
52
|
+
noop(typeof u === "object" && u !== null);
|
|
53
|
+
`,
|
|
54
|
+
output: dedent`
|
|
55
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
56
|
+
import { noop } from './noop.mjs';
|
|
57
|
+
|
|
58
|
+
const u = {};
|
|
59
|
+
noop(isNonNullObject(u));
|
|
60
|
+
`,
|
|
61
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'replaces multiple typeof object checks with isNonNullObject imports',
|
|
65
|
+
code: dedent`
|
|
66
|
+
const u = {};
|
|
67
|
+
const ok = typeof u === "object" && u !== null;
|
|
68
|
+
const ok2 = typeof u === "object" && u !== null;
|
|
69
|
+
`,
|
|
70
|
+
output: dedent`
|
|
71
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
72
|
+
const u = {};
|
|
73
|
+
const ok = isNonNullObject(u);
|
|
74
|
+
const ok2 = isNonNullObject(u);
|
|
75
|
+
`,
|
|
76
|
+
errors: [
|
|
77
|
+
{ messageId: 'useIsNonNullObject' },
|
|
78
|
+
{ messageId: 'useIsNonNullObject' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
] as const),
|
|
82
|
+
|
|
83
|
+
...([
|
|
84
|
+
{
|
|
85
|
+
name: 'keeps existing isNonNullObject import',
|
|
86
|
+
code: dedent`
|
|
87
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
88
|
+
|
|
89
|
+
const ok = typeof u === "object" && u !== null;
|
|
90
|
+
`,
|
|
91
|
+
output: dedent`
|
|
92
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
93
|
+
|
|
94
|
+
const ok = isNonNullObject(u);
|
|
95
|
+
`,
|
|
96
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'keeps existing isNonNullObject import when other imports exist',
|
|
100
|
+
code: dedent`
|
|
101
|
+
import { noop } from './noop.mjs';
|
|
102
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
103
|
+
|
|
104
|
+
const u = {};
|
|
105
|
+
noop(typeof u === "object" && u !== null);
|
|
106
|
+
`,
|
|
107
|
+
output: dedent`
|
|
108
|
+
import { noop } from './noop.mjs';
|
|
109
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
110
|
+
|
|
111
|
+
const u = {};
|
|
112
|
+
noop(isNonNullObject(u));
|
|
113
|
+
`,
|
|
114
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
115
|
+
},
|
|
116
|
+
] as const),
|
|
117
|
+
|
|
118
|
+
...([
|
|
119
|
+
{
|
|
120
|
+
name: 'adds import even if ts-data-forge import exists',
|
|
121
|
+
code: dedent`
|
|
122
|
+
import { asInt } from 'ts-data-forge';
|
|
123
|
+
|
|
124
|
+
const ok = typeof u === "object" && u !== null;
|
|
125
|
+
`,
|
|
126
|
+
output: dedent`
|
|
127
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
128
|
+
import { asInt } from 'ts-data-forge';
|
|
129
|
+
|
|
130
|
+
const ok = isNonNullObject(u);
|
|
131
|
+
`,
|
|
132
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'adds import even if ts-data-forge import exists (with other imports)',
|
|
136
|
+
code: dedent`
|
|
137
|
+
import { noop } from './noop.mjs';
|
|
138
|
+
import { asInt } from 'ts-data-forge';
|
|
139
|
+
|
|
140
|
+
const u = {};
|
|
141
|
+
noop(typeof u === "object" && u !== null);
|
|
142
|
+
`,
|
|
143
|
+
output: dedent`
|
|
144
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
145
|
+
import { noop } from './noop.mjs';
|
|
146
|
+
import { asInt } from 'ts-data-forge';
|
|
147
|
+
|
|
148
|
+
const u = {};
|
|
149
|
+
noop(isNonNullObject(u));
|
|
150
|
+
`,
|
|
151
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
152
|
+
},
|
|
153
|
+
] as const),
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
}, 20000);
|