@flint.fyi/ts 0.19.0 → 0.19.1
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/lib/plugin.d.ts
CHANGED
|
@@ -2015,6 +2015,22 @@ declare const ts: _flint_fyi_core0.Plugin<_flint_fyi_core0.RuleAbout<string>, "j
|
|
|
2015
2015
|
readonly pluginId: string;
|
|
2016
2016
|
readonly url: string;
|
|
2017
2017
|
}, "preferAs", undefined>, _flint_fyi_core0.Rule<{
|
|
2018
|
+
readonly description: "Reports imports that do not match the configured type import style.";
|
|
2019
|
+
readonly id: "typeImports";
|
|
2020
|
+
readonly presets: readonly ["stylistic"];
|
|
2021
|
+
} & {
|
|
2022
|
+
readonly pluginId: string;
|
|
2023
|
+
readonly url: string;
|
|
2024
|
+
}, "avoidImportType" | "someImportsAreOnlyTypes" | "typeOverValue", {
|
|
2025
|
+
fixStyle: zod_v40.ZodDefault<zod_v40.ZodEnum<{
|
|
2026
|
+
"inline-type-imports": "inline-type-imports";
|
|
2027
|
+
"separate-type-imports": "separate-type-imports";
|
|
2028
|
+
}>>;
|
|
2029
|
+
prefer: zod_v40.ZodDefault<zod_v40.ZodEnum<{
|
|
2030
|
+
"no-type-imports": "no-type-imports";
|
|
2031
|
+
"type-imports": "type-imports";
|
|
2032
|
+
}>>;
|
|
2033
|
+
}>, _flint_fyi_core0.Rule<{
|
|
2018
2034
|
readonly description: "Reports typeof expressions that compare impossible string literals.";
|
|
2019
2035
|
readonly id: "typeofComparisons";
|
|
2020
2036
|
readonly presets: readonly ["javascript"];
|
package/lib/plugin.js
CHANGED
|
@@ -274,6 +274,7 @@ import tripleSlashReferenceValidity_default from "./rules/tripleSlashReferenceVa
|
|
|
274
274
|
import tsComments_default from "./rules/tsComments.js";
|
|
275
275
|
import tslintComments_default from "./rules/tslintComments.js";
|
|
276
276
|
import typeAssertionStyles_default from "./rules/typeAssertionStyles.js";
|
|
277
|
+
import typeImports_default from "./rules/typeImports.js";
|
|
277
278
|
import typeofComparisons_default from "./rules/typeofComparisons.js";
|
|
278
279
|
import unassignedVariables_default from "./rules/unassignedVariables.js";
|
|
279
280
|
import undefinedTypeofChecks_default from "./rules/undefinedTypeofChecks.js";
|
|
@@ -585,6 +586,7 @@ const ts = createPlugin({
|
|
|
585
586
|
tsComments_default,
|
|
586
587
|
tslintComments_default,
|
|
587
588
|
typeAssertionStyles_default,
|
|
589
|
+
typeImports_default,
|
|
588
590
|
typeofComparisons_default,
|
|
589
591
|
unassignedVariables_default,
|
|
590
592
|
undefinedTypeofChecks_default,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
declare const _default: import("@flint.fyi/core").Rule<{
|
|
3
|
+
readonly description: "Reports imports that do not match the configured type import style.";
|
|
4
|
+
readonly id: "typeImports";
|
|
5
|
+
readonly presets: readonly ["stylistic"];
|
|
6
|
+
} & {
|
|
7
|
+
readonly pluginId: string;
|
|
8
|
+
readonly url: string;
|
|
9
|
+
}, "avoidImportType" | "someImportsAreOnlyTypes" | "typeOverValue", {
|
|
10
|
+
fixStyle: z.ZodDefault<z.ZodEnum<{
|
|
11
|
+
"inline-type-imports": "inline-type-imports";
|
|
12
|
+
"separate-type-imports": "separate-type-imports";
|
|
13
|
+
}>>;
|
|
14
|
+
prefer: z.ZodDefault<z.ZodEnum<{
|
|
15
|
+
"no-type-imports": "no-type-imports";
|
|
16
|
+
"type-imports": "type-imports";
|
|
17
|
+
}>>;
|
|
18
|
+
}>;
|
|
19
|
+
export default _default;
|
|
20
|
+
//# sourceMappingURL=typeImports.d.ts.map
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
+
import { forEachChild } from "./utils/forEachChild.js";
|
|
3
|
+
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
+
import ts, { SyntaxKind } from "typescript";
|
|
5
|
+
import { z } from "zod/v4";
|
|
6
|
+
//#region src/rules/typeImports.ts
|
|
7
|
+
function containsDecorator(node) {
|
|
8
|
+
if (node.kind === SyntaxKind.Decorator) return true;
|
|
9
|
+
return forEachChild(node, containsDecorator) ?? false;
|
|
10
|
+
}
|
|
11
|
+
function formatWordList(words) {
|
|
12
|
+
if (words.length < 2) return words[0] ?? "";
|
|
13
|
+
const last = words.at(-1);
|
|
14
|
+
return `${words.slice(0, -1).join(", ")} and ${last}`;
|
|
15
|
+
}
|
|
16
|
+
function getImportSource(node, sourceFile) {
|
|
17
|
+
return ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : node.moduleSpecifier.getText(sourceFile);
|
|
18
|
+
}
|
|
19
|
+
function getImportSpecifiers(node) {
|
|
20
|
+
const importClause = node.importClause;
|
|
21
|
+
if (!importClause) return [];
|
|
22
|
+
const specifiers = [];
|
|
23
|
+
if (importClause.name) specifiers.push(importClause.name);
|
|
24
|
+
const namedBindings = importClause.namedBindings;
|
|
25
|
+
if (namedBindings) if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) specifiers.push(namedBindings);
|
|
26
|
+
else specifiers.push(...namedBindings.elements);
|
|
27
|
+
return specifiers;
|
|
28
|
+
}
|
|
29
|
+
function getReferencedSymbol(typeChecker, node) {
|
|
30
|
+
const symbol = typeChecker.getSymbolAtLocation(node);
|
|
31
|
+
return symbol?.flags && (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? typeChecker.getAliasedSymbol(symbol) : symbol;
|
|
32
|
+
}
|
|
33
|
+
function getSpecifierLocal(specifier) {
|
|
34
|
+
return specifier.kind === SyntaxKind.Identifier ? specifier : specifier.name;
|
|
35
|
+
}
|
|
36
|
+
function getTypeImportFix(report, sourceFile, fixStyle) {
|
|
37
|
+
const node = report.node;
|
|
38
|
+
const importText = node.getText(sourceFile);
|
|
39
|
+
const importClause = node.importClause;
|
|
40
|
+
const namedBindings = importClause?.namedBindings;
|
|
41
|
+
const range = getTSNodeRange(node, sourceFile);
|
|
42
|
+
if (report.valueSpecifiers.length || report.unusedSpecifiers.length) {
|
|
43
|
+
if (!importClause || importClause.name || namedBindings?.kind !== ts.SyntaxKind.NamedImports) return;
|
|
44
|
+
const typeSpecifiers = new Set(report.typeSpecifiers);
|
|
45
|
+
const remainingSpecifiers = namedBindings.elements.filter((specifier) => !typeSpecifiers.has(specifier));
|
|
46
|
+
const typeText = report.typeSpecifiers.map((specifier) => specifier.getText(sourceFile)).join(", ");
|
|
47
|
+
const remainingText = remainingSpecifiers.map((specifier) => specifier.getText(sourceFile)).join(", ");
|
|
48
|
+
const moduleText = node.moduleSpecifier.getText(sourceFile);
|
|
49
|
+
return {
|
|
50
|
+
range,
|
|
51
|
+
text: `import type { ${typeText}} from ${moduleText};\nimport { ${remainingText} } from ${moduleText};`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (fixStyle === "inline-type-imports" && importClause && !importClause.name && namedBindings?.kind === ts.SyntaxKind.NamedImports) {
|
|
55
|
+
const moduleText = node.moduleSpecifier.getText(sourceFile);
|
|
56
|
+
return {
|
|
57
|
+
range,
|
|
58
|
+
text: `import { ${namedBindings.elements.map((specifier) => specifier.isTypeOnly ? specifier.getText(sourceFile) : `type ${specifier.getText(sourceFile)}`).join(", ")} } from ${moduleText};`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
range,
|
|
63
|
+
text: importText.replace(/^import\b/, "import type")
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function getTypeKeywordFixRange(node, sourceFile) {
|
|
67
|
+
const range = getTypeKeywordRange(node, sourceFile);
|
|
68
|
+
if (sourceFile.text[range.end] === " ") range.end += 1;
|
|
69
|
+
return range;
|
|
70
|
+
}
|
|
71
|
+
function getTypeKeywordRange(node, sourceFile) {
|
|
72
|
+
for (const child of node.getChildren(sourceFile)) if (child.kind === SyntaxKind.TypeKeyword) {
|
|
73
|
+
const range = getTSNodeRange(child, sourceFile);
|
|
74
|
+
if (node.kind === SyntaxKind.ImportSpecifier && sourceFile.text[range.end] === " ") range.end += 1;
|
|
75
|
+
return range;
|
|
76
|
+
}
|
|
77
|
+
return getTSNodeRange(node, sourceFile);
|
|
78
|
+
}
|
|
79
|
+
function isInImportDeclaration(node) {
|
|
80
|
+
let current = node;
|
|
81
|
+
while (current) {
|
|
82
|
+
if (current.kind === SyntaxKind.ImportDeclaration) return true;
|
|
83
|
+
current = current.parent;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function isInlineTypeSpecifier(specifier) {
|
|
88
|
+
return specifier.kind === SyntaxKind.ImportSpecifier && specifier.isTypeOnly;
|
|
89
|
+
}
|
|
90
|
+
function isOnlyTypeReference(node) {
|
|
91
|
+
if (isTypeQueryReference(node) || isTypeOnlyExportReference(node) || isPropertySignatureComputedReference(node)) return true;
|
|
92
|
+
let current;
|
|
93
|
+
let parent = node.parent;
|
|
94
|
+
while (parent) {
|
|
95
|
+
if (parent.kind === ts.SyntaxKind.HeritageClause) return parent.token === SyntaxKind.ImplementsKeyword;
|
|
96
|
+
if (ts.isTypeAliasDeclaration(parent) || ts.isInterfaceDeclaration(parent) || ts.isTypeParameterDeclaration(parent)) return true;
|
|
97
|
+
if (ts.isTypeNode(parent) && !ts.isExpressionWithTypeArguments(parent)) return true;
|
|
98
|
+
current = parent;
|
|
99
|
+
parent = current.parent;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function isPropertySignatureComputedReference(node) {
|
|
104
|
+
let child = node;
|
|
105
|
+
let parent = node.parent;
|
|
106
|
+
while (parent) switch (parent.kind) {
|
|
107
|
+
case SyntaxKind.PropertySignature: return parent.name === child;
|
|
108
|
+
case SyntaxKind.ComputedPropertyName:
|
|
109
|
+
if (parent.expression !== child) return false;
|
|
110
|
+
child = parent;
|
|
111
|
+
parent = parent.parent;
|
|
112
|
+
continue;
|
|
113
|
+
case SyntaxKind.PropertyAccessExpression:
|
|
114
|
+
if (parent.expression !== child) return false;
|
|
115
|
+
child = parent;
|
|
116
|
+
parent = parent.parent;
|
|
117
|
+
continue;
|
|
118
|
+
default: return false;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function isTypeImportDeclaration(node) {
|
|
123
|
+
return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword;
|
|
124
|
+
}
|
|
125
|
+
function isTypeOnlyExportReference(node) {
|
|
126
|
+
const parent = node.parent;
|
|
127
|
+
if (parent.kind !== SyntaxKind.ExportSpecifier) return false;
|
|
128
|
+
const exportDeclaration = parent.parent.parent;
|
|
129
|
+
return parent.name === node && exportDeclaration.isTypeOnly;
|
|
130
|
+
}
|
|
131
|
+
function isTypeQueryReference(node) {
|
|
132
|
+
let child = node;
|
|
133
|
+
let parent = node.parent;
|
|
134
|
+
while (parent) switch (parent.kind) {
|
|
135
|
+
case SyntaxKind.TypeQuery: return true;
|
|
136
|
+
case SyntaxKind.QualifiedName:
|
|
137
|
+
if (parent.left !== child) return false;
|
|
138
|
+
child = parent;
|
|
139
|
+
parent = parent.parent;
|
|
140
|
+
continue;
|
|
141
|
+
default: return false;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
var typeImports_default = ruleCreator.createRule(typescriptLanguage, {
|
|
146
|
+
about: {
|
|
147
|
+
description: "Reports imports that do not match the configured type import style.",
|
|
148
|
+
id: "typeImports",
|
|
149
|
+
presets: ["stylistic"]
|
|
150
|
+
},
|
|
151
|
+
messages: {
|
|
152
|
+
avoidImportType: {
|
|
153
|
+
primary: "Use a regular import instead of a type-only import.",
|
|
154
|
+
secondary: ["This configuration prefers regular imports over `import type` syntax."],
|
|
155
|
+
suggestions: ["Change this type-only import to a regular import."]
|
|
156
|
+
},
|
|
157
|
+
someImportsAreOnlyTypes: {
|
|
158
|
+
primary: "Imports {{ typeImports }} are only used as types.",
|
|
159
|
+
secondary: ["These imports are only used in type positions.", "Using 'import type' improves tree-shaking and makes intent clear."],
|
|
160
|
+
suggestions: ["Change these imports to use 'import type'."]
|
|
161
|
+
},
|
|
162
|
+
typeOverValue: {
|
|
163
|
+
primary: "All imports in this declaration are only used as types. Use 'import type'.",
|
|
164
|
+
secondary: ["This import is only used in type positions.", "Using 'import type' improves tree-shaking and makes intent clear."],
|
|
165
|
+
suggestions: ["Change this declaration to use 'import type'."]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
options: {
|
|
169
|
+
fixStyle: z.enum(["inline-type-imports", "separate-type-imports"]).default("inline-type-imports"),
|
|
170
|
+
prefer: z.enum(["no-type-imports", "type-imports"]).default("type-imports")
|
|
171
|
+
},
|
|
172
|
+
setup(context) {
|
|
173
|
+
return { visitors: { SourceFile(node, { options, program, sourceFile, typeChecker }) {
|
|
174
|
+
if (options.prefer === "no-type-imports") {
|
|
175
|
+
for (const statement of node.statements) {
|
|
176
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
|
|
177
|
+
if (isTypeImportDeclaration(statement)) {
|
|
178
|
+
context.report({
|
|
179
|
+
fix: {
|
|
180
|
+
range: getTypeKeywordFixRange(statement.importClause, sourceFile),
|
|
181
|
+
text: ""
|
|
182
|
+
},
|
|
183
|
+
message: "avoidImportType",
|
|
184
|
+
range: getTypeKeywordRange(statement.importClause, sourceFile)
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const specifier of getImportSpecifiers(statement)) {
|
|
189
|
+
if (!isInlineTypeSpecifier(specifier)) continue;
|
|
190
|
+
context.report({
|
|
191
|
+
fix: {
|
|
192
|
+
range: getTypeKeywordFixRange(specifier, sourceFile),
|
|
193
|
+
text: ""
|
|
194
|
+
},
|
|
195
|
+
message: "avoidImportType",
|
|
196
|
+
range: getTypeKeywordRange(specifier, sourceFile)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const compilerOptions = program.getCompilerOptions();
|
|
203
|
+
if (compilerOptions.experimentalDecorators && compilerOptions.emitDecoratorMetadata && containsDecorator(node)) return;
|
|
204
|
+
const sourceImportsMap = {};
|
|
205
|
+
const importedSpecifiers = [];
|
|
206
|
+
const references = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const statement of node.statements) {
|
|
208
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
|
|
209
|
+
const source = getImportSource(statement, sourceFile);
|
|
210
|
+
sourceImportsMap[source] ??= { reportValueImports: [] };
|
|
211
|
+
if (isTypeImportDeclaration(statement)) continue;
|
|
212
|
+
for (const specifier of getImportSpecifiers(statement)) {
|
|
213
|
+
if (isInlineTypeSpecifier(specifier)) continue;
|
|
214
|
+
const local = getSpecifierLocal(specifier);
|
|
215
|
+
const importedSpecifier = {
|
|
216
|
+
local,
|
|
217
|
+
specifier,
|
|
218
|
+
symbol: getReferencedSymbol(typeChecker, local)
|
|
219
|
+
};
|
|
220
|
+
importedSpecifiers.push(importedSpecifier);
|
|
221
|
+
references.set(importedSpecifier, []);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function collectReferences(node) {
|
|
225
|
+
if (ts.isIdentifier(node) && !isInImportDeclaration(node)) {
|
|
226
|
+
const symbol = getReferencedSymbol(typeChecker, node);
|
|
227
|
+
const importedSpecifier = importedSpecifiers.find((specifier) => specifier.local.text === node.text && (!specifier.symbol || specifier.symbol === symbol));
|
|
228
|
+
if (importedSpecifier) references.get(importedSpecifier)?.push(node);
|
|
229
|
+
}
|
|
230
|
+
forEachChild(node, collectReferences);
|
|
231
|
+
}
|
|
232
|
+
collectReferences(node);
|
|
233
|
+
for (const statement of node.statements) {
|
|
234
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || isTypeImportDeclaration(statement)) continue;
|
|
235
|
+
const typeSpecifiers = [];
|
|
236
|
+
const valueSpecifiers = [];
|
|
237
|
+
const unusedSpecifiers = [];
|
|
238
|
+
for (const specifier of getImportSpecifiers(statement)) {
|
|
239
|
+
if (isInlineTypeSpecifier(specifier)) continue;
|
|
240
|
+
const importedSpecifier = importedSpecifiers.find(({ specifier: found }) => found === specifier);
|
|
241
|
+
const specifierReferences = importedSpecifier ? references.get(importedSpecifier) : void 0;
|
|
242
|
+
if (!specifierReferences?.length) unusedSpecifiers.push(specifier);
|
|
243
|
+
else if (specifierReferences.every(isOnlyTypeReference)) typeSpecifiers.push(specifier);
|
|
244
|
+
else valueSpecifiers.push(specifier);
|
|
245
|
+
}
|
|
246
|
+
if (typeSpecifiers.length) {
|
|
247
|
+
const sourceImports = sourceImportsMap[getImportSource(statement, sourceFile)];
|
|
248
|
+
if (!sourceImports) continue;
|
|
249
|
+
sourceImports.reportValueImports.push({
|
|
250
|
+
node: statement,
|
|
251
|
+
typeSpecifiers,
|
|
252
|
+
unusedSpecifiers,
|
|
253
|
+
valueSpecifiers
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const sourceImports of Object.values(sourceImportsMap)) {
|
|
258
|
+
if (!sourceImports.reportValueImports.length) continue;
|
|
259
|
+
for (const report of sourceImports.reportValueImports) {
|
|
260
|
+
const fix = getTypeImportFix(report, sourceFile, options.fixStyle);
|
|
261
|
+
if (!report.valueSpecifiers.length && !report.unusedSpecifiers.length && !report.node.attributes?.elements.length) {
|
|
262
|
+
context.report({
|
|
263
|
+
fix,
|
|
264
|
+
message: "typeOverValue",
|
|
265
|
+
range: getTSNodeRange(report.node, sourceFile)
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const importNames = report.typeSpecifiers.map((specifier) => `"${getSpecifierLocal(specifier).text}"`);
|
|
270
|
+
context.report({
|
|
271
|
+
data: { typeImports: formatWordList(importNames) },
|
|
272
|
+
fix,
|
|
273
|
+
message: "someImportsAreOnlyTypes",
|
|
274
|
+
range: getTSNodeRange(report.node, sourceFile)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} } };
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
//#endregion
|
|
282
|
+
export { typeImports_default as default };
|
|
283
|
+
|
|
284
|
+
//# sourceMappingURL=typeImports.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AST } from "@flint.fyi/typescript-language";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
export declare const forEachChild: <T>(node: AST.AnyNode, cbNode: (node: AST.AnyNode) => T | undefined, cbNodes?: (nodes: ts.NodeArray<AST.AnyNode>) => T | undefined) => T | undefined;
|
|
4
|
+
//# sourceMappingURL=forEachChild.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flint.fyi/ts",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "[Experimental] TypeScript language plugin for Flint.",
|
|
5
5
|
"homepage": "https://flint.fyi/rules/ts",
|
|
6
6
|
"repository": {
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"ts-api-utils": "^2.4.0",
|
|
30
30
|
"typescript": "^5.9.3 || ^6.0.0",
|
|
31
31
|
"zod": "^4.3.6",
|
|
32
|
-
"@flint.fyi/core": "^0.
|
|
33
|
-
"@flint.fyi/
|
|
34
|
-
"@flint.fyi/
|
|
32
|
+
"@flint.fyi/core": "^0.22.0",
|
|
33
|
+
"@flint.fyi/utils": "^0.14.1",
|
|
34
|
+
"@flint.fyi/typescript-language": "^0.18.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"tsdown": "0.21.0",
|
|
38
38
|
"vitest": "4.1.0",
|
|
39
|
-
"@flint.fyi/rule-tester": "^0.16.
|
|
39
|
+
"@flint.fyi/rule-tester": "^0.16.3"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=24.0.0"
|