@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
@@ -0,0 +1,7 @@
1
+ import ts from "typescript";
2
+ //#region src/rules/utils/forEachChild.ts
3
+ const forEachChild = ts.forEachChild;
4
+ //#endregion
5
+ export { forEachChild };
6
+
7
+ //# sourceMappingURL=forEachChild.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flint.fyi/ts",
3
- "version": "0.19.0",
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.21.2",
33
- "@flint.fyi/typescript-language": "^0.18.1",
34
- "@flint.fyi/utils": "^0.14.1"
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.2"
39
+ "@flint.fyi/rule-tester": "^0.16.3"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=24.0.0"