eslint-plugin-ts-type-forge 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +80 -13
  2. package/dist/plugin.d.mts +8 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +3 -1
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +79 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rules/ast-utils.d.mts +2 -0
  9. package/dist/rules/ast-utils.d.mts.map +1 -1
  10. package/dist/rules/ast-utils.mjs +10 -3
  11. package/dist/rules/ast-utils.mjs.map +1 -1
  12. package/dist/rules/import-style.d.mts +21 -0
  13. package/dist/rules/import-style.d.mts.map +1 -0
  14. package/dist/rules/import-style.mjs +29 -0
  15. package/dist/rules/import-style.mjs.map +1 -0
  16. package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -1
  17. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -1
  18. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
  19. package/dist/rules/prefer-readonly-or-mutable-record.d.mts +10 -0
  20. package/dist/rules/prefer-readonly-or-mutable-record.d.mts.map +1 -0
  21. package/dist/rules/prefer-readonly-or-mutable-record.mjs +49 -0
  22. package/dist/rules/prefer-readonly-or-mutable-record.mjs.map +1 -0
  23. package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts +10 -0
  24. package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts.map +1 -0
  25. package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs +49 -0
  26. package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs.map +1 -0
  27. package/dist/rules/rules.d.mts +6 -0
  28. package/dist/rules/rules.d.mts.map +1 -1
  29. package/dist/rules/rules.mjs +5 -1
  30. package/dist/rules/rules.mjs.map +1 -1
  31. package/dist/rules/std-type-rule-utils.d.mts +22 -0
  32. package/dist/rules/std-type-rule-utils.d.mts.map +1 -0
  33. package/dist/rules/std-type-rule-utils.mjs +40 -0
  34. package/dist/rules/std-type-rule-utils.mjs.map +1 -0
  35. package/dist/rules/tuple-rule-utils.d.mts +1 -19
  36. package/dist/rules/tuple-rule-utils.d.mts.map +1 -1
  37. package/dist/rules/tuple-rule-utils.mjs +7 -29
  38. package/dist/rules/tuple-rule-utils.mjs.map +1 -1
  39. package/package.json +4 -3
  40. package/src/plugin.mts +2 -0
  41. package/src/rule-types.mts +93 -1
  42. package/src/rules/ast-utils.mts +32 -3
  43. package/src/rules/import-style.mts +60 -0
  44. package/src/rules/prefer-canonical-length-constrained-tuple.mts +2 -3
  45. package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +84 -15
  46. package/src/rules/prefer-readonly-or-mutable-record.mts +74 -0
  47. package/src/rules/prefer-readonly-or-mutable-record.test.mts +160 -0
  48. package/src/rules/prefer-strict-or-relaxed-utility-type.mts +80 -0
  49. package/src/rules/prefer-strict-or-relaxed-utility-type.test.mts +279 -0
  50. package/src/rules/rules.mts +4 -0
  51. package/src/rules/std-type-rule-utils.mts +103 -0
  52. package/src/rules/tuple-rule-utils.mts +12 -66
@@ -0,0 +1,49 @@
1
+ import { DEFAULT_IMPORT_STYLE, IMPORT_STYLE_SCHEMA_PROPERTY } from './import-style.mjs';
2
+ import { reportStdTypeReference } from './std-type-rule-utils.mjs';
3
+
4
+ const ALTERNATIVES = /* @__PURE__ */ new Map([
5
+ ["Exclude", ["StrictExclude", "RelaxedExclude"]],
6
+ ["Extract", ["StrictExtract", "RelaxedExtract"]],
7
+ ["Omit", ["StrictOmit", "RelaxedOmit"]],
8
+ ["Pick", ["StrictPick", "RelaxedPick"]]
9
+ ]);
10
+ const preferStrictOrRelaxedUtilityType = {
11
+ meta: {
12
+ type: "suggestion",
13
+ docs: {
14
+ description: "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."
15
+ },
16
+ hasSuggestions: true,
17
+ schema: [
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ importStyle: IMPORT_STYLE_SCHEMA_PROPERTY
22
+ },
23
+ additionalProperties: false
24
+ }
25
+ ],
26
+ messages: {
27
+ useStrictOrRelaxed: "Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.",
28
+ replaceWith: "Replace `{{name}}` with `{{replacement}}`."
29
+ }
30
+ },
31
+ create: (context) => {
32
+ const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
33
+ return {
34
+ TSTypeReference: (node) => {
35
+ reportStdTypeReference(
36
+ context,
37
+ node,
38
+ ALTERNATIVES,
39
+ { report: "useStrictOrRelaxed", suggestion: "replaceWith" },
40
+ importStyle
41
+ );
42
+ }
43
+ };
44
+ },
45
+ defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }]
46
+ };
47
+
48
+ export { preferStrictOrRelaxedUtilityType };
49
+ //# sourceMappingURL=prefer-strict-or-relaxed-utility-type.mjs.map
@@ -0,0 +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;;;;"}
@@ -3,5 +3,11 @@ export declare const tsTypeForgeRules: {
3
3
  importStyle?: 'global' | 'named';
4
4
  maxLength?: number;
5
5
  }> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
+ readonly 'prefer-readonly-or-mutable-record': import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWith" | "useReadonlyOrMutableRecord", readonly [(Readonly<{
7
+ importStyle?: 'global' | 'named';
8
+ }> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
9
+ readonly 'prefer-strict-or-relaxed-utility-type': import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWith" | "useStrictOrRelaxed", readonly [(Readonly<{
10
+ importStyle?: 'global' | 'named';
11
+ }> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
12
  };
7
13
  //# sourceMappingURL=rules.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB;;;;;CAGa,CAAC"}
1
+ {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB;;;;;;;;;;;CAKa,CAAC"}
@@ -1,7 +1,11 @@
1
1
  import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
2
+ import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
3
+ import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
2
4
 
3
5
  const tsTypeForgeRules = {
4
- "prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple
6
+ "prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple,
7
+ "prefer-readonly-or-mutable-record": preferReadonlyOrMutableRecord,
8
+ "prefer-strict-or-relaxed-utility-type": preferStrictOrRelaxedUtilityType
5
9
  };
6
10
 
7
11
  export { tsTypeForgeRules };
@@ -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';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;AAGO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,2CAAA,EACE;AACJ;;;;"}
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 { 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-readonly-or-mutable-record': preferReadonlyOrMutableRecord,\n 'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;;;AAKO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,2CAAA,EACE,qCAAA;AAAA,EACF,mCAAA,EAAqC,6BAAA;AAAA,EACrC,uCAAA,EAAyC;AAC3C;;;;"}
@@ -0,0 +1,22 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ import { type ImportStyle } from './import-style.mjs';
3
+ /**
4
+ * The ts-type-forge replacements offered for one standard-library type, in the
5
+ * order they are suggested.
6
+ */
7
+ export type StdTypeAlternatives = ReadonlyMap<string, readonly string[]>;
8
+ /**
9
+ * Reports a reference to a banned standard-library type and offers one
10
+ * suggestion per ts-type-forge alternative.
11
+ *
12
+ * Choosing between the alternatives is a semantic decision — `StrictExclude`
13
+ * additionally checks that the subtrahend is part of the union, `ReadonlyRecord`
14
+ * and `MutableRecord` describe different types — so this is deliberately *not*
15
+ * an autofix. Each suggestion carries its own import fix, which is safe because
16
+ * ESLint applies suggestions one at a time.
17
+ */
18
+ export declare const reportStdTypeReference: <MessageIds extends string>(context: TSESLint.RuleContext<MessageIds, readonly unknown[]>, node: TSESTree.TSTypeReference, alternatives: StdTypeAlternatives, messageIds: Readonly<{
19
+ report: MessageIds;
20
+ suggestion: MessageIds;
21
+ }>, importStyle: ImportStyle) => void;
22
+ //# sourceMappingURL=std-type-rule-utils.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"std-type-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/std-type-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKvE;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;AAEzE;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAU,SAAS,MAAM,WACrD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,QACvD,QAAQ,CAAC,eAAe,gBAChB,mBAAmB,cACrB,QAAQ,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,CAAC,eACvD,WAAW,KACvB,IA4CF,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';
2
+ import { Arr } from 'ts-data-forge';
3
+ import { resolveTypeName } from './import-style.mjs';
4
+ import { buildTypeImportFix } from './import-utils.mjs';
5
+
6
+ const reportStdTypeReference = (context, node, alternatives, messageIds, importStyle) => {
7
+ const typeName = node.typeName;
8
+ if (typeName.type !== AST_NODE_TYPES.Identifier) return;
9
+ const stdName = typeName.name;
10
+ const candidates = alternatives.get(stdName);
11
+ if (candidates === void 0) return;
12
+ if (isLocallyBound(context.sourceCode, node, stdName)) return;
13
+ const program = context.sourceCode.ast;
14
+ context.report({
15
+ node: typeName,
16
+ messageId: messageIds.report,
17
+ data: { name: stdName, alternatives: formatAlternatives(candidates) },
18
+ suggest: candidates.flatMap((candidate) => {
19
+ const target = resolveTypeName(program, candidate, importStyle);
20
+ return target === void 0 ? [] : [
21
+ {
22
+ messageId: messageIds.suggestion,
23
+ data: { name: stdName, replacement: target.localName },
24
+ fix: (fixer) => Arr.toPushed(
25
+ target.needsImport ? buildTypeImportFix(fixer, program, candidate) : [],
26
+ fixer.replaceText(typeName, target.localName)
27
+ )
28
+ }
29
+ ];
30
+ })
31
+ });
32
+ };
33
+ const formatAlternatives = (candidates) => candidates.map((candidate) => `\`${candidate}\``).join(" or ");
34
+ const isLocallyBound = (sourceCode, node, name) => {
35
+ const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);
36
+ return variable !== null && Arr.isNonEmpty(variable.defs);
37
+ };
38
+
39
+ export { reportStdTypeReference };
40
+ //# sourceMappingURL=std-type-rule-utils.mjs.map
@@ -0,0 +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 ASTUtils,\n type TSESLint,\n type TSESTree,\n} 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/**\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\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 */\nconst 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"],"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;AAY/D,MAAM,cAAA,GAAiB,CACrB,UAAA,EACA,IAAA,EACA,IAAA,KACY;AACZ,EAAA,MAAM,WAAW,QAAA,CAAS,YAAA,CAAa,WAAW,QAAA,CAAS,IAAI,GAAG,IAAI,CAAA;AAEtE,EAAA,OAAO,QAAA,KAAa,IAAA,IAAQ,GAAA,CAAI,UAAA,CAAW,SAAS,IAAI,CAAA;AAC1D,CAAA;;;;"}
@@ -1,22 +1,5 @@
1
1
  import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- /** `JSONSchema4`, reached through the public rule-module types. */
3
- type JsonSchema = Exclude<TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'], readonly unknown[]>;
4
- export type ImportStyle = 'global' | 'named';
5
- export declare const DEFAULT_IMPORT_STYLE: ImportStyle;
6
- export declare const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema;
7
- /**
8
- * Resolves how a ts-type-forge type name should be written in this file, and
9
- * whether the autofix has to add an import for it.
10
- *
11
- * Returns `undefined` when the name is already bound to something else (a local
12
- * alias, or an import from another module) — rewriting to it would then silently
13
- * resolve to the wrong declaration, which is exactly what happens inside
14
- * ts-type-forge's own sources.
15
- */
16
- export declare const resolveTypeName: (program: TSESTree.Program, canonicalName: string, importStyle: ImportStyle) => Readonly<{
17
- localName: string;
18
- needsImport: boolean;
19
- }> | undefined;
2
+ import { type ImportStyle } from './import-style.mjs';
20
3
  export type TupleRewrite = Readonly<{
21
4
  node: TSESTree.Node;
22
5
  canonicalName: string;
@@ -33,5 +16,4 @@ export type TupleRewrite = Readonly<{
33
16
  * survives per pass.
34
17
  */
35
18
  export declare const reportTupleRewrites: <MessageIds extends string>(context: TSESLint.RuleContext<MessageIds, readonly unknown[]>, rewrites: readonly TupleRewrite[], messageId: MessageIds, importStyle: ImportStyle) => void;
36
- export {};
37
19
  //# sourceMappingURL=tuple-rule-utils.d.mts.map
@@ -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;AAExE,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKvE,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,29 +1,7 @@
1
- import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
2
- import { buildTypeImportFix, getImportedLocalName, getTsTypeForgeImports, hasConflictingDeclaration } from './import-utils.mjs';
1
+ import { Arr } from 'ts-data-forge';
2
+ import { resolveTypeName } from './import-style.mjs';
3
+ import { buildTypeImportFix } from './import-utils.mjs';
3
4
 
4
- const DEFAULT_IMPORT_STYLE = "global";
5
- const IMPORT_STYLE_SCHEMA_PROPERTY = {
6
- type: "string",
7
- enum: ["global", "named"],
8
- description: [
9
- "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",
13
- `\`import { type \u2026 } from '${TS_TYPE_FORGE_MODULE}';\``,
14
- "when the name is not imported yet."
15
- ].join(" ")
16
- };
17
- const resolveTypeName = (program, canonicalName, importStyle) => {
18
- const importedLocalName = getImportedLocalName(
19
- getTsTypeForgeImports(program),
20
- canonicalName
21
- );
22
- if (importedLocalName !== void 0) {
23
- return { localName: importedLocalName, needsImport: false };
24
- }
25
- return hasConflictingDeclaration(program, canonicalName) ? void 0 : { localName: canonicalName, needsImport: importStyle === "named" };
26
- };
27
5
  const reportTupleRewrites = (context, rewrites, messageId, importStyle) => {
28
6
  const sourceCode = context.sourceCode;
29
7
  const program = sourceCode.ast;
@@ -42,15 +20,15 @@ const reportTupleRewrites = (context, rewrites, messageId, importStyle) => {
42
20
  node: rewrite.node,
43
21
  messageId,
44
22
  data: { original: sourceCode.getText(rewrite.node), replacement },
45
- fix: (fixer) => [
46
- ...index === 0 ? importNames.flatMap(
23
+ fix: (fixer) => Arr.toPushed(
24
+ index === 0 ? importNames.flatMap(
47
25
  (name) => buildTypeImportFix(fixer, program, name)
48
26
  ) : [],
49
27
  fixer.replaceText(rewrite.node, replacement)
50
- ]
28
+ )
51
29
  });
52
30
  }
53
31
  };
54
32
 
55
- export { DEFAULT_IMPORT_STYLE, IMPORT_STYLE_SCHEMA_PROPERTY, reportTupleRewrites, resolveTypeName };
33
+ export { reportTupleRewrites };
56
34
  //# sourceMappingURL=tuple-rule-utils.mjs.map
@@ -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"],"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 { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\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":";;;;AAuBO,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.3",
3
+ "version": "0.6.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.1"
38
+ "@typescript-eslint/utils": "~8.65.0",
39
+ "ts-data-forge": "~14.0.0",
40
+ "ts-type-forge": "9.1.2"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@typescript-eslint/parser": "8.65.0",
package/src/plugin.mts CHANGED
@@ -9,6 +9,8 @@ import { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';
9
9
  */
10
10
  const recommendedRules = {
11
11
  'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',
12
+ 'ts-type-forge/prefer-readonly-or-mutable-record': 'error',
13
+ 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error',
12
14
  } as const satisfies Readonly<
13
15
  Record<`ts-type-forge/${keyof typeof tsTypeForgeRules}`, 'error'>
14
16
  >;
@@ -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",
@@ -76,12 +76,104 @@ namespace PreferCanonicalLengthConstrainedTuple {
76
76
  | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
77
77
  }
78
78
 
79
+ /**
80
+ * @description 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.
81
+ *
82
+ * ```md
83
+ * | key | value |
84
+ * | :--------- | :--------- |
85
+ * | type | suggestion |
86
+ * | deprecated | false |
87
+ * | fixable | false |
88
+ * ```
89
+ */
90
+ namespace PreferReadonlyOrMutableRecord {
91
+ /**
92
+ * ### schema
93
+ *
94
+ * ```json
95
+ * [
96
+ * {
97
+ * "type": "object",
98
+ * "properties": {
99
+ * "importStyle": {
100
+ * "type": "string",
101
+ * "enum": [
102
+ * "global",
103
+ * "named"
104
+ * ],
105
+ * "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."
106
+ * }
107
+ * },
108
+ * "additionalProperties": false
109
+ * }
110
+ * ]
111
+ * ```
112
+ */
113
+ export type Options = NonNullable<
114
+ RuleOptionsOf<'prefer-readonly-or-mutable-record'>[0]
115
+ >;
116
+
117
+ export type RuleEntry =
118
+ | 'off'
119
+ | Linter.Severity
120
+ | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
121
+ }
122
+
123
+ /**
124
+ * @description 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.
125
+ *
126
+ * ```md
127
+ * | key | value |
128
+ * | :--------- | :--------- |
129
+ * | type | suggestion |
130
+ * | deprecated | false |
131
+ * | fixable | false |
132
+ * ```
133
+ */
134
+ namespace PreferStrictOrRelaxedUtilityType {
135
+ /**
136
+ * ### schema
137
+ *
138
+ * ```json
139
+ * [
140
+ * {
141
+ * "type": "object",
142
+ * "properties": {
143
+ * "importStyle": {
144
+ * "type": "string",
145
+ * "enum": [
146
+ * "global",
147
+ * "named"
148
+ * ],
149
+ * "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."
150
+ * }
151
+ * },
152
+ * "additionalProperties": false
153
+ * }
154
+ * ]
155
+ * ```
156
+ */
157
+ export type Options = NonNullable<
158
+ RuleOptionsOf<'prefer-strict-or-relaxed-utility-type'>[0]
159
+ >;
160
+
161
+ export type RuleEntry =
162
+ | 'off'
163
+ | Linter.Severity
164
+ | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
165
+ }
166
+
79
167
  export type EslintTsTypeForgeRules = Readonly<{
80
168
  'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
169
+ 'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.RuleEntry;
170
+ 'ts-type-forge/prefer-strict-or-relaxed-utility-type': PreferStrictOrRelaxedUtilityType.RuleEntry;
81
171
  }>;
82
172
 
83
173
  export type EslintTsTypeForgeRulesOption = Readonly<{
84
174
  'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.Options;
175
+ 'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.Options;
176
+ 'ts-type-forge/prefer-strict-or-relaxed-utility-type': PreferStrictOrRelaxedUtilityType.Options;
85
177
  }>;
86
178
 
87
179
  // If this assertion fails to type-check, this generated file has drifted from
@@ -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,
@@ -0,0 +1,60 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
3
+ import {
4
+ getImportedLocalName,
5
+ getTsTypeForgeImports,
6
+ hasConflictingDeclaration,
7
+ } from './import-utils.mjs';
8
+
9
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
10
+
11
+ /** `JSONSchema4`, reached through the public rule-module types. */
12
+ type JsonSchema = Exclude<
13
+ TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],
14
+ readonly unknown[]
15
+ >;
16
+
17
+ export type ImportStyle = 'global' | 'named';
18
+
19
+ export const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';
20
+
21
+ export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
22
+ type: 'string',
23
+ enum: ['global', 'named'],
24
+ description: [
25
+ 'How the ts-type-forge type is brought into scope.',
26
+ "'named' (default) makes the autofix add the corresponding",
27
+ `\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\``,
28
+ "when the name is not imported yet; 'global' assumes the",
29
+ 'ambient globals of `ts-type-forge/global` and never touches',
30
+ 'imports.',
31
+ ].join(' '),
32
+ };
33
+
34
+ /**
35
+ * Resolves how a ts-type-forge type name should be written in this file, and
36
+ * whether the fix has to add an import for it.
37
+ *
38
+ * Returns `undefined` when the name is already bound to something else (a local
39
+ * alias, or an import from another module) — rewriting to it would then silently
40
+ * resolve to the wrong declaration, which is exactly what happens inside
41
+ * ts-type-forge's own sources.
42
+ */
43
+ export const resolveTypeName = (
44
+ program: TSESTree.Program,
45
+ canonicalName: string,
46
+ importStyle: ImportStyle,
47
+ ): Readonly<{ localName: string; needsImport: boolean }> | undefined => {
48
+ const importedLocalName = getImportedLocalName(
49
+ getTsTypeForgeImports(program),
50
+ canonicalName,
51
+ );
52
+
53
+ if (importedLocalName !== undefined) {
54
+ return { localName: importedLocalName, needsImport: false };
55
+ }
56
+
57
+ return hasConflictingDeclaration(program, canonicalName)
58
+ ? undefined
59
+ : { localName: canonicalName, needsImport: importStyle === 'named' };
60
+ };
@@ -12,9 +12,8 @@ import {
12
12
  import {
13
13
  DEFAULT_IMPORT_STYLE,
14
14
  IMPORT_STYLE_SCHEMA_PROPERTY,
15
- reportTupleRewrites,
16
- type TupleRewrite,
17
- } from './tuple-rule-utils.mjs';
15
+ } from './import-style.mjs';
16
+ import { reportTupleRewrites, type TupleRewrite } from './tuple-rule-utils.mjs';
18
17
 
19
18
  type Options = readonly [
20
19
  Readonly<{