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.
- package/README.md +80 -13
- package/dist/plugin.d.mts +8 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +3 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +79 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +2 -0
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +10 -3
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-style.d.mts +21 -0
- package/dist/rules/import-style.d.mts.map +1 -0
- package/dist/rules/import-style.mjs +29 -0
- package/dist/rules/import-style.mjs.map +1 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
- package/dist/rules/prefer-readonly-or-mutable-record.d.mts +10 -0
- package/dist/rules/prefer-readonly-or-mutable-record.d.mts.map +1 -0
- package/dist/rules/prefer-readonly-or-mutable-record.mjs +49 -0
- package/dist/rules/prefer-readonly-or-mutable-record.mjs.map +1 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts +10 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts.map +1 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs +49 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs.map +1 -0
- package/dist/rules/rules.d.mts +6 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +5 -1
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/rules/std-type-rule-utils.d.mts +22 -0
- package/dist/rules/std-type-rule-utils.d.mts.map +1 -0
- package/dist/rules/std-type-rule-utils.mjs +40 -0
- package/dist/rules/std-type-rule-utils.mjs.map +1 -0
- package/dist/rules/tuple-rule-utils.d.mts +1 -19
- package/dist/rules/tuple-rule-utils.d.mts.map +1 -1
- package/dist/rules/tuple-rule-utils.mjs +7 -29
- package/dist/rules/tuple-rule-utils.mjs.map +1 -1
- package/package.json +4 -3
- package/src/plugin.mts +2 -0
- package/src/rule-types.mts +93 -1
- package/src/rules/ast-utils.mts +32 -3
- package/src/rules/import-style.mts +60 -0
- package/src/rules/prefer-canonical-length-constrained-tuple.mts +2 -3
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +84 -15
- package/src/rules/prefer-readonly-or-mutable-record.mts +74 -0
- package/src/rules/prefer-readonly-or-mutable-record.test.mts +160 -0
- package/src/rules/prefer-strict-or-relaxed-utility-type.mts +80 -0
- package/src/rules/prefer-strict-or-relaxed-utility-type.test.mts +279 -0
- package/src/rules/rules.mts +4 -0
- package/src/rules/std-type-rule-utils.mts +103 -0
- 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;;;;"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB;;;;;;;;;;;CAKa,CAAC"}
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -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 };
|
package/dist/rules/rules.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { 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
|
-
|
|
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;
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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-
|
|
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
|
>;
|
package/src/rule-types.mts
CHANGED
|
@@ -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. '
|
|
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
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -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 ?
|
|
60
|
+
const leading = hasRest ? Arr.butLast(elements) : elements;
|
|
56
61
|
|
|
57
62
|
if (
|
|
58
|
-
leading
|
|
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
|
-
...
|
|
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
|
-
|
|
16
|
-
|
|
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<{
|