eslint-plugin-ts-type-forge 0.7.3 → 0.8.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 +31 -8
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +15 -0
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +1 -1
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/import-style.mjs.map +1 -1
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +19 -0
- package/dist/rules/no-side-effect-import.d.mts.map +1 -0
- package/dist/rules/no-side-effect-import.mjs +41 -0
- package/dist/rules/no-side-effect-import.mjs.map +1 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
- package/dist/rules/prefer-canonical-mutable-record.mjs.map +1 -1
- package/dist/rules/prefer-readonly-or-mutable-record.mjs.map +1 -1
- package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs.map +1 -1
- package/dist/rules/rewrite-rule-utils.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/rules/std-type-rule-utils.mjs.map +1 -1
- package/package.json +5 -5
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +16 -0
- package/src/rules/no-side-effect-import.mts +73 -0
- package/src/rules/no-side-effect-import.test.mts +83 -0
- package/src/rules/rules.mts +2 -0
package/README.md
CHANGED
|
@@ -72,12 +72,13 @@ export default [
|
|
|
72
72
|
|
|
73
73
|
## Rules
|
|
74
74
|
|
|
75
|
-
| Rule | Fix | Description
|
|
76
|
-
| :------------------------------------------ | :--------- |
|
|
77
|
-
| `prefer-canonical-length-constrained-tuple` | autofix | Replace hand-rolled uniform tuple spellings with the canonical ts-type-forge tuple type.
|
|
78
|
-
| `prefer-canonical-mutable-record` | autofix | Replace `Mutable<Record<K, V>>` with the canonical `MutableRecord<K, V>`.
|
|
79
|
-
| `prefer-strict-or-relaxed-utility-type` | suggestion | Replace `Exclude` / `Extract` / `Omit` / `Pick` with the `Strict*` or `Relaxed*` variant.
|
|
80
|
-
| `prefer-readonly-or-mutable-record` | suggestion | Replace `Record` with `ReadonlyRecord` or `MutableRecord`.
|
|
75
|
+
| Rule | Fix | Description |
|
|
76
|
+
| :------------------------------------------ | :--------- | :--------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| `prefer-canonical-length-constrained-tuple` | autofix | Replace hand-rolled uniform tuple spellings with the canonical ts-type-forge tuple type. |
|
|
78
|
+
| `prefer-canonical-mutable-record` | autofix | Replace `Mutable<Record<K, V>>` with the canonical `MutableRecord<K, V>`. |
|
|
79
|
+
| `prefer-strict-or-relaxed-utility-type` | suggestion | Replace `Exclude` / `Extract` / `Omit` / `Pick` with the `Strict*` or `Relaxed*` variant. |
|
|
80
|
+
| `prefer-readonly-or-mutable-record` | suggestion | Replace `Record` with `ReadonlyRecord` or `MutableRecord`. |
|
|
81
|
+
| `no-side-effect-import` | autofix | Remove a side-effect-only `import 'ts-type-forge';`, which binds no name and has no runtime entry point to resolve to. |
|
|
81
82
|
|
|
82
83
|
### `prefer-canonical-length-constrained-tuple`
|
|
83
84
|
|
|
@@ -208,10 +209,32 @@ type Counters = MutableRecord<string, number>;
|
|
|
208
209
|
these rules report **suggestions** rather than an autofix. Editors offer both
|
|
209
210
|
replacements; `--fix` changes nothing.
|
|
210
211
|
|
|
212
|
+
### `no-side-effect-import`
|
|
213
|
+
|
|
214
|
+
`ts-type-forge` ships declarations and nothing else — its `exports` map offers
|
|
215
|
+
no runtime condition — so `import 'ts-type-forge';` binds no name _and_ fails to
|
|
216
|
+
resolve the moment the module graph is loaded. A side-effect import is the one
|
|
217
|
+
kind TypeScript never elides, so the line survives compilation: a runtime error
|
|
218
|
+
sitting in a file that type-checks.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// ❌
|
|
222
|
+
import 'ts-type-forge';
|
|
223
|
+
|
|
224
|
+
// ✅ types are reached by name
|
|
225
|
+
import { type NonEmptyArray } from 'ts-type-forge';
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The fix deletes the import along with its line. Only the bare specifier matches:
|
|
229
|
+
`ts-type-forge/global` — the ambient globals, usually brought in through
|
|
230
|
+
`compilerOptions.types` or `/// <reference types="ts-type-forge/global" />` — is
|
|
231
|
+
left alone, as is every other module.
|
|
232
|
+
|
|
211
233
|
## Options
|
|
212
234
|
|
|
213
|
-
`importStyle` is accepted by every rule; `maxLength` only
|
|
214
|
-
`prefer-canonical-length-constrained-tuple`.
|
|
235
|
+
`importStyle` is accepted by every rule that rewrites a type; `maxLength` only
|
|
236
|
+
by `prefer-canonical-length-constrained-tuple`. `no-side-effect-import` takes no
|
|
237
|
+
options.
|
|
215
238
|
|
|
216
239
|
| Option | Type | Default | Description |
|
|
217
240
|
| :------------ | :-------------------- | :-------- | :------------------------------------------------------------------ |
|
package/dist/plugin.d.mts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const eslintPluginTsTypeForge: {
|
|
|
4
4
|
readonly name: 'eslint-plugin-ts-type-forge';
|
|
5
5
|
};
|
|
6
6
|
readonly rules: {
|
|
7
|
+
readonly 'no-side-effect-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"removeSideEffectImport", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
7
8
|
readonly 'prefer-canonical-length-constrained-tuple': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalTuple", readonly [(Readonly<{
|
|
8
9
|
importStyle?: 'global' | 'named';
|
|
9
10
|
maxLength?: number;
|
|
@@ -26,6 +27,7 @@ export declare const eslintPluginTsTypeForge: {
|
|
|
26
27
|
readonly 'ts-type-forge': ESLintPlugin;
|
|
27
28
|
};
|
|
28
29
|
readonly rules: {
|
|
30
|
+
readonly 'ts-type-forge/no-side-effect-import': 'error';
|
|
29
31
|
readonly 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error';
|
|
30
32
|
readonly 'ts-type-forge/prefer-canonical-mutable-record': 'error';
|
|
31
33
|
readonly 'ts-type-forge/prefer-readonly-or-mutable-record': 'error';
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAkCvE,eAAO,MAAM,uBAAuB;;iBAEhC,IAAI,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;QAInC,oDAAoD;iBACpD,WAAW;2BAtBP,2BAA2B;;yBAQ3B,eAAe,EAAI,YAAY;;;gEAlBE,OAAO;oFACa,OAAO;0EACjB,OAAO;4EACL,OAAO;gFACH,OAAO;;;;CA8B/B,CAAC"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tsTypeForgeRules } from './rules/rules.mjs';
|
|
2
2
|
|
|
3
3
|
const recommendedRules = {
|
|
4
|
+
"ts-type-forge/no-side-effect-import": "error",
|
|
4
5
|
"ts-type-forge/prefer-canonical-length-constrained-tuple": "error",
|
|
5
6
|
"ts-type-forge/prefer-canonical-mutable-record": "error",
|
|
6
7
|
"ts-type-forge/prefer-readonly-or-mutable-record": "error",
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { tsTypeForgeRules } 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 tsTypeForgeRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',\n 'ts-type-forge/prefer-canonical-mutable-record': 'error',\n 'ts-type-forge/prefer-readonly-or-mutable-record': 'error',\n 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error',\n} as const satisfies Readonly<\n Record<`ts-type-forge/${keyof typeof tsTypeForgeRules}`, 'error'>\n>;\n\nconst recommendedConfig = {\n name: 'ts-type-forge/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsTypeForge` 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-type-forge': eslintPluginTsTypeForge }` entry would hit\n // if the preset embedded a separate copy of the plugin.\n get 'ts-type-forge'(): ESLintPlugin {\n return eslintPluginTsTypeForge;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsTypeForge = {\n meta: {\n name: 'eslint-plugin-ts-type-forge',\n },\n rules: tsTypeForgeRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AASA,MAAM,gBAAA,GAAmB;AAAA,EACvB,yDAAA,EAA2D,OAAA;AAAA,EAC3D,+CAAA,EAAiD,OAAA;AAAA,EACjD,iDAAA,EAAmD,OAAA;AAAA,EACnD,qDAAA,EAAuD;AACzD,CAAA;AAIA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,2BAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,eAAA,GAAgC;AAClC,MAAA,OAAO,uBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,uBAAA,GAA0B;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,gBAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB
|
|
1
|
+
{"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { tsTypeForgeRules } 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 tsTypeForgeRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-type-forge/no-side-effect-import': 'error',\n 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',\n 'ts-type-forge/prefer-canonical-mutable-record': 'error',\n 'ts-type-forge/prefer-readonly-or-mutable-record': 'error',\n 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error',\n} as const satisfies Readonly<\n Record<`ts-type-forge/${keyof typeof tsTypeForgeRules}`, 'error'>\n>;\n\nconst recommendedConfig = {\n name: 'ts-type-forge/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsTypeForge` 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-type-forge': eslintPluginTsTypeForge }` entry would hit\n // if the preset embedded a separate copy of the plugin.\n get 'ts-type-forge'(): ESLintPlugin {\n return eslintPluginTsTypeForge;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsTypeForge = {\n meta: {\n name: 'eslint-plugin-ts-type-forge',\n },\n rules: tsTypeForgeRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AASA,MAAM,gBAAA,GAAmB;AAAA,EACvB,qCAAA,EAAuC,OAAA;AAAA,EACvC,yDAAA,EAA2D,OAAA;AAAA,EAC3D,+CAAA,EAAiD,OAAA;AAAA,EACjD,iDAAA,EAAmD,OAAA;AAAA,EACnD,qDAAA,EAAuD;AACzD,CAAA;AAIA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,2BAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,eAAA,GAAgC;AAClC,MAAA,OAAO,uBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,uBAAA,GAA0B;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,gBAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB;;"}
|
package/dist/rule-types.d.mts
CHANGED
|
@@ -9,6 +9,20 @@ type SpreadOptionsIfIsArray<T extends readonly [Linter.StringSeverity, unknown]>
|
|
|
9
9
|
* without converting `meta.schema` (JSON Schema) back into TypeScript types.
|
|
10
10
|
*/
|
|
11
11
|
type RuleOptionsOf<Name extends keyof typeof tsTypeForgeRules> = (typeof tsTypeForgeRules)[Name] extends TSESLint.RuleModule<infer _MessageIds extends string, infer Options extends readonly unknown[]> ? Options : never;
|
|
12
|
+
/**
|
|
13
|
+
* @description Remove a side-effect-only `import 'ts-type-forge';`, which binds no name and has no runtime entry point to resolve to. `ts-type-forge/global` is left alone.
|
|
14
|
+
*
|
|
15
|
+
* ```md
|
|
16
|
+
* | key | value |
|
|
17
|
+
* | :--------- | :------ |
|
|
18
|
+
* | type | problem |
|
|
19
|
+
* | deprecated | false |
|
|
20
|
+
* | fixable | code |
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare namespace NoSideEffectImport {
|
|
24
|
+
type RuleEntry = Linter.StringSeverity;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* @description Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.
|
|
14
28
|
*
|
|
@@ -163,6 +177,7 @@ declare namespace PreferStrictOrRelaxedUtilityType {
|
|
|
163
177
|
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
164
178
|
}
|
|
165
179
|
export type EslintTsTypeForgeRules = Readonly<{
|
|
180
|
+
'ts-type-forge/no-side-effect-import': NoSideEffectImport.RuleEntry;
|
|
166
181
|
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
|
|
167
182
|
'ts-type-forge/prefer-canonical-mutable-record': PreferCanonicalMutableRecord.RuleEntry;
|
|
168
183
|
'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.RuleEntry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"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"}
|
package/dist/rules/ast-utils.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n ASTUtils,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr, hasKey, isNonNullObject, isRecord } from 'ts-data-forge';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `name` resolves to a declaration in this file (a type alias, an\n * interface, a class, an import, …) rather than to the ambient standard-library\n * type of the same name.\n *\n * The `defs` check is what separates the two: ESLint materializes an *implicit*\n * global variable for every unresolved reference, so `findVariable` also\n * succeeds for `Exclude` in a file that never declares it. Only a variable with\n * at least one definition was actually written down somewhere in this file.\n */\nexport const isLocallyBound = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);\n\n return variable !== null && Arr.isNonEmpty(variable.defs);\n};\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n * - A tuple inside the `extends` clause of a conditional type is a match\n * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n if (isInConditionalTypeExtendsClause(tuple)) return undefined;\n\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ? Arr.butLast(elements) : elements;\n\n if (\n Arr.isEmpty(leading) ||\n leading.some(\n (element) =>\n element.type === AST_NODE_TYPES.TSRestType ||\n element.type === AST_NODE_TYPES.TSOptionalType ||\n element.type === AST_NODE_TYPES.TSNamedTupleMember,\n )\n ) {\n return undefined;\n }\n\n const restElementType =\n last !== undefined && hasRest\n ? getArrayElementType(last.typeAnnotation)\n : undefined;\n\n if (hasRest && restElementType === undefined) return undefined;\n\n const [head] = leading;\n\n if (head === undefined) return undefined;\n\n const elementText = sourceCode.getText(head);\n\n const expected = normalizeWhitespace(elementText);\n\n const others = [\n ...Arr.tail(leading),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n if (\n others.some(\n (element) =>\n normalizeWhitespace(sourceCode.getText(element)) !== expected,\n )\n ) {\n return undefined;\n }\n\n return definesRecursiveTypeAlias(tuple, sourceCode)\n ? undefined\n : {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n };\n};\n\n/**\n * Whether `node` sits inside the `extends` clause of a conditional type, where\n * a tuple is a *match pattern* rather than a type.\n *\n * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are\n * still generic. The canonical spellings resolve through a mapped type\n * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and\n * against one of those the checker defers the whole conditional instead —\n * yielding `boolean` where the tuple pattern yielded `true` or `false`, which\n * silently widens everything downstream. So those positions are left alone.\n */\nconst isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {\n // The root has no parent, so it terminates the walk.\n if (node.type === AST_NODE_TYPES.Program) return false;\n\n const { parent } = node;\n\n return (\n (parent.type === AST_NODE_TYPES.TSConditionalType &&\n parent.extendsType === node) ||\n isInConditionalTypeExtendsClause(parent)\n );\n};\n\n/**\n * Whether the tuple spells out an alias that its own element type leads back\n * to.\n *\n * A tuple literal is how TypeScript's cycle-breaking works: `type T = readonly\n * [T, T]` resolves, because the element positions are deferred. Rewriting it to\n * `FixedLengthTuple<2, T>` routes the same cycle through a mapped type, which\n * the checker will not compute — `T` becomes an error type, and every use of it\n * silently reads as `any`. `tsc` reports the alias once; the typed linter then\n * reports each use, which is how this was found.\n *\n * The cycle is often indirect. `LambdaApplication = readonly [LambdaTerm,\n * LambdaTerm]` looks fine on its own, and only recurses because `LambdaTerm` is\n * a union that includes `LambdaApplication`. So this follows references through\n * the other aliases in the file rather than looking for the enclosing name in\n * the element text.\n *\n * **Any cycle counts, which leaves some rewritable tuples alone.** Not every\n * cycle actually breaks: TypeScript resolves an object type, an array, a tuple\n * and a function type without resolving what is inside them, so `type Pair =\n * FixedLengthTuple<2, Foo>` alongside `type Foo = { p: Pair }` compiles, and so\n * does `type Tree = { kids: FixedLengthTuple<2, Tree> }`. Skipping those\n * constructs while following references was tried and does not hold up — the\n * deferral is undone by anything that asks for the structure, and that can sit\n * anywhere in the file:\n *\n * ```ts\n * type Pair = readonly [Foo, Foo]; // rewriting this breaks Pair and Foo\n * type Foo = Wrapper[number]; // …because of the indexed access here\n * type Wrapper = readonly Pair[];\n * ```\n *\n * Deciding it properly means knowing every use of every alias, which no\n * syntactic rule can do — so a cycle of any shape suppresses the rewrite. The\n * cost is a tuple left spelled out; the alternative is an autofix that turns a\n * type into `any`.\n *\n * A cycle closed through an `import` is not visible here and is not detected.\n */\nconst definesRecursiveTypeAlias = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): boolean => {\n const alias = enclosingTypeAlias(tuple);\n\n if (alias === undefined) return false;\n\n const references = aliasReferenceGraph(programOf(alias), sourceCode);\n\n const mut_seen = new Set<string>();\n\n const mut_queue: string[] = tuple.elementTypes.flatMap((element) =>\n Array.from(referencedTypeNames(element, sourceCode)),\n );\n\n while (true) {\n const next = mut_queue.pop();\n\n if (next === undefined) return false;\n\n if (next === alias.id.name) return true;\n\n if (mut_seen.has(next)) continue;\n\n mut_seen.add(next);\n\n const referenced = references.get(next);\n\n if (referenced !== undefined) {\n mut_queue.push(...referenced);\n }\n }\n};\n\n/** The `type X = …` declaration `node` sits in, if any. */\nconst enclosingTypeAlias = (\n node: TSESTree.Node,\n): TSESTree.TSTypeAliasDeclaration | undefined => {\n if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) return node;\n\n if (node.type === AST_NODE_TYPES.Program) return undefined;\n\n return enclosingTypeAlias(node.parent);\n};\n\n/**\n * Which other type names each `type X = …` in the file refers to, keyed by `X`.\n *\n * Built once per file and cached on the `Program` node: the search below runs\n * per candidate tuple, and rebuilding this for each of them made the rule cost\n * `O(tuples × file size)` — a full second on the largest generated files here.\n */\nconst aliasReferenceGraph = (\n program: TSESTree.Program,\n sourceCode: TSESLint.SourceCode,\n): ReadonlyMap<string, ReadonlySet<string>> => {\n const cached = aliasReferenceGraphCache.get(program);\n\n if (cached !== undefined) return cached;\n\n const mut_graph = new Map<string, ReadonlySet<string>>();\n\n walk(program, sourceCode, (visited) => {\n if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {\n mut_graph.set(\n visited.id.name,\n referencedTypeNames(visited.typeAnnotation, sourceCode),\n );\n }\n });\n\n aliasReferenceGraphCache.set(program, mut_graph);\n\n return mut_graph;\n};\n\nconst aliasReferenceGraphCache = new WeakMap<\n TSESTree.Program,\n ReadonlyMap<string, ReadonlySet<string>>\n>();\n\nconst programOf = (node: TSESTree.Node): TSESTree.Program =>\n node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);\n\n/** The names every `X` / `X<…>` reference in the subtree points at. */\nconst referencedTypeNames = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n): ReadonlySet<string> => {\n const mut_names = new Set<string>();\n\n walk(node, sourceCode, (visited) => {\n if (\n visited.type === AST_NODE_TYPES.TSTypeReference &&\n visited.typeName.type === AST_NODE_TYPES.Identifier\n ) {\n mut_names.add(visited.typeName.name);\n }\n });\n\n return mut_names;\n};\n\n/**\n * Visits `node` and its descendants.\n *\n * Children are found through the parser's visitor keys, so only the properties\n * that actually hold one are read. Reading every own property instead works\n * too, and is the fallback for a parser that declares no keys for a node type,\n * but it allocates an entry pair per property and walks `loc` / `range` as\n * well — on the generated rule-type files that alone was most of the cost.\n */\nconst walk = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n visit: (visited: TSESTree.Node) => void,\n): void => {\n visit(node);\n\n if (!isRecord(node)) return;\n\n const keys =\n sourceCode.visitorKeys[node.type] ??\n Object.keys(node).filter((key) => key !== 'parent');\n\n for (const key of keys) {\n const value: unknown = hasKey(node, key) ? node[key] : undefined;\n\n if (Arr.isArray(value)) {\n for (const element of value) {\n if (isNode(element)) walk(element, sourceCode, visit);\n }\n } else if (isNode(value)) {\n walk(value, sourceCode, visit);\n }\n }\n};\n\nconst isNode = (value: unknown): value is TSESTree.Node =>\n isNonNullObject(value) &&\n typeof (value as Readonly<{ type?: unknown }>).type === 'string';\n\n/** Unwraps `V[]` and `readonly V[]` to `V`. */\nconst getArrayElementType = (\n node: TSESTree.TypeNode,\n): TSESTree.TypeNode | undefined => {\n if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;\n\n return node.type === AST_NODE_TYPES.TSTypeOperator &&\n node.operator === 'readonly' &&\n node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType\n ? node.typeAnnotation.elementType\n : undefined;\n};\n\nconst normalizeWhitespace = (text: string): string =>\n text.replaceAll(/\\s+/gu, ' ').trim();\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n ASTUtils,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr, hasKey, isNonNullObject, isRecord } from 'ts-data-forge';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `name` resolves to a declaration in this file (a type alias, an\n * interface, a class, an import, …) rather than to the ambient standard-library\n * type of the same name.\n *\n * The `defs` check is what separates the two: ESLint materializes an *implicit*\n * global variable for every unresolved reference, so `findVariable` also\n * succeeds for `Exclude` in a file that never declares it. Only a variable with\n * at least one definition was actually written down somewhere in this file.\n */\nexport const isLocallyBound = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);\n\n return variable !== null && Arr.isNonEmpty(variable.defs);\n};\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n * - A tuple inside the `extends` clause of a conditional type is a match\n * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n if (isInConditionalTypeExtendsClause(tuple)) return undefined;\n\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ? Arr.butLast(elements) : elements;\n\n if (\n Arr.isEmpty(leading) ||\n leading.some(\n (element) =>\n element.type === AST_NODE_TYPES.TSRestType ||\n element.type === AST_NODE_TYPES.TSOptionalType ||\n element.type === AST_NODE_TYPES.TSNamedTupleMember,\n )\n ) {\n return undefined;\n }\n\n const restElementType =\n last !== undefined && hasRest\n ? getArrayElementType(last.typeAnnotation)\n : undefined;\n\n if (hasRest && restElementType === undefined) return undefined;\n\n const [head] = leading;\n\n if (head === undefined) return undefined;\n\n const elementText = sourceCode.getText(head);\n\n const expected = normalizeWhitespace(elementText);\n\n const others = [\n ...Arr.tail(leading),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n if (\n others.some(\n (element) =>\n normalizeWhitespace(sourceCode.getText(element)) !== expected,\n )\n ) {\n return undefined;\n }\n\n return definesRecursiveTypeAlias(tuple, sourceCode)\n ? undefined\n : {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n };\n};\n\n/**\n * Whether `node` sits inside the `extends` clause of a conditional type, where\n * a tuple is a *match pattern* rather than a type.\n *\n * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are\n * still generic. The canonical spellings resolve through a mapped type\n * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and\n * against one of those the checker defers the whole conditional instead —\n * yielding `boolean` where the tuple pattern yielded `true` or `false`, which\n * silently widens everything downstream. So those positions are left alone.\n */\nconst isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {\n // The root has no parent, so it terminates the walk.\n if (node.type === AST_NODE_TYPES.Program) return false;\n\n const { parent } = node;\n\n return (\n (parent.type === AST_NODE_TYPES.TSConditionalType &&\n parent.extendsType === node) ||\n isInConditionalTypeExtendsClause(parent)\n );\n};\n\n/**\n * Whether the tuple spells out an alias that its own element type leads back\n * to.\n *\n * A tuple literal is how TypeScript's cycle-breaking works: `type T = readonly\n * [T, T]` resolves, because the element positions are deferred. Rewriting it to\n * `FixedLengthTuple<2, T>` routes the same cycle through a mapped type, which\n * the checker will not compute — `T` becomes an error type, and every use of it\n * silently reads as `any`. `tsc` reports the alias once; the typed linter then\n * reports each use, which is how this was found.\n *\n * The cycle is often indirect. `LambdaApplication = readonly [LambdaTerm,\n * LambdaTerm]` looks fine on its own, and only recurses because `LambdaTerm` is\n * a union that includes `LambdaApplication`. So this follows references through\n * the other aliases in the file rather than looking for the enclosing name in\n * the element text.\n *\n * **Any cycle counts, which leaves some rewritable tuples alone.** Not every\n * cycle actually breaks: TypeScript resolves an object type, an array, a tuple\n * and a function type without resolving what is inside them, so `type Pair =\n * FixedLengthTuple<2, Foo>` alongside `type Foo = { p: Pair }` compiles, and so\n * does `type Tree = { kids: FixedLengthTuple<2, Tree> }`. Skipping those\n * constructs while following references was tried and does not hold up — the\n * deferral is undone by anything that asks for the structure, and that can sit\n * anywhere in the file:\n *\n * ```ts\n * type Pair = readonly [Foo, Foo]; // rewriting this breaks Pair and Foo\n * type Foo = Wrapper[number]; // …because of the indexed access here\n * type Wrapper = readonly Pair[];\n * ```\n *\n * Deciding it properly means knowing every use of every alias, which no\n * syntactic rule can do — so a cycle of any shape suppresses the rewrite. The\n * cost is a tuple left spelled out; the alternative is an autofix that turns a\n * type into `any`.\n *\n * A cycle closed through an `import` is not visible here and is not detected.\n */\nconst definesRecursiveTypeAlias = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): boolean => {\n const alias = enclosingTypeAlias(tuple);\n\n if (alias === undefined) return false;\n\n const references = aliasReferenceGraph(programOf(alias), sourceCode);\n\n const mut_seen = new Set<string>();\n\n const mut_queue: string[] = tuple.elementTypes.flatMap((element) =>\n Array.from(referencedTypeNames(element, sourceCode)),\n );\n\n while (true) {\n const next = mut_queue.pop();\n\n if (next === undefined) return false;\n\n if (next === alias.id.name) return true;\n\n if (mut_seen.has(next)) continue;\n\n mut_seen.add(next);\n\n const referenced = references.get(next);\n\n if (referenced !== undefined) {\n mut_queue.push(...referenced);\n }\n }\n};\n\n/** The `type X = …` declaration `node` sits in, if any. */\nconst enclosingTypeAlias = (\n node: TSESTree.Node,\n): TSESTree.TSTypeAliasDeclaration | undefined => {\n if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) return node;\n\n if (node.type === AST_NODE_TYPES.Program) return undefined;\n\n return enclosingTypeAlias(node.parent);\n};\n\n/**\n * Which other type names each `type X = …` in the file refers to, keyed by `X`.\n *\n * Built once per file and cached on the `Program` node: the search below runs\n * per candidate tuple, and rebuilding this for each of them made the rule cost\n * `O(tuples × file size)` — a full second on the largest generated files here.\n */\nconst aliasReferenceGraph = (\n program: TSESTree.Program,\n sourceCode: TSESLint.SourceCode,\n): ReadonlyMap<string, ReadonlySet<string>> => {\n const cached = aliasReferenceGraphCache.get(program);\n\n if (cached !== undefined) return cached;\n\n const mut_graph = new Map<string, ReadonlySet<string>>();\n\n walk(program, sourceCode, (visited) => {\n if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {\n mut_graph.set(\n visited.id.name,\n referencedTypeNames(visited.typeAnnotation, sourceCode),\n );\n }\n });\n\n aliasReferenceGraphCache.set(program, mut_graph);\n\n return mut_graph;\n};\n\nconst aliasReferenceGraphCache = new WeakMap<\n TSESTree.Program,\n ReadonlyMap<string, ReadonlySet<string>>\n>();\n\nconst programOf = (node: TSESTree.Node): TSESTree.Program =>\n node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);\n\n/** The names every `X` / `X<…>` reference in the subtree points at. */\nconst referencedTypeNames = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n): ReadonlySet<string> => {\n const mut_names = new Set<string>();\n\n walk(node, sourceCode, (visited) => {\n if (\n visited.type === AST_NODE_TYPES.TSTypeReference &&\n visited.typeName.type === AST_NODE_TYPES.Identifier\n ) {\n mut_names.add(visited.typeName.name);\n }\n });\n\n return mut_names;\n};\n\n/**\n * Visits `node` and its descendants.\n *\n * Children are found through the parser's visitor keys, so only the properties\n * that actually hold one are read. Reading every own property instead works\n * too, and is the fallback for a parser that declares no keys for a node type,\n * but it allocates an entry pair per property and walks `loc` / `range` as\n * well — on the generated rule-type files that alone was most of the cost.\n */\nconst walk = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n visit: (visited: TSESTree.Node) => void,\n): void => {\n visit(node);\n\n if (!isRecord(node)) return;\n\n const keys =\n sourceCode.visitorKeys[node.type] ??\n Object.keys(node).filter((key) => key !== 'parent');\n\n for (const key of keys) {\n const value: unknown = hasKey(node, key) ? node[key] : undefined;\n\n if (Arr.isArray(value)) {\n for (const element of value) {\n if (isNode(element)) walk(element, sourceCode, visit);\n }\n } else if (isNode(value)) {\n walk(value, sourceCode, visit);\n }\n }\n};\n\nconst isNode = (value: unknown): value is TSESTree.Node =>\n isNonNullObject(value) &&\n typeof (value as Readonly<{ type?: unknown }>).type === 'string';\n\n/** Unwraps `V[]` and `readonly V[]` to `V`. */\nconst getArrayElementType = (\n node: TSESTree.TypeNode,\n): TSESTree.TypeNode | undefined => {\n if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;\n\n return node.type === AST_NODE_TYPES.TSTypeOperator &&\n node.operator === 'readonly' &&\n node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType\n ? node.typeAnnotation.elementType\n : undefined;\n};\n\nconst normalizeWhitespace = (text: string): string =>\n text.replaceAll(/\\s+/gu, ' ').trim();\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_TYPE_FORGE_MODULE = 'ts-type-forge';\n\nexport const NON_EMPTY_TUPLE_TYPE_NAME = 'NonEmptyTuple';\n\nexport const MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME = 'MutableNonEmptyTuple';\n\nexport const MIN_LENGTH_TUPLE_TYPE_NAME = 'MinLengthTuple';\n\nexport const MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME = 'MutableMinLengthTuple';\n\nexport const FIXED_LENGTH_TUPLE_TYPE_NAME = 'FixedLengthTuple';\n\nexport const MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME = 'MutableFixedLengthTuple';\n\n/**\n * Upper bound for the `maxLength` option of `prefer-length-constrained-tuple`.\n * Beyond this, spelling the tuple out is usually intentional (e.g. a matrix\n * row), and the named form stops being more readable.\n */\nexport const DEFAULT_MAX_TUPLE_LENGTH = 10;\n"],"names":[],"mappings":"AAAO,MAAM,oBAAA,GAAuB;AAE7B,MAAM,yBAAA,GAA4B;AAElC,MAAM,iCAAA,GAAoC;AAE1C,MAAM,0BAAA,GAA6B;AAEnC,MAAM,kCAAA,GAAqC;AAE3C,MAAM,4BAAA,GAA+B;AAErC,MAAM,oCAAA,GAAuC;AAO7C,MAAM,wBAAA,GAA2B
|
|
1
|
+
{"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_TYPE_FORGE_MODULE = 'ts-type-forge';\n\nexport const NON_EMPTY_TUPLE_TYPE_NAME = 'NonEmptyTuple';\n\nexport const MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME = 'MutableNonEmptyTuple';\n\nexport const MIN_LENGTH_TUPLE_TYPE_NAME = 'MinLengthTuple';\n\nexport const MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME = 'MutableMinLengthTuple';\n\nexport const FIXED_LENGTH_TUPLE_TYPE_NAME = 'FixedLengthTuple';\n\nexport const MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME = 'MutableFixedLengthTuple';\n\n/**\n * Upper bound for the `maxLength` option of `prefer-length-constrained-tuple`.\n * Beyond this, spelling the tuple out is usually intentional (e.g. a matrix\n * row), and the named form stops being more readable.\n */\nexport const DEFAULT_MAX_TUPLE_LENGTH = 10;\n"],"names":[],"mappings":"AAAO,MAAM,oBAAA,GAAuB;AAE7B,MAAM,yBAAA,GAA4B;AAElC,MAAM,iCAAA,GAAoC;AAE1C,MAAM,0BAAA,GAA6B;AAEnC,MAAM,kCAAA,GAAqC;AAE3C,MAAM,4BAAA,GAA+B;AAErC,MAAM,oCAAA,GAAuC;AAO7C,MAAM,wBAAA,GAA2B;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-style.mjs","sources":["../../src/rules/import-style.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\nimport {\n getImportedLocalName,\n getTsTypeForgeImports,\n hasConflictingDeclaration,\n} from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** `JSONSchema4`, reached through the public rule-module types. */\ntype JsonSchema = Exclude<\n TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],\n readonly unknown[]\n>;\n\nexport type ImportStyle = 'global' | 'named';\n\nexport const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';\n\nexport const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {\n type: 'string',\n enum: ['global', 'named'],\n description: [\n 'How the ts-type-forge type is brought into scope.',\n \"'named' (default) makes the autofix add the corresponding\",\n `\\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\\``,\n \"when the name is not imported yet; 'global' assumes the\",\n 'ambient globals of `ts-type-forge/global` and never touches',\n 'imports.',\n ].join(' '),\n};\n\n/**\n * Resolves how a ts-type-forge type name should be written in this file, and\n * whether the fix has to add an import for it.\n *\n * Returns `undefined` when the name is already bound to something else (a local\n * alias, or an import from another module) — rewriting to it would then silently\n * resolve to the wrong declaration, which is exactly what happens inside\n * ts-type-forge's own sources.\n */\nexport const resolveTypeName = (\n program: TSESTree.Program,\n canonicalName: string,\n importStyle: ImportStyle,\n): Readonly<{ localName: string; needsImport: boolean }> | undefined => {\n const importedLocalName = getImportedLocalName(\n getTsTypeForgeImports(program),\n canonicalName,\n );\n\n if (importedLocalName !== undefined) {\n return { localName: importedLocalName, needsImport: false };\n }\n\n return hasConflictingDeclaration(program, canonicalName)\n ? undefined\n : { localName: canonicalName, needsImport: importStyle === 'named' };\n};\n"],"names":[],"mappings":";;;AAkBO,MAAM,oBAAA,GAAoC;AAE1C,MAAM,4BAAA,GAA2C;AAAA,EACtD,IAAA,EAAM,QAAA;AAAA,EACN,IAAA,EAAM,CAAC,QAAA,EAAU,OAAO,CAAA;AAAA,EACxB,WAAA,EAAa;AAAA,IACX,mDAAA;AAAA,IACA,2DAAA;AAAA,IACA,kCAA6B,oBAAoB,CAAA,IAAA,CAAA;AAAA,IACjD,yDAAA;AAAA,IACA,6DAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,GAAG;AACZ;AAWO,MAAM,eAAA,GAAkB,CAC7B,OAAA,EACA,aAAA,EACA,WAAA,KACsE;AACtE,EAAA,MAAM,iBAAA,GAAoB,oBAAA;AAAA,IACxB,sBAAsB,OAAO,CAAA;AAAA,IAC7B;AAAA,GACF;AAEA,EAAA,IAAI,sBAAsB,MAAA,EAAW;AACnC,IAAA,OAAO,EAAE,SAAA,EAAW,iBAAA,EAAmB,WAAA,EAAa,KAAA,EAAM;AAAA,EAC5D;AAEA,EAAA,OAAO,yBAAA,CAA0B,OAAA,EAAS,aAAa,CAAA,GACnD,MAAA,GACA,EAAE,SAAA,EAAW,aAAA,EAAe,WAAA,EAAa,WAAA,KAAgB,OAAA,EAAQ;AACvE
|
|
1
|
+
{"version":3,"file":"import-style.mjs","sources":["../../src/rules/import-style.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\nimport {\n getImportedLocalName,\n getTsTypeForgeImports,\n hasConflictingDeclaration,\n} from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** `JSONSchema4`, reached through the public rule-module types. */\ntype JsonSchema = Exclude<\n TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],\n readonly unknown[]\n>;\n\nexport type ImportStyle = 'global' | 'named';\n\nexport const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';\n\nexport const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {\n type: 'string',\n enum: ['global', 'named'],\n description: [\n 'How the ts-type-forge type is brought into scope.',\n \"'named' (default) makes the autofix add the corresponding\",\n `\\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\\``,\n \"when the name is not imported yet; 'global' assumes the\",\n 'ambient globals of `ts-type-forge/global` and never touches',\n 'imports.',\n ].join(' '),\n};\n\n/**\n * Resolves how a ts-type-forge type name should be written in this file, and\n * whether the fix has to add an import for it.\n *\n * Returns `undefined` when the name is already bound to something else (a local\n * alias, or an import from another module) — rewriting to it would then silently\n * resolve to the wrong declaration, which is exactly what happens inside\n * ts-type-forge's own sources.\n */\nexport const resolveTypeName = (\n program: TSESTree.Program,\n canonicalName: string,\n importStyle: ImportStyle,\n): Readonly<{ localName: string; needsImport: boolean }> | undefined => {\n const importedLocalName = getImportedLocalName(\n getTsTypeForgeImports(program),\n canonicalName,\n );\n\n if (importedLocalName !== undefined) {\n return { localName: importedLocalName, needsImport: false };\n }\n\n return hasConflictingDeclaration(program, canonicalName)\n ? undefined\n : { localName: canonicalName, needsImport: importStyle === 'named' };\n};\n"],"names":[],"mappings":";;;AAkBO,MAAM,oBAAA,GAAoC;AAE1C,MAAM,4BAAA,GAA2C;AAAA,EACtD,IAAA,EAAM,QAAA;AAAA,EACN,IAAA,EAAM,CAAC,QAAA,EAAU,OAAO,CAAA;AAAA,EACxB,WAAA,EAAa;AAAA,IACX,mDAAA;AAAA,IACA,2DAAA;AAAA,IACA,kCAA6B,oBAAoB,CAAA,IAAA,CAAA;AAAA,IACjD,yDAAA;AAAA,IACA,6DAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,GAAG;AACZ;AAWO,MAAM,eAAA,GAAkB,CAC7B,OAAA,EACA,aAAA,EACA,WAAA,KACsE;AACtE,EAAA,MAAM,iBAAA,GAAoB,oBAAA;AAAA,IACxB,sBAAsB,OAAO,CAAA;AAAA,IAC7B;AAAA,GACF;AAEA,EAAA,IAAI,sBAAsB,MAAA,EAAW;AACnC,IAAA,OAAO,EAAE,SAAA,EAAW,iBAAA,EAAmB,WAAA,EAAa,KAAA,EAAM;AAAA,EAC5D;AAEA,EAAA,OAAO,yBAAA,CAA0B,OAAA,EAAS,aAAa,CAAA,GACnD,MAAA,GACA,EAAE,SAAA,EAAW,aAAA,EAAe,WAAA,EAAa,WAAA,KAAgB,OAAA,EAAQ;AACvE;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** Every `import ... from 'ts-type-forge'` declaration in the file. */\nexport const getTsTypeForgeImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_TYPE_FORGE_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-type-forge export is imported as\n * (handling aliases such as `import type { NonEmptyArray as NEA }` → `'NEA'`),\n * or `undefined` when it is not imported. Used so autofixes reference the\n * binding that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Whether the file already binds `name` to something that is not the\n * ts-type-forge export of the same name — a local type alias / interface /\n * class / enum, or an import from another module. Autofixes that introduce\n * `name` must bail out in that case, because the rewritten type would resolve\n * to the wrong declaration. (ts-type-forge's own sources, which declare\n * `NonEmptyArray` themselves, are the motivating example.)\n */\nexport const hasConflictingDeclaration = (\n program: TSESTree.Program,\n name: string,\n): boolean =>\n program.body.some((node) => {\n if (node.type === AST_NODE_TYPES.ImportDeclaration) {\n return (\n node.source.value !== TS_TYPE_FORGE_MODULE &&\n node.specifiers.some((specifier) => specifier.local.name === name)\n );\n }\n\n return declaresName(\n node.type === AST_NODE_TYPES.ExportNamedDeclaration ||\n node.type === AST_NODE_TYPES.ExportDefaultDeclaration\n ? node.declaration\n : node,\n name,\n );\n });\n\n/**\n * Inserts `import { type <name> } from 'ts-type-forge';` at the top of the\n * file. Placement is deliberately naive: prettier-plugin-organize-imports (or\n * any equivalent) merges and sorts it afterwards.\n */\nexport const buildTypeImportFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n name: string,\n): readonly TSESLint.RuleFix[] =>\n [\n fixer.insertTextBefore(\n program,\n `import { type ${name} } from '${TS_TYPE_FORGE_MODULE}';\\n`,\n ),\n ] as const;\n\nconst declaresName = (\n node: TSESTree.Node | null | undefined,\n name: string,\n): boolean => {\n if (node === null || node === undefined) return false;\n\n if (\n node.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||\n node.type === AST_NODE_TYPES.TSInterfaceDeclaration ||\n node.type === AST_NODE_TYPES.TSEnumDeclaration\n ) {\n return node.id.name === name;\n }\n\n return (\n node.type === AST_NODE_TYPES.ClassDeclaration && node.id?.name === name\n );\n};\n"],"names":[],"mappings":";;;AAUO,MAAM,qBAAA,GAAwB,CACnC,OAAA,KAEA,OAAA,CAAQ,IAAA,CAAK,MAAA;AAAA,EACX,CAAC,SACC,IAAA,CAAK,IAAA,KAAS,eAAe,iBAAA,IAC7B,IAAA,CAAK,OAAO,KAAA,KAAU;AAC1B;AAQK,MAAM,oBAAA,GAAuB,CAClC,kBAAA,EACA,YAAA,KAEA,kBAAA,CACG,QAAQ,CAAC,WAAA,KAAgB,WAAA,CAAY,UAAU,CAAA,CAC/C,IAAA;AAAA,EACC,CAAC,SAAA,KACC,SAAA,CAAU,IAAA,KAAS,cAAA,CAAe,oBACjC,SAAA,CAAU,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,aACxC,SAAA,CAAU,QAAA,CAAS,IAAA,GACnB,SAAA,CAAU,SAAS,KAAA,MAAW;AACtC,CAAA,EAAG,KAAA,CAAM;AAUN,MAAM,yBAAA,GAA4B,CACvC,OAAA,EACA,IAAA,KAEA,QAAQ,IAAA,CAAK,IAAA,CAAK,CAAC,IAAA,KAAS;AAC1B,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,iBAAA,EAAmB;AAClD,IAAA,OACE,IAAA,CAAK,MAAA,CAAO,KAAA,KAAU,oBAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,CAAC,SAAA,KAAc,SAAA,CAAU,KAAA,CAAM,IAAA,KAAS,IAAI,CAAA;AAAA,EAErE;AAEA,EAAA,OAAO,YAAA;AAAA,IACL,IAAA,CAAK,SAAS,cAAA,CAAe,sBAAA,IAC3B,KAAK,IAAA,KAAS,cAAA,CAAe,wBAAA,GAC3B,IAAA,CAAK,WAAA,GACL,IAAA;AAAA,IACJ;AAAA,GACF;AACF,CAAC;AAOI,MAAM,kBAAA,GAAqB,CAChC,KAAA,EACA,OAAA,EACA,IAAA,KAEA;AAAA,EACE,KAAA,CAAM,gBAAA;AAAA,IACJ,OAAA;AAAA,IACA,CAAA,cAAA,EAAiB,IAAI,CAAA,SAAA,EAAY,oBAAoB,CAAA;AAAA;AAAA;AAEzD;AAEF,MAAM,YAAA,GAAe,CACnB,IAAA,EACA,IAAA,KACY;AACZ,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,MAAA,EAAW,OAAO,KAAA;AAEhD,EAAA,IACE,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,IAC7B,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,IAC7B,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,iBAAA,EAC7B;AACA,IAAA,OAAO,IAAA,CAAK,GAAG,IAAA,KAAS,IAAA;AAAA,EAC1B;AAEA,EAAA,OACE,KAAK,IAAA,KAAS,cAAA,CAAe,gBAAA,IAAoB,IAAA,CAAK,IAAI,IAAA,KAAS,IAAA;AAEvE,CAAA
|
|
1
|
+
{"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** Every `import ... from 'ts-type-forge'` declaration in the file. */\nexport const getTsTypeForgeImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_TYPE_FORGE_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-type-forge export is imported as\n * (handling aliases such as `import type { NonEmptyArray as NEA }` → `'NEA'`),\n * or `undefined` when it is not imported. Used so autofixes reference the\n * binding that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Whether the file already binds `name` to something that is not the\n * ts-type-forge export of the same name — a local type alias / interface /\n * class / enum, or an import from another module. Autofixes that introduce\n * `name` must bail out in that case, because the rewritten type would resolve\n * to the wrong declaration. (ts-type-forge's own sources, which declare\n * `NonEmptyArray` themselves, are the motivating example.)\n */\nexport const hasConflictingDeclaration = (\n program: TSESTree.Program,\n name: string,\n): boolean =>\n program.body.some((node) => {\n if (node.type === AST_NODE_TYPES.ImportDeclaration) {\n return (\n node.source.value !== TS_TYPE_FORGE_MODULE &&\n node.specifiers.some((specifier) => specifier.local.name === name)\n );\n }\n\n return declaresName(\n node.type === AST_NODE_TYPES.ExportNamedDeclaration ||\n node.type === AST_NODE_TYPES.ExportDefaultDeclaration\n ? node.declaration\n : node,\n name,\n );\n });\n\n/**\n * Inserts `import { type <name> } from 'ts-type-forge';` at the top of the\n * file. Placement is deliberately naive: prettier-plugin-organize-imports (or\n * any equivalent) merges and sorts it afterwards.\n */\nexport const buildTypeImportFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n name: string,\n): readonly TSESLint.RuleFix[] =>\n [\n fixer.insertTextBefore(\n program,\n `import { type ${name} } from '${TS_TYPE_FORGE_MODULE}';\\n`,\n ),\n ] as const;\n\nconst declaresName = (\n node: TSESTree.Node | null | undefined,\n name: string,\n): boolean => {\n if (node === null || node === undefined) return false;\n\n if (\n node.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||\n node.type === AST_NODE_TYPES.TSInterfaceDeclaration ||\n node.type === AST_NODE_TYPES.TSEnumDeclaration\n ) {\n return node.id.name === name;\n }\n\n return (\n node.type === AST_NODE_TYPES.ClassDeclaration && node.id?.name === name\n );\n};\n"],"names":[],"mappings":";;;AAUO,MAAM,qBAAA,GAAwB,CACnC,OAAA,KAEA,OAAA,CAAQ,IAAA,CAAK,MAAA;AAAA,EACX,CAAC,SACC,IAAA,CAAK,IAAA,KAAS,eAAe,iBAAA,IAC7B,IAAA,CAAK,OAAO,KAAA,KAAU;AAC1B;AAQK,MAAM,oBAAA,GAAuB,CAClC,kBAAA,EACA,YAAA,KAEA,kBAAA,CACG,QAAQ,CAAC,WAAA,KAAgB,WAAA,CAAY,UAAU,CAAA,CAC/C,IAAA;AAAA,EACC,CAAC,SAAA,KACC,SAAA,CAAU,IAAA,KAAS,cAAA,CAAe,oBACjC,SAAA,CAAU,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,aACxC,SAAA,CAAU,QAAA,CAAS,IAAA,GACnB,SAAA,CAAU,SAAS,KAAA,MAAW;AACtC,CAAA,EAAG,KAAA,CAAM;AAUN,MAAM,yBAAA,GAA4B,CACvC,OAAA,EACA,IAAA,KAEA,QAAQ,IAAA,CAAK,IAAA,CAAK,CAAC,IAAA,KAAS;AAC1B,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,iBAAA,EAAmB;AAClD,IAAA,OACE,IAAA,CAAK,MAAA,CAAO,KAAA,KAAU,oBAAA,IACtB,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,CAAC,SAAA,KAAc,SAAA,CAAU,KAAA,CAAM,IAAA,KAAS,IAAI,CAAA;AAAA,EAErE;AAEA,EAAA,OAAO,YAAA;AAAA,IACL,IAAA,CAAK,SAAS,cAAA,CAAe,sBAAA,IAC3B,KAAK,IAAA,KAAS,cAAA,CAAe,wBAAA,GAC3B,IAAA,CAAK,WAAA,GACL,IAAA;AAAA,IACJ;AAAA,GACF;AACF,CAAC;AAOI,MAAM,kBAAA,GAAqB,CAChC,KAAA,EACA,OAAA,EACA,IAAA,KAEA;AAAA,EACE,KAAA,CAAM,gBAAA;AAAA,IACJ,OAAA;AAAA,IACA,CAAA,cAAA,EAAiB,IAAI,CAAA,SAAA,EAAY,oBAAoB,CAAA;AAAA;AAAA;AAEzD;AAEF,MAAM,YAAA,GAAe,CACnB,IAAA,EACA,IAAA,KACY;AACZ,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,MAAA,EAAW,OAAO,KAAA;AAEhD,EAAA,IACE,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,IAC7B,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,IAC7B,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,iBAAA,EAC7B;AACA,IAAA,OAAO,IAAA,CAAK,GAAG,IAAA,KAAS,IAAA;AAAA,EAC1B;AAEA,EAAA,OACE,KAAK,IAAA,KAAS,cAAA,CAAe,gBAAA,IAAoB,IAAA,CAAK,IAAI,IAAA,KAAS,IAAA;AAEvE,CAAA;;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = readonly [];
|
|
3
|
+
type MessageIds = 'removeSideEffectImport';
|
|
4
|
+
/**
|
|
5
|
+
* `import 'ts-type-forge';` binds nothing, and there is nothing for it to run:
|
|
6
|
+
* the package ships declarations only, and its `exports` map offers no runtime
|
|
7
|
+
* condition at all, so the import fails to resolve the moment the module graph
|
|
8
|
+
* is loaded. TypeScript keeps a side-effect import in the emitted JavaScript —
|
|
9
|
+
* that is the one kind it never elides — so this is a runtime error waiting in
|
|
10
|
+
* a file that type-checks.
|
|
11
|
+
*
|
|
12
|
+
* The ambient globals are a separate entry point, `ts-type-forge/global`,
|
|
13
|
+
* usually brought in through `compilerOptions.types` or a triple-slash
|
|
14
|
+
* reference. This rule matches the bare specifier exactly, so that subpath is
|
|
15
|
+
* left alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare const noSideEffectImport: TSESLint.RuleModule<MessageIds, Options>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=no-side-effect-import.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-side-effect-import.d.mts","sourceRoot":"","sources":["../../src/rules/no-side-effect-import.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,0BAA0B,CAAC;AAIxE,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,wBAAwB,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA8B9D,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
2
|
+
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
3
|
+
|
|
4
|
+
const noSideEffectImport = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: `Remove a side-effect-only \`import '${TS_TYPE_FORGE_MODULE}';\`, which binds no name and has no runtime entry point to resolve to. \`${TS_TYPE_FORGE_MODULE}/global\` is left alone.`
|
|
9
|
+
},
|
|
10
|
+
fixable: "code",
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
removeSideEffectImport: `\`import '${TS_TYPE_FORGE_MODULE}';\` binds no name, and \`${TS_TYPE_FORGE_MODULE}\` ships types only \u2014 it has no runtime entry point, so the import throws when the module is loaded. Remove it; the ambient globals come from \`${TS_TYPE_FORGE_MODULE}/global\`, not from this.`
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
create: (context) => ({
|
|
17
|
+
ImportDeclaration: (node) => {
|
|
18
|
+
if (node.source.value !== TS_TYPE_FORGE_MODULE || !Arr.isEmpty(node.specifiers)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
messageId: "removeSideEffectImport",
|
|
24
|
+
fix: (fixer) => removeStatement(fixer, context.sourceCode, node)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
defaultOptions: []
|
|
29
|
+
};
|
|
30
|
+
const removeStatement = (fixer, sourceCode, node) => {
|
|
31
|
+
const restOfLine = /^[^\S\n]*\n/u.exec(
|
|
32
|
+
sourceCode.getText().slice(node.range[1])
|
|
33
|
+
);
|
|
34
|
+
return fixer.removeRange([
|
|
35
|
+
node.range[0],
|
|
36
|
+
node.range[1] + (restOfLine?.[0].length ?? 0)
|
|
37
|
+
]);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { noSideEffectImport };
|
|
41
|
+
//# sourceMappingURL=no-side-effect-import.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-side-effect-import.mjs","sources":["../../src/rules/no-side-effect-import.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'removeSideEffectImport';\n\n/**\n * `import 'ts-type-forge';` binds nothing, and there is nothing for it to run:\n * the package ships declarations only, and its `exports` map offers no runtime\n * condition at all, so the import fails to resolve the moment the module graph\n * is loaded. TypeScript keeps a side-effect import in the emitted JavaScript —\n * that is the one kind it never elides — so this is a runtime error waiting in\n * a file that type-checks.\n *\n * The ambient globals are a separate entry point, `ts-type-forge/global`,\n * usually brought in through `compilerOptions.types` or a triple-slash\n * reference. This rule matches the bare specifier exactly, so that subpath is\n * left alone.\n */\nexport const noSideEffectImport: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'problem',\n docs: {\n description: `Remove a side-effect-only \\`import '${TS_TYPE_FORGE_MODULE}';\\`, which binds no name and has no runtime entry point to resolve to. \\`${TS_TYPE_FORGE_MODULE}/global\\` is left alone.`,\n },\n fixable: 'code',\n schema: [],\n messages: {\n removeSideEffectImport: `\\`import '${TS_TYPE_FORGE_MODULE}';\\` binds no name, and \\`${TS_TYPE_FORGE_MODULE}\\` ships types only — it has no runtime entry point, so the import throws when the module is loaded. Remove it; the ambient globals come from \\`${TS_TYPE_FORGE_MODULE}/global\\`, not from this.`,\n },\n },\n\n create: (context) => ({\n ImportDeclaration: (node) => {\n if (\n node.source.value !== TS_TYPE_FORGE_MODULE ||\n !Arr.isEmpty(node.specifiers)\n ) {\n return;\n }\n\n context.report({\n node,\n messageId: 'removeSideEffectImport',\n fix: (fixer) => removeStatement(fixer, context.sourceCode, node),\n });\n },\n }),\n defaultOptions: [],\n} as const;\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Removes a statement along with the rest of its line, so that deleting it does\n * not leave a blank line where it was.\n */\nconst removeStatement = (\n fixer: TSESLint.RuleFixer,\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n): TSESLint.RuleFix => {\n const restOfLine = /^[^\\S\\n]*\\n/u.exec(\n sourceCode.getText().slice(node.range[1]),\n );\n\n return fixer.removeRange([\n node.range[0],\n node.range[1] + (restOfLine?.[0].length ?? 0),\n ]);\n};\n"],"names":[],"mappings":";;;AAqBO,MAAM,kBAAA,GAA+D;AAAA,EAC1E,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EAAa,CAAA,oCAAA,EAAuC,oBAAoB,CAAA,0EAAA,EAA6E,oBAAoB,CAAA,wBAAA;AAAA,KAC3K;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,QAAQ,EAAC;AAAA,IACT,QAAA,EAAU;AAAA,MACR,wBAAwB,CAAA,UAAA,EAAa,oBAAoB,CAAA,0BAAA,EAA6B,oBAAoB,wJAAmJ,oBAAoB,CAAA,yBAAA;AAAA;AACnR,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,MAAa;AAAA,IACpB,iBAAA,EAAmB,CAAC,IAAA,KAAS;AAC3B,MAAA,IACE,IAAA,CAAK,OAAO,KAAA,KAAU,oBAAA,IACtB,CAAC,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,UAAU,CAAA,EAC5B;AACA,QAAA;AAAA,MACF;AAEA,MAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,QACb,IAAA;AAAA,QACA,SAAA,EAAW,wBAAA;AAAA,QACX,KAAK,CAAC,KAAA,KAAU,gBAAgB,KAAA,EAAO,OAAA,CAAQ,YAAY,IAAI;AAAA,OAChE,CAAA;AAAA,IACH;AAAA,GACF,CAAA;AAAA,EACA,gBAAgB;AAClB;AAQA,MAAM,eAAA,GAAkB,CACtB,KAAA,EACA,UAAA,EACA,IAAA,KACqB;AACrB,EAAA,MAAM,aAAa,cAAA,CAAe,IAAA;AAAA,IAChC,WAAW,OAAA,EAAQ,CAAE,MAAM,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC;AAAA,GAC1C;AAEA,EAAA,OAAO,MAAM,WAAA,CAAY;AAAA,IACvB,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,IACZ,KAAK,KAAA,CAAM,CAAC,KAAK,UAAA,GAAa,CAAC,EAAE,MAAA,IAAU,CAAA;AAAA,GAC5C,CAAA;AACH,CAAA;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-canonical-length-constrained-tuple.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-tuple.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport { analyzeUniformTuple, type UniformTupleShape } from './ast-utils.mjs';\nimport {\n DEFAULT_MAX_TUPLE_LENGTH,\n FIXED_LENGTH_TUPLE_TYPE_NAME,\n MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME,\n NON_EMPTY_TUPLE_TYPE_NAME,\n} from './constants.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n maxLength?: number;\n }>?,\n];\n\ntype MessageIds = 'useCanonicalTuple';\n\n/**\n * A single fixed element without a rest (`[V]`) is left alone: it reads better\n * than `FixedLengthTuple<1, V>`.\n */\nconst MIN_FIXED_LENGTH = 2;\n\nexport const preferCanonicalLengthConstrainedTuple: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n maxLength: {\n type: 'integer',\n minimum: MIN_FIXED_LENGTH,\n description: [\n 'Longest tuple the rule rewrites (default',\n `${DEFAULT_MAX_TUPLE_LENGTH}).`,\n 'Longer tuples are usually spelled out on purpose, and the named',\n 'form stops being more readable.',\n ].join(' '),\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useCanonicalTuple:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const options = context.options[0];\n\n const importStyle = options?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const maxLength = options?.maxLength ?? DEFAULT_MAX_TUPLE_LENGTH;\n\n const mut_rewrites: TypeRewrite[] = [];\n\n return {\n TSTupleType: (node) => {\n const shape = analyzeUniformTuple(node, sourceCode);\n\n if (shape === undefined || shape.fixedCount > maxLength) return;\n\n const canonicalName = canonicalNameFor(shape);\n\n if (canonicalName === undefined) return;\n\n mut_rewrites.push({\n node: shape.node,\n canonicalName,\n typeArgs:\n shape.fixedCount === 1\n ? shape.elementText\n : `${shape.fixedCount}, ${shape.elementText}`,\n });\n },\n 'Program:exit': () => {\n reportTypeRewrites(\n context,\n mut_rewrites,\n 'useCanonicalTuple',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [\n { importStyle: DEFAULT_IMPORT_STYLE, maxLength: DEFAULT_MAX_TUPLE_LENGTH },\n ],\n} as const;\n\n/**\n * The ts-type-forge type a given tuple shape spells out, or `undefined` when the\n * shape is deliberately left alone.\n *\n * | shape | readonly | mutable |\n * | :-------------------- | :------------------ | :------------------------- |\n * | `[V, ...V[]]` | `NonEmptyTuple` | `MutableNonEmptyTuple` |\n * | `[V, …×N, ...V[]]` | `MinLengthTuple` | `MutableMinLengthTuple` |\n * | `[V, …×N]` (N >= 2) | `FixedLengthTuple` | `MutableFixedLengthTuple` |\n */\nconst canonicalNameFor = (\n // `UniformTupleShape` carries a `TSESTree.Node`, which is not deeply readonly.\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n shape: UniformTupleShape,\n): string | undefined => {\n const { isReadonly, hasRest, fixedCount } = shape;\n\n if (hasRest) {\n if (fixedCount === 1) {\n return isReadonly\n ? NON_EMPTY_TUPLE_TYPE_NAME\n : MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME;\n }\n\n return isReadonly\n ? MIN_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME;\n }\n\n if (fixedCount < MIN_FIXED_LENGTH) return undefined;\n\n return isReadonly\n ? FIXED_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME;\n};\n"],"names":[],"mappings":";;;;;AA8BA,MAAM,gBAAA,GAAmB,CAAA;AAElB,MAAM,qCAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa,4BAAA;AAAA,UACb,SAAA,EAAW;AAAA,YACT,IAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAS,gBAAA;AAAA,YACT,WAAA,EAAa;AAAA,cACX,0CAAA;AAAA,cACA,GAAG,wBAAwB,CAAA,EAAA,CAAA;AAAA,cAC3B,iEAAA;AAAA,cACA;AAAA,aACF,CAAE,KAAK,GAAG;AAAA;AACZ,SACF;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,iBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA;AAEjC,IAAA,MAAM,WAAA,GAAc,SAAS,WAAA,IAAe,oBAAA;AAE5C,IAAA,MAAM,SAAA,GAAY,SAAS,SAAA,IAAa,wBAAA;AAExC,IAAA,MAAM,eAA8B,EAAC;AAErC,IAAA,OAAO;AAAA,MACL,WAAA,EAAa,CAAC,IAAA,KAAS;AACrB,QAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,IAAA,EAAM,UAAU,CAAA;AAElD,QAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,CAAM,UAAA,GAAa,SAAA,EAAW;AAEzD,QAAA,MAAM,aAAA,GAAgB,iBAAiB,KAAK,CAAA;AAE5C,QAAA,IAAI,kBAAkB,MAAA,EAAW;AAEjC,QAAA,YAAA,CAAa,IAAA,CAAK;AAAA,UAChB,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,aAAA;AAAA,UACA,QAAA,EACE,KAAA,CAAM,UAAA,KAAe,CAAA,GACjB,KAAA,CAAM,WAAA,GACN,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,EAAA,EAAK,KAAA,CAAM,WAAW,CAAA;AAAA,SAChD,CAAA;AAAA,MACH,CAAA;AAAA,MACA,gBAAgB,MAAM;AACpB,QAAA,kBAAA;AAAA,UACE,OAAA;AAAA,UACA,YAAA;AAAA,UACA,mBAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,EAAE,WAAA,EAAa,oBAAA,EAAsB,SAAA,EAAW,wBAAA;AAAyB;AAE7E;AAYA,MAAM,gBAAA,GAAmB,CAGvB,KAAA,KACuB;AACvB,EAAA,MAAM,EAAE,UAAA,EAAY,OAAA,EAAS,UAAA,EAAW,GAAI,KAAA;AAE5C,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,eAAe,CAAA,EAAG;AACpB,MAAA,OAAO,aACH,yBAAA,GACA,iCAAA;AAAA,IACN;AAEA,IAAA,OAAO,aACH,0BAAA,GACA,kCAAA;AAAA,EACN;AAEA,EAAA,IAAI,UAAA,GAAa,kBAAkB,OAAO,MAAA;AAE1C,EAAA,OAAO,aACH,4BAAA,GACA,oCAAA;AACN,CAAA
|
|
1
|
+
{"version":3,"file":"prefer-canonical-length-constrained-tuple.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-tuple.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport { analyzeUniformTuple, type UniformTupleShape } from './ast-utils.mjs';\nimport {\n DEFAULT_MAX_TUPLE_LENGTH,\n FIXED_LENGTH_TUPLE_TYPE_NAME,\n MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME,\n NON_EMPTY_TUPLE_TYPE_NAME,\n} from './constants.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n maxLength?: number;\n }>?,\n];\n\ntype MessageIds = 'useCanonicalTuple';\n\n/**\n * A single fixed element without a rest (`[V]`) is left alone: it reads better\n * than `FixedLengthTuple<1, V>`.\n */\nconst MIN_FIXED_LENGTH = 2;\n\nexport const preferCanonicalLengthConstrainedTuple: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n maxLength: {\n type: 'integer',\n minimum: MIN_FIXED_LENGTH,\n description: [\n 'Longest tuple the rule rewrites (default',\n `${DEFAULT_MAX_TUPLE_LENGTH}).`,\n 'Longer tuples are usually spelled out on purpose, and the named',\n 'form stops being more readable.',\n ].join(' '),\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useCanonicalTuple:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const options = context.options[0];\n\n const importStyle = options?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const maxLength = options?.maxLength ?? DEFAULT_MAX_TUPLE_LENGTH;\n\n const mut_rewrites: TypeRewrite[] = [];\n\n return {\n TSTupleType: (node) => {\n const shape = analyzeUniformTuple(node, sourceCode);\n\n if (shape === undefined || shape.fixedCount > maxLength) return;\n\n const canonicalName = canonicalNameFor(shape);\n\n if (canonicalName === undefined) return;\n\n mut_rewrites.push({\n node: shape.node,\n canonicalName,\n typeArgs:\n shape.fixedCount === 1\n ? shape.elementText\n : `${shape.fixedCount}, ${shape.elementText}`,\n });\n },\n 'Program:exit': () => {\n reportTypeRewrites(\n context,\n mut_rewrites,\n 'useCanonicalTuple',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [\n { importStyle: DEFAULT_IMPORT_STYLE, maxLength: DEFAULT_MAX_TUPLE_LENGTH },\n ],\n} as const;\n\n/**\n * The ts-type-forge type a given tuple shape spells out, or `undefined` when the\n * shape is deliberately left alone.\n *\n * | shape | readonly | mutable |\n * | :-------------------- | :------------------ | :------------------------- |\n * | `[V, ...V[]]` | `NonEmptyTuple` | `MutableNonEmptyTuple` |\n * | `[V, …×N, ...V[]]` | `MinLengthTuple` | `MutableMinLengthTuple` |\n * | `[V, …×N]` (N >= 2) | `FixedLengthTuple` | `MutableFixedLengthTuple` |\n */\nconst canonicalNameFor = (\n // `UniformTupleShape` carries a `TSESTree.Node`, which is not deeply readonly.\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n shape: UniformTupleShape,\n): string | undefined => {\n const { isReadonly, hasRest, fixedCount } = shape;\n\n if (hasRest) {\n if (fixedCount === 1) {\n return isReadonly\n ? NON_EMPTY_TUPLE_TYPE_NAME\n : MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME;\n }\n\n return isReadonly\n ? MIN_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME;\n }\n\n if (fixedCount < MIN_FIXED_LENGTH) return undefined;\n\n return isReadonly\n ? FIXED_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME;\n};\n"],"names":[],"mappings":";;;;;AA8BA,MAAM,gBAAA,GAAmB,CAAA;AAElB,MAAM,qCAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa,4BAAA;AAAA,UACb,SAAA,EAAW;AAAA,YACT,IAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAS,gBAAA;AAAA,YACT,WAAA,EAAa;AAAA,cACX,0CAAA;AAAA,cACA,GAAG,wBAAwB,CAAA,EAAA,CAAA;AAAA,cAC3B,iEAAA;AAAA,cACA;AAAA,aACF,CAAE,KAAK,GAAG;AAAA;AACZ,SACF;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,iBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA;AAEjC,IAAA,MAAM,WAAA,GAAc,SAAS,WAAA,IAAe,oBAAA;AAE5C,IAAA,MAAM,SAAA,GAAY,SAAS,SAAA,IAAa,wBAAA;AAExC,IAAA,MAAM,eAA8B,EAAC;AAErC,IAAA,OAAO;AAAA,MACL,WAAA,EAAa,CAAC,IAAA,KAAS;AACrB,QAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,IAAA,EAAM,UAAU,CAAA;AAElD,QAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,CAAM,UAAA,GAAa,SAAA,EAAW;AAEzD,QAAA,MAAM,aAAA,GAAgB,iBAAiB,KAAK,CAAA;AAE5C,QAAA,IAAI,kBAAkB,MAAA,EAAW;AAEjC,QAAA,YAAA,CAAa,IAAA,CAAK;AAAA,UAChB,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,aAAA;AAAA,UACA,QAAA,EACE,KAAA,CAAM,UAAA,KAAe,CAAA,GACjB,KAAA,CAAM,WAAA,GACN,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,EAAA,EAAK,KAAA,CAAM,WAAW,CAAA;AAAA,SAChD,CAAA;AAAA,MACH,CAAA;AAAA,MACA,gBAAgB,MAAM;AACpB,QAAA,kBAAA;AAAA,UACE,OAAA;AAAA,UACA,YAAA;AAAA,UACA,mBAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,EAAE,WAAA,EAAa,oBAAA,EAAsB,SAAA,EAAW,wBAAA;AAAyB;AAE7E;AAYA,MAAM,gBAAA,GAAmB,CAGvB,KAAA,KACuB;AACvB,EAAA,MAAM,EAAE,UAAA,EAAY,OAAA,EAAS,UAAA,EAAW,GAAI,KAAA;AAE5C,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,eAAe,CAAA,EAAG;AACpB,MAAA,OAAO,aACH,yBAAA,GACA,iCAAA;AAAA,IACN;AAEA,IAAA,OAAO,aACH,0BAAA,GACA,kCAAA;AAAA,EACN;AAEA,EAAA,IAAI,UAAA,GAAa,kBAAkB,OAAO,MAAA;AAE1C,EAAA,OAAO,aACH,4BAAA,GACA,oCAAA;AACN,CAAA;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-canonical-mutable-record.mjs","sources":["../../src/rules/prefer-canonical-mutable-record.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { isLocallyBound } from './ast-utils.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n getImportedLocalName,\n getTsTypeForgeImports,\n} from './import-utils.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useMutableRecord';\n\n/**\n * The record spellings that `Mutable<…>` collapses to `MutableRecord<K, V>`.\n * `Mutable` strips the `readonly` modifier from every property, so applied to\n * any of these it produces exactly the type `MutableRecord` spells directly.\n */\nconst WRAPPED_RECORD_NAMES = [\n 'Record',\n 'ReadonlyRecord',\n 'MutableRecord',\n] as const;\n\nconst MUTABLE_TYPE_NAME = 'Mutable';\n\nconst MUTABLE_RECORD_TYPE_NAME = 'MutableRecord';\n\nexport const preferCanonicalMutableRecord: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Mutable<Record<K, V>>` (and `Mutable` applied to `ReadonlyRecord` / `MutableRecord`) with the canonical ts-type-forge `MutableRecord<K, V>`.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useMutableRecord:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const sourceCode = context.sourceCode;\n\n const tsTypeForgeImports = getTsTypeForgeImports(sourceCode.ast);\n\n /**\n * Whether this reference denotes the ts-type-forge type `canonicalName` —\n * either through an import from ts-type-forge (aliases included) or as the\n * bare ambient global. A name bound to anything else in this file (a local\n * alias, an import from another module) denotes that declaration instead.\n * The built-in `Record` goes through the same check: ts-type-forge exports\n * no `Record`, so only the unbound global spelling matches.\n */\n const referencesTsTypeForgeType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.TSTypeReference,\n canonicalName: string,\n ): boolean => {\n const typeName = node.typeName;\n\n // `ns.Mutable` / `import('…').Mutable` never denote the ts-type-forge type.\n if (typeName.type !== AST_NODE_TYPES.Identifier) return false;\n\n return (\n typeName.name ===\n getImportedLocalName(tsTypeForgeImports, canonicalName) ||\n (typeName.name === canonicalName &&\n !isLocallyBound(sourceCode, node, canonicalName))\n );\n };\n\n const mut_rewrites: TypeRewrite[] = [];\n\n return {\n TSTypeReference: (node) => {\n if (!referencesTsTypeForgeType(node, MUTABLE_TYPE_NAME)) return;\n\n const wrapped = node.typeArguments?.params;\n\n if (wrapped === undefined || !Arr.isFixedLengthArray(1, wrapped)) {\n return;\n }\n\n const record = wrapped[0];\n\n if (\n record.type !== AST_NODE_TYPES.TSTypeReference ||\n WRAPPED_RECORD_NAMES.every(\n (name) => !referencesTsTypeForgeType(record, name),\n )\n ) {\n return;\n }\n\n const recordTypeArgs = record.typeArguments?.params;\n\n if (\n recordTypeArgs === undefined ||\n !Arr.isFixedLengthArray(2, recordTypeArgs)\n ) {\n return;\n }\n\n const [K, V] = recordTypeArgs;\n\n mut_rewrites.push({\n node,\n canonicalName: MUTABLE_RECORD_TYPE_NAME,\n typeArgs: `${sourceCode.getText(K)}, ${sourceCode.getText(V)}`,\n });\n },\n 'Program:exit': () => {\n reportTypeRewrites(\n context,\n mut_rewrites,\n 'useMutableRecord',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":";;;;;;;AA8BA,MAAM,oBAAA,GAAuB;AAAA,EAC3B,QAAA;AAAA,EACA,gBAAA;AAAA,EACA;AACF,CAAA;AAEA,MAAM,iBAAA,GAAoB,SAAA;AAE1B,MAAM,wBAAA,GAA2B,eAAA;AAE1B,MAAM,4BAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa;AAAA,SACf;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,gBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAG,WAAA,IAAe,oBAAA;AAEvD,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,kBAAA,GAAqB,qBAAA,CAAsB,UAAA,CAAW,GAAG,CAAA;AAU/D,IAAA,MAAM,yBAAA,GAA4B,CAEhC,IAAA,EACA,aAAA,KACY;AACZ,MAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AAGtB,MAAA,IAAI,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,UAAA,EAAY,OAAO,KAAA;AAExD,MAAA,OACE,QAAA,CAAS,IAAA,KACP,oBAAA,CAAqB,kBAAA,EAAoB,aAAa,CAAA,IACvD,QAAA,CAAS,IAAA,KAAS,aAAA,IACjB,CAAC,cAAA,CAAe,UAAA,EAAY,MAAM,aAAa,CAAA;AAAA,IAErD,CAAA;AAEA,IAAA,MAAM,eAA8B,EAAC;AAErC,IAAA,OAAO;AAAA,MACL,eAAA,EAAiB,CAAC,IAAA,KAAS;AACzB,QAAA,IAAI,CAAC,yBAAA,CAA0B,IAAA,EAAM,iBAAiB,CAAA,EAAG;AAEzD,QAAA,MAAM,OAAA,GAAU,KAAK,aAAA,EAAe,MAAA;AAEpC,QAAA,IAAI,YAAY,MAAA,IAAa,CAAC,IAAI,kBAAA,CAAmB,CAAA,EAAG,OAAO,CAAA,EAAG;AAChE,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,MAAA,GAAS,QAAQ,CAAC,CAAA;AAExB,QAAA,IACE,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,eAAA,IAC/B,oBAAA,CAAqB,KAAA;AAAA,UACnB,CAAC,IAAA,KAAS,CAAC,yBAAA,CAA0B,QAAQ,IAAI;AAAA,SACnD,EACA;AACA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,cAAA,GAAiB,OAAO,aAAA,EAAe,MAAA;AAE7C,QAAA,IACE,mBAAmB,MAAA,IACnB,CAAC,IAAI,kBAAA,CAAmB,CAAA,EAAG,cAAc,CAAA,EACzC;AACA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,CAAC,CAAA,EAAG,CAAC,CAAA,GAAI,cAAA;AAEf,QAAA,YAAA,CAAa,IAAA,CAAK;AAAA,UAChB,IAAA;AAAA,UACA,aAAA,EAAe,wBAAA;AAAA,UACf,QAAA,EAAU,CAAA,EAAG,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAA,EAAK,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAC,CAAA;AAAA,SAC7D,CAAA;AAAA,MACH,CAAA;AAAA,MACA,gBAAgB,MAAM;AACpB,QAAA,kBAAA;AAAA,UACE,OAAA;AAAA,UACA,YAAA;AAAA,UACA,kBAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,WAAA,EAAa,sBAAsB;AACxD
|
|
1
|
+
{"version":3,"file":"prefer-canonical-mutable-record.mjs","sources":["../../src/rules/prefer-canonical-mutable-record.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { isLocallyBound } from './ast-utils.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n getImportedLocalName,\n getTsTypeForgeImports,\n} from './import-utils.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useMutableRecord';\n\n/**\n * The record spellings that `Mutable<…>` collapses to `MutableRecord<K, V>`.\n * `Mutable` strips the `readonly` modifier from every property, so applied to\n * any of these it produces exactly the type `MutableRecord` spells directly.\n */\nconst WRAPPED_RECORD_NAMES = [\n 'Record',\n 'ReadonlyRecord',\n 'MutableRecord',\n] as const;\n\nconst MUTABLE_TYPE_NAME = 'Mutable';\n\nconst MUTABLE_RECORD_TYPE_NAME = 'MutableRecord';\n\nexport const preferCanonicalMutableRecord: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Mutable<Record<K, V>>` (and `Mutable` applied to `ReadonlyRecord` / `MutableRecord`) with the canonical ts-type-forge `MutableRecord<K, V>`.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useMutableRecord:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const sourceCode = context.sourceCode;\n\n const tsTypeForgeImports = getTsTypeForgeImports(sourceCode.ast);\n\n /**\n * Whether this reference denotes the ts-type-forge type `canonicalName` —\n * either through an import from ts-type-forge (aliases included) or as the\n * bare ambient global. A name bound to anything else in this file (a local\n * alias, an import from another module) denotes that declaration instead.\n * The built-in `Record` goes through the same check: ts-type-forge exports\n * no `Record`, so only the unbound global spelling matches.\n */\n const referencesTsTypeForgeType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.TSTypeReference,\n canonicalName: string,\n ): boolean => {\n const typeName = node.typeName;\n\n // `ns.Mutable` / `import('…').Mutable` never denote the ts-type-forge type.\n if (typeName.type !== AST_NODE_TYPES.Identifier) return false;\n\n return (\n typeName.name ===\n getImportedLocalName(tsTypeForgeImports, canonicalName) ||\n (typeName.name === canonicalName &&\n !isLocallyBound(sourceCode, node, canonicalName))\n );\n };\n\n const mut_rewrites: TypeRewrite[] = [];\n\n return {\n TSTypeReference: (node) => {\n if (!referencesTsTypeForgeType(node, MUTABLE_TYPE_NAME)) return;\n\n const wrapped = node.typeArguments?.params;\n\n if (wrapped === undefined || !Arr.isFixedLengthArray(1, wrapped)) {\n return;\n }\n\n const record = wrapped[0];\n\n if (\n record.type !== AST_NODE_TYPES.TSTypeReference ||\n WRAPPED_RECORD_NAMES.every(\n (name) => !referencesTsTypeForgeType(record, name),\n )\n ) {\n return;\n }\n\n const recordTypeArgs = record.typeArguments?.params;\n\n if (\n recordTypeArgs === undefined ||\n !Arr.isFixedLengthArray(2, recordTypeArgs)\n ) {\n return;\n }\n\n const [K, V] = recordTypeArgs;\n\n mut_rewrites.push({\n node,\n canonicalName: MUTABLE_RECORD_TYPE_NAME,\n typeArgs: `${sourceCode.getText(K)}, ${sourceCode.getText(V)}`,\n });\n },\n 'Program:exit': () => {\n reportTypeRewrites(\n context,\n mut_rewrites,\n 'useMutableRecord',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-readonly-or-mutable-record.mjs","sources":["../../src/rules/prefer-readonly-or-mutable-record.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n reportStdTypeReference,\n type StdTypeAlternatives,\n} from './std-type-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useReadonlyOrMutableRecord' | 'replaceWith';\n\n/**\n * `Record` says nothing about whether the properties may be reassigned, so the\n * mutability of every record type spelled with it is decided by whoever reads\n * it. The ts-type-forge pair spells it out.\n */\nconst ALTERNATIVES: StdTypeAlternatives = new Map([\n ['Record', ['ReadonlyRecord', 'MutableRecord']],\n]);\n\nexport const preferReadonlyOrMutableRecord: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace the built-in `Record` with the ts-type-forge `ReadonlyRecord` or `MutableRecord`, so that the mutability of the record is stated rather than implied.',\n },\n hasSuggestions: true,\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useReadonlyOrMutableRecord:\n 'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',\n replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n return {\n TSTypeReference: (node) => {\n reportStdTypeReference(\n context,\n node,\n ALTERNATIVES,\n {\n report: 'useReadonlyOrMutableRecord',\n suggestion: 'replaceWith',\n },\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":";;;AAuBA,MAAM,YAAA,uBAAwC,GAAA,CAAI;AAAA,EAChD,CAAC,QAAA,EAAU,CAAC,gBAAA,EAAkB,eAAe,CAAC;AAChD,CAAC,CAAA;AAEM,MAAM,6BAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,cAAA,EAAgB,IAAA;AAAA,IAChB,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa;AAAA,SACf;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,0BAAA,EACE,6EAAA;AAAA,MACF,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAG,WAAA,IAAe,oBAAA;AAEvD,IAAA,OAAO;AAAA,MACL,eAAA,EAAiB,CAAC,IAAA,KAAS;AACzB,QAAA,sBAAA;AAAA,UACE,OAAA;AAAA,UACA,IAAA;AAAA,UACA,YAAA;AAAA,UACA;AAAA,YACE,MAAA,EAAQ,4BAAA;AAAA,YACR,UAAA,EAAY;AAAA,WACd;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,WAAA,EAAa,sBAAsB;AACxD
|
|
1
|
+
{"version":3,"file":"prefer-readonly-or-mutable-record.mjs","sources":["../../src/rules/prefer-readonly-or-mutable-record.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n reportStdTypeReference,\n type StdTypeAlternatives,\n} from './std-type-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useReadonlyOrMutableRecord' | 'replaceWith';\n\n/**\n * `Record` says nothing about whether the properties may be reassigned, so the\n * mutability of every record type spelled with it is decided by whoever reads\n * it. The ts-type-forge pair spells it out.\n */\nconst ALTERNATIVES: StdTypeAlternatives = new Map([\n ['Record', ['ReadonlyRecord', 'MutableRecord']],\n]);\n\nexport const preferReadonlyOrMutableRecord: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace the built-in `Record` with the ts-type-forge `ReadonlyRecord` or `MutableRecord`, so that the mutability of the record is stated rather than implied.',\n },\n hasSuggestions: true,\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useReadonlyOrMutableRecord:\n 'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',\n replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n return {\n TSTypeReference: (node) => {\n reportStdTypeReference(\n context,\n node,\n ALTERNATIVES,\n {\n report: 'useReadonlyOrMutableRecord',\n suggestion: 'replaceWith',\n },\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":";;;AAuBA,MAAM,YAAA,uBAAwC,GAAA,CAAI;AAAA,EAChD,CAAC,QAAA,EAAU,CAAC,gBAAA,EAAkB,eAAe,CAAC;AAChD,CAAC,CAAA;AAEM,MAAM,6BAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,cAAA,EAAgB,IAAA;AAAA,IAChB,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa;AAAA,SACf;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,0BAAA,EACE,6EAAA;AAAA,MACF,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAG,WAAA,IAAe,oBAAA;AAEvD,IAAA,OAAO;AAAA,MACL,eAAA,EAAiB,CAAC,IAAA,KAAS;AACzB,QAAA,sBAAA;AAAA,UACE,OAAA;AAAA,UACA,IAAA;AAAA,UACA,YAAA;AAAA,UACA;AAAA,YACE,MAAA,EAAQ,4BAAA;AAAA,YACR,UAAA,EAAY;AAAA,WACd;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,WAAA,EAAa,sBAAsB;AACxD;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-strict-or-relaxed-utility-type.mjs","sources":["../../src/rules/prefer-strict-or-relaxed-utility-type.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n reportStdTypeReference,\n type StdTypeAlternatives,\n} from './std-type-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useStrictOrRelaxed' | 'replaceWith';\n\n/**\n * The standard-library types whose second argument is unchecked, and the\n * ts-type-forge pair that makes the choice explicit.\n *\n * `Strict*` constrains the second argument (`U extends T` / `K extends keyof T`),\n * so a key that does not exist — typically a leftover after a rename — is a\n * compile error instead of a silently empty result. `Relaxed*` keeps the\n * standard-library behavior for the cases that genuinely need it (a subtrahend\n * that is not provably part of the union, a key set computed from a still\n * deferred type parameter).\n */\nconst ALTERNATIVES: StdTypeAlternatives = new Map([\n ['Exclude', ['StrictExclude', 'RelaxedExclude']],\n ['Extract', ['StrictExtract', 'RelaxedExtract']],\n ['Omit', ['StrictOmit', 'RelaxedOmit']],\n ['Pick', ['StrictPick', 'RelaxedPick']],\n]);\n\nexport const preferStrictOrRelaxedUtilityType: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace the built-in `Exclude` / `Extract` / `Omit` / `Pick` with the ts-type-forge `Strict*` or `Relaxed*` counterpart, so that the unchecked second argument becomes an explicit choice.',\n },\n hasSuggestions: true,\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useStrictOrRelaxed:\n 'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',\n replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n return {\n TSTypeReference: (node) => {\n reportStdTypeReference(\n context,\n node,\n ALTERNATIVES,\n { report: 'useStrictOrRelaxed', suggestion: 'replaceWith' },\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":";;;AA6BA,MAAM,YAAA,uBAAwC,GAAA,CAAI;AAAA,EAChD,CAAC,SAAA,EAAW,CAAC,eAAA,EAAiB,gBAAgB,CAAC,CAAA;AAAA,EAC/C,CAAC,SAAA,EAAW,CAAC,eAAA,EAAiB,gBAAgB,CAAC,CAAA;AAAA,EAC/C,CAAC,MAAA,EAAQ,CAAC,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,EACtC,CAAC,MAAA,EAAQ,CAAC,YAAA,EAAc,aAAa,CAAC;AACxC,CAAC,CAAA;AAEM,MAAM,gCAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,cAAA,EAAgB,IAAA;AAAA,IAChB,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa;AAAA,SACf;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,kBAAA,EACE,6EAAA;AAAA,MACF,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAG,WAAA,IAAe,oBAAA;AAEvD,IAAA,OAAO;AAAA,MACL,eAAA,EAAiB,CAAC,IAAA,KAAS;AACzB,QAAA,sBAAA;AAAA,UACE,OAAA;AAAA,UACA,IAAA;AAAA,UACA,YAAA;AAAA,UACA,EAAE,MAAA,EAAQ,oBAAA,EAAsB,UAAA,EAAY,aAAA,EAAc;AAAA,UAC1D;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,WAAA,EAAa,sBAAsB;AACxD
|
|
1
|
+
{"version":3,"file":"prefer-strict-or-relaxed-utility-type.mjs","sources":["../../src/rules/prefer-strict-or-relaxed-utility-type.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n reportStdTypeReference,\n type StdTypeAlternatives,\n} from './std-type-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useStrictOrRelaxed' | 'replaceWith';\n\n/**\n * The standard-library types whose second argument is unchecked, and the\n * ts-type-forge pair that makes the choice explicit.\n *\n * `Strict*` constrains the second argument (`U extends T` / `K extends keyof T`),\n * so a key that does not exist — typically a leftover after a rename — is a\n * compile error instead of a silently empty result. `Relaxed*` keeps the\n * standard-library behavior for the cases that genuinely need it (a subtrahend\n * that is not provably part of the union, a key set computed from a still\n * deferred type parameter).\n */\nconst ALTERNATIVES: StdTypeAlternatives = new Map([\n ['Exclude', ['StrictExclude', 'RelaxedExclude']],\n ['Extract', ['StrictExtract', 'RelaxedExtract']],\n ['Omit', ['StrictOmit', 'RelaxedOmit']],\n ['Pick', ['StrictPick', 'RelaxedPick']],\n]);\n\nexport const preferStrictOrRelaxedUtilityType: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace the built-in `Exclude` / `Extract` / `Omit` / `Pick` with the ts-type-forge `Strict*` or `Relaxed*` counterpart, so that the unchecked second argument becomes an explicit choice.',\n },\n hasSuggestions: true,\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useStrictOrRelaxed:\n 'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',\n replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n return {\n TSTypeReference: (node) => {\n reportStdTypeReference(\n context,\n node,\n ALTERNATIVES,\n { report: 'useStrictOrRelaxed', suggestion: 'replaceWith' },\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":";;;AA6BA,MAAM,YAAA,uBAAwC,GAAA,CAAI;AAAA,EAChD,CAAC,SAAA,EAAW,CAAC,eAAA,EAAiB,gBAAgB,CAAC,CAAA;AAAA,EAC/C,CAAC,SAAA,EAAW,CAAC,eAAA,EAAiB,gBAAgB,CAAC,CAAA;AAAA,EAC/C,CAAC,MAAA,EAAQ,CAAC,YAAA,EAAc,aAAa,CAAC,CAAA;AAAA,EACtC,CAAC,MAAA,EAAQ,CAAC,YAAA,EAAc,aAAa,CAAC;AACxC,CAAC,CAAA;AAEM,MAAM,gCAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,cAAA,EAAgB,IAAA;AAAA,IAChB,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa;AAAA,SACf;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,kBAAA,EACE,6EAAA;AAAA,MACF,WAAA,EAAa;AAAA;AACf,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAG,WAAA,IAAe,oBAAA;AAEvD,IAAA,OAAO;AAAA,MACL,eAAA,EAAiB,CAAC,IAAA,KAAS;AACzB,QAAA,sBAAA;AAAA,UACE,OAAA;AAAA,UACA,IAAA;AAAA,UACA,YAAA;AAAA,UACA,EAAE,MAAA,EAAQ,oBAAA,EAAsB,UAAA,EAAY,aAAA,EAAc;AAAA,UAC1D;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB,CAAC,EAAE,WAAA,EAAa,sBAAsB;AACxD;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rewrite-rule-utils.mjs","sources":["../../src/rules/rewrite-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** One deterministic replacement of a type node by a named ts-type-forge type. */\nexport type TypeRewrite = Readonly<{\n node: TSESTree.Node;\n canonicalName: string;\n /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */\n typeArgs: string;\n}>;\n\n/**\n * Emits one report per rewrite, resolving each target name and attaching every\n * required import to the *first* report.\n *\n * All import fixes must live on a single report: `insertTextBefore(program, …)`\n * produces overlapping ranges, and ESLint merges each report's fixes into one\n * span — so spreading them across reports makes the fixes conflict and only one\n * survives per pass.\n */\nexport const reportTypeRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TypeRewrite[],\n messageId: MessageIds,\n importStyle: ImportStyle,\n): void => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const resolved = rewrites.flatMap((rewrite) => {\n const target = resolveTypeName(program, rewrite.canonicalName, importStyle);\n\n // The name is already bound to something else in this file, so the rewrite\n // would resolve to the wrong declaration.\n return target === undefined ? [] : [{ rewrite, target }];\n });\n\n const importNames = Array.from(\n new Set(\n resolved\n .filter(({ target }) => target.needsImport)\n .map(({ rewrite }) => rewrite.canonicalName),\n ),\n );\n\n for (const [index, { rewrite, target }] of resolved.entries()) {\n const replacement = `${target.localName}<${rewrite.typeArgs}>`;\n\n context.report({\n node: rewrite.node,\n messageId,\n data: { original: sourceCode.getText(rewrite.node), replacement },\n fix: (fixer) =>\n Arr.toPushed(\n index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : [],\n fixer.replaceText(rewrite.node, replacement),\n ),\n });\n }\n};\n"],"names":[],"mappings":";;;;AAwBO,MAAM,kBAAA,GAAqB,CAChC,OAAA,EACA,QAAA,EACA,WACA,WAAA,KACS;AACT,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,EAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC7C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,eAAe,WAAW,CAAA;AAI1E,IAAA,OAAO,MAAA,KAAW,SAAY,EAAC,GAAI,CAAC,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA;AAAA,IACxB,IAAI,GAAA;AAAA,MACF,QAAA,CACG,MAAA,CAAO,CAAC,EAAE,QAAO,KAAM,MAAA,CAAO,WAAW,CAAA,CACzC,IAAI,CAAC,EAAE,OAAA,EAAQ,KAAM,QAAQ,aAAa;AAAA;AAC/C,GACF;AAEA,EAAA,KAAA,MAAW,CAAC,OAAO,EAAE,OAAA,EAAS,QAAQ,CAAA,IAAK,QAAA,CAAS,OAAA,EAAQ,EAAG;AAC7D,IAAA,MAAM,cAAc,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,CAAA,EAAI,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAE3D,IAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,MACb,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA;AAAA,MACA,IAAA,EAAM,EAAE,QAAA,EAAU,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAI,GAAG,WAAA,EAAY;AAAA,MAChE,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,QACF,KAAA,KAAU,IACN,WAAA,CAAY,OAAA;AAAA,UAAQ,CAAC,IAAA,KACnB,kBAAA,CAAmB,KAAA,EAAO,SAAS,IAAI;AAAA,YAEzC,EAAC;AAAA,QACL,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,WAAW;AAAA;AAC7C,KACH,CAAA;AAAA,EACH;AACF
|
|
1
|
+
{"version":3,"file":"rewrite-rule-utils.mjs","sources":["../../src/rules/rewrite-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** One deterministic replacement of a type node by a named ts-type-forge type. */\nexport type TypeRewrite = Readonly<{\n node: TSESTree.Node;\n canonicalName: string;\n /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */\n typeArgs: string;\n}>;\n\n/**\n * Emits one report per rewrite, resolving each target name and attaching every\n * required import to the *first* report.\n *\n * All import fixes must live on a single report: `insertTextBefore(program, …)`\n * produces overlapping ranges, and ESLint merges each report's fixes into one\n * span — so spreading them across reports makes the fixes conflict and only one\n * survives per pass.\n */\nexport const reportTypeRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TypeRewrite[],\n messageId: MessageIds,\n importStyle: ImportStyle,\n): void => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const resolved = rewrites.flatMap((rewrite) => {\n const target = resolveTypeName(program, rewrite.canonicalName, importStyle);\n\n // The name is already bound to something else in this file, so the rewrite\n // would resolve to the wrong declaration.\n return target === undefined ? [] : [{ rewrite, target }];\n });\n\n const importNames = Array.from(\n new Set(\n resolved\n .filter(({ target }) => target.needsImport)\n .map(({ rewrite }) => rewrite.canonicalName),\n ),\n );\n\n for (const [index, { rewrite, target }] of resolved.entries()) {\n const replacement = `${target.localName}<${rewrite.typeArgs}>`;\n\n context.report({\n node: rewrite.node,\n messageId,\n data: { original: sourceCode.getText(rewrite.node), replacement },\n fix: (fixer) =>\n Arr.toPushed(\n index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : [],\n fixer.replaceText(rewrite.node, replacement),\n ),\n });\n }\n};\n"],"names":[],"mappings":";;;;AAwBO,MAAM,kBAAA,GAAqB,CAChC,OAAA,EACA,QAAA,EACA,WACA,WAAA,KACS;AACT,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,EAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC7C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,eAAe,WAAW,CAAA;AAI1E,IAAA,OAAO,MAAA,KAAW,SAAY,EAAC,GAAI,CAAC,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA;AAAA,IACxB,IAAI,GAAA;AAAA,MACF,QAAA,CACG,MAAA,CAAO,CAAC,EAAE,QAAO,KAAM,MAAA,CAAO,WAAW,CAAA,CACzC,IAAI,CAAC,EAAE,OAAA,EAAQ,KAAM,QAAQ,aAAa;AAAA;AAC/C,GACF;AAEA,EAAA,KAAA,MAAW,CAAC,OAAO,EAAE,OAAA,EAAS,QAAQ,CAAA,IAAK,QAAA,CAAS,OAAA,EAAQ,EAAG;AAC7D,IAAA,MAAM,cAAc,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,CAAA,EAAI,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAE3D,IAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,MACb,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA;AAAA,MACA,IAAA,EAAM,EAAE,QAAA,EAAU,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAI,GAAG,WAAA,EAAY;AAAA,MAChE,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,QACF,KAAA,KAAU,IACN,WAAA,CAAY,OAAA;AAAA,UAAQ,CAAC,IAAA,KACnB,kBAAA,CAAmB,KAAA,EAAO,SAAS,IAAI;AAAA,YAEzC,EAAC;AAAA,QACL,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,WAAW;AAAA;AAC7C,KACH,CAAA;AAAA,EACH;AACF;;"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const tsTypeForgeRules: {
|
|
2
|
+
readonly 'no-side-effect-import': import("@typescript-eslint/utils/ts-eslint").RuleModule<"removeSideEffectImport", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
2
3
|
readonly 'prefer-canonical-length-constrained-tuple': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalTuple", readonly [(Readonly<{
|
|
3
4
|
importStyle?: 'global' | 'named';
|
|
4
5
|
maxLength?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAOA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;CAOa,CAAC"}
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { noSideEffectImport } from './no-side-effect-import.mjs';
|
|
1
2
|
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
2
3
|
import { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';
|
|
3
4
|
import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
|
|
4
5
|
import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
|
|
5
6
|
|
|
6
7
|
const tsTypeForgeRules = {
|
|
8
|
+
"no-side-effect-import": noSideEffectImport,
|
|
7
9
|
"prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple,
|
|
8
10
|
"prefer-canonical-mutable-record": preferCanonicalMutableRecord,
|
|
9
11
|
"prefer-readonly-or-mutable-record": preferReadonlyOrMutableRecord,
|
package/dist/rules/rules.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';\nimport { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';\nimport { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';\nimport { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n 'prefer-canonical-mutable-record': preferCanonicalMutableRecord,\n 'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,\n 'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { noSideEffectImport } from './no-side-effect-import.mjs';\nimport { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';\nimport { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';\nimport { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';\nimport { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';\n\nexport const tsTypeForgeRules = {\n 'no-side-effect-import': noSideEffectImport,\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n 'prefer-canonical-mutable-record': preferCanonicalMutableRecord,\n 'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,\n 'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;;;;;AAOO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,uBAAA,EAAyB,kBAAA;AAAA,EACzB,2CAAA,EACE,qCAAA;AAAA,EACF,iCAAA,EAAmC,4BAAA;AAAA,EACnC,mCAAA,EAAqC,6BAAA;AAAA,EACrC,uCAAA,EAAyC;AAC3C;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"std-type-rule-utils.mjs","sources":["../../src/rules/std-type-rule-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { isLocallyBound } from './ast-utils.mjs';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * The ts-type-forge replacements offered for one standard-library type, in the\n * order they are suggested.\n */\nexport type StdTypeAlternatives = ReadonlyMap<string, readonly string[]>;\n\n/**\n * Reports a reference to a banned standard-library type and offers one\n * suggestion per ts-type-forge alternative.\n *\n * Choosing between the alternatives is a semantic decision — `StrictExclude`\n * additionally checks that the subtrahend is part of the union, `ReadonlyRecord`\n * and `MutableRecord` describe different types — so this is deliberately *not*\n * an autofix. Each suggestion carries its own import fix, which is safe because\n * ESLint applies suggestions one at a time.\n */\nexport const reportStdTypeReference = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n node: TSESTree.TSTypeReference,\n alternatives: StdTypeAlternatives,\n messageIds: Readonly<{ report: MessageIds; suggestion: MessageIds }>,\n importStyle: ImportStyle,\n): void => {\n const typeName = node.typeName;\n\n // `ns.Pick` / `import('…').Pick` never denote the standard-library type.\n if (typeName.type !== AST_NODE_TYPES.Identifier) return;\n\n const stdName = typeName.name;\n\n const candidates = alternatives.get(stdName);\n\n if (candidates === undefined) return;\n\n // A file that declares — or imports — its own `Pick` does not mean the\n // standard-library one, so there is nothing to steer.\n if (isLocallyBound(context.sourceCode, node, stdName)) return;\n\n const program = context.sourceCode.ast;\n\n context.report({\n node: typeName,\n messageId: messageIds.report,\n data: { name: stdName, alternatives: formatAlternatives(candidates) },\n suggest: candidates.flatMap((candidate) => {\n const target = resolveTypeName(program, candidate, importStyle);\n\n // The replacement name is already bound to something else in this file,\n // so rewriting to it would resolve to the wrong declaration.\n return target === undefined\n ? []\n : [\n {\n messageId: messageIds.suggestion,\n data: { name: stdName, replacement: target.localName },\n fix: (fixer: TSESLint.RuleFixer): readonly TSESLint.RuleFix[] =>\n Arr.toPushed(\n target.needsImport\n ? buildTypeImportFix(fixer, program, candidate)\n : [],\n fixer.replaceText(typeName, target.localName),\n ),\n },\n ];\n }),\n });\n};\n\n/** `['StrictPick', 'RelaxedPick']` → `` '`StrictPick` or `RelaxedPick`' ``. */\nconst formatAlternatives = (candidates: readonly string[]): string =>\n candidates.map((candidate) => `\\`${candidate}\\``).join(' or ');\n"],"names":[],"mappings":";;;;;;AA4BO,MAAM,yBAAyB,CACpC,OAAA,EACA,IAAA,EACA,YAAA,EACA,YACA,WAAA,KACS;AACT,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AAGtB,EAAA,IAAI,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,UAAA,EAAY;AAEjD,EAAA,MAAM,UAAU,QAAA,CAAS,IAAA;AAEzB,EAAA,MAAM,UAAA,GAAa,YAAA,CAAa,GAAA,CAAI,OAAO,CAAA;AAE3C,EAAA,IAAI,eAAe,MAAA,EAAW;AAI9B,EAAA,IAAI,cAAA,CAAe,OAAA,CAAQ,UAAA,EAAY,IAAA,EAAM,OAAO,CAAA,EAAG;AAEvD,EAAA,MAAM,OAAA,GAAU,QAAQ,UAAA,CAAW,GAAA;AAEnC,EAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,IACb,IAAA,EAAM,QAAA;AAAA,IACN,WAAW,UAAA,CAAW,MAAA;AAAA,IACtB,MAAM,EAAE,IAAA,EAAM,SAAS,YAAA,EAAc,kBAAA,CAAmB,UAAU,CAAA,EAAE;AAAA,IACpE,OAAA,EAAS,UAAA,CAAW,OAAA,CAAQ,CAAC,SAAA,KAAc;AACzC,MAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,SAAA,EAAW,WAAW,CAAA;AAI9D,MAAA,OAAO,MAAA,KAAW,MAAA,GACd,EAAC,GACD;AAAA,QACE;AAAA,UACE,WAAW,UAAA,CAAW,UAAA;AAAA,UACtB,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAAA,UACrD,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,YACF,OAAO,WAAA,GACH,kBAAA,CAAmB,OAAO,OAAA,EAAS,SAAS,IAC5C,EAAC;AAAA,YACL,KAAA,CAAM,WAAA,CAAY,QAAA,EAAU,MAAA,CAAO,SAAS;AAAA;AAC9C;AACJ,OACF;AAAA,IACN,CAAC;AAAA,GACF,CAAA;AACH;AAGA,MAAM,kBAAA,GAAqB,CAAC,UAAA,KAC1B,UAAA,CAAW,GAAA,CAAI,CAAC,SAAA,KAAc,CAAA,EAAA,EAAK,SAAS,CAAA,EAAA,CAAI,CAAA,CAAE,IAAA,CAAK,MAAM,CAAA
|
|
1
|
+
{"version":3,"file":"std-type-rule-utils.mjs","sources":["../../src/rules/std-type-rule-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { isLocallyBound } from './ast-utils.mjs';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * The ts-type-forge replacements offered for one standard-library type, in the\n * order they are suggested.\n */\nexport type StdTypeAlternatives = ReadonlyMap<string, readonly string[]>;\n\n/**\n * Reports a reference to a banned standard-library type and offers one\n * suggestion per ts-type-forge alternative.\n *\n * Choosing between the alternatives is a semantic decision — `StrictExclude`\n * additionally checks that the subtrahend is part of the union, `ReadonlyRecord`\n * and `MutableRecord` describe different types — so this is deliberately *not*\n * an autofix. Each suggestion carries its own import fix, which is safe because\n * ESLint applies suggestions one at a time.\n */\nexport const reportStdTypeReference = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n node: TSESTree.TSTypeReference,\n alternatives: StdTypeAlternatives,\n messageIds: Readonly<{ report: MessageIds; suggestion: MessageIds }>,\n importStyle: ImportStyle,\n): void => {\n const typeName = node.typeName;\n\n // `ns.Pick` / `import('…').Pick` never denote the standard-library type.\n if (typeName.type !== AST_NODE_TYPES.Identifier) return;\n\n const stdName = typeName.name;\n\n const candidates = alternatives.get(stdName);\n\n if (candidates === undefined) return;\n\n // A file that declares — or imports — its own `Pick` does not mean the\n // standard-library one, so there is nothing to steer.\n if (isLocallyBound(context.sourceCode, node, stdName)) return;\n\n const program = context.sourceCode.ast;\n\n context.report({\n node: typeName,\n messageId: messageIds.report,\n data: { name: stdName, alternatives: formatAlternatives(candidates) },\n suggest: candidates.flatMap((candidate) => {\n const target = resolveTypeName(program, candidate, importStyle);\n\n // The replacement name is already bound to something else in this file,\n // so rewriting to it would resolve to the wrong declaration.\n return target === undefined\n ? []\n : [\n {\n messageId: messageIds.suggestion,\n data: { name: stdName, replacement: target.localName },\n fix: (fixer: TSESLint.RuleFixer): readonly TSESLint.RuleFix[] =>\n Arr.toPushed(\n target.needsImport\n ? buildTypeImportFix(fixer, program, candidate)\n : [],\n fixer.replaceText(typeName, target.localName),\n ),\n },\n ];\n }),\n });\n};\n\n/** `['StrictPick', 'RelaxedPick']` → `` '`StrictPick` or `RelaxedPick`' ``. */\nconst formatAlternatives = (candidates: readonly string[]): string =>\n candidates.map((candidate) => `\\`${candidate}\\``).join(' or ');\n"],"names":[],"mappings":";;;;;;AA4BO,MAAM,yBAAyB,CACpC,OAAA,EACA,IAAA,EACA,YAAA,EACA,YACA,WAAA,KACS;AACT,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AAGtB,EAAA,IAAI,QAAA,CAAS,IAAA,KAAS,cAAA,CAAe,UAAA,EAAY;AAEjD,EAAA,MAAM,UAAU,QAAA,CAAS,IAAA;AAEzB,EAAA,MAAM,UAAA,GAAa,YAAA,CAAa,GAAA,CAAI,OAAO,CAAA;AAE3C,EAAA,IAAI,eAAe,MAAA,EAAW;AAI9B,EAAA,IAAI,cAAA,CAAe,OAAA,CAAQ,UAAA,EAAY,IAAA,EAAM,OAAO,CAAA,EAAG;AAEvD,EAAA,MAAM,OAAA,GAAU,QAAQ,UAAA,CAAW,GAAA;AAEnC,EAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,IACb,IAAA,EAAM,QAAA;AAAA,IACN,WAAW,UAAA,CAAW,MAAA;AAAA,IACtB,MAAM,EAAE,IAAA,EAAM,SAAS,YAAA,EAAc,kBAAA,CAAmB,UAAU,CAAA,EAAE;AAAA,IACpE,OAAA,EAAS,UAAA,CAAW,OAAA,CAAQ,CAAC,SAAA,KAAc;AACzC,MAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,SAAA,EAAW,WAAW,CAAA;AAI9D,MAAA,OAAO,MAAA,KAAW,MAAA,GACd,EAAC,GACD;AAAA,QACE;AAAA,UACE,WAAW,UAAA,CAAW,UAAA;AAAA,UACtB,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAAA,UACrD,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,YACF,OAAO,WAAA,GACH,kBAAA,CAAmB,OAAO,OAAA,EAAS,SAAS,IAC5C,EAAC;AAAA,YACL,KAAA,CAAM,WAAA,CAAY,QAAA,EAAU,MAAA,CAAO,SAAS;AAAA;AAC9C;AACJ,OACF;AAAA,IACN,CAAC;AAAA,GACF,CAAA;AACH;AAGA,MAAM,kBAAA,GAAqB,CAAC,UAAA,KAC1B,UAAA,CAAW,GAAA,CAAI,CAAC,SAAA,KAAc,CAAA,EAAA,EAAK,SAAS,CAAA,EAAA,CAAI,CAAA,CAAE,IAAA,CAAK,MAAM,CAAA;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ts-type-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer type declarations toward ts-type-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@typescript-eslint/parser": "8.68.0",
|
|
44
44
|
"@typescript-eslint/rule-tester": "8.68.0",
|
|
45
45
|
"dedent": "1.7.2",
|
|
46
|
-
"eslint-config-typed": "5.10.
|
|
47
|
-
"eslint-plugin-ts-data-forge": "0.
|
|
48
|
-
"eslint-plugin-ts-fortress": "1.0
|
|
46
|
+
"eslint-config-typed": "5.10.4",
|
|
47
|
+
"eslint-plugin-ts-data-forge": "0.6.0",
|
|
48
|
+
"eslint-plugin-ts-fortress": "1.1.0",
|
|
49
49
|
"prettier": "3.9.6",
|
|
50
50
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
51
51
|
"prettier-plugin-packagejson": "3.0.2",
|
|
52
|
-
"rollup": "4.
|
|
52
|
+
"rollup": "4.63.0",
|
|
53
53
|
"ts-repo-utils": "10.4.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
package/src/plugin.mts
CHANGED
|
@@ -8,6 +8,7 @@ import { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
|
|
|
8
8
|
* a rule without listing it here fails to type-check.
|
|
9
9
|
*/
|
|
10
10
|
const recommendedRules = {
|
|
11
|
+
'ts-type-forge/no-side-effect-import': 'error',
|
|
11
12
|
'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',
|
|
12
13
|
'ts-type-forge/prefer-canonical-mutable-record': 'error',
|
|
13
14
|
'ts-type-forge/prefer-readonly-or-mutable-record': 'error',
|
package/src/rule-types.mts
CHANGED
|
@@ -27,6 +27,21 @@ type RuleOptionsOf<Name extends keyof typeof tsTypeForgeRules> =
|
|
|
27
27
|
? Options
|
|
28
28
|
: never;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @description Remove a side-effect-only `import 'ts-type-forge';`, which binds no name and has no runtime entry point to resolve to. `ts-type-forge/global` is left alone.
|
|
32
|
+
*
|
|
33
|
+
* ```md
|
|
34
|
+
* | key | value |
|
|
35
|
+
* | :--------- | :------ |
|
|
36
|
+
* | type | problem |
|
|
37
|
+
* | deprecated | false |
|
|
38
|
+
* | fixable | code |
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
namespace NoSideEffectImport {
|
|
42
|
+
export type RuleEntry = Linter.StringSeverity;
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
46
|
* @description Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.
|
|
32
47
|
*
|
|
@@ -209,6 +224,7 @@ namespace PreferStrictOrRelaxedUtilityType {
|
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
export type EslintTsTypeForgeRules = Readonly<{
|
|
227
|
+
'ts-type-forge/no-side-effect-import': NoSideEffectImport.RuleEntry;
|
|
212
228
|
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
|
|
213
229
|
'ts-type-forge/prefer-canonical-mutable-record': PreferCanonicalMutableRecord.RuleEntry;
|
|
214
230
|
'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.RuleEntry;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
4
|
+
|
|
5
|
+
type Options = readonly [];
|
|
6
|
+
|
|
7
|
+
type MessageIds = 'removeSideEffectImport';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `import 'ts-type-forge';` binds nothing, and there is nothing for it to run:
|
|
11
|
+
* the package ships declarations only, and its `exports` map offers no runtime
|
|
12
|
+
* condition at all, so the import fails to resolve the moment the module graph
|
|
13
|
+
* is loaded. TypeScript keeps a side-effect import in the emitted JavaScript —
|
|
14
|
+
* that is the one kind it never elides — so this is a runtime error waiting in
|
|
15
|
+
* a file that type-checks.
|
|
16
|
+
*
|
|
17
|
+
* The ambient globals are a separate entry point, `ts-type-forge/global`,
|
|
18
|
+
* usually brought in through `compilerOptions.types` or a triple-slash
|
|
19
|
+
* reference. This rule matches the bare specifier exactly, so that subpath is
|
|
20
|
+
* left alone.
|
|
21
|
+
*/
|
|
22
|
+
export const noSideEffectImport: TSESLint.RuleModule<MessageIds, Options> = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
description: `Remove a side-effect-only \`import '${TS_TYPE_FORGE_MODULE}';\`, which binds no name and has no runtime entry point to resolve to. \`${TS_TYPE_FORGE_MODULE}/global\` is left alone.`,
|
|
27
|
+
},
|
|
28
|
+
fixable: 'code',
|
|
29
|
+
schema: [],
|
|
30
|
+
messages: {
|
|
31
|
+
removeSideEffectImport: `\`import '${TS_TYPE_FORGE_MODULE}';\` binds no name, and \`${TS_TYPE_FORGE_MODULE}\` ships types only — it has no runtime entry point, so the import throws when the module is loaded. Remove it; the ambient globals come from \`${TS_TYPE_FORGE_MODULE}/global\`, not from this.`,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create: (context) => ({
|
|
36
|
+
ImportDeclaration: (node) => {
|
|
37
|
+
if (
|
|
38
|
+
node.source.value !== TS_TYPE_FORGE_MODULE ||
|
|
39
|
+
!Arr.isEmpty(node.specifiers)
|
|
40
|
+
) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
messageId: 'removeSideEffectImport',
|
|
47
|
+
fix: (fixer) => removeStatement(fixer, context.sourceCode, node),
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
defaultOptions: [],
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Removes a statement along with the rest of its line, so that deleting it does
|
|
58
|
+
* not leave a blank line where it was.
|
|
59
|
+
*/
|
|
60
|
+
const removeStatement = (
|
|
61
|
+
fixer: TSESLint.RuleFixer,
|
|
62
|
+
sourceCode: TSESLint.SourceCode,
|
|
63
|
+
node: TSESTree.Node,
|
|
64
|
+
): TSESLint.RuleFix => {
|
|
65
|
+
const restOfLine = /^[^\S\n]*\n/u.exec(
|
|
66
|
+
sourceCode.getText().slice(node.range[1]),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return fixer.removeRange([
|
|
70
|
+
node.range[0],
|
|
71
|
+
node.range[1] + (restOfLine?.[0].length ?? 0),
|
|
72
|
+
]);
|
|
73
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { noSideEffectImport } from './no-side-effect-import.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('no-side-effect-import', () => {
|
|
14
|
+
tester.run('no-side-effect-import', noSideEffectImport, {
|
|
15
|
+
valid: [
|
|
16
|
+
{
|
|
17
|
+
name: 'accepts a named import',
|
|
18
|
+
code: dedent`
|
|
19
|
+
import { type NonEmptyArray } from 'ts-type-forge';
|
|
20
|
+
|
|
21
|
+
export type Names = NonEmptyArray<string>;
|
|
22
|
+
`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'leaves the ambient globals entry point alone',
|
|
26
|
+
code: dedent`
|
|
27
|
+
import 'ts-type-forge/global';
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'leaves other side-effect imports alone',
|
|
32
|
+
code: dedent`
|
|
33
|
+
import './polyfills.mjs';
|
|
34
|
+
import 'ts-type-forge-extras';
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
invalid: [
|
|
39
|
+
{
|
|
40
|
+
name: 'removes a side-effect-only import',
|
|
41
|
+
code: dedent`
|
|
42
|
+
import 'ts-type-forge';
|
|
43
|
+
`,
|
|
44
|
+
output: '',
|
|
45
|
+
errors: [{ messageId: 'removeSideEffectImport' }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'removes it along with its line',
|
|
49
|
+
code: dedent`
|
|
50
|
+
import 'ts-type-forge';
|
|
51
|
+
import { type Uint32 } from 'ts-type-forge';
|
|
52
|
+
|
|
53
|
+
export type Size = Uint32;
|
|
54
|
+
`,
|
|
55
|
+
output: dedent`
|
|
56
|
+
import { type Uint32 } from 'ts-type-forge';
|
|
57
|
+
|
|
58
|
+
export type Size = Uint32;
|
|
59
|
+
`,
|
|
60
|
+
errors: [{ messageId: 'removeSideEffectImport' }],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'removes every side-effect-only import',
|
|
64
|
+
code: dedent`
|
|
65
|
+
import 'ts-type-forge';
|
|
66
|
+
import 'ts-type-forge/global';
|
|
67
|
+
import 'ts-type-forge';
|
|
68
|
+
|
|
69
|
+
export type Size = number;
|
|
70
|
+
`,
|
|
71
|
+
output: dedent`
|
|
72
|
+
import 'ts-type-forge/global';
|
|
73
|
+
|
|
74
|
+
export type Size = number;
|
|
75
|
+
`,
|
|
76
|
+
errors: [
|
|
77
|
+
{ messageId: 'removeSideEffectImport' },
|
|
78
|
+
{ messageId: 'removeSideEffectImport' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/rules/rules.mts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ESLintPlugin } from '../types.mjs';
|
|
2
|
+
import { noSideEffectImport } from './no-side-effect-import.mjs';
|
|
2
3
|
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
3
4
|
import { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';
|
|
4
5
|
import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
|
|
5
6
|
import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
|
|
6
7
|
|
|
7
8
|
export const tsTypeForgeRules = {
|
|
9
|
+
'no-side-effect-import': noSideEffectImport,
|
|
8
10
|
'prefer-canonical-length-constrained-tuple':
|
|
9
11
|
preferCanonicalLengthConstrainedTuple,
|
|
10
12
|
'prefer-canonical-mutable-record': preferCanonicalMutableRecord,
|