eslint-plugin-ts-std-forge 0.1.4 → 0.1.5
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/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +1 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +41 -0
- package/dist/rules/ast-utils.d.mts.map +1 -0
- package/dist/rules/ast-utils.mjs +101 -0
- package/dist/rules/ast-utils.mjs.map +1 -0
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +26 -12
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +12 -12
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.mjs +15 -13
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.mjs +11 -13
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -16
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse.mjs +7 -15
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +3 -3
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +2 -2
- package/src/rules/ast-utils.mts +147 -0
- package/src/rules/import-utils.mts +1 -1
- package/src/rules/prefer-is-non-null-object.mts +34 -13
- package/src/rules/prefer-is-non-null-object.test.mts +54 -0
- package/src/rules/prefer-is-record-and-has-key.mts +19 -16
- package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
- package/src/rules/prefer-safe-array-is-array.mts +22 -18
- package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
- package/src/rules/prefer-safe-array-length-guard.mts +20 -19
- package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
- package/src/rules/prefer-safe-number-parse-integer.mts +18 -24
- package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
- package/src/rules/prefer-safe-number-parse.mts +14 -22
- package/src/rules/prefer-safe-number-parse.test.mts +97 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-safe-number-parse-integer.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse-integer.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAM5B;;;;;GAKG;AACH,8EAA8E;AAC9E,MAAM,YAAY,GAAG,CAAC,IAAa,EAAW,EAAE;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAW,CAAC;IAE9D,OAAO,CACL,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAC9D,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAGrC;IACF,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,uIAAuI;SAC1I;QACD,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,yBAAyB,EACvB,sHAAsH;SACzH;KACF;IAED,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;QAClB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QAEtC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC;QAE/B,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAEtD,MAAM,
|
|
1
|
+
{"version":3,"file":"prefer-safe-number-parse-integer.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse-integer.mts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-safe-number-parse.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"prefer-safe-number-parse.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAclC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,oBAAoB,CAAC;AAkBvC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAiHjE,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
2
2
|
import * as ts from 'typescript';
|
|
3
|
+
import { getArgumentText, getValueType, isIdentifierNamed, skipTypeWrappers, } from './ast-utils.mjs';
|
|
3
4
|
import { buildImportFixes, getNamedImports, getTsStdForgeImport, } from './import-utils.mjs';
|
|
4
5
|
|
|
5
6
|
|
|
@@ -42,19 +43,17 @@ export const preferSafeNumberParse = {
|
|
|
42
43
|
const sourceCode = context.sourceCode;
|
|
43
44
|
const program = sourceCode.ast;
|
|
44
45
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
45
|
-
const services = sourceCode.parserServices;
|
|
46
46
|
const mut_nodesToFix = [];
|
|
47
47
|
return {
|
|
48
48
|
CallExpression: (node) => {
|
|
49
|
-
const
|
|
49
|
+
const callee = skipTypeWrappers(node.callee);
|
|
50
50
|
|
|
51
51
|
const isGlobalParseFloat = callee.type === AST_NODE_TYPES.Identifier &&
|
|
52
52
|
callee.name === 'parseFloat';
|
|
53
53
|
|
|
54
54
|
const isNumberParseFloat = callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
55
55
|
!callee.computed &&
|
|
56
|
-
callee.object
|
|
57
|
-
callee.object.name === 'Number' &&
|
|
56
|
+
isIdentifierNamed(callee.object, 'Number') &&
|
|
58
57
|
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
59
58
|
callee.property.name === 'parseFloat';
|
|
60
59
|
|
|
@@ -74,16 +73,9 @@ export const preferSafeNumberParse = {
|
|
|
74
73
|
|
|
75
74
|
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const checker = services.program.getTypeChecker();
|
|
81
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
|
|
82
|
-
if (tsNode === undefined) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const argType = checker.getTypeAtLocation(tsNode);
|
|
86
|
-
if (!isStringType(argType)) {
|
|
76
|
+
|
|
77
|
+
const argType = getValueType(sourceCode, firstArg);
|
|
78
|
+
if (argType === undefined || !isStringType(argType)) {
|
|
87
79
|
return;
|
|
88
80
|
}
|
|
89
81
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
@@ -92,7 +84,7 @@ export const preferSafeNumberParse = {
|
|
|
92
84
|
const namedImports = getNamedImports(tsStdForgeImport);
|
|
93
85
|
const missingImports = ['Result', 'SafeNumber'].filter((name) => !namedImports.includes(name));
|
|
94
86
|
for (const [index, { node, argExpression },] of mut_nodesToFix.entries()) {
|
|
95
|
-
const argText = sourceCode
|
|
87
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
96
88
|
context.report({
|
|
97
89
|
node,
|
|
98
90
|
messageId: 'useSafeNumberParse',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAM5B;;;;;GAKG;AACH,8EAA8E;AAC9E,MAAM,YAAY,GAAG,CAAC,IAAa,EAAW,EAAE;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAW,CAAC;IAE9D,OAAO,CACL,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAC9D,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAA6C;IAC7E,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,mKAAmK;SACtK;QACD,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,kBAAkB,EAChB,+GAA+G;SAClH;KACF;IAED,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;QAClB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QAEtC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC;QAE/B,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAEtD,MAAM,
|
|
1
|
+
{"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"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"}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
3
3
|
export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
|
|
4
4
|
export type ESLintFlatConfig = DeepReadonly<TSESLint.FlatConfig.Config>;
|
|
5
5
|
//# sourceMappingURL=types.d.mts.map
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ts-std-forge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer code toward ts-std-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@typescript-eslint/parser": "8.70.1",
|
|
43
43
|
"@typescript-eslint/rule-tester": "8.70.1",
|
|
44
44
|
"dedent": "1.7.2",
|
|
45
|
-
"eslint-config-typed": "5.
|
|
46
|
-
"eslint-plugin-ts-fortress": "1.
|
|
45
|
+
"eslint-config-typed": "5.17.0",
|
|
46
|
+
"eslint-plugin-ts-fortress": "1.3.0",
|
|
47
47
|
"eslint-plugin-ts-type-forge": "0.8.5",
|
|
48
48
|
"prettier": "3.9.9",
|
|
49
49
|
"prettier-plugin-organize-imports": "4.3.0",
|
package/src/plugin.mts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
2
2
|
import { tsStdForgeRules } from './rules/index.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import type { ESLintFlatConfig, ESLintPlugin } from './types.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Every rule this plugin ships, at `error`.
|
|
7
7
|
*
|
|
8
|
-
* The `satisfies` clause below is keyed off {@link tsStdForgeRules}, so
|
|
9
|
-
*
|
|
8
|
+
* The `satisfies` clause below is keyed off {@link tsStdForgeRules}, so leaving a
|
|
9
|
+
* rule out, naming one that does not exist, or setting one to anything
|
|
10
|
+
* but `error` fails to type-check. That is the whole check; no test repeats it.
|
|
10
11
|
*/
|
|
11
12
|
const recommendedRules = {
|
|
12
13
|
'ts-std-forge/prefer-is-non-null-object': 'error',
|
package/src/plugin.test.mts
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { eslintPluginTsStdForge } from './plugin.mjs';
|
|
2
|
-
import { tsStdForgeRules } from './rules/index.mjs';
|
|
3
2
|
|
|
4
3
|
describe('eslintPluginTsStdForge.configs.recommended', () => {
|
|
5
4
|
const recommended = eslintPluginTsStdForge.configs.recommended;
|
|
6
5
|
|
|
7
|
-
test('enables every rule of the plugin, and nothing else, at "error"', () => {
|
|
8
|
-
assert.deepStrictEqual(
|
|
9
|
-
recommended.rules,
|
|
10
|
-
Object.fromEntries(
|
|
11
|
-
Object.keys(tsStdForgeRules).map((name) => [
|
|
12
|
-
`ts-std-forge/${name}`,
|
|
13
|
-
'error',
|
|
14
|
-
]),
|
|
15
|
-
),
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
6
|
test('registers the exported plugin object itself', () => {
|
|
20
7
|
// Registering a *copy* would make `Cannot redefine plugin` errors possible
|
|
21
8
|
// for users who also list the plugin in their own `plugins` record.
|
package/src/rule-types.mts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// This file is auto-generated by `pnpm run gen:rule-types`
|
|
4
4
|
// (`scripts/cmd/gen-rule-types.mts`). Do not edit it manually.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { Linter } from 'eslint';
|
|
7
7
|
import { expectType } from 'ts-std-forge';
|
|
8
|
-
import {
|
|
8
|
+
import type { tsStdForgeRules } from './rules/index.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @description Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-std-forge.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ASTUtils,
|
|
4
|
+
type TSESLint,
|
|
5
|
+
type TSESTree,
|
|
6
|
+
} from '@typescript-eslint/utils';
|
|
7
|
+
import type * as ts from 'typescript';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `node` with every `as`, `satisfies`, `!` and `<T>` around it taken off: the
|
|
11
|
+
* expression whose value it is. Judge syntax and ask types on what this
|
|
12
|
+
* returns, so that a wrapper neither hides a pattern (`xs.length > (0 as
|
|
13
|
+
* const)`) nor vouches for a type (`Number(b as unknown as string)`, whose
|
|
14
|
+
* value is still a boolean), and keep the original node for fix text. See
|
|
15
|
+
* `docs/writing-lint-rules.md` at the repository root.
|
|
16
|
+
*
|
|
17
|
+
* A copy of the helper in `eslint-config-typed`, which this package, published
|
|
18
|
+
* on its own, cannot import.
|
|
19
|
+
*/
|
|
20
|
+
export const skipTypeWrappers = (
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
22
|
+
node: TSESTree.Node,
|
|
23
|
+
): TSESTree.Node =>
|
|
24
|
+
isTypeWrapper(node) ? skipTypeWrappers(node.expression) : node;
|
|
25
|
+
|
|
26
|
+
export const isTypeWrapper = (
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
28
|
+
node: TSESTree.Node,
|
|
29
|
+
): node is TypeWrapper => TYPE_WRAPPERS.has(node.type);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
33
|
+
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
34
|
+
*/
|
|
35
|
+
export const isIdentifierNamed = (
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
37
|
+
node: TSESTree.Node,
|
|
38
|
+
name: string,
|
|
39
|
+
): boolean => {
|
|
40
|
+
const unwrapped = skipTypeWrappers(node);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type TypeWrapper =
|
|
48
|
+
| TSESTree.TSAsExpression
|
|
49
|
+
| TSESTree.TSNonNullExpression
|
|
50
|
+
| TSESTree.TSSatisfiesExpression
|
|
51
|
+
| TSESTree.TSTypeAssertion;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The type of the value `node` evaluates to, asked of the expression inside
|
|
55
|
+
* its type wrappers rather than of the cast, or `undefined` without type
|
|
56
|
+
* information. A `!` among the wrappers still takes `null` and `undefined`
|
|
57
|
+
* out, since that much the source asserts about the value itself.
|
|
58
|
+
*/
|
|
59
|
+
export const getValueType = (
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
61
|
+
sourceCode: TSESLint.SourceCode,
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
63
|
+
node: TSESTree.Node,
|
|
64
|
+
): ts.Type | undefined => {
|
|
65
|
+
const services = sourceCode.parserServices;
|
|
66
|
+
|
|
67
|
+
const checker = services?.program?.getTypeChecker();
|
|
68
|
+
|
|
69
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(skipTypeWrappers(node));
|
|
70
|
+
|
|
71
|
+
if (checker === undefined || tsNode === undefined) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
76
|
+
|
|
77
|
+
return hasNonNullAssertion(node) ? checker.getNonNullableType(type) : type;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The source text of `node` to put in a call's argument list. `getText`
|
|
82
|
+
* leaves out the parentheses the source puts around a node, and a sequence
|
|
83
|
+
* `(a, b)` without them would be read as two arguments.
|
|
84
|
+
*/
|
|
85
|
+
export const getArgumentText = (
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
87
|
+
sourceCode: TSESLint.SourceCode,
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
89
|
+
node: TSESTree.Node,
|
|
90
|
+
): string => {
|
|
91
|
+
const text = sourceCode.getText(node);
|
|
92
|
+
|
|
93
|
+
return node.type === AST_NODE_TYPES.SequenceExpression ? `(${text})` : text;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The range of `node` together with the parentheses the source puts around it,
|
|
98
|
+
* which `node.range` leaves out: all of `((Array.isArray))` for the member
|
|
99
|
+
* expression inside.
|
|
100
|
+
*/
|
|
101
|
+
export const getRangeWithParens = (
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
103
|
+
sourceCode: TSESLint.SourceCode,
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
105
|
+
node: TSESTree.Node,
|
|
106
|
+
): TSESTree.Range => {
|
|
107
|
+
const depth = parenthesesDepth(sourceCode, node);
|
|
108
|
+
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
return node.range;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const opening = sourceCode.getTokenBefore(node, { skip: depth - 1 });
|
|
114
|
+
|
|
115
|
+
const closing = sourceCode.getTokenAfter(node, { skip: depth - 1 });
|
|
116
|
+
|
|
117
|
+
return opening === null || closing === null
|
|
118
|
+
? node.range
|
|
119
|
+
: [opening.range[0], closing.range[1]];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** How many pairs of parentheses the source puts around `node`. */
|
|
123
|
+
const parenthesesDepth = (
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
125
|
+
sourceCode: TSESLint.SourceCode,
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
127
|
+
node: TSESTree.Node,
|
|
128
|
+
depth: number = 0,
|
|
129
|
+
): number =>
|
|
130
|
+
ASTUtils.isParenthesized(depth + 1, node, sourceCode)
|
|
131
|
+
? parenthesesDepth(sourceCode, node, depth + 1)
|
|
132
|
+
: depth;
|
|
133
|
+
|
|
134
|
+
const hasNonNullAssertion = (
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
136
|
+
node: TSESTree.Node,
|
|
137
|
+
): boolean =>
|
|
138
|
+
isTypeWrapper(node) &&
|
|
139
|
+
(node.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
140
|
+
hasNonNullAssertion(node.expression));
|
|
141
|
+
|
|
142
|
+
const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set([
|
|
143
|
+
AST_NODE_TYPES.TSAsExpression,
|
|
144
|
+
AST_NODE_TYPES.TSNonNullExpression,
|
|
145
|
+
AST_NODE_TYPES.TSSatisfiesExpression,
|
|
146
|
+
AST_NODE_TYPES.TSTypeAssertion,
|
|
147
|
+
]);
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
7
7
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
8
8
|
|
|
9
9
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
7
|
import {
|
|
8
8
|
buildImportFixes,
|
|
9
9
|
getNamedImports,
|
|
@@ -85,16 +85,24 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
85
85
|
defaultOptions: [],
|
|
86
86
|
} as const;
|
|
87
87
|
|
|
88
|
+
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
89
|
+
// unknown) === 'object' && u! !== null` is the same check.
|
|
90
|
+
|
|
88
91
|
const getNonNullObjectIdentifierName = (
|
|
89
|
-
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
93
|
+
node: TSESTree.LogicalExpression,
|
|
90
94
|
): string | undefined => {
|
|
91
95
|
if (node.operator !== '&&') {
|
|
92
96
|
return undefined;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
const leftIdentifierName = getTypeofObjectIdentifierName(
|
|
99
|
+
const leftIdentifierName = getTypeofObjectIdentifierName(
|
|
100
|
+
skipTypeWrappers(node.left),
|
|
101
|
+
);
|
|
96
102
|
|
|
97
|
-
const rightIdentifierName = getNonNullCheckIdentifierName(
|
|
103
|
+
const rightIdentifierName = getNonNullCheckIdentifierName(
|
|
104
|
+
skipTypeWrappers(node.right),
|
|
105
|
+
);
|
|
98
106
|
|
|
99
107
|
return leftIdentifierName === undefined ||
|
|
100
108
|
rightIdentifierName === undefined ||
|
|
@@ -104,7 +112,8 @@ const getNonNullObjectIdentifierName = (
|
|
|
104
112
|
};
|
|
105
113
|
|
|
106
114
|
const getTypeofObjectIdentifierName = (
|
|
107
|
-
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
116
|
+
node: TSESTree.Node,
|
|
108
117
|
): string | undefined => {
|
|
109
118
|
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
110
119
|
return undefined;
|
|
@@ -114,19 +123,29 @@ const getTypeofObjectIdentifierName = (
|
|
|
114
123
|
return undefined;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
const
|
|
126
|
+
const left = skipTypeWrappers(node.left);
|
|
127
|
+
|
|
128
|
+
const right = skipTypeWrappers(node.right);
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
if (
|
|
131
|
+
left.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
120
132
|
left.operator !== 'typeof' ||
|
|
121
133
|
right.type !== AST_NODE_TYPES.Literal ||
|
|
122
|
-
right.value !== 'object'
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
right.value !== 'object'
|
|
135
|
+
) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const argument = skipTypeWrappers(left.argument);
|
|
140
|
+
|
|
141
|
+
return argument.type === AST_NODE_TYPES.Identifier
|
|
142
|
+
? argument.name
|
|
143
|
+
: undefined;
|
|
126
144
|
};
|
|
127
145
|
|
|
128
146
|
const getNonNullCheckIdentifierName = (
|
|
129
|
-
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
148
|
+
node: TSESTree.Node,
|
|
130
149
|
): string | undefined => {
|
|
131
150
|
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
132
151
|
return undefined;
|
|
@@ -136,7 +155,9 @@ const getNonNullCheckIdentifierName = (
|
|
|
136
155
|
return undefined;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
const
|
|
158
|
+
const left = skipTypeWrappers(node.left);
|
|
159
|
+
|
|
160
|
+
const right = skipTypeWrappers(node.right);
|
|
140
161
|
|
|
141
162
|
return left.type !== AST_NODE_TYPES.Identifier ||
|
|
142
163
|
right.type !== AST_NODE_TYPES.Literal ||
|
|
@@ -52,3 +52,57 @@ ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
// The replacement names the bare identifier: that is the reference both
|
|
57
|
+
// halves narrow, and a wrapper around the argument would stop the guard from
|
|
58
|
+
// narrowing it.
|
|
59
|
+
describe('prefer-is-non-null-object through type wrappers', () => {
|
|
60
|
+
ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
61
|
+
valid: [
|
|
62
|
+
{
|
|
63
|
+
name: 'The two halves are about different identifiers behind wrappers',
|
|
64
|
+
code: dedent`
|
|
65
|
+
const ok = typeof (u satisfies unknown) === 'object' && v! !== null;
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
invalid: [
|
|
70
|
+
{
|
|
71
|
+
name: 'reads the typeof operand through `satisfies`',
|
|
72
|
+
code: dedent`
|
|
73
|
+
const ok = typeof (u satisfies unknown) === 'object' && u !== null;
|
|
74
|
+
`,
|
|
75
|
+
output: dedent`
|
|
76
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
77
|
+
const ok = isNonNullObject(u);
|
|
78
|
+
`,
|
|
79
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'reads the null check through `!` and `as`',
|
|
83
|
+
code: dedent`
|
|
84
|
+
const ok = typeof u === 'object' && u! !== (null as null);
|
|
85
|
+
`,
|
|
86
|
+
output: dedent`
|
|
87
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
88
|
+
const ok = isNonNullObject(u);
|
|
89
|
+
`,
|
|
90
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'reads the literal and both halves through wrappers',
|
|
94
|
+
code: dedent`
|
|
95
|
+
const ok =
|
|
96
|
+
((typeof (<unknown>u) === ('object' as const)) satisfies boolean) &&
|
|
97
|
+
((u !== null) as boolean);
|
|
98
|
+
`,
|
|
99
|
+
output: dedent`
|
|
100
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
101
|
+
const ok =
|
|
102
|
+
isNonNullObject(u);
|
|
103
|
+
`,
|
|
104
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { SafeArray } from 'ts-std-forge';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
|
+
import {
|
|
9
|
+
getArgumentText,
|
|
10
|
+
getValueType,
|
|
11
|
+
isIdentifierNamed,
|
|
12
|
+
skipTypeWrappers,
|
|
13
|
+
} from './ast-utils.mjs';
|
|
8
14
|
import {
|
|
9
15
|
buildImportFixes,
|
|
10
16
|
getNamedImports,
|
|
@@ -40,15 +46,14 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
40
46
|
|
|
41
47
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
42
48
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const checker = services?.program?.getTypeChecker();
|
|
49
|
+
const checker = sourceCode.parserServices?.program?.getTypeChecker();
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
49
53
|
* because the object already satisfies `hasKey`'s
|
|
50
54
|
* `R extends UnknownRecord` constraint. Without type information nothing
|
|
51
|
-
* is known, so the guard is kept.
|
|
55
|
+
* is known, so the guard is kept. The type is the value's, not a cast's:
|
|
56
|
+
* an array cast to a record is still rejected by `isRecord`.
|
|
52
57
|
*/
|
|
53
58
|
const isKnownRecord = (
|
|
54
59
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -58,12 +63,9 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
const
|
|
66
|
+
const type = getValueType(sourceCode, expression);
|
|
62
67
|
|
|
63
|
-
return (
|
|
64
|
-
tsNode !== undefined &&
|
|
65
|
-
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
66
|
-
);
|
|
68
|
+
return type !== undefined && isAlreadyRecordType(checker, type);
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
const mut_nodesToFix: {
|
|
@@ -75,12 +77,13 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
75
77
|
return {
|
|
76
78
|
// Handle Object.hasOwn(obj, key)
|
|
77
79
|
CallExpression: (node) => {
|
|
80
|
+
const callee = skipTypeWrappers(node.callee);
|
|
81
|
+
|
|
78
82
|
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
node.callee.property.name !== 'hasOwn'
|
|
83
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
84
|
+
!isIdentifierNamed(callee.object, 'Object') ||
|
|
85
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
86
|
+
callee.property.name !== 'hasOwn'
|
|
84
87
|
) {
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
@@ -157,9 +160,9 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
157
160
|
index,
|
|
158
161
|
{ node, objExpression, keyExpression, needsIsRecord },
|
|
159
162
|
] of rewrites.entries()) {
|
|
160
|
-
const objText = sourceCode
|
|
163
|
+
const objText = getArgumentText(sourceCode, objExpression);
|
|
161
164
|
|
|
162
|
-
const keyText = sourceCode
|
|
165
|
+
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
163
166
|
|
|
164
167
|
const originalText = sourceCode.getText(node);
|
|
165
168
|
|