eslint-plugin-ts-fortress 1.0.7 → 1.2.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/README.md +150 -4
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.d.mts +9 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +32 -27
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +90 -2
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs +2 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/constants.mjs +17 -12
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/import-utils.mjs +96 -52
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/index.mjs +2 -2
- package/dist/rules/index.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-constrained-type.mjs +226 -157
- package/dist/rules/prefer-canonical-length-constrained-type.mjs.map +1 -1
- package/dist/rules/prefer-namespace-import.d.mts +20 -0
- package/dist/rules/prefer-namespace-import.d.mts.map +1 -0
- package/dist/rules/prefer-namespace-import.mjs +216 -0
- package/dist/rules/prefer-namespace-import.mjs.map +1 -0
- package/dist/rules/prefer-schema-over-guard-chain.d.mts +56 -0
- package/dist/rules/prefer-schema-over-guard-chain.d.mts.map +1 -0
- package/dist/rules/prefer-schema-over-guard-chain.mjs +212 -0
- package/dist/rules/prefer-schema-over-guard-chain.mjs.map +1 -0
- package/dist/rules/rules.d.mts +7 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +7 -6
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/types.mjs +2 -2
- package/dist/types.mjs.map +1 -1
- package/package.json +6 -7
- package/src/plugin.mts +2 -0
- package/src/rule-types.mts +116 -2
- package/src/rules/prefer-canonical-length-constrained-type.mts +9 -2
- package/src/rules/prefer-namespace-import.mts +341 -0
- package/src/rules/prefer-namespace-import.test.mts +320 -0
- package/src/rules/prefer-schema-over-guard-chain.mts +257 -0
- package/src/rules/prefer-schema-over-guard-chain.test.mts +143 -0
- package/src/rules/rules.mts +4 -0
package/README.md
CHANGED
|
@@ -67,9 +67,11 @@ export default [
|
|
|
67
67
|
|
|
68
68
|
## Rules
|
|
69
69
|
|
|
70
|
-
| Rule | Description
|
|
71
|
-
| :----------------------------------------- |
|
|
72
|
-
| `prefer-canonical-length-constrained-type` | Normalize a length-constrained array combinator with degenerate bounds to its canonical form.
|
|
70
|
+
| Rule | Description |
|
|
71
|
+
| :----------------------------------------- | :--------------------------------------------------------------------------------------------- |
|
|
72
|
+
| `prefer-canonical-length-constrained-type` | Normalize a length-constrained array combinator with degenerate bounds to its canonical form. |
|
|
73
|
+
| `prefer-namespace-import` | Require `ts-fortress` to be imported as a namespace rather than by name. |
|
|
74
|
+
| `prefer-schema-over-guard-chain` | Report a hand-written chain of type guards on one value once it is long enough to be a schema. |
|
|
73
75
|
|
|
74
76
|
### `prefer-canonical-length-constrained-type`
|
|
75
77
|
|
|
@@ -133,7 +135,10 @@ bound it carries over to the other combinator is a literal within `0..10`.
|
|
|
133
135
|
Both the namespace style (`import * as t from 'ts-fortress'`) and named imports
|
|
134
136
|
— including aliases — are recognized, and the autofix reuses whatever binding
|
|
135
137
|
the file already has, adding `import { … } from 'ts-fortress';` only when a
|
|
136
|
-
named call needs one.
|
|
138
|
+
named call needs one. With
|
|
139
|
+
[`prefer-namespace-import`](#prefer-namespace-import) on as well — as it is in
|
|
140
|
+
the `recommended` preset — such an import is rewritten to a namespace access on
|
|
141
|
+
a later fix pass.
|
|
137
142
|
|
|
138
143
|
#### The rule deliberately leaves alone
|
|
139
144
|
|
|
@@ -149,6 +154,147 @@ named call needs one.
|
|
|
149
154
|
- calls where the target name is already bound to something else at the call
|
|
150
155
|
site.
|
|
151
156
|
|
|
157
|
+
### `prefer-namespace-import`
|
|
158
|
+
|
|
159
|
+
`ts-fortress` is meant to be reached through a namespace. Its exports are short,
|
|
160
|
+
generic names — `string`, `number`, `record`, `array`, `Type` — that collide with
|
|
161
|
+
globals and with local declarations the moment they are pulled into a file's
|
|
162
|
+
scope, and every schema in the wild reads `t.string()`. The rule makes that the
|
|
163
|
+
only spelling:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// ❌
|
|
167
|
+
import { record, string } from 'ts-fortress';
|
|
168
|
+
|
|
169
|
+
const User = record({ name: string() });
|
|
170
|
+
|
|
171
|
+
// ✅
|
|
172
|
+
import * as t from 'ts-fortress';
|
|
173
|
+
|
|
174
|
+
const User = t.record({ name: t.string() });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`import type * as t from 'ts-fortress';` is accepted too, and is what the
|
|
178
|
+
autofix writes when every offending import in the file is type-only. A namespace
|
|
179
|
+
import may carry any local name — the rule does not rename an existing one.
|
|
180
|
+
|
|
181
|
+
A default import is reported as well, and its references become the namespace
|
|
182
|
+
itself. `ts-fortress` has no default export, so such a file did not type-check
|
|
183
|
+
to begin with — the rewrite is what it was reaching for.
|
|
184
|
+
|
|
185
|
+
A bare `import 'ts-fortress';` is reported and **deleted**: it binds no name,
|
|
186
|
+
and `ts-fortress` declares `sideEffects: false`, so importing it for its own
|
|
187
|
+
sake does nothing.
|
|
188
|
+
|
|
189
|
+
The autofix rewrites every reference along with the import: aliases resolve back
|
|
190
|
+
to the canonical export (`import { nonEmptyArray as nea }` → `t.nonEmptyArray`),
|
|
191
|
+
shorthand properties are expanded (`{ string }` → `{ string: t.string }`), and
|
|
192
|
+
type positions need nothing special, since `t.Type` reads as a qualified name
|
|
193
|
+
there. Several ts-fortress imports in one file collapse into a single namespace
|
|
194
|
+
import; when the file already has one, the others merge into it under its name.
|
|
195
|
+
|
|
196
|
+
#### Options
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
'ts-fortress/prefer-namespace-import': ['error', { namespaceName: 'tf' }],
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- `namespaceName` (default `'t'`) — the local name the autofix gives the
|
|
203
|
+
namespace import it creates. A namespace import already in the file is reused
|
|
204
|
+
under its own name, whatever this is set to.
|
|
205
|
+
|
|
206
|
+
#### When the fix is withheld
|
|
207
|
+
|
|
208
|
+
The rule still reports, but leaves the file alone, when the rewrite could not be
|
|
209
|
+
completed safely:
|
|
210
|
+
|
|
211
|
+
- the namespace name is already bound to something else, at the import or at any
|
|
212
|
+
reference being rewritten (a fix would silently resolve to that binding);
|
|
213
|
+
- a binding is re-exported by name (`export { string };`), which no member access
|
|
214
|
+
can spell;
|
|
215
|
+
- a value import would have to merge into an `import type * as t`, which cannot
|
|
216
|
+
carry it;
|
|
217
|
+
- the file has two namespace imports of `ts-fortress`, or one declaration that
|
|
218
|
+
mixes a namespace specifier with a named or default one.
|
|
219
|
+
|
|
220
|
+
### `prefer-schema-over-guard-chain`
|
|
221
|
+
|
|
222
|
+
A long chain of hand-written type guards is a shape declaration that cannot be
|
|
223
|
+
read as one:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// ❌
|
|
227
|
+
const isProject = (value: unknown): value is Project =>
|
|
228
|
+
isRecord(value) &&
|
|
229
|
+
hasKey(value, 'name') &&
|
|
230
|
+
isString(value.name) &&
|
|
231
|
+
hasKey(value, 'path') &&
|
|
232
|
+
isString(value.path);
|
|
233
|
+
|
|
234
|
+
// ✅
|
|
235
|
+
const PROJECT = t.record({ name: t.string(), path: t.string() });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The length is the least of it. What the chain cannot do is say what was wrong:
|
|
239
|
+
it returns `false`, so the caller filtering on it drops the value silently, with
|
|
240
|
+
nothing left of which field was missing or what type it turned out to be. The
|
|
241
|
+
shape is declared and checked in the same breath, so it cannot be named, reused,
|
|
242
|
+
or read on its own. `validate` returns structured `ValidationError`s, ready for
|
|
243
|
+
`validationErrorsToMessages`.
|
|
244
|
+
|
|
245
|
+
The rule reports; there is no fix. Writing the schema is the work, and it is not
|
|
246
|
+
mechanical — the chain says what the value must _have_, never what it _is_, so
|
|
247
|
+
an optional member, a union, and a member that is simply never checked are
|
|
248
|
+
indistinguishable from the guards alone.
|
|
249
|
+
|
|
250
|
+
#### What is counted
|
|
251
|
+
|
|
252
|
+
Guards within one `&&` chain, or one `||` chain of negated guards — the
|
|
253
|
+
early-return spelling of the same check — grouped by the identifier their first
|
|
254
|
+
argument is rooted at, so `hasKey(a, 'b')` and `isString(a.b)` count together
|
|
255
|
+
while guards on two different values do not add up.
|
|
256
|
+
|
|
257
|
+
A whole function body is deliberately not the unit. The same value checked once
|
|
258
|
+
per field across a `fill`-style function is a series of defaults rather than a
|
|
259
|
+
shape check, and measuring both units over one repository showed those to be
|
|
260
|
+
almost all of what a function-scoped count reports.
|
|
261
|
+
|
|
262
|
+
#### Options
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
'ts-fortress/prefer-schema-over-guard-chain': ['error', { threshold: 4 }],
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- `threshold` (default `5`) — how many guards on one value a single chain may
|
|
269
|
+
contain before it is reported.
|
|
270
|
+
- `guards` (default: the `ts-data-forge` narrowing helpers — `hasKey`,
|
|
271
|
+
`isRecord`, `isString`, `isNumber`, `isBoolean`, `isBigint`, `isSymbol`,
|
|
272
|
+
`isArray`, `isNonEmpty`, `isNonNullish`, `isNotNull`, `isNotUndefined`,
|
|
273
|
+
`isPrimitive`) — the names to count, replacing the default list. A member call
|
|
274
|
+
is matched on its property name, so `Arr.isArray` is named `isArray`.
|
|
275
|
+
|
|
276
|
+
The default threshold comes from a distribution rather than from taste. Counted
|
|
277
|
+
over the 3420 files of the repository this rule was written in, as they stood
|
|
278
|
+
before it was applied to them:
|
|
279
|
+
|
|
280
|
+
| guards | chains |
|
|
281
|
+
| -----: | -----: |
|
|
282
|
+
| 1 | 103 |
|
|
283
|
+
| 2 | 41 |
|
|
284
|
+
| 3 | 17 |
|
|
285
|
+
| 4 | 1 |
|
|
286
|
+
| 5 | 1 |
|
|
287
|
+
| 6+ | 0 |
|
|
288
|
+
|
|
289
|
+
One and two are ordinary narrowing and reporting them would be noise; three is
|
|
290
|
+
still common enough to be a style rather than a smell. Four and five have one
|
|
291
|
+
chain each, and they differ in exactly the way the threshold has to: the chain
|
|
292
|
+
of four narrows a value that is already typed, which is the case this rule is
|
|
293
|
+
least interested in, while the chain of five narrows the result of a
|
|
294
|
+
`JSON.parse` — the case it exists for. So the default sits between them, at the
|
|
295
|
+
length of the chain that prompted the rule. That chain of five is the one site
|
|
296
|
+
the rule reported when it was turned on, and it has since been rewritten.
|
|
297
|
+
|
|
152
298
|
## License
|
|
153
299
|
|
|
154
300
|
[Apache-2.0](https://github.com/noshiro-pf/mono/blob/main/libs/eslint-plugin-ts-fortress/LICENSE)
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { eslintPluginTsFortress } from './plugin.mjs';
|
|
2
|
-
export { tsFortressRules } from './rules/
|
|
3
|
-
//# sourceMappingURL=index.mjs.map
|
|
2
|
+
export { tsFortressRules } from './rules/index.mjs';
|
|
3
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","
|
|
1
|
+
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAKtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/plugin.d.mts
CHANGED
|
@@ -5,6 +5,13 @@ export declare const eslintPluginTsFortress: {
|
|
|
5
5
|
};
|
|
6
6
|
readonly rules: {
|
|
7
7
|
readonly 'prefer-canonical-length-constrained-type': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalType", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
8
|
+
readonly 'prefer-namespace-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"removeSideEffectImport" | "useNamespaceImport", readonly [(Readonly<{
|
|
9
|
+
namespaceName?: string;
|
|
10
|
+
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
11
|
+
readonly 'prefer-schema-over-guard-chain': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSchema", readonly [(Readonly<{
|
|
12
|
+
threshold?: number;
|
|
13
|
+
guards?: readonly string[];
|
|
14
|
+
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
8
15
|
};
|
|
9
16
|
readonly configs: {
|
|
10
17
|
/** Enables every rule of this plugin at `error`. */
|
|
@@ -15,6 +22,8 @@ export declare const eslintPluginTsFortress: {
|
|
|
15
22
|
};
|
|
16
23
|
readonly rules: {
|
|
17
24
|
readonly 'ts-fortress/prefer-canonical-length-constrained-type': 'error';
|
|
25
|
+
readonly 'ts-fortress/prefer-namespace-import': 'error';
|
|
26
|
+
readonly 'ts-fortress/prefer-schema-over-guard-chain': 'error';
|
|
18
27
|
};
|
|
19
28
|
};
|
|
20
29
|
};
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AAEA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AAEA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAiCvE,eAAO,MAAM,sBAAsB;;iBAE/B,IAAI,EAAE,2BAA2B;;;;;;;;;;;;;QAIjC,oDAAoD;iBACpD,WAAW;2BAtBP,yBAAyB;;yBAQzB,aAAa,EAAI,YAAY;;;iFAjBqB,OAAO;gEACxB,OAAO;uEACA,OAAO;;;;CA+BtB,CAAC"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
|
-
import { tsFortressRules } from './rules/
|
|
1
|
+
import { tsFortressRules } from './rules/index.mjs';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
2
7
|
|
|
3
8
|
const recommendedRules = {
|
|
4
|
-
|
|
9
|
+
'ts-fortress/prefer-canonical-length-constrained-type': 'error',
|
|
10
|
+
'ts-fortress/prefer-namespace-import': 'error',
|
|
11
|
+
'ts-fortress/prefer-schema-over-guard-chain': 'error',
|
|
5
12
|
};
|
|
6
13
|
const recommendedConfig = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
const eslintPluginTsFortress = {
|
|
22
|
-
meta: {
|
|
23
|
-
name: "eslint-plugin-ts-fortress"
|
|
24
|
-
},
|
|
25
|
-
rules: tsFortressRules,
|
|
26
|
-
configs: {
|
|
27
|
-
/** Enables every rule of this plugin at `error`. */
|
|
28
|
-
recommended: recommendedConfig
|
|
29
|
-
}
|
|
14
|
+
name: 'ts-fortress/recommended',
|
|
15
|
+
plugins: {
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
get 'ts-fortress'() {
|
|
23
|
+
return eslintPluginTsFortress;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
rules: recommendedRules,
|
|
30
27
|
};
|
|
28
|
+
export const eslintPluginTsFortress = {
|
|
29
|
+
meta: {
|
|
30
|
+
name: 'eslint-plugin-ts-fortress',
|
|
31
|
+
},
|
|
32
|
+
rules: tsFortressRules,
|
|
33
|
+
configs: {
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
recommended: recommendedConfig,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","
|
|
1
|
+
{"version":3,"file":"plugin.mjs","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGpD;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG;IACvB,sDAAsD,EAAE,OAAO;IAC/D,qCAAqC,EAAE,OAAO;IAC9C,4CAA4C,EAAE,OAAO;CAItD,CAAC;AAEF,MAAM,iBAAiB,GAAG;IACxB,IAAI,EAAE,yBAAyB;IAC/B,OAAO,EAAE;QACP,uEAAuE;QACvE,wEAAwE;QACxE,wEAAwE;QACxE,mDAAmD;QACnD,0EAA0E;QAC1E,qDAAqD;QACrD,IAAI,aAAa;YACf,OAAO,sBAAsB,CAAC;QAChC,CAAC;KACF;IACD,KAAK,EAAE,gBAAgB;CACY,CAAC;AAEtC,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,IAAI,EAAE;QACJ,IAAI,EAAE,2BAA2B;KAClC;IACD,KAAK,EAAE,eAAe;IACtB,OAAO,EAAE;QACP,oDAAoD;QACpD,WAAW,EAAE,iBAAiB;KAC/B;CAC8B,CAAC"}
|
package/dist/rule-types.d.mts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
import { type Linter } from 'eslint';
|
|
2
|
-
import { type
|
|
3
|
+
import { type TypeEq } from 'ts-type-forge';
|
|
3
4
|
import { type tsFortressRules } from './rules/index.mjs';
|
|
5
|
+
type SpreadOptionsIfIsArray<T extends readonly [Linter.StringSeverity, unknown]> = T[1] extends readonly unknown[] ? readonly [Linter.StringSeverity, ...T[1]] : T;
|
|
6
|
+
/**
|
|
7
|
+
* Extracts the options tuple type of a rule from its TypeScript
|
|
8
|
+
* implementation, so that option types stay in sync with the rule source
|
|
9
|
+
* without converting `meta.schema` (JSON Schema) back into TypeScript types.
|
|
10
|
+
*/
|
|
11
|
+
type RuleOptionsOf<Name extends keyof typeof tsFortressRules> = (typeof tsFortressRules)[Name] extends TSESLint.RuleModule<infer _MessageIds extends string, infer Options extends readonly unknown[]> ? Options : never;
|
|
4
12
|
/**
|
|
5
13
|
* @description Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.
|
|
6
14
|
*
|
|
@@ -15,10 +23,90 @@ import { type tsFortressRules } from './rules/index.mjs';
|
|
|
15
23
|
declare namespace PreferCanonicalLengthConstrainedType {
|
|
16
24
|
type RuleEntry = Linter.StringSeverity;
|
|
17
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* @description Require `ts-fortress` to be imported as a namespace (`import * as t from 'ts-fortress';` or `import type * as t from 'ts-fortress';`) rather than through named, default or bare side-effect imports.
|
|
28
|
+
*
|
|
29
|
+
* ```md
|
|
30
|
+
* | key | value |
|
|
31
|
+
* | :--------- | :--------- |
|
|
32
|
+
* | type | suggestion |
|
|
33
|
+
* | deprecated | false |
|
|
34
|
+
* | fixable | code |
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare namespace PreferNamespaceImport {
|
|
38
|
+
/**
|
|
39
|
+
* ### schema
|
|
40
|
+
*
|
|
41
|
+
* ```json
|
|
42
|
+
* [
|
|
43
|
+
* {
|
|
44
|
+
* "type": "object",
|
|
45
|
+
* "properties": {
|
|
46
|
+
* "namespaceName": {
|
|
47
|
+
* "type": "string",
|
|
48
|
+
* "pattern": "^[$A-Z_a-z][$\\w]*$",
|
|
49
|
+
* "description": "Local name the autofix gives the namespace import when the file has none to merge into. Defaults to 't'. A namespace import already in the file is reused under its own name, whatever this is set to."
|
|
50
|
+
* }
|
|
51
|
+
* },
|
|
52
|
+
* "additionalProperties": false
|
|
53
|
+
* }
|
|
54
|
+
* ]
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
type Options = NonNullable<RuleOptionsOf<'prefer-namespace-import'>[0]>;
|
|
58
|
+
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @description Report a chain of hand-written type guards narrowing one value, once it is long enough to be a schema, and point at `ts-fortress`
|
|
62
|
+
*
|
|
63
|
+
* ```md
|
|
64
|
+
* | key | value |
|
|
65
|
+
* | :--------- | :--------- |
|
|
66
|
+
* | type | suggestion |
|
|
67
|
+
* | deprecated | false |
|
|
68
|
+
* | fixable | false |
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare namespace PreferSchemaOverGuardChain {
|
|
72
|
+
/**
|
|
73
|
+
* ### schema
|
|
74
|
+
*
|
|
75
|
+
* ```json
|
|
76
|
+
* [
|
|
77
|
+
* {
|
|
78
|
+
* "type": "object",
|
|
79
|
+
* "properties": {
|
|
80
|
+
* "threshold": {
|
|
81
|
+
* "type": "integer",
|
|
82
|
+
* "minimum": 2,
|
|
83
|
+
* "description": "How many guards on one value a single chain may contain before it is reported. Defaults to 5."
|
|
84
|
+
* },
|
|
85
|
+
* "guards": {
|
|
86
|
+
* "type": "array",
|
|
87
|
+
* "items": {
|
|
88
|
+
* "type": "string"
|
|
89
|
+
* },
|
|
90
|
+
* "description": "Names of the guard functions to count, replacing the default list. A member call is matched on its property name, so `Arr.isArray` is named `isArray`."
|
|
91
|
+
* }
|
|
92
|
+
* },
|
|
93
|
+
* "additionalProperties": false
|
|
94
|
+
* }
|
|
95
|
+
* ]
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
type Options = NonNullable<RuleOptionsOf<'prefer-schema-over-guard-chain'>[0]>;
|
|
99
|
+
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
100
|
+
}
|
|
18
101
|
export type EslintTsFortressRules = Readonly<{
|
|
19
102
|
'ts-fortress/prefer-canonical-length-constrained-type': PreferCanonicalLengthConstrainedType.RuleEntry;
|
|
103
|
+
'ts-fortress/prefer-namespace-import': PreferNamespaceImport.RuleEntry;
|
|
104
|
+
'ts-fortress/prefer-schema-over-guard-chain': PreferSchemaOverGuardChain.RuleEntry;
|
|
105
|
+
}>;
|
|
106
|
+
export type EslintTsFortressRulesOption = Readonly<{
|
|
107
|
+
'ts-fortress/prefer-namespace-import': PreferNamespaceImport.Options;
|
|
108
|
+
'ts-fortress/prefer-schema-over-guard-chain': PreferSchemaOverGuardChain.Options;
|
|
20
109
|
}>;
|
|
21
|
-
export type EslintTsFortressRulesOption = ReadonlyRecord<never, never>;
|
|
22
110
|
type AssertTrue<B extends true> = B;
|
|
23
111
|
export type RuleNamesAreInSync = AssertTrue<TypeEq<keyof EslintTsFortressRules, `ts-fortress/${keyof typeof tsFortressRules}`>>;
|
|
24
112
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;GAIG;AACH,KAAK,aAAa,CAAC,IAAI,SAAS,MAAM,OAAO,eAAe,IAC1D,CAAC,OAAO,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,UAAU,CACxD,MAAM,WAAW,SAAS,MAAM,EAChC,MAAM,OAAO,SAAS,SAAS,OAAO,EAAE,CACzC,GACG,OAAO,GACP,KAAK,CAAC;AAEZ;;;;;;;;;;GAUG;AACH,kBAAU,oCAAoC,CAAC;IAC7C,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;;GAUG;AACH,kBAAU,qBAAqB,CAAC;IAC9B;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAY,OAAO,GAAG,WAAW,CAC/B,aAAa,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAC5C,CAAC;IAEF,KAAY,SAAS,GACjB,KAAK,GACL,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;CACvE;AAED;;;;;;;;;;GAUG;AACH,kBAAU,0BAA0B,CAAC;IACnC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,KAAY,OAAO,GAAG,WAAW,CAC/B,aAAa,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CACnD,CAAC;IAEF,KAAY,SAAS,GACjB,KAAK,GACL,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;CACvE;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,sDAAsD,EAAE,oCAAoC,CAAC,SAAS,CAAC;IACvG,qCAAqC,EAAE,qBAAqB,CAAC,SAAS,CAAC;IACvE,4CAA4C,EAAE,0BAA0B,CAAC,SAAS,CAAC;CACpF,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,qCAAqC,EAAE,qBAAqB,CAAC,OAAO,CAAC;IACrE,4CAA4C,EAAE,0BAA0B,CAAC,OAAO,CAAC;CAClF,CAAC,CAAC;AAIH,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,qBAAqB,EAC3B,eAAe,MAAM,OAAO,eAAe,EAAE,CAC9C,CACF,CAAC"}
|
package/dist/rule-types.mjs
CHANGED
package/dist/rule-types.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-types.mjs","
|
|
1
|
+
{"version":3,"file":"rule-types.mjs","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAAA,oBAAoB"}
|
package/dist/rules/constants.mjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
const TS_FORTRESS_MODULE =
|
|
2
|
-
const ARRAY_FN =
|
|
3
|
-
const BOUNDED_LENGTH_TUPLE_FN =
|
|
4
|
-
const FIXED_LENGTH_TUPLE_FN =
|
|
5
|
-
const MAX_LENGTH_TUPLE_FN =
|
|
6
|
-
const MIN_LENGTH_ARRAY_FN =
|
|
7
|
-
const MIN_LENGTH_TUPLE_FN =
|
|
8
|
-
const NON_EMPTY_ARRAY_FN =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
export const TS_FORTRESS_MODULE = 'ts-fortress';
|
|
2
|
+
export const ARRAY_FN = 'array';
|
|
3
|
+
export const BOUNDED_LENGTH_TUPLE_FN = 'boundedLengthTuple';
|
|
4
|
+
export const FIXED_LENGTH_TUPLE_FN = 'fixedLengthTuple';
|
|
5
|
+
export const MAX_LENGTH_TUPLE_FN = 'maxLengthTuple';
|
|
6
|
+
export const MIN_LENGTH_ARRAY_FN = 'minLengthArray';
|
|
7
|
+
export const MIN_LENGTH_TUPLE_FN = 'minLengthTuple';
|
|
8
|
+
export const NON_EMPTY_ARRAY_FN = 'nonEmptyArray';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const STRUCTURAL_PREFIX_CAP = 10;
|
|
17
|
+
//# sourceMappingURL=constants.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"
|
|
1
|
+
{"version":3,"file":"constants.mjs","sourceRoot":"","sources":["../../src/rules/constants.mts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEhD,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAEhC,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAE5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AAExD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AAEpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC"}
|
|
@@ -1,58 +1,102 @@
|
|
|
1
|
-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
1
|
+
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
2
2
|
import { Arr } from 'ts-data-forge';
|
|
3
3
|
import { TS_FORTRESS_MODULE } from './constants.mjs';
|
|
4
4
|
|
|
5
|
-
const getTsFortressImports = (program) => program.body.filter(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
5
|
+
export const getTsFortressImports = (program) => program.body.filter((node) => node.type === AST_NODE_TYPES.ImportDeclaration &&
|
|
6
|
+
node.source.value === TS_FORTRESS_MODULE);
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export const getImportedLocalName = (importDeclarations, importedName) => importDeclarations
|
|
14
|
+
.flatMap((declaration) => declaration.specifiers)
|
|
15
|
+
.find((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
16
|
+
(specifier.imported.type === AST_NODE_TYPES.Identifier
|
|
17
|
+
? specifier.imported.name
|
|
18
|
+
: specifier.imported.value) === importedName)?.local.name;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export const buildCalleeResolver = (importDeclarations) => {
|
|
27
|
+
const specifiers = importDeclarations.flatMap((declaration) => declaration.specifiers);
|
|
28
|
+
const namespaceNames = new Set(specifiers
|
|
29
|
+
.filter((specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier)
|
|
30
|
+
.map((specifier) => specifier.local.name));
|
|
31
|
+
const localToCanonical = new Map(specifiers.flatMap((specifier) => {
|
|
32
|
+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier)
|
|
33
|
+
return [];
|
|
34
|
+
const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier
|
|
35
|
+
? specifier.imported.name
|
|
36
|
+
: specifier.imported.value;
|
|
37
|
+
return typeof importedName === 'string'
|
|
38
|
+
? [[specifier.local.name, importedName]]
|
|
39
|
+
: [];
|
|
40
|
+
}));
|
|
41
|
+
return (callee) => {
|
|
42
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
43
|
+
const canonicalName = localToCanonical.get(callee.name);
|
|
44
|
+
return canonicalName === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { canonicalName, propertyNode: callee, isNamespace: false };
|
|
47
|
+
}
|
|
48
|
+
return callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
49
|
+
!callee.computed &&
|
|
50
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
51
|
+
namespaceNames.has(callee.object.name) &&
|
|
52
|
+
callee.property.type === AST_NODE_TYPES.Identifier
|
|
53
|
+
? {
|
|
54
|
+
canonicalName: callee.property.name,
|
|
55
|
+
propertyNode: callee.property,
|
|
56
|
+
isNamespace: true,
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
};
|
|
38
60
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export const hasForeignBinding = (sourceCode, node, name) => {
|
|
71
|
+
for (let mut_scope = sourceCode.getScope(node); mut_scope !== null; mut_scope = mut_scope.upper) {
|
|
72
|
+
const variable = mut_scope.set.get(name);
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if (variable !== undefined) {
|
|
76
|
+
return variable.defs.some((def) => !isTsFortressImportDef(def.node));
|
|
77
|
+
}
|
|
44
78
|
}
|
|
45
|
-
|
|
46
|
-
return false;
|
|
79
|
+
return false;
|
|
47
80
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export
|
|
58
|
-
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export const buildImportsFix = (fixer, program, names) => Arr.isEmpty(names)
|
|
91
|
+
? []
|
|
92
|
+
: [
|
|
93
|
+
fixer.insertTextBefore(program, names
|
|
94
|
+
.map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';\n`)
|
|
95
|
+
.join('')),
|
|
96
|
+
];
|
|
97
|
+
const isTsFortressImportDef = (node) => (node.type === AST_NODE_TYPES.ImportSpecifier ||
|
|
98
|
+
node.type === AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
99
|
+
node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) &&
|
|
100
|
+
node.parent.type === AST_NODE_TYPES.ImportDeclaration &&
|
|
101
|
+
node.parent.source.value === TS_FORTRESS_MODULE;
|
|
102
|
+
//# sourceMappingURL=import-utils.mjs.map
|