eslint-config-typed 5.6.2 → 5.7.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/dist/plugins/ts-restrictions/rules/no-string-spread.d.mts +32 -0
- package/dist/plugins/ts-restrictions/rules/no-string-spread.d.mts.map +1 -0
- package/dist/plugins/ts-restrictions/rules/no-string-spread.mjs +102 -0
- package/dist/plugins/ts-restrictions/rules/no-string-spread.mjs.map +1 -0
- package/dist/plugins/ts-restrictions/rules/rules.d.mts +1 -0
- package/dist/plugins/ts-restrictions/rules/rules.d.mts.map +1 -1
- package/dist/plugins/ts-restrictions/rules/rules.mjs +2 -0
- package/dist/plugins/ts-restrictions/rules/rules.mjs.map +1 -1
- package/dist/rules/eslint-ts-restrictions-rules.d.mts +1 -0
- package/dist/rules/eslint-ts-restrictions-rules.d.mts.map +1 -1
- package/dist/rules/eslint-ts-restrictions-rules.mjs +1 -0
- package/dist/rules/eslint-ts-restrictions-rules.mjs.map +1 -1
- package/dist/types/rules/eslint-ts-restrictions-rules.d.mts +14 -0
- package/dist/types/rules/eslint-ts-restrictions-rules.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/plugins/ts-restrictions/rules/no-string-spread.mts +118 -0
- package/src/plugins/ts-restrictions/rules/no-string-spread.test.mts +175 -0
- package/src/plugins/ts-restrictions/rules/rules.mts +2 -0
- package/src/rules/eslint-ts-restrictions-rules.mts +1 -0
- package/src/types/rules/eslint-ts-restrictions-rules.mts +15 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = readonly [];
|
|
3
|
+
type MessageIds = 'noStringSpread';
|
|
4
|
+
/**
|
|
5
|
+
* Disallow spreading a `string` value with the spread operator `...`.
|
|
6
|
+
*
|
|
7
|
+
* ## Why
|
|
8
|
+
*
|
|
9
|
+
* Spreading a string iterates it into its individual characters, so
|
|
10
|
+
* `[...someString]` (or `f(...someString)`) evaluates to a `string[]` — the
|
|
11
|
+
* *exact same result type* as spreading an actual `string[]`. That makes an
|
|
12
|
+
* accidental string spread silent: neither the reader nor the type checker can
|
|
13
|
+
* tell `[...anArray]` from `[...aString]` by the result type alone, so a bug
|
|
14
|
+
* where an array was expected but a `string` slipped in goes unnoticed.
|
|
15
|
+
*
|
|
16
|
+
* ## Why not simply "allow array / object types only"
|
|
17
|
+
*
|
|
18
|
+
* The naive framing — "only permit `...` on arrays or objects" — would also
|
|
19
|
+
* reject perfectly idiomatic spreads of other iterables (`Set`, `Map`,
|
|
20
|
+
* `Map.prototype.keys()`, generators, `NodeList`, typed arrays, `arguments`,
|
|
21
|
+
* …), which have no equivalent silent hazard. And TypeScript *already* rejects
|
|
22
|
+
* spreading a non-iterable primitive such as `number` / `boolean` in an
|
|
23
|
+
* iterable position. The only case TypeScript accepts yet is genuinely
|
|
24
|
+
* error-prone is `string`, so that is exactly what this rule targets.
|
|
25
|
+
*
|
|
26
|
+
* If a character split is really intended, spell it out with
|
|
27
|
+
* `Array.from(str)` (identical to the spread, code-point aware) or
|
|
28
|
+
* `str.split('')`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const noStringSpread: TSESLint.RuleModule<MessageIds, Options>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=no-string-spread.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-string-spread.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/no-string-spread.mts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGtE,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,gBAAgB,CAAC;AAanC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAyE1D,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deferred (non-concrete) type flags whose actual members are only known once
|
|
6
|
+
* the type is instantiated. They are reduced to their base constraint before
|
|
7
|
+
* checking whether they are string-like.
|
|
8
|
+
*/
|
|
9
|
+
const DEFERRED_TYPE_FLAGS = ts.TypeFlags.TypeParameter |
|
|
10
|
+
ts.TypeFlags.IndexedAccess |
|
|
11
|
+
ts.TypeFlags.Conditional |
|
|
12
|
+
ts.TypeFlags.Substitution;
|
|
13
|
+
/**
|
|
14
|
+
* Disallow spreading a `string` value with the spread operator `...`.
|
|
15
|
+
*
|
|
16
|
+
* ## Why
|
|
17
|
+
*
|
|
18
|
+
* Spreading a string iterates it into its individual characters, so
|
|
19
|
+
* `[...someString]` (or `f(...someString)`) evaluates to a `string[]` — the
|
|
20
|
+
* *exact same result type* as spreading an actual `string[]`. That makes an
|
|
21
|
+
* accidental string spread silent: neither the reader nor the type checker can
|
|
22
|
+
* tell `[...anArray]` from `[...aString]` by the result type alone, so a bug
|
|
23
|
+
* where an array was expected but a `string` slipped in goes unnoticed.
|
|
24
|
+
*
|
|
25
|
+
* ## Why not simply "allow array / object types only"
|
|
26
|
+
*
|
|
27
|
+
* The naive framing — "only permit `...` on arrays or objects" — would also
|
|
28
|
+
* reject perfectly idiomatic spreads of other iterables (`Set`, `Map`,
|
|
29
|
+
* `Map.prototype.keys()`, generators, `NodeList`, typed arrays, `arguments`,
|
|
30
|
+
* …), which have no equivalent silent hazard. And TypeScript *already* rejects
|
|
31
|
+
* spreading a non-iterable primitive such as `number` / `boolean` in an
|
|
32
|
+
* iterable position. The only case TypeScript accepts yet is genuinely
|
|
33
|
+
* error-prone is `string`, so that is exactly what this rule targets.
|
|
34
|
+
*
|
|
35
|
+
* If a character split is really intended, spell it out with
|
|
36
|
+
* `Array.from(str)` (identical to the spread, code-point aware) or
|
|
37
|
+
* `str.split('')`.
|
|
38
|
+
*/
|
|
39
|
+
const noStringSpread = {
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
docs: {
|
|
43
|
+
description: 'Disallow spreading a `string` (e.g. `[...str]`), which silently splits it into characters and yields a `string[]` indistinguishable from spreading an array',
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
messages: {
|
|
47
|
+
noStringSpread: 'Spreading a `string` splits it into individual characters and yields a `string[]` indistinguishable from spreading an array, so accidental string spreads go unnoticed. Spread an array or object instead; if a character split is intended, make it explicit with `Array.from(str)`.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
create: (context) => {
|
|
51
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
52
|
+
const checker = parserServices.program.getTypeChecker();
|
|
53
|
+
/**
|
|
54
|
+
* Returns `true` if the given type could be spread as a string (it is
|
|
55
|
+
* string-like, or a union / intersection with a string-like constituent).
|
|
56
|
+
*
|
|
57
|
+
* - `any` / `unknown` are treated conservatively as *not* string-like:
|
|
58
|
+
* they cannot be proven to be a string, and flagging them would fire on
|
|
59
|
+
* every untyped spread.
|
|
60
|
+
* - Unions are flagged when *any* branch is string-like, because such a
|
|
61
|
+
* branch reintroduces the silent char-split hazard.
|
|
62
|
+
* - Intersections are flagged when *any* constituent is string-like: a
|
|
63
|
+
* branded string (`string & Brand`) is still a `string` at runtime.
|
|
64
|
+
* - A deferred type (type parameter, indexed access, conditional, …) is
|
|
65
|
+
* reduced to its base constraint; one with no resolvable constraint is
|
|
66
|
+
* left alone to avoid false positives on unconstrained generics.
|
|
67
|
+
*/
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
69
|
+
const isStringLikeType = (type) => {
|
|
70
|
+
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (type.isUnion() || type.isIntersection()) {
|
|
74
|
+
return type.types.some(isStringLikeType);
|
|
75
|
+
}
|
|
76
|
+
if ((type.flags & DEFERRED_TYPE_FLAGS) !== 0) {
|
|
77
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
78
|
+
return constraint !== undefined && isStringLikeType(constraint);
|
|
79
|
+
}
|
|
80
|
+
return (type.flags & ts.TypeFlags.StringLike) !== 0;
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
// `SpreadElement` covers every value-spread position: array literals
|
|
84
|
+
// (`[...x]`), call / new arguments (`f(...x)`), and object literals
|
|
85
|
+
// (`{...x}`). Destructuring rest (`[a, ...rest]`, `{ ...rest }`) is a
|
|
86
|
+
// `RestElement` / `RestType` and is intentionally not matched.
|
|
87
|
+
SpreadElement: (node) => {
|
|
88
|
+
const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(node.argument);
|
|
89
|
+
if (!isStringLikeType(checker.getTypeAtLocation(argTsNode)))
|
|
90
|
+
return;
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
messageId: 'noStringSpread',
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
defaultOptions: [],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { noStringSpread };
|
|
102
|
+
//# sourceMappingURL=no-string-spread.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-string-spread.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/no-string-spread.mts"],"sourcesContent":[null],"names":[],"mappings":";;;AAOA;;;;AAIG;AACH,MAAM,mBAAmB,GACvB,EAAE,CAAC,SAAS,CAAC,aAAa;IAC1B,EAAE,CAAC,SAAS,CAAC,aAAa;IAC1B,EAAE,CAAC,SAAS,CAAC,WAAW;AACxB,IAAA,EAAE,CAAC,SAAS,CAAC,YAAY;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,cAAc,GAA6C;AACtE,IAAA,IAAI,EAAE;AACJ,QAAA,IAAI,EAAE,SAAS;AACf,QAAA,IAAI,EAAE;AACJ,YAAA,WAAW,EACT,6JAA6J;AAChK,SAAA;AACD,QAAA,MAAM,EAAE,EAAE;AACV,QAAA,QAAQ,EAAE;AACR,YAAA,cAAc,EACZ,uRAAuR;AAC1R,SAAA;AACF,KAAA;AAED,IAAA,MAAM,EAAE,CAAC,OAAO,KAAI;QAClB,MAAM,cAAc,GAAG,WAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC;QAE7D,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,cAAc,EAAE;AAEvD;;;;;;;;;;;;;;AAcG;;AAEH,QAAA,MAAM,gBAAgB,GAAG,CAAC,IAAa,KAAa;YAClD,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,SAAS,CAAC,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;AAClE,gBAAA,OAAO,KAAK;YACd;YAEA,IAAI,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE;gBAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC;YAC1C;YAEA,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,mBAAmB,MAAM,CAAC,EAAE;gBAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,uBAAuB,CAAC,IAAI,CAAC;gBAExD,OAAO,UAAU,KAAK,SAAS,IAAI,gBAAgB,CAAC,UAAU,CAAC;YACjE;AAEA,YAAA,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,UAAU,MAAM,CAAC;AACrD,QAAA,CAAC;QAED,OAAO;;;;;AAKL,YAAA,aAAa,EAAE,CAAC,IAAI,KAAI;AACtB,gBAAA,MAAM,SAAS,GAAG,cAAc,CAAC,qBAAqB,CAAC,GAAG,CACxD,IAAI,CAAC,QAAQ,CACd;gBAED,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;oBAAE;gBAE7D,OAAO,CAAC,MAAM,CAAC;oBACb,IAAI;AACJ,oBAAA,SAAS,EAAE,gBAAgB;AAC5B,iBAAA,CAAC;YACJ,CAAC;SACF;IACH,CAAC;AACD,IAAA,cAAc,EAAE,EAAE;;;;;"}
|
|
@@ -14,6 +14,7 @@ export declare const tsRestrictionsRules: {
|
|
|
14
14
|
}>;
|
|
15
15
|
}>)[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
16
16
|
readonly 'no-restricted-syntax': import("eslint").Rule.RuleModule;
|
|
17
|
+
readonly 'no-string-spread': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noStringSpread", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
17
18
|
readonly 'no-unnecessary-array-from': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnecessaryArrayFrom", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
18
19
|
readonly 'no-unnecessary-coalesce-undefined': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnecessaryCoalesceUndefined", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
19
20
|
readonly 'prefer-non-mutating-array-method': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferNonMutatingMethod", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"names":[],"mappings":"AASA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;CAQU,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkDestructuringCompleteness } from './check-destructuring-completeness.mjs';
|
|
2
2
|
import { noRestrictedCastName } from './no-restricted-cast-name.mjs';
|
|
3
3
|
import { noRestrictedSyntax } from './no-restricted-syntax.mjs';
|
|
4
|
+
import { noStringSpread } from './no-string-spread.mjs';
|
|
4
5
|
import { noUnnecessaryArrayFrom } from './no-unnecessary-array-from.mjs';
|
|
5
6
|
import { noUnnecessaryCoalesceUndefined } from './no-unnecessary-coalesce-undefined.mjs';
|
|
6
7
|
import { preferNonMutatingArrayMethod } from './prefer-non-mutating-array-method.mjs';
|
|
@@ -9,6 +10,7 @@ const tsRestrictionsRules = {
|
|
|
9
10
|
'check-destructuring-completeness': checkDestructuringCompleteness,
|
|
10
11
|
'no-restricted-cast-name': noRestrictedCastName,
|
|
11
12
|
'no-restricted-syntax': noRestrictedSyntax,
|
|
13
|
+
'no-string-spread': noStringSpread,
|
|
12
14
|
'no-unnecessary-array-from': noUnnecessaryArrayFrom,
|
|
13
15
|
'no-unnecessary-coalesce-undefined': noUnnecessaryCoalesceUndefined,
|
|
14
16
|
'prefer-non-mutating-array-method': preferNonMutatingArrayMethod,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.mjs","sources":["../../../../src/plugins/ts-restrictions/rules/rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;AASO,MAAM,mBAAmB,GAAG;AACjC,IAAA,kCAAkC,EAAE,8BAA8B;AAClE,IAAA,yBAAyB,EAAE,oBAAoB;AAC/C,IAAA,sBAAsB,EAAE,kBAAkB;AAC1C,IAAA,kBAAkB,EAAE,cAAc;AAClC,IAAA,2BAA2B,EAAE,sBAAsB;AACnD,IAAA,mCAAmC,EAAE,8BAA8B;AACnE,IAAA,kCAAkC,EAAE,4BAA4B;;;;;"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const eslintTsRestrictionsRules: {
|
|
2
2
|
readonly 'ts-restrictions/no-restricted-syntax': "off";
|
|
3
3
|
readonly 'ts-restrictions/no-restricted-cast-name': "off";
|
|
4
|
+
readonly 'ts-restrictions/no-string-spread': "error";
|
|
4
5
|
readonly 'ts-restrictions/no-unnecessary-array-from': "error";
|
|
5
6
|
readonly 'ts-restrictions/no-unnecessary-coalesce-undefined': "error";
|
|
6
7
|
readonly 'ts-restrictions/prefer-non-mutating-array-method': "error";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,yBAAyB
|
|
1
|
+
{"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,yBAAyB;;;;;;;;CASQ,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { withDefaultOption } from '../types/rule-severity-with-default-option.mj
|
|
|
3
3
|
const eslintTsRestrictionsRules = {
|
|
4
4
|
'ts-restrictions/no-restricted-syntax': 'off',
|
|
5
5
|
'ts-restrictions/no-restricted-cast-name': 'off',
|
|
6
|
+
'ts-restrictions/no-string-spread': 'error',
|
|
6
7
|
'ts-restrictions/no-unnecessary-array-from': 'error',
|
|
7
8
|
'ts-restrictions/no-unnecessary-coalesce-undefined': 'error',
|
|
8
9
|
'ts-restrictions/prefer-non-mutating-array-method': 'error',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eslint-ts-restrictions-rules.mjs","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;AAKO,MAAM,yBAAyB,GAAG;AACvC,IAAA,sCAAsC,EAAE,KAAK;AAC7C,IAAA,yCAAyC,EAAE,KAAK;AAChD,IAAA,2CAA2C,EAAE,OAAO;AACpD,IAAA,mDAAmD,EAAE,OAAO;AAC5D,IAAA,kDAAkD,EAAE,OAAO;AAC3D,IAAA,kDAAkD,EAChD,iBAAiB,CAAC,OAAO,CAAC;;;;;"}
|
|
1
|
+
{"version":3,"file":"eslint-ts-restrictions-rules.mjs","sources":["../../src/rules/eslint-ts-restrictions-rules.mts"],"sourcesContent":[null],"names":[],"mappings":";;AAKO,MAAM,yBAAyB,GAAG;AACvC,IAAA,sCAAsC,EAAE,KAAK;AAC7C,IAAA,yCAAyC,EAAE,KAAK;AAChD,IAAA,kCAAkC,EAAE,OAAO;AAC3C,IAAA,2CAA2C,EAAE,OAAO;AACpD,IAAA,mDAAmD,EAAE,OAAO;AAC5D,IAAA,kDAAkD,EAAE,OAAO;AAC3D,IAAA,kDAAkD,EAChD,iBAAiB,CAAC,OAAO,CAAC;;;;;"}
|
|
@@ -204,6 +204,19 @@ declare namespace NoRestrictedSyntax {
|
|
|
204
204
|
}>)[];
|
|
205
205
|
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* @description Disallow spreading a `string` (e.g. `[...str]`), which silently splits it into characters and yields a `string[]` indistinguishable from spreading an array
|
|
209
|
+
*
|
|
210
|
+
* ```md
|
|
211
|
+
* | key | value |
|
|
212
|
+
* | :--------- | :------ |
|
|
213
|
+
* | type | problem |
|
|
214
|
+
* | deprecated | false |
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
declare namespace NoStringSpread {
|
|
218
|
+
type RuleEntry = Linter.StringSeverity;
|
|
219
|
+
}
|
|
207
220
|
/**
|
|
208
221
|
* @description Disallow wrapping an array in `Array.from()` before a non-mutating array method (e.g. `Array.from(x).toSorted()`), since the method already returns a new array
|
|
209
222
|
*
|
|
@@ -250,6 +263,7 @@ export type EslintTsRestrictionsRules = Readonly<{
|
|
|
250
263
|
'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.RuleEntry;
|
|
251
264
|
'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.RuleEntry;
|
|
252
265
|
'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.RuleEntry;
|
|
266
|
+
'ts-restrictions/no-string-spread': NoStringSpread.RuleEntry;
|
|
253
267
|
'ts-restrictions/no-unnecessary-array-from': NoUnnecessaryArrayFrom.RuleEntry;
|
|
254
268
|
'ts-restrictions/no-unnecessary-coalesce-undefined': NoUnnecessaryCoalesceUndefined.RuleEntry;
|
|
255
269
|
'ts-restrictions/prefer-non-mutating-array-method': PreferNonMutatingArrayMethod.RuleEntry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"eslint-ts-restrictions-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-ts-restrictions-rules.mts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ESLintUtils, type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
type Options = readonly [];
|
|
5
|
+
|
|
6
|
+
type MessageIds = 'noStringSpread';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deferred (non-concrete) type flags whose actual members are only known once
|
|
10
|
+
* the type is instantiated. They are reduced to their base constraint before
|
|
11
|
+
* checking whether they are string-like.
|
|
12
|
+
*/
|
|
13
|
+
const DEFERRED_TYPE_FLAGS =
|
|
14
|
+
ts.TypeFlags.TypeParameter |
|
|
15
|
+
ts.TypeFlags.IndexedAccess |
|
|
16
|
+
ts.TypeFlags.Conditional |
|
|
17
|
+
ts.TypeFlags.Substitution;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Disallow spreading a `string` value with the spread operator `...`.
|
|
21
|
+
*
|
|
22
|
+
* ## Why
|
|
23
|
+
*
|
|
24
|
+
* Spreading a string iterates it into its individual characters, so
|
|
25
|
+
* `[...someString]` (or `f(...someString)`) evaluates to a `string[]` — the
|
|
26
|
+
* *exact same result type* as spreading an actual `string[]`. That makes an
|
|
27
|
+
* accidental string spread silent: neither the reader nor the type checker can
|
|
28
|
+
* tell `[...anArray]` from `[...aString]` by the result type alone, so a bug
|
|
29
|
+
* where an array was expected but a `string` slipped in goes unnoticed.
|
|
30
|
+
*
|
|
31
|
+
* ## Why not simply "allow array / object types only"
|
|
32
|
+
*
|
|
33
|
+
* The naive framing — "only permit `...` on arrays or objects" — would also
|
|
34
|
+
* reject perfectly idiomatic spreads of other iterables (`Set`, `Map`,
|
|
35
|
+
* `Map.prototype.keys()`, generators, `NodeList`, typed arrays, `arguments`,
|
|
36
|
+
* …), which have no equivalent silent hazard. And TypeScript *already* rejects
|
|
37
|
+
* spreading a non-iterable primitive such as `number` / `boolean` in an
|
|
38
|
+
* iterable position. The only case TypeScript accepts yet is genuinely
|
|
39
|
+
* error-prone is `string`, so that is exactly what this rule targets.
|
|
40
|
+
*
|
|
41
|
+
* If a character split is really intended, spell it out with
|
|
42
|
+
* `Array.from(str)` (identical to the spread, code-point aware) or
|
|
43
|
+
* `str.split('')`.
|
|
44
|
+
*/
|
|
45
|
+
export const noStringSpread: TSESLint.RuleModule<MessageIds, Options> = {
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'problem',
|
|
48
|
+
docs: {
|
|
49
|
+
description:
|
|
50
|
+
'Disallow spreading a `string` (e.g. `[...str]`), which silently splits it into characters and yields a `string[]` indistinguishable from spreading an array',
|
|
51
|
+
},
|
|
52
|
+
schema: [],
|
|
53
|
+
messages: {
|
|
54
|
+
noStringSpread:
|
|
55
|
+
'Spreading a `string` splits it into individual characters and yields a `string[]` indistinguishable from spreading an array, so accidental string spreads go unnoticed. Spread an array or object instead; if a character split is intended, make it explicit with `Array.from(str)`.',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
create: (context) => {
|
|
60
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
61
|
+
|
|
62
|
+
const checker = parserServices.program.getTypeChecker();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns `true` if the given type could be spread as a string (it is
|
|
66
|
+
* string-like, or a union / intersection with a string-like constituent).
|
|
67
|
+
*
|
|
68
|
+
* - `any` / `unknown` are treated conservatively as *not* string-like:
|
|
69
|
+
* they cannot be proven to be a string, and flagging them would fire on
|
|
70
|
+
* every untyped spread.
|
|
71
|
+
* - Unions are flagged when *any* branch is string-like, because such a
|
|
72
|
+
* branch reintroduces the silent char-split hazard.
|
|
73
|
+
* - Intersections are flagged when *any* constituent is string-like: a
|
|
74
|
+
* branded string (`string & Brand`) is still a `string` at runtime.
|
|
75
|
+
* - A deferred type (type parameter, indexed access, conditional, …) is
|
|
76
|
+
* reduced to its base constraint; one with no resolvable constraint is
|
|
77
|
+
* left alone to avoid false positives on unconstrained generics.
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
80
|
+
const isStringLikeType = (type: ts.Type): boolean => {
|
|
81
|
+
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (type.isUnion() || type.isIntersection()) {
|
|
86
|
+
return type.types.some(isStringLikeType);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ((type.flags & DEFERRED_TYPE_FLAGS) !== 0) {
|
|
90
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
91
|
+
|
|
92
|
+
return constraint !== undefined && isStringLikeType(constraint);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (type.flags & ts.TypeFlags.StringLike) !== 0;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
// `SpreadElement` covers every value-spread position: array literals
|
|
100
|
+
// (`[...x]`), call / new arguments (`f(...x)`), and object literals
|
|
101
|
+
// (`{...x}`). Destructuring rest (`[a, ...rest]`, `{ ...rest }`) is a
|
|
102
|
+
// `RestElement` / `RestType` and is intentionally not matched.
|
|
103
|
+
SpreadElement: (node) => {
|
|
104
|
+
const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(
|
|
105
|
+
node.argument,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!isStringLikeType(checker.getTypeAtLocation(argTsNode))) return;
|
|
109
|
+
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: 'noStringSpread',
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
defaultOptions: [],
|
|
118
|
+
} as const;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { noStringSpread } from './no-string-spread.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('no-string-spread', () => {
|
|
21
|
+
tester.run('no-string-spread', noStringSpread, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'spreading an array into an array literal',
|
|
25
|
+
code: dedent`
|
|
26
|
+
declare const xs: readonly number[];
|
|
27
|
+
const ys = [...xs];
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'spreading a tuple into an array literal',
|
|
32
|
+
code: dedent`
|
|
33
|
+
declare const xs: readonly [number, string];
|
|
34
|
+
const ys = [...xs];
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'spreading an array into a function call',
|
|
39
|
+
code: dedent`
|
|
40
|
+
declare const xs: readonly number[];
|
|
41
|
+
const m = Math.max(...xs);
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'spreading an object into an object literal',
|
|
46
|
+
code: dedent`
|
|
47
|
+
declare const obj: { readonly a: number; readonly b: number };
|
|
48
|
+
const clone = { ...obj };
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'spreading a Set is a legitimate iterable spread',
|
|
53
|
+
code: dedent`
|
|
54
|
+
declare const s: ReadonlySet<number>;
|
|
55
|
+
const ys = [...s];
|
|
56
|
+
`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'spreading a Map is a legitimate iterable spread',
|
|
60
|
+
code: dedent`
|
|
61
|
+
declare const m: ReadonlyMap<string, number>;
|
|
62
|
+
const entries = [...m];
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'spreading a generic iterable is not a string',
|
|
67
|
+
code: dedent`
|
|
68
|
+
declare const it: Iterable<number>;
|
|
69
|
+
const ys = [...it];
|
|
70
|
+
`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'any is treated conservatively and not flagged',
|
|
74
|
+
code: dedent`
|
|
75
|
+
declare const x: any;
|
|
76
|
+
const ys = [...x];
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'array destructuring rest is not a value spread',
|
|
81
|
+
code: dedent`
|
|
82
|
+
declare const xs: readonly number[];
|
|
83
|
+
const [first, ...rest] = xs;
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'object destructuring rest is not a value spread',
|
|
88
|
+
code: dedent`
|
|
89
|
+
declare const obj: { readonly a: number; readonly b: number };
|
|
90
|
+
const { a, ...others } = obj;
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'spreading an array of strings is fine (not a string itself)',
|
|
95
|
+
code: dedent`
|
|
96
|
+
declare const xs: readonly string[];
|
|
97
|
+
const ys = [...xs];
|
|
98
|
+
`,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'unconstrained generic is left alone',
|
|
102
|
+
code: dedent`
|
|
103
|
+
const f = <T>(xs: readonly T[]): readonly T[] => [...xs];
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
invalid: [
|
|
108
|
+
{
|
|
109
|
+
name: 'spreading a string into an array literal',
|
|
110
|
+
code: dedent`
|
|
111
|
+
declare const s: string;
|
|
112
|
+
const chars = [...s];
|
|
113
|
+
`,
|
|
114
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'spreading a string into a function call',
|
|
118
|
+
code: dedent`
|
|
119
|
+
declare const s: string;
|
|
120
|
+
declare function f(...args: readonly string[]): void;
|
|
121
|
+
f(...s);
|
|
122
|
+
`,
|
|
123
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'spreading a string into an object literal',
|
|
127
|
+
code: dedent`
|
|
128
|
+
declare const s: string;
|
|
129
|
+
const o = { ...s };
|
|
130
|
+
`,
|
|
131
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'spreading a string literal type',
|
|
135
|
+
code: dedent`
|
|
136
|
+
declare const s: 'abc';
|
|
137
|
+
const chars = [...s];
|
|
138
|
+
`,
|
|
139
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'spreading a template literal type',
|
|
143
|
+
code: dedent`
|
|
144
|
+
declare const s: \`id_\${string}\`;
|
|
145
|
+
const chars = [...s];
|
|
146
|
+
`,
|
|
147
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'spreading a union that includes string is still flagged',
|
|
151
|
+
code: dedent`
|
|
152
|
+
declare const s: string | readonly string[];
|
|
153
|
+
const ys = [...s];
|
|
154
|
+
`,
|
|
155
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'spreading a branded string (intersection) is flagged',
|
|
159
|
+
code: dedent`
|
|
160
|
+
type UserId = string & { readonly __brand: unique symbol };
|
|
161
|
+
declare const s: UserId;
|
|
162
|
+
const chars = [...s];
|
|
163
|
+
`,
|
|
164
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'spreading a generic constrained to string is flagged',
|
|
168
|
+
code: dedent`
|
|
169
|
+
const f = <T extends string>(s: T): readonly string[] => [...s];
|
|
170
|
+
`,
|
|
171
|
+
errors: [{ messageId: 'noStringSpread' }],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -2,6 +2,7 @@ import { type ESLintPlugin } from '../../../types/index.mjs';
|
|
|
2
2
|
import { checkDestructuringCompleteness } from './check-destructuring-completeness.mjs';
|
|
3
3
|
import { noRestrictedCastName } from './no-restricted-cast-name.mjs';
|
|
4
4
|
import { noRestrictedSyntax } from './no-restricted-syntax.mjs';
|
|
5
|
+
import { noStringSpread } from './no-string-spread.mjs';
|
|
5
6
|
import { noUnnecessaryArrayFrom } from './no-unnecessary-array-from.mjs';
|
|
6
7
|
import { noUnnecessaryCoalesceUndefined } from './no-unnecessary-coalesce-undefined.mjs';
|
|
7
8
|
import { preferNonMutatingArrayMethod } from './prefer-non-mutating-array-method.mjs';
|
|
@@ -10,6 +11,7 @@ export const tsRestrictionsRules = {
|
|
|
10
11
|
'check-destructuring-completeness': checkDestructuringCompleteness,
|
|
11
12
|
'no-restricted-cast-name': noRestrictedCastName,
|
|
12
13
|
'no-restricted-syntax': noRestrictedSyntax,
|
|
14
|
+
'no-string-spread': noStringSpread,
|
|
13
15
|
'no-unnecessary-array-from': noUnnecessaryArrayFrom,
|
|
14
16
|
'no-unnecessary-coalesce-undefined': noUnnecessaryCoalesceUndefined,
|
|
15
17
|
'prefer-non-mutating-array-method': preferNonMutatingArrayMethod,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
export const eslintTsRestrictionsRules = {
|
|
7
7
|
'ts-restrictions/no-restricted-syntax': 'off',
|
|
8
8
|
'ts-restrictions/no-restricted-cast-name': 'off',
|
|
9
|
+
'ts-restrictions/no-string-spread': 'error',
|
|
9
10
|
'ts-restrictions/no-unnecessary-array-from': 'error',
|
|
10
11
|
'ts-restrictions/no-unnecessary-coalesce-undefined': 'error',
|
|
11
12
|
'ts-restrictions/prefer-non-mutating-array-method': 'error',
|
|
@@ -235,6 +235,20 @@ namespace NoRestrictedSyntax {
|
|
|
235
235
|
| SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* @description Disallow spreading a `string` (e.g. `[...str]`), which silently splits it into characters and yields a `string[]` indistinguishable from spreading an array
|
|
240
|
+
*
|
|
241
|
+
* ```md
|
|
242
|
+
* | key | value |
|
|
243
|
+
* | :--------- | :------ |
|
|
244
|
+
* | type | problem |
|
|
245
|
+
* | deprecated | false |
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
namespace NoStringSpread {
|
|
249
|
+
export type RuleEntry = Linter.StringSeverity;
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
/**
|
|
239
253
|
* @description Disallow wrapping an array in `Array.from()` before a non-mutating array method (e.g. `Array.from(x).toSorted()`), since the method already returns a new array
|
|
240
254
|
*
|
|
@@ -284,6 +298,7 @@ export type EslintTsRestrictionsRules = Readonly<{
|
|
|
284
298
|
'ts-restrictions/check-destructuring-completeness': CheckDestructuringCompleteness.RuleEntry;
|
|
285
299
|
'ts-restrictions/no-restricted-cast-name': NoRestrictedCastName.RuleEntry;
|
|
286
300
|
'ts-restrictions/no-restricted-syntax': NoRestrictedSyntax.RuleEntry;
|
|
301
|
+
'ts-restrictions/no-string-spread': NoStringSpread.RuleEntry;
|
|
287
302
|
'ts-restrictions/no-unnecessary-array-from': NoUnnecessaryArrayFrom.RuleEntry;
|
|
288
303
|
'ts-restrictions/no-unnecessary-coalesce-undefined': NoUnnecessaryCoalesceUndefined.RuleEntry;
|
|
289
304
|
'ts-restrictions/prefer-non-mutating-array-method': PreferNonMutatingArrayMethod.RuleEntry;
|