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.
Files changed (42) hide show
  1. package/README.md +150 -4
  2. package/dist/index.mjs +2 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/dist/plugin.d.mts +9 -0
  5. package/dist/plugin.d.mts.map +1 -1
  6. package/dist/plugin.mjs +32 -27
  7. package/dist/plugin.mjs.map +1 -1
  8. package/dist/rule-types.d.mts +90 -2
  9. package/dist/rule-types.d.mts.map +1 -1
  10. package/dist/rule-types.mjs +2 -1
  11. package/dist/rule-types.mjs.map +1 -1
  12. package/dist/rules/constants.mjs +17 -12
  13. package/dist/rules/constants.mjs.map +1 -1
  14. package/dist/rules/import-utils.mjs +96 -52
  15. package/dist/rules/import-utils.mjs.map +1 -1
  16. package/dist/rules/index.mjs +2 -2
  17. package/dist/rules/index.mjs.map +1 -1
  18. package/dist/rules/prefer-canonical-length-constrained-type.mjs +226 -157
  19. package/dist/rules/prefer-canonical-length-constrained-type.mjs.map +1 -1
  20. package/dist/rules/prefer-namespace-import.d.mts +20 -0
  21. package/dist/rules/prefer-namespace-import.d.mts.map +1 -0
  22. package/dist/rules/prefer-namespace-import.mjs +216 -0
  23. package/dist/rules/prefer-namespace-import.mjs.map +1 -0
  24. package/dist/rules/prefer-schema-over-guard-chain.d.mts +56 -0
  25. package/dist/rules/prefer-schema-over-guard-chain.d.mts.map +1 -0
  26. package/dist/rules/prefer-schema-over-guard-chain.mjs +212 -0
  27. package/dist/rules/prefer-schema-over-guard-chain.mjs.map +1 -0
  28. package/dist/rules/rules.d.mts +7 -0
  29. package/dist/rules/rules.d.mts.map +1 -1
  30. package/dist/rules/rules.mjs +7 -6
  31. package/dist/rules/rules.mjs.map +1 -1
  32. package/dist/types.mjs +2 -2
  33. package/dist/types.mjs.map +1 -1
  34. package/package.json +6 -7
  35. package/src/plugin.mts +2 -0
  36. package/src/rule-types.mts +116 -2
  37. package/src/rules/prefer-canonical-length-constrained-type.mts +9 -2
  38. package/src/rules/prefer-namespace-import.mts +341 -0
  39. package/src/rules/prefer-namespace-import.test.mts +320 -0
  40. package/src/rules/prefer-schema-over-guard-chain.mts +257 -0
  41. package/src/rules/prefer-schema-over-guard-chain.test.mts +143 -0
  42. 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/rules.mjs';
3
- //# sourceMappingURL=index.mjs.map
2
+ export { tsFortressRules } from './rules/index.mjs';
3
+ //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
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
  };
@@ -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;AA+BvE,eAAO,MAAM,sBAAsB;;iBAE/B,IAAI,EAAE,2BAA2B;;;;;;QAIjC,oDAAoD;iBACpD,WAAW;2BAtBP,yBAAyB;;yBAQzB,aAAa,EAAI,YAAY;;;iFAfqB,OAAO;;;;CA+BhC,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/rules.mjs';
1
+ import { tsFortressRules } from './rules/index.mjs';
2
+
3
+
4
+
5
+
6
+
2
7
 
3
8
  const recommendedRules = {
4
- "ts-fortress/prefer-canonical-length-constrained-type": "error"
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
- name: "ts-fortress/recommended",
8
- plugins: {
9
- // Resolved lazily so that this config registers the *same* object that
10
- // `eslintPluginTsFortress` refers to. ESLint rejects a plugin name that
11
- // maps to two different objects with `Cannot redefine plugin`, which is
12
- // what a user combining this preset with their own
13
- // `plugins: { 'ts-fortress': eslintPluginTsFortress }` entry would hit if
14
- // the preset embedded a separate copy of the plugin.
15
- get "ts-fortress"() {
16
- return eslintPluginTsFortress;
17
- }
18
- },
19
- rules: recommendedRules
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
- export { eslintPluginTsFortress };
33
- //# sourceMappingURL=plugin.mjs.map
35
+ recommended: recommendedConfig,
36
+ },
37
+ };
38
+ //# sourceMappingURL=plugin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { type ReadonlyRecord } from 'ts-type-forge';\nimport { tsFortressRules } from './rules/index.mjs';\nimport { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';\n\n/**\n * Every rule this plugin ships, at `error`.\n *\n * The `satisfies` clause below is keyed off {@link tsFortressRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-fortress/prefer-canonical-length-constrained-type': 'error',\n} as const satisfies ReadonlyRecord<\n `ts-fortress/${keyof typeof tsFortressRules}`,\n 'error'\n>;\n\nconst recommendedConfig = {\n name: 'ts-fortress/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsFortress` refers to. ESLint rejects a plugin name that\n // maps to two different objects with `Cannot redefine plugin`, which is\n // what a user combining this preset with their own\n // `plugins: { 'ts-fortress': eslintPluginTsFortress }` entry would hit if\n // the preset embedded a separate copy of the plugin.\n get 'ts-fortress'(): ESLintPlugin {\n return eslintPluginTsFortress;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsFortress = {\n meta: {\n name: 'eslint-plugin-ts-fortress',\n },\n rules: tsFortressRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AAUA,MAAM,gBAAA,GAAmB;AAAA,EACvB,sDAAA,EAAwD;AAC1D,CAAA;AAKA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,yBAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,aAAA,GAA8B;AAChC,MAAA,OAAO,sBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,sBAAA,GAAyB;AAAA,EACpC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,eAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB;;;;"}
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"}
@@ -1,6 +1,14 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
1
2
  import { type Linter } from 'eslint';
2
- import { type ReadonlyRecord, type TypeEq } from 'ts-type-forge';
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,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,kBAAU,oCAAoC,CAAC;IAC7C,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,sDAAsD,EAAE,oCAAoC,CAAC,SAAS,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAIvE,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"}
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"}
@@ -1,2 +1,3 @@
1
1
 
2
- //# sourceMappingURL=rule-types.mjs.map
2
+ export {};
3
+ //# sourceMappingURL=rule-types.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-types.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
1
+ {"version":3,"file":"rule-types.mjs","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAAA,oBAAoB"}
@@ -1,12 +1,17 @@
1
- const TS_FORTRESS_MODULE = "ts-fortress";
2
- const ARRAY_FN = "array";
3
- const BOUNDED_LENGTH_TUPLE_FN = "boundedLengthTuple";
4
- const FIXED_LENGTH_TUPLE_FN = "fixedLengthTuple";
5
- const MAX_LENGTH_TUPLE_FN = "maxLengthTuple";
6
- const MIN_LENGTH_ARRAY_FN = "minLengthArray";
7
- const MIN_LENGTH_TUPLE_FN = "minLengthTuple";
8
- const NON_EMPTY_ARRAY_FN = "nonEmptyArray";
9
- const STRUCTURAL_PREFIX_CAP = 10;
10
-
11
- export { ARRAY_FN, BOUNDED_LENGTH_TUPLE_FN, FIXED_LENGTH_TUPLE_FN, MAX_LENGTH_TUPLE_FN, MIN_LENGTH_ARRAY_FN, MIN_LENGTH_TUPLE_FN, NON_EMPTY_ARRAY_FN, STRUCTURAL_PREFIX_CAP, TS_FORTRESS_MODULE };
12
- //# sourceMappingURL=constants.mjs.map
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"],"sourcesContent":["export const TS_FORTRESS_MODULE = 'ts-fortress';\n\nexport const ARRAY_FN = 'array';\n\nexport const BOUNDED_LENGTH_TUPLE_FN = 'boundedLengthTuple';\n\nexport const FIXED_LENGTH_TUPLE_FN = 'fixedLengthTuple';\n\nexport const MAX_LENGTH_TUPLE_FN = 'maxLengthTuple';\n\nexport const MIN_LENGTH_ARRAY_FN = 'minLengthArray';\n\nexport const MIN_LENGTH_TUPLE_FN = 'minLengthTuple';\n\nexport const NON_EMPTY_ARRAY_FN = 'nonEmptyArray';\n\n/**\n * Inclusive upper bound of ts-type-forge's `StructuralPrefixLength` (`0..10`) —\n * the lengths the structural `*Tuple` combinators encode in their result type.\n * A bound outside this range selects a different overload (one that drops the\n * constraint), so a rewrite that carries a bound over to another combinator\n * only stays type-identical while the bound is within it.\n */\nexport const STRUCTURAL_PREFIX_CAP = 10;\n"],"names":[],"mappings":"AAAO,MAAM,kBAAA,GAAqB;AAE3B,MAAM,QAAA,GAAW;AAEjB,MAAM,uBAAA,GAA0B;AAEhC,MAAM,qBAAA,GAAwB;AAE9B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,kBAAA,GAAqB;AAS3B,MAAM,qBAAA,GAAwB;;;;"}
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
- (node) => node.type === AST_NODE_TYPES.ImportDeclaration && node.source.value === TS_FORTRESS_MODULE
7
- );
8
- const getImportedLocalName = (importDeclarations, importedName) => importDeclarations.flatMap((declaration) => declaration.specifiers).find(
9
- (specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value) === importedName
10
- )?.local.name;
11
- const buildCalleeResolver = (importDeclarations) => {
12
- const specifiers = importDeclarations.flatMap(
13
- (declaration) => declaration.specifiers
14
- );
15
- const namespaceNames = new Set(
16
- specifiers.filter(
17
- (specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier
18
- ).map((specifier) => specifier.local.name)
19
- );
20
- const localToCanonical = new Map(
21
- specifiers.flatMap((specifier) => {
22
- if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) return [];
23
- const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
24
- return typeof importedName === "string" ? [[specifier.local.name, importedName]] : [];
25
- })
26
- );
27
- return (callee) => {
28
- if (callee.type === AST_NODE_TYPES.Identifier) {
29
- const canonicalName = localToCanonical.get(callee.name);
30
- return canonicalName === void 0 ? void 0 : { canonicalName, propertyNode: callee, isNamespace: false };
31
- }
32
- return callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES.Identifier && namespaceNames.has(callee.object.name) && callee.property.type === AST_NODE_TYPES.Identifier ? {
33
- canonicalName: callee.property.name,
34
- propertyNode: callee.property,
35
- isNamespace: true
36
- } : void 0;
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
- const hasForeignBinding = (sourceCode, node, name) => {
40
- for (let mut_scope = sourceCode.getScope(node); mut_scope !== null; mut_scope = mut_scope.upper) {
41
- const variable = mut_scope.set.get(name);
42
- if (variable !== void 0) {
43
- return variable.defs.some((def) => !isTsFortressImportDef(def.node));
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
- const buildImportsFix = (fixer, program, names) => Arr.isEmpty(names) ? [] : [
49
- fixer.insertTextBefore(
50
- program,
51
- names.map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';
52
- `).join("")
53
- )
54
- ];
55
- const isTsFortressImportDef = (node) => (node.type === AST_NODE_TYPES.ImportSpecifier || node.type === AST_NODE_TYPES.ImportDefaultSpecifier || node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) && node.parent.type === AST_NODE_TYPES.ImportDeclaration && node.parent.source.value === TS_FORTRESS_MODULE;
56
-
57
- export { buildCalleeResolver, buildImportsFix, getImportedLocalName, getTsFortressImports, hasForeignBinding };
58
- //# sourceMappingURL=import-utils.mjs.map
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