eslint-plugin-ts-std-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 +104 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.d.mts +32 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +41 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/rule-types.d.mts +96 -0
- package/dist/rule-types.d.mts.map +1 -0
- package/dist/rule-types.mjs +6 -0
- package/dist/rule-types.mjs.map +1 -0
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -0
- package/dist/rules/constants.mjs +2 -0
- package/dist/rules/constants.mjs.map +1 -0
- package/dist/rules/import-utils.d.mts +6 -0
- package/dist/rules/import-utils.d.mts.map +1 -0
- package/dist/rules/import-utils.mjs +27 -0
- package/dist/rules/import-utils.mjs.map +1 -0
- package/dist/rules/index.d.mts +2 -0
- package/dist/rules/index.d.mts.map +1 -0
- package/dist/rules/index.mjs +2 -0
- package/dist/rules/index.mjs.map +1 -0
- package/dist/rules/prefer-is-non-null-object.d.mts +6 -0
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -0
- package/dist/rules/prefer-is-non-null-object.mjs +97 -0
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -0
- package/dist/rules/prefer-is-record-and-has-key.d.mts +6 -0
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -0
- package/dist/rules/prefer-is-record-and-has-key.mjs +154 -0
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -0
- package/dist/rules/prefer-safe-array-is-array.d.mts +6 -0
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -0
- package/dist/rules/prefer-safe-array-is-array.mjs +68 -0
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -0
- package/dist/rules/prefer-safe-array-length-guard.d.mts +6 -0
- package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -0
- package/dist/rules/prefer-safe-array-length-guard.mjs +143 -0
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -0
- package/dist/rules/prefer-safe-number-parse-integer.d.mts +13 -0
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -0
- package/dist/rules/prefer-safe-number-parse-integer.mjs +109 -0
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -0
- package/dist/rules/prefer-safe-number-parse.d.mts +19 -0
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -0
- package/dist/rules/prefer-safe-number-parse.mjs +111 -0
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -0
- package/dist/rules/rules.d.mts +9 -0
- package/dist/rules/rules.d.mts.map +1 -0
- package/dist/rules/rules.mjs +15 -0
- package/dist/rules/rules.mjs.map +1 -0
- package/dist/types.d.mts +5 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +2 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +84 -0
- package/src/index.mts +7 -0
- package/src/plugin.mts +48 -0
- package/src/plugin.test.mts +24 -0
- package/src/rule-types.mts +116 -0
- package/src/rules/constants.mts +1 -0
- package/src/rules/import-utils.mts +56 -0
- package/src/rules/index.mts +1 -0
- package/src/rules/prefer-is-non-null-object.mts +142 -0
- package/src/rules/prefer-is-non-null-object.test.mts +54 -0
- package/src/rules/prefer-is-record-and-has-key.mts +217 -0
- package/src/rules/prefer-is-record-and-has-key.test.mts +80 -0
- package/src/rules/prefer-safe-array-is-array.mts +99 -0
- package/src/rules/prefer-safe-array-is-array.test.mts +65 -0
- package/src/rules/prefer-safe-array-length-guard.mts +214 -0
- package/src/rules/prefer-safe-array-length-guard.test.mts +152 -0
- package/src/rules/prefer-safe-number-parse-integer.mts +162 -0
- package/src/rules/prefer-safe-number-parse-integer.test.mts +67 -0
- package/src/rules/prefer-safe-number-parse.mts +161 -0
- package/src/rules/prefer-safe-number-parse.test.mts +109 -0
- package/src/rules/rules.mts +16 -0
- package/src/types.mts +6 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
buildImportFixes,
|
|
9
|
+
getNamedImports,
|
|
10
|
+
getTsStdForgeImport,
|
|
11
|
+
} from './import-utils.mjs';
|
|
12
|
+
|
|
13
|
+
type Options = readonly [];
|
|
14
|
+
|
|
15
|
+
type MessageIds = 'useSafeNumberParse';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns `true` when every constituent of `type` is string-like, i.e. the
|
|
19
|
+
* type is assignable to the `string` parameter of `SafeNumber.parse`. A union
|
|
20
|
+
* such as `string | undefined` is rejected so the autofix never produces a
|
|
21
|
+
* type error.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
24
|
+
const isStringType = (type: ts.Type): boolean => {
|
|
25
|
+
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
parts.length > 0 &&
|
|
29
|
+
parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detects `parseFloat(x)`, `Number.parseFloat(x)`, and `Number(x)` (when x
|
|
35
|
+
* is typed as `string`) and replaces them with
|
|
36
|
+
* `Result.unwrapOkOr(SafeNumber.parse(x), Number.NaN)` (ts-std-forge).
|
|
37
|
+
*
|
|
38
|
+
* `Number(x)` is only flagged when x is purely a string type to avoid false
|
|
39
|
+
* positives for `Number(someBoolean)` or `Number(someNumber)` uses.
|
|
40
|
+
*
|
|
41
|
+
* The ts-data-forge rule of the same shape names `Num.safeParseFloat`, which
|
|
42
|
+
* has delegated to `SafeNumber.parse` since the D-49 inversion and wraps the
|
|
43
|
+
* result in a `FiniteNumber` brand. This one names the implementation, for
|
|
44
|
+
* callers who do not want the brand or the extra dependency.
|
|
45
|
+
*/
|
|
46
|
+
export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'suggestion',
|
|
49
|
+
docs: {
|
|
50
|
+
description:
|
|
51
|
+
'Replace `parseFloat(x)`, `Number.parseFloat(x)`, or `Number(x)` (when x is a string) with `Result.unwrapOkOr(SafeNumber.parse(x), Number.NaN)` from ts-std-forge.',
|
|
52
|
+
},
|
|
53
|
+
fixable: 'code',
|
|
54
|
+
schema: [],
|
|
55
|
+
messages: {
|
|
56
|
+
useSafeNumberParse:
|
|
57
|
+
'Replace `{{original}}` with `Result.unwrapOkOr(SafeNumber.parse({{argName}}), Number.NaN)` from ts-std-forge.',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
create: (context) => {
|
|
62
|
+
const sourceCode = context.sourceCode;
|
|
63
|
+
|
|
64
|
+
const program = sourceCode.ast;
|
|
65
|
+
|
|
66
|
+
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
67
|
+
|
|
68
|
+
const services = sourceCode.parserServices;
|
|
69
|
+
|
|
70
|
+
const mut_nodesToFix: {
|
|
71
|
+
node: TSESTree.CallExpression;
|
|
72
|
+
argExpression: TSESTree.Expression;
|
|
73
|
+
}[] = [];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
CallExpression: (node) => {
|
|
77
|
+
const { callee } = node;
|
|
78
|
+
|
|
79
|
+
// Match `parseFloat(...)` (global function).
|
|
80
|
+
const isGlobalParseFloat =
|
|
81
|
+
callee.type === AST_NODE_TYPES.Identifier &&
|
|
82
|
+
callee.name === 'parseFloat';
|
|
83
|
+
|
|
84
|
+
// Match `Number.parseFloat(...)`.
|
|
85
|
+
const isNumberParseFloat =
|
|
86
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
87
|
+
!callee.computed &&
|
|
88
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
89
|
+
callee.object.name === 'Number' &&
|
|
90
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
91
|
+
callee.property.name === 'parseFloat';
|
|
92
|
+
|
|
93
|
+
// Match `Number(...)` called as a function (not `new Number(...)`).
|
|
94
|
+
const isNumberCall =
|
|
95
|
+
callee.type === AST_NODE_TYPES.Identifier && callee.name === 'Number';
|
|
96
|
+
|
|
97
|
+
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) return;
|
|
98
|
+
|
|
99
|
+
const args = node.arguments;
|
|
100
|
+
|
|
101
|
+
const firstArg = args[0];
|
|
102
|
+
|
|
103
|
+
if (firstArg === undefined) return;
|
|
104
|
+
|
|
105
|
+
// Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
|
|
106
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
|
|
107
|
+
|
|
108
|
+
// The argument must be purely `string` so the autofix is type-safe
|
|
109
|
+
// against `SafeNumber.parse(value: string)`. Without type information,
|
|
110
|
+
// skip.
|
|
111
|
+
if (services?.program == null) return;
|
|
112
|
+
|
|
113
|
+
const checker = services.program.getTypeChecker();
|
|
114
|
+
|
|
115
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
|
|
116
|
+
|
|
117
|
+
if (tsNode === undefined) return;
|
|
118
|
+
|
|
119
|
+
const argType = checker.getTypeAtLocation(tsNode);
|
|
120
|
+
|
|
121
|
+
if (!isStringType(argType)) return;
|
|
122
|
+
|
|
123
|
+
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
124
|
+
},
|
|
125
|
+
'Program:exit': () => {
|
|
126
|
+
const namedImports = getNamedImports(tsStdForgeImport);
|
|
127
|
+
|
|
128
|
+
const missingImports = (['Result', 'SafeNumber'] as const).filter(
|
|
129
|
+
(name) => !namedImports.includes(name),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
for (const [
|
|
133
|
+
index,
|
|
134
|
+
{ node, argExpression },
|
|
135
|
+
] of mut_nodesToFix.entries()) {
|
|
136
|
+
const argText = sourceCode.getText(argExpression);
|
|
137
|
+
|
|
138
|
+
context.report({
|
|
139
|
+
node,
|
|
140
|
+
messageId: 'useSafeNumberParse',
|
|
141
|
+
data: {
|
|
142
|
+
original: sourceCode.getText(node),
|
|
143
|
+
argName: argText,
|
|
144
|
+
},
|
|
145
|
+
fix: (fixer) => {
|
|
146
|
+
const core = `Result.unwrapOkOr(SafeNumber.parse(${argText}), Number.NaN)`;
|
|
147
|
+
|
|
148
|
+
const importFixes =
|
|
149
|
+
index === 0 && missingImports.length > 0
|
|
150
|
+
? buildImportFixes(fixer, program, missingImports)
|
|
151
|
+
: [];
|
|
152
|
+
|
|
153
|
+
return [...importFixes, fixer.replaceText(node, core)];
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
defaultOptions: [],
|
|
161
|
+
} as const;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferSafeNumberParse } from './prefer-safe-number-parse.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-safe-number-parse', () => {
|
|
21
|
+
tester.run('prefer-safe-number-parse', preferSafeNumberParse, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'ignores number arguments to Number() (autofix would be unsafe)',
|
|
25
|
+
code: dedent`
|
|
26
|
+
const x = 42;
|
|
27
|
+
const n = Number(x);
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'ignores boolean arguments to Number()',
|
|
32
|
+
code: dedent`
|
|
33
|
+
const b = true;
|
|
34
|
+
const n = Number(b);
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ignores a union that is not purely string',
|
|
39
|
+
code: dedent`
|
|
40
|
+
declare const s: string | undefined;
|
|
41
|
+
const n = parseFloat(s as string | undefined as string & never);
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'ignores new Number(...)',
|
|
46
|
+
code: dedent`
|
|
47
|
+
const s = '1';
|
|
48
|
+
const n = new Number(s);
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
invalid: [
|
|
53
|
+
{
|
|
54
|
+
name: 'rewrites parseFloat on a string',
|
|
55
|
+
code: dedent`
|
|
56
|
+
declare const s: string;
|
|
57
|
+
const n = parseFloat(s);
|
|
58
|
+
`,
|
|
59
|
+
output: dedent`
|
|
60
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
61
|
+
declare const s: string;
|
|
62
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
63
|
+
`,
|
|
64
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'rewrites Number.parseFloat on a string',
|
|
68
|
+
code: dedent`
|
|
69
|
+
declare const s: string;
|
|
70
|
+
const n = Number.parseFloat(s);
|
|
71
|
+
`,
|
|
72
|
+
output: dedent`
|
|
73
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
74
|
+
declare const s: string;
|
|
75
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
76
|
+
`,
|
|
77
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'rewrites Number(x) when x is a string',
|
|
81
|
+
code: dedent`
|
|
82
|
+
declare const s: string;
|
|
83
|
+
const n = Number(s);
|
|
84
|
+
`,
|
|
85
|
+
output: dedent`
|
|
86
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
87
|
+
declare const s: string;
|
|
88
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
89
|
+
`,
|
|
90
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'adds only the missing import',
|
|
94
|
+
code: dedent`
|
|
95
|
+
import { Result } from 'ts-std-forge';
|
|
96
|
+
declare const s: string;
|
|
97
|
+
const n = parseFloat(s);
|
|
98
|
+
`,
|
|
99
|
+
output: dedent`
|
|
100
|
+
import { SafeNumber } from 'ts-std-forge';
|
|
101
|
+
import { Result } from 'ts-std-forge';
|
|
102
|
+
declare const s: string;
|
|
103
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
104
|
+
`,
|
|
105
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ESLintPlugin } from '../types.mjs';
|
|
2
|
+
import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
3
|
+
import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
|
|
4
|
+
import { preferSafeArrayIsArray } from './prefer-safe-array-is-array.mjs';
|
|
5
|
+
import { preferSafeArrayLengthGuard } from './prefer-safe-array-length-guard.mjs';
|
|
6
|
+
import { preferSafeNumberParseInteger } from './prefer-safe-number-parse-integer.mjs';
|
|
7
|
+
import { preferSafeNumberParse } from './prefer-safe-number-parse.mjs';
|
|
8
|
+
|
|
9
|
+
export const tsStdForgeRules = {
|
|
10
|
+
'prefer-is-non-null-object': preferIsNonNullObject,
|
|
11
|
+
'prefer-is-record-and-has-key': preferIsRecordAndHasKey,
|
|
12
|
+
'prefer-safe-array-is-array': preferSafeArrayIsArray,
|
|
13
|
+
'prefer-safe-array-length-guard': preferSafeArrayLengthGuard,
|
|
14
|
+
'prefer-safe-number-parse': preferSafeNumberParse,
|
|
15
|
+
'prefer-safe-number-parse-integer': preferSafeNumberParseInteger,
|
|
16
|
+
} as const satisfies ESLintPlugin['rules'];
|
package/src/types.mts
ADDED