eslint-plugin-ts-type-forge 0.4.2 → 0.5.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.
@@ -35,7 +35,7 @@ declare namespace PreferCanonicalLengthConstrainedTuple {
35
35
  * "global",
36
36
  * "named"
37
37
  * ],
38
- * "description": "How the ts-type-forge type is brought into scope. 'global' (default) assumes the ambient globals of `ts-type-forge/global` and never touches imports; 'named' makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet."
38
+ * "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
39
39
  * },
40
40
  * "maxLength": {
41
41
  * "type": "integer",
@@ -27,6 +27,8 @@ export type UniformTupleShape = Readonly<{
27
27
  * rewriting them would drop information.
28
28
  * - A tuple that is only a rest element (`[...V[]]`) is not a length
29
29
  * constraint at all.
30
+ * - A tuple inside the `extends` clause of a conditional type is a match
31
+ * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).
30
32
  */
31
33
  export declare const analyzeUniformTuple: (tuple: TSESTree.TSTupleType, sourceCode: TSESLint.SourceCode) => UniformTupleShape | undefined;
32
34
  //# sourceMappingURL=ast-utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAIlC,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,+EAA+E;IAC/E,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,UACvB,QAAQ,CAAC,WAAW,cACf,QAAQ,CAAC,UAAU,KAC9B,iBAAiB,GAAG,SA0DtB,CAAC"}
1
+ {"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,+EAA+E;IAC/E,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,UACvB,QAAQ,CAAC,WAAW,cACf,QAAQ,CAAC,UAAU,KAC9B,iBAAiB,GAAG,SA4DtB,CAAC"}
@@ -1,13 +1,15 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
2
3
 
3
4
  const analyzeUniformTuple = (tuple, sourceCode) => {
5
+ if (isInConditionalTypeExtendsClause(tuple)) return void 0;
4
6
  const { parent } = tuple;
5
7
  const isReadonly = parent.type === AST_NODE_TYPES.TSTypeOperator && parent.operator === "readonly";
6
8
  const elements = tuple.elementTypes;
7
9
  const last = elements.at(-1);
8
10
  const hasRest = last?.type === AST_NODE_TYPES.TSRestType;
9
- const leading = hasRest ? elements.slice(0, -1) : elements;
10
- if (leading.length === 0 || leading.some(
11
+ const leading = hasRest ? Arr.butLast(elements) : elements;
12
+ if (Arr.isEmpty(leading) || leading.some(
11
13
  (element) => element.type === AST_NODE_TYPES.TSRestType || element.type === AST_NODE_TYPES.TSOptionalType || element.type === AST_NODE_TYPES.TSNamedTupleMember
12
14
  )) {
13
15
  return void 0;
@@ -19,7 +21,7 @@ const analyzeUniformTuple = (tuple, sourceCode) => {
19
21
  const elementText = sourceCode.getText(head);
20
22
  const expected = normalizeWhitespace(elementText);
21
23
  const others = [
22
- ...leading.slice(1),
24
+ ...Arr.tail(leading),
23
25
  ...restElementType === void 0 ? [] : [restElementType]
24
26
  ];
25
27
  return others.every(
@@ -32,6 +34,11 @@ const analyzeUniformTuple = (tuple, sourceCode) => {
32
34
  hasRest
33
35
  } : void 0;
34
36
  };
37
+ const isInConditionalTypeExtendsClause = (node) => {
38
+ if (node.type === AST_NODE_TYPES.Program) return false;
39
+ const { parent } = node;
40
+ return parent.type === AST_NODE_TYPES.TSConditionalType && parent.extendsType === node || isInConditionalTypeExtendsClause(parent);
41
+ };
35
42
  const getArrayElementType = (node) => {
36
43
  if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;
37
44
  return node.type === AST_NODE_TYPES.TSTypeOperator && node.operator === "readonly" && node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType ? node.typeAnnotation.elementType : void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\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 */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\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 ? elements.slice(0, -1) : elements;\n\n if (\n leading.length === 0 ||\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 ...leading.slice(1),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n return others.every(\n (element) => normalizeWhitespace(sourceCode.getText(element)) === expected,\n )\n ? {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n }\n : undefined;\n};\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\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":";;AAsCO,MAAM,mBAAA,GAAsB,CACjC,KAAA,EACA,UAAA,KACkC;AAClC,EAAA,MAAM,EAAE,QAAO,GAAI,KAAA;AAEnB,EAAA,MAAM,aACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cAAA,IAC/B,OAAO,QAAA,KAAa,UAAA;AAEtB,EAAA,MAAM,WAAW,KAAA,CAAM,YAAA;AAEvB,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,EAAA,CAAG,EAAE,CAAA;AAE3B,EAAA,MAAM,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,cAAA,CAAe,UAAA;AAE9C,EAAA,MAAM,UAAU,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,QAAA;AAElD,EAAA,IACE,OAAA,CAAQ,MAAA,KAAW,CAAA,IACnB,OAAA,CAAQ,IAAA;AAAA,IACN,CAAC,OAAA,KACC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,UAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,cAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe;AAAA,GACpC,EACA;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,kBACJ,IAAA,KAAS,MAAA,IAAa,UAClB,mBAAA,CAAoB,IAAA,CAAK,cAAc,CAAA,GACvC,MAAA;AAEN,EAAA,IAAI,OAAA,IAAW,eAAA,KAAoB,MAAA,EAAW,OAAO,MAAA;AAErD,EAAA,MAAM,CAAC,IAAI,CAAA,GAAI,OAAA;AAEf,EAAA,IAAI,IAAA,KAAS,QAAW,OAAO,MAAA;AAE/B,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,OAAA,CAAQ,IAAI,CAAA;AAE3C,EAAA,MAAM,QAAA,GAAW,oBAAoB,WAAW,CAAA;AAEhD,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,GAAG,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAA;AAAA,IAClB,GAAI,eAAA,KAAoB,MAAA,GAAY,EAAC,GAAI,CAAC,eAAe;AAAA,GAC3D;AAEA,EAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IACZ,CAAC,OAAA,KAAY,mBAAA,CAAoB,WAAW,OAAA,CAAQ,OAAO,CAAC,CAAA,KAAM;AAAA,GACpE,GACI;AAAA,IACE,IAAA,EAAM,aAAa,MAAA,GAAS,KAAA;AAAA,IAC5B,UAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAY,OAAA,CAAQ,MAAA;AAAA,IACpB;AAAA,GACF,GACA,MAAA;AACN;AAGA,MAAM,mBAAA,GAAsB,CAC1B,IAAA,KACkC;AAClC,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,WAAA,SAAoB,IAAA,CAAK,WAAA;AAE1D,EAAA,OAAO,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,cAAA,IAClC,KAAK,QAAA,KAAa,UAAA,IAClB,IAAA,CAAK,cAAA,EAAgB,IAAA,KAAS,cAAA,CAAe,WAAA,GAC3C,IAAA,CAAK,eAAe,WAAA,GACpB,MAAA;AACN,CAAA;AAEA,MAAM,mBAAA,GAAsB,CAAC,IAAA,KAC3B,IAAA,CAAK,WAAW,OAAA,EAAS,GAAG,EAAE,IAAA,EAAK;;;;"}
1
+ {"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-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';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\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 return others.every(\n (element) => normalizeWhitespace(sourceCode.getText(element)) === expected,\n )\n ? {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n }\n : undefined;\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/** 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":";;;AAyCO,MAAM,mBAAA,GAAsB,CACjC,KAAA,EACA,UAAA,KACkC;AAClC,EAAA,IAAI,gCAAA,CAAiC,KAAK,CAAA,EAAG,OAAO,MAAA;AAEpD,EAAA,MAAM,EAAE,QAAO,GAAI,KAAA;AAEnB,EAAA,MAAM,aACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cAAA,IAC/B,OAAO,QAAA,KAAa,UAAA;AAEtB,EAAA,MAAM,WAAW,KAAA,CAAM,YAAA;AAEvB,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,EAAA,CAAG,EAAE,CAAA;AAE3B,EAAA,MAAM,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,cAAA,CAAe,UAAA;AAE9C,EAAA,MAAM,OAAA,GAAU,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAA,GAAI,QAAA;AAElD,EAAA,IACE,GAAA,CAAI,OAAA,CAAQ,OAAO,CAAA,IACnB,OAAA,CAAQ,IAAA;AAAA,IACN,CAAC,OAAA,KACC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,UAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,cAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe;AAAA,GACpC,EACA;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,kBACJ,IAAA,KAAS,MAAA,IAAa,UAClB,mBAAA,CAAoB,IAAA,CAAK,cAAc,CAAA,GACvC,MAAA;AAEN,EAAA,IAAI,OAAA,IAAW,eAAA,KAAoB,MAAA,EAAW,OAAO,MAAA;AAErD,EAAA,MAAM,CAAC,IAAI,CAAA,GAAI,OAAA;AAEf,EAAA,IAAI,IAAA,KAAS,QAAW,OAAO,MAAA;AAE/B,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,OAAA,CAAQ,IAAI,CAAA;AAE3C,EAAA,MAAM,QAAA,GAAW,oBAAoB,WAAW,CAAA;AAEhD,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,GAAG,GAAA,CAAI,IAAA,CAAK,OAAO,CAAA;AAAA,IACnB,GAAI,eAAA,KAAoB,MAAA,GAAY,EAAC,GAAI,CAAC,eAAe;AAAA,GAC3D;AAEA,EAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IACZ,CAAC,OAAA,KAAY,mBAAA,CAAoB,WAAW,OAAA,CAAQ,OAAO,CAAC,CAAA,KAAM;AAAA,GACpE,GACI;AAAA,IACE,IAAA,EAAM,aAAa,MAAA,GAAS,KAAA;AAAA,IAC5B,UAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAY,OAAA,CAAQ,MAAA;AAAA,IACpB;AAAA,GACF,GACA,MAAA;AACN;AAaA,MAAM,gCAAA,GAAmC,CAAC,IAAA,KAAiC;AAEzE,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,OAAA,EAAS,OAAO,KAAA;AAEjD,EAAA,MAAM,EAAE,QAAO,GAAI,IAAA;AAEnB,EAAA,OACG,MAAA,CAAO,SAAS,cAAA,CAAe,iBAAA,IAC9B,OAAO,WAAA,KAAgB,IAAA,IACzB,iCAAiC,MAAM,CAAA;AAE3C,CAAA;AAGA,MAAM,mBAAA,GAAsB,CAC1B,IAAA,KACkC;AAClC,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,WAAA,SAAoB,IAAA,CAAK,WAAA;AAE1D,EAAA,OAAO,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,cAAA,IAClC,KAAK,QAAA,KAAa,UAAA,IAClB,IAAA,CAAK,cAAA,EAAgB,IAAA,KAAS,cAAA,CAAe,WAAA,GAC3C,IAAA,CAAK,eAAe,WAAA,GACpB,MAAA;AACN,CAAA;AAEA,MAAM,mBAAA,GAAsB,CAAC,IAAA,KAC3B,IAAA,CAAK,WAAW,OAAA,EAAS,GAAG,EAAE,IAAA,EAAK;;;;"}
@@ -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\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\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;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"tuple-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/tuple-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAWxE,mEAAmE;AACnE,KAAK,UAAU,GAAG,OAAO,CACvB,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EACjE,SAAS,OAAO,EAAE,CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAE,WAAsB,CAAC;AAE1D,eAAO,MAAM,4BAA4B,EAAE,UAW1C,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,YACjB,QAAQ,CAAC,OAAO,iBACV,MAAM,eACR,WAAW,KACvB,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,SAa1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,SAAS,MAAM,WAClD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,YACnD,SAAS,YAAY,EAAE,aACtB,UAAU,eACR,WAAW,KACvB,IAsCF,CAAC"}
1
+ {"version":3,"file":"tuple-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/tuple-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAYxE,mEAAmE;AACnE,KAAK,UAAU,GAAG,OAAO,CACvB,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EACjE,SAAS,OAAO,EAAE,CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAE,WAAqB,CAAC;AAEzD,eAAO,MAAM,4BAA4B,EAAE,UAW1C,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,YACjB,QAAQ,CAAC,OAAO,iBACV,MAAM,eACR,WAAW,KACvB,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,SAa1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,SAAS,MAAM,WAClD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,YACnD,SAAS,YAAY,EAAE,aACtB,UAAU,eACR,WAAW,KACvB,IAuCF,CAAC"}
@@ -1,17 +1,18 @@
1
+ import { Arr } from 'ts-data-forge';
1
2
  import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
2
3
  import { buildTypeImportFix, getImportedLocalName, getTsTypeForgeImports, hasConflictingDeclaration } from './import-utils.mjs';
3
4
 
4
- const DEFAULT_IMPORT_STYLE = "global";
5
+ const DEFAULT_IMPORT_STYLE = "named";
5
6
  const IMPORT_STYLE_SCHEMA_PROPERTY = {
6
7
  type: "string",
7
8
  enum: ["global", "named"],
8
9
  description: [
9
10
  "How the ts-type-forge type is brought into scope.",
10
- "'global' (default) assumes the ambient globals of",
11
- "`ts-type-forge/global` and never touches imports;",
12
- "'named' makes the autofix add the corresponding",
11
+ "'named' (default) makes the autofix add the corresponding",
13
12
  `\`import { type \u2026 } from '${TS_TYPE_FORGE_MODULE}';\``,
14
- "when the name is not imported yet."
13
+ "when the name is not imported yet; 'global' assumes the",
14
+ "ambient globals of `ts-type-forge/global` and never touches",
15
+ "imports."
15
16
  ].join(" ")
16
17
  };
17
18
  const resolveTypeName = (program, canonicalName, importStyle) => {
@@ -42,12 +43,12 @@ const reportTupleRewrites = (context, rewrites, messageId, importStyle) => {
42
43
  node: rewrite.node,
43
44
  messageId,
44
45
  data: { original: sourceCode.getText(rewrite.node), replacement },
45
- fix: (fixer) => [
46
- ...index === 0 ? importNames.flatMap(
46
+ fix: (fixer) => Arr.toPushed(
47
+ index === 0 ? importNames.flatMap(
47
48
  (name) => buildTypeImportFix(fixer, program, name)
48
49
  ) : [],
49
50
  fixer.replaceText(rewrite.node, replacement)
50
- ]
51
+ )
51
52
  });
52
53
  }
53
54
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tuple-rule-utils.mjs","sources":["../../src/rules/tuple-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\nimport {\n buildTypeImportFix,\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 = 'global';\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 \"'global' (default) assumes the ambient globals of\",\n '`ts-type-forge/global` and never touches imports;',\n \"'named' makes the autofix add the corresponding\",\n `\\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\\``,\n 'when the name is not imported yet.',\n ].join(' '),\n};\n\n/**\n * Resolves how a ts-type-forge type name should be written in this file, and\n * whether the autofix 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\nexport type TupleRewrite = 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 reportTupleRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TupleRewrite[],\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 ...(index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : []),\n fixer.replaceText(rewrite.node, replacement),\n ],\n });\n }\n};\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":";;;AAmBO,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,mDAAA;AAAA,IACA,mDAAA;AAAA,IACA,iDAAA;AAAA,IACA,kCAA6B,oBAAoB,CAAA,IAAA,CAAA;AAAA,IACjD;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;AAkBO,MAAM,mBAAA,GAAsB,CACjC,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,KAAU;AAAA,QACd,GAAI,KAAA,KAAU,CAAA,GACV,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,KACD,CAAA;AAAA,EACH;AACF;;;;"}
1
+ {"version":3,"file":"tuple-rule-utils.mjs","sources":["../../src/rules/tuple-rule-utils.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';\nimport {\n buildTypeImportFix,\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 autofix 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\nexport type TupleRewrite = 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 reportTupleRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TupleRewrite[],\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":";;;;AAoBO,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;AAkBO,MAAM,mBAAA,GAAsB,CACjC,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-type-forge",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer type declarations toward ts-type-forge idioms.",
6
6
  "keywords": [
@@ -35,8 +35,9 @@
35
35
  "LICENSE"
36
36
  ],
37
37
  "dependencies": {
38
- "@typescript-eslint/utils": "8.65.0",
39
- "ts-type-forge": "9.1.0"
38
+ "@typescript-eslint/utils": "~8.65.0",
39
+ "ts-data-forge": "~14.0.0",
40
+ "ts-type-forge": "9.1.1"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@typescript-eslint/parser": "8.65.0",
@@ -53,7 +53,7 @@ namespace PreferCanonicalLengthConstrainedTuple {
53
53
  * "global",
54
54
  * "named"
55
55
  * ],
56
- * "description": "How the ts-type-forge type is brought into scope. 'global' (default) assumes the ambient globals of `ts-type-forge/global` and never touches imports; 'named' makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet."
56
+ * "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
57
57
  * },
58
58
  * "maxLength": {
59
59
  * "type": "integer",
@@ -3,6 +3,7 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
+ import { Arr } from 'ts-data-forge';
6
7
 
7
8
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
8
9
 
@@ -35,11 +36,15 @@ export type UniformTupleShape = Readonly<{
35
36
  * rewriting them would drop information.
36
37
  * - A tuple that is only a rest element (`[...V[]]`) is not a length
37
38
  * constraint at all.
39
+ * - A tuple inside the `extends` clause of a conditional type is a match
40
+ * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).
38
41
  */
39
42
  export const analyzeUniformTuple = (
40
43
  tuple: TSESTree.TSTupleType,
41
44
  sourceCode: TSESLint.SourceCode,
42
45
  ): UniformTupleShape | undefined => {
46
+ if (isInConditionalTypeExtendsClause(tuple)) return undefined;
47
+
43
48
  const { parent } = tuple;
44
49
 
45
50
  const isReadonly =
@@ -52,10 +57,10 @@ export const analyzeUniformTuple = (
52
57
 
53
58
  const hasRest = last?.type === AST_NODE_TYPES.TSRestType;
54
59
 
55
- const leading = hasRest ? elements.slice(0, -1) : elements;
60
+ const leading = hasRest ? Arr.butLast(elements) : elements;
56
61
 
57
62
  if (
58
- leading.length === 0 ||
63
+ Arr.isEmpty(leading) ||
59
64
  leading.some(
60
65
  (element) =>
61
66
  element.type === AST_NODE_TYPES.TSRestType ||
@@ -82,7 +87,7 @@ export const analyzeUniformTuple = (
82
87
  const expected = normalizeWhitespace(elementText);
83
88
 
84
89
  const others = [
85
- ...leading.slice(1),
90
+ ...Arr.tail(leading),
86
91
  ...(restElementType === undefined ? [] : [restElementType]),
87
92
  ];
88
93
 
@@ -99,6 +104,30 @@ export const analyzeUniformTuple = (
99
104
  : undefined;
100
105
  };
101
106
 
107
+ /**
108
+ * Whether `node` sits inside the `extends` clause of a conditional type, where
109
+ * a tuple is a *match pattern* rather than a type.
110
+ *
111
+ * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are
112
+ * still generic. The canonical spellings resolve through a mapped type
113
+ * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and
114
+ * against one of those the checker defers the whole conditional instead —
115
+ * yielding `boolean` where the tuple pattern yielded `true` or `false`, which
116
+ * silently widens everything downstream. So those positions are left alone.
117
+ */
118
+ const isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {
119
+ // The root has no parent, so it terminates the walk.
120
+ if (node.type === AST_NODE_TYPES.Program) return false;
121
+
122
+ const { parent } = node;
123
+
124
+ return (
125
+ (parent.type === AST_NODE_TYPES.TSConditionalType &&
126
+ parent.extendsType === node) ||
127
+ isInConditionalTypeExtendsClause(parent)
128
+ );
129
+ };
130
+
102
131
  /** Unwraps `V[]` and `readonly V[]` to `V`. */
103
132
  const getArrayElementType = (
104
133
  node: TSESTree.TypeNode,
@@ -114,5 +143,3 @@ const getArrayElementType = (
114
143
 
115
144
  const normalizeWhitespace = (text: string): string =>
116
145
  text.replaceAll(/\s+/gu, ' ').trim();
117
-
118
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -101,5 +101,3 @@ const declaresName = (
101
101
  node.type === AST_NODE_TYPES.ClassDeclaration && node.id?.name === name
102
102
  );
103
103
  };
104
-
105
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -45,6 +45,21 @@ describe('prefer-canonical-length-constrained-tuple', () => {
45
45
  name: 'ignores plain arrays',
46
46
  code: 'type T = readonly string[];',
47
47
  },
48
+ {
49
+ name: 'ignores tuple patterns in a conditional type `extends` clause',
50
+ code: dedent`
51
+ type And<A extends boolean, B extends boolean> = [A, B] extends [
52
+ true,
53
+ true,
54
+ ]
55
+ ? true
56
+ : false;
57
+ `,
58
+ },
59
+ {
60
+ name: 'ignores tuple patterns nested in an `extends` clause',
61
+ code: 'type T<A> = A extends readonly (readonly [string, string])[] ? 1 : 2;',
62
+ },
48
63
  {
49
64
  name: 'stays silent when the target name is bound locally',
50
65
  code: dedent`
@@ -54,75 +69,116 @@ describe('prefer-canonical-length-constrained-tuple', () => {
54
69
  },
55
70
  ],
56
71
  invalid: [
72
+ {
73
+ name: 'still rewrites the branches of a conditional type',
74
+ code: 'type T<A> = A extends 1 ? [true, true] : 2;',
75
+ output: dedent`
76
+ import { type MutableFixedLengthTuple } from 'ts-type-forge';
77
+ type T<A> = A extends 1 ? MutableFixedLengthTuple<2, true> : 2;
78
+ `,
79
+ errors: [{ messageId: 'useCanonicalTuple' }],
80
+ },
57
81
  {
58
82
  name: 'readonly non-empty tuple',
59
83
  code: 'type T = readonly [string, ...string[]];',
60
- output: 'type T = NonEmptyTuple<string>;',
84
+ output: dedent`
85
+ import { type NonEmptyTuple } from 'ts-type-forge';
86
+ type T = NonEmptyTuple<string>;
87
+ `,
61
88
  errors: [{ messageId: 'useCanonicalTuple' }],
62
89
  },
63
90
  {
64
91
  name: 'mutable non-empty tuple',
65
92
  code: 'type T = [string, ...string[]];',
66
- output: 'type T = MutableNonEmptyTuple<string>;',
93
+ output: dedent`
94
+ import { type MutableNonEmptyTuple } from 'ts-type-forge';
95
+ type T = MutableNonEmptyTuple<string>;
96
+ `,
67
97
  errors: [{ messageId: 'useCanonicalTuple' }],
68
98
  },
69
99
  {
70
100
  name: 'readonly non-empty tuple with a readonly rest spelling',
71
101
  code: 'type T = readonly [string, ...(readonly string[])];',
72
- output: 'type T = NonEmptyTuple<string>;',
102
+ output: dedent`
103
+ import { type NonEmptyTuple } from 'ts-type-forge';
104
+ type T = NonEmptyTuple<string>;
105
+ `,
73
106
  errors: [{ messageId: 'useCanonicalTuple' }],
74
107
  },
75
108
  {
76
109
  name: 'readonly fixed-length tuple',
77
110
  code: 'type T = readonly [string, string, string];',
78
- output: 'type T = FixedLengthTuple<3, string>;',
111
+ output: dedent`
112
+ import { type FixedLengthTuple } from 'ts-type-forge';
113
+ type T = FixedLengthTuple<3, string>;
114
+ `,
79
115
  errors: [{ messageId: 'useCanonicalTuple' }],
80
116
  },
81
117
  {
82
118
  name: 'mutable fixed-length tuple',
83
119
  code: 'type T = [string, string, string];',
84
- output: 'type T = MutableFixedLengthTuple<3, string>;',
120
+ output: dedent`
121
+ import { type MutableFixedLengthTuple } from 'ts-type-forge';
122
+ type T = MutableFixedLengthTuple<3, string>;
123
+ `,
85
124
  errors: [{ messageId: 'useCanonicalTuple' }],
86
125
  },
87
126
  {
88
127
  name: 'readonly min-length tuple',
89
128
  code: 'type T = readonly [number, number, ...number[]];',
90
- output: 'type T = MinLengthTuple<2, number>;',
129
+ output: dedent`
130
+ import { type MinLengthTuple } from 'ts-type-forge';
131
+ type T = MinLengthTuple<2, number>;
132
+ `,
91
133
  errors: [{ messageId: 'useCanonicalTuple' }],
92
134
  },
93
135
  {
94
136
  name: 'mutable min-length tuple',
95
137
  code: 'type T = [number, number, ...number[]];',
96
- output: 'type T = MutableMinLengthTuple<2, number>;',
138
+ output: dedent`
139
+ import { type MutableMinLengthTuple } from 'ts-type-forge';
140
+ type T = MutableMinLengthTuple<2, number>;
141
+ `,
97
142
  errors: [{ messageId: 'useCanonicalTuple' }],
98
143
  },
99
144
  {
100
145
  name: 'readonly rest spelling inside a readonly tuple',
101
146
  code: 'type T = readonly [number, number, ...(readonly number[])];',
102
- output: 'type T = MinLengthTuple<2, number>;',
147
+ output: dedent`
148
+ import { type MinLengthTuple } from 'ts-type-forge';
149
+ type T = MinLengthTuple<2, number>;
150
+ `,
103
151
  errors: [{ messageId: 'useCanonicalTuple' }],
104
152
  },
105
153
  {
106
154
  name: 'handles the default maximum length of 10',
107
155
  code: 'type T = readonly [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];',
108
- output: 'type T = FixedLengthTuple<10, 1>;',
156
+ output: dedent`
157
+ import { type FixedLengthTuple } from 'ts-type-forge';
158
+ type T = FixedLengthTuple<10, 1>;
159
+ `,
109
160
  errors: [{ messageId: 'useCanonicalTuple' }],
110
161
  },
111
162
  {
112
163
  name: 'keeps a composite element type intact',
113
164
  code: 'type T = readonly [string | number, string | number];',
114
- output: 'type T = FixedLengthTuple<2, string | number>;',
165
+ output: dedent`
166
+ import { type FixedLengthTuple } from 'ts-type-forge';
167
+ type T = FixedLengthTuple<2, string | number>;
168
+ `,
115
169
  errors: [{ messageId: 'useCanonicalTuple' }],
116
170
  },
117
171
  {
118
172
  name: 'handles generic element types',
119
173
  code: 'type T<V> = [V, V, ...V[]];',
120
- output: 'type T<V> = MutableMinLengthTuple<2, V>;',
174
+ output: dedent`
175
+ import { type MutableMinLengthTuple } from 'ts-type-forge';
176
+ type T<V> = MutableMinLengthTuple<2, V>;
177
+ `,
121
178
  errors: [{ messageId: 'useCanonicalTuple' }],
122
179
  },
123
180
  {
124
181
  name: 'adds named imports once per target type',
125
- options: [{ importStyle: 'named' }],
126
182
  code: dedent`
127
183
  type A = readonly [string, string];
128
184
  type B = readonly [number, number, number];
@@ -141,6 +197,13 @@ describe('prefer-canonical-length-constrained-tuple', () => {
141
197
  { messageId: 'useCanonicalTuple' },
142
198
  ],
143
199
  },
200
+ {
201
+ name: 'adds no import under importStyle: global',
202
+ options: [{ importStyle: 'global' }],
203
+ code: 'type T = readonly [string, string];',
204
+ output: 'type T = FixedLengthTuple<2, string>;',
205
+ errors: [{ messageId: 'useCanonicalTuple' }],
206
+ },
144
207
  {
145
208
  name: 'reuses an aliased import',
146
209
  code: dedent`
@@ -158,8 +221,11 @@ describe('prefer-canonical-length-constrained-tuple', () => {
158
221
  {
159
222
  name: 'rewrites parameter and return type positions',
160
223
  code: 'declare const f: (xs: readonly [number, number]) => [string, string];',
161
- output:
162
- 'declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;',
224
+ output: dedent`
225
+ import { type FixedLengthTuple } from 'ts-type-forge';
226
+ import { type MutableFixedLengthTuple } from 'ts-type-forge';
227
+ declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;
228
+ `,
163
229
  errors: [
164
230
  { messageId: 'useCanonicalTuple' },
165
231
  { messageId: 'useCanonicalTuple' },
@@ -169,7 +235,10 @@ describe('prefer-canonical-length-constrained-tuple', () => {
169
235
  name: 'respects a raised maxLength',
170
236
  options: [{ maxLength: 12 }],
171
237
  code: 'type T = readonly [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];',
172
- output: 'type T = FixedLengthTuple<12, 0>;',
238
+ output: dedent`
239
+ import { type FixedLengthTuple } from 'ts-type-forge';
240
+ type T = FixedLengthTuple<12, 0>;
241
+ `,
173
242
  errors: [{ messageId: 'useCanonicalTuple' }],
174
243
  },
175
244
  ],
@@ -1,4 +1,5 @@
1
1
  import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
2
3
  import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
3
4
  import {
4
5
  buildTypeImportFix,
@@ -17,18 +18,18 @@ type JsonSchema = Exclude<
17
18
 
18
19
  export type ImportStyle = 'global' | 'named';
19
20
 
20
- export const DEFAULT_IMPORT_STYLE: ImportStyle = 'global';
21
+ export const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';
21
22
 
22
23
  export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
23
24
  type: 'string',
24
25
  enum: ['global', 'named'],
25
26
  description: [
26
27
  'How the ts-type-forge type is brought into scope.',
27
- "'global' (default) assumes the ambient globals of",
28
- '`ts-type-forge/global` and never touches imports;',
29
- "'named' makes the autofix add the corresponding",
28
+ "'named' (default) makes the autofix add the corresponding",
30
29
  `\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\``,
31
- 'when the name is not imported yet.',
30
+ "when the name is not imported yet; 'global' assumes the",
31
+ 'ambient globals of `ts-type-forge/global` and never touches',
32
+ 'imports.',
32
33
  ].join(' '),
33
34
  };
34
35
 
@@ -109,16 +110,15 @@ export const reportTupleRewrites = <MessageIds extends string>(
109
110
  node: rewrite.node,
110
111
  messageId,
111
112
  data: { original: sourceCode.getText(rewrite.node), replacement },
112
- fix: (fixer) => [
113
- ...(index === 0
114
- ? importNames.flatMap((name) =>
115
- buildTypeImportFix(fixer, program, name),
116
- )
117
- : []),
118
- fixer.replaceText(rewrite.node, replacement),
119
- ],
113
+ fix: (fixer) =>
114
+ Arr.toPushed(
115
+ index === 0
116
+ ? importNames.flatMap((name) =>
117
+ buildTypeImportFix(fixer, program, name),
118
+ )
119
+ : [],
120
+ fixer.replaceText(rewrite.node, replacement),
121
+ ),
120
122
  });
121
123
  }
122
124
  };
123
-
124
- /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */