@flint.fyi/ts 0.13.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 (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE.md +20 -0
  3. package/lib/collectReferencedFilePaths.d.ts +2 -0
  4. package/lib/collectReferencedFilePaths.js +21 -0
  5. package/lib/createTypeScriptFileFromProgram.d.ts +3 -0
  6. package/lib/createTypeScriptFileFromProgram.js +56 -0
  7. package/lib/createTypeScriptFileFromProjectService.d.ts +3 -0
  8. package/lib/createTypeScriptFileFromProjectService.js +17 -0
  9. package/lib/formatDiagnostic.d.ts +10 -0
  10. package/lib/formatDiagnostic.js +121 -0
  11. package/lib/getNodeRange.d.ts +3 -0
  12. package/lib/getNodeRange.js +6 -0
  13. package/lib/index.d.ts +2 -0
  14. package/lib/index.js +2 -0
  15. package/lib/language.d.ts +7 -0
  16. package/lib/language.js +62 -0
  17. package/lib/nodes.d.ts +141 -0
  18. package/lib/nodes.js +1 -0
  19. package/lib/normalizeRange.d.ts +3 -0
  20. package/lib/normalizeRange.js +16 -0
  21. package/lib/plugin.d.ts +15 -0
  22. package/lib/plugin.js +11 -0
  23. package/lib/rules/consecutiveNonNullAssertions.d.ts +5 -0
  24. package/lib/rules/consecutiveNonNullAssertions.js +39 -0
  25. package/lib/rules/consecutiveNonNullAssertions.test.d.ts +1 -0
  26. package/lib/rules/consecutiveNonNullAssertions.test.js +32 -0
  27. package/lib/rules/forInArrays.d.ts +5 -0
  28. package/lib/rules/forInArrays.js +50 -0
  29. package/lib/rules/forInArrays.test.d.ts +1 -0
  30. package/lib/rules/forInArrays.test.js +24 -0
  31. package/lib/rules/namespaceDeclarations.d.ts +9 -0
  32. package/lib/rules/namespaceDeclarations.js +48 -0
  33. package/lib/rules/namespaceDeclarations.test.d.ts +1 -0
  34. package/lib/rules/namespaceDeclarations.test.js +40 -0
  35. package/lib/rules/ruleTester.d.ts +2 -0
  36. package/lib/rules/ruleTester.js +3 -0
  37. package/lib/rules/utils/getConstrainedType.d.ts +2 -0
  38. package/lib/rules/utils/getConstrainedType.js +4 -0
  39. package/lib/rules/utils/isTypeRecursive.d.ts +2 -0
  40. package/lib/rules/utils/isTypeRecursive.js +5 -0
  41. package/package.json +34 -0
  42. package/src/collectReferencedFilePaths.ts +37 -0
  43. package/src/createTypeScriptFileFromProgram.ts +77 -0
  44. package/src/createTypeScriptFileFromProjectService.ts +29 -0
  45. package/src/formatDiagnostic.ts +166 -0
  46. package/src/getNodeRange.ts +13 -0
  47. package/src/index.ts +2 -0
  48. package/src/language.ts +112 -0
  49. package/src/nodes.ts +145 -0
  50. package/src/normalizeRange.ts +30 -0
  51. package/src/plugin.ts +13 -0
  52. package/src/rules/consecutiveNonNullAssertions.test.ts +33 -0
  53. package/src/rules/consecutiveNonNullAssertions.ts +43 -0
  54. package/src/rules/forInArrays.test.ts +25 -0
  55. package/src/rules/forInArrays.ts +67 -0
  56. package/src/rules/namespaceDeclarations.test.ts +41 -0
  57. package/src/rules/namespaceDeclarations.ts +58 -0
  58. package/src/rules/ruleTester.ts +4 -0
  59. package/src/rules/utils/getConstrainedType.ts +9 -0
  60. package/src/rules/utils/isTypeRecursive.ts +10 -0
  61. package/tsconfig.json +9 -0
  62. package/tsconfig.tsbuildinfo +1 -0
package/src/nodes.ts ADDED
@@ -0,0 +1,145 @@
1
+ import type * as ts from "typescript";
2
+
3
+ // TODO: Surely there's a better way to do this...
4
+ // ...but I haven't checked how to do it without slow type operations.
5
+
6
+ export interface TSNodesByName {
7
+ ArrayBindingPattern: ts.ArrayBindingPattern;
8
+ ArrayLiteralExpression: ts.ArrayLiteralExpression;
9
+ ArrowFunction: ts.ArrowFunction;
10
+ AsExpression: ts.AsExpression;
11
+ AwaitExpression: ts.AwaitExpression;
12
+ BigIntLiteral: ts.BigIntLiteral;
13
+ BinaryExpression: ts.BinaryExpression;
14
+ BindingElement: ts.BindingElement;
15
+ Block: ts.Block;
16
+ BreakStatement: ts.BreakStatement;
17
+ Bundle: ts.Bundle;
18
+ CallExpression: ts.CallExpression;
19
+ CaseBlock: ts.CaseBlock;
20
+ CaseClause: ts.CaseClause;
21
+ CatchClause: ts.CatchClause;
22
+ ClassDeclaration: ts.ClassDeclaration;
23
+ ClassExpression: ts.ClassExpression;
24
+ ClassStaticBlockDeclaration: ts.ClassStaticBlockDeclaration;
25
+ CommaListExpression: ts.CommaListExpression;
26
+ ComputedPropertyName: ts.ComputedPropertyName;
27
+ ConditionalExpression: ts.ConditionalExpression;
28
+ ConditionalType: ts.ConditionalType;
29
+ ContinueStatement: ts.ContinueStatement;
30
+ DebuggerStatement: ts.DebuggerStatement;
31
+ Decorator: ts.Decorator;
32
+ DefaultClause: ts.DefaultClause;
33
+ DeleteExpression: ts.DeleteExpression;
34
+ DoStatement: ts.DoStatement;
35
+ ElementAccessExpression: ts.ElementAccessExpression;
36
+ EmptyStatement: ts.EmptyStatement;
37
+ EnumDeclaration: ts.EnumDeclaration;
38
+ EnumMember: ts.EnumMember;
39
+ ExportAssignment: ts.ExportAssignment;
40
+ ExportDeclaration: ts.ExportDeclaration;
41
+ ExportSpecifier: ts.ExportSpecifier;
42
+ ExpressionStatement: ts.ExpressionStatement;
43
+ ExpressionWithTypeArguments: ts.ExpressionWithTypeArguments;
44
+ ExternalModuleReference: ts.ExternalModuleReference;
45
+ ForInStatement: ts.ForInStatement;
46
+ ForOfStatement: ts.ForOfStatement;
47
+ ForStatement: ts.ForStatement;
48
+ FunctionDeclaration: ts.FunctionDeclaration;
49
+ FunctionExpression: ts.FunctionExpression;
50
+ HeritageClause: ts.HeritageClause;
51
+ Identifier: ts.Identifier;
52
+ IfStatement: ts.IfStatement;
53
+ ImportAttribute: ts.ImportAttribute;
54
+ ImportAttributes: ts.ImportAttributes;
55
+ ImportClause: ts.ImportClause;
56
+ ImportDeclaration: ts.ImportDeclaration;
57
+ ImportEqualsDeclaration: ts.ImportEqualsDeclaration;
58
+ ImportSpecifier: ts.ImportSpecifier;
59
+ IndexedAccessType: ts.IndexedAccessType;
60
+ InterfaceDeclaration: ts.InterfaceDeclaration;
61
+ IntersectionType: ts.IntersectionType;
62
+ JsxAttribute: ts.JsxAttribute;
63
+ JsxAttributes: ts.JsxAttributes;
64
+ JsxClosingElement: ts.JsxClosingElement;
65
+ JsxClosingFragment: ts.JsxClosingFragment;
66
+ JsxElement: ts.JsxElement;
67
+ JsxExpression: ts.JsxExpression;
68
+ JsxFragment: ts.JsxFragment;
69
+ JsxNamespacedName: ts.JsxNamespacedName;
70
+ JsxOpeningElement: ts.JsxOpeningElement;
71
+ JsxOpeningFragment: ts.JsxOpeningFragment;
72
+ JsxSelfClosingElement: ts.JsxSelfClosingElement;
73
+ JsxSpreadAttribute: ts.JsxSpreadAttribute;
74
+ JsxText: ts.JsxText;
75
+ LabeledStatement: ts.LabeledStatement;
76
+ LiteralType: ts.LiteralType;
77
+ MappedTypeNode: ts.MappedTypeNode;
78
+ MetaProperty: ts.MetaProperty;
79
+ MethodDeclaration: ts.MethodDeclaration;
80
+ MethodSignature: ts.MethodSignature;
81
+ MissingDeclaration: ts.MissingDeclaration;
82
+ ModuleBlock: ts.ModuleBlock;
83
+ ModuleDeclaration: ts.ModuleDeclaration;
84
+ NamedExports: ts.NamedExports;
85
+ NamedImports: ts.NamedImports;
86
+ NamedTupleMember: ts.NamedTupleMember;
87
+ NamespaceExport: ts.NamespaceExport;
88
+ NamespaceExportDeclaration: ts.NamespaceExportDeclaration;
89
+ NamespaceImport: ts.NamespaceImport;
90
+ NewExpression: ts.NewExpression;
91
+ NonNullExpression: ts.NonNullExpression;
92
+ NoSubstitutionTemplateLiteral: ts.NoSubstitutionTemplateLiteral;
93
+ NotEmittedStatement: ts.NotEmittedStatement;
94
+ NotEmittedTypeElement: ts.NotEmittedTypeElement;
95
+ NumericLiteral: ts.NumericLiteral;
96
+ ObjectBindingPattern: ts.ObjectBindingPattern;
97
+ ObjectLiteralExpression: ts.ObjectLiteralExpression;
98
+ OmittedExpression: ts.OmittedExpression;
99
+ ParenthesizedExpression: ts.ParenthesizedExpression;
100
+ PartiallyEmittedExpression: ts.PartiallyEmittedExpression;
101
+ PostfixUnaryExpression: ts.PostfixUnaryExpression;
102
+ PrefixUnaryExpression: ts.PrefixUnaryExpression;
103
+ PrivateIdentifier: ts.PrivateIdentifier;
104
+ PropertyAccessExpression: ts.PropertyAccessExpression;
105
+ PropertyAssignment: ts.PropertyAssignment;
106
+ PropertyDeclaration: ts.PropertyDeclaration;
107
+ PropertySignature: ts.PropertySignature;
108
+ QualifiedName: ts.QualifiedName;
109
+ RegularExpressionLiteral: ts.RegularExpressionLiteral;
110
+ ReturnStatement: ts.ReturnStatement;
111
+ SatisfiesExpression: ts.SatisfiesExpression;
112
+ SemicolonClassElement: ts.SemicolonClassElement;
113
+ ShorthandPropertyAssignment: ts.ShorthandPropertyAssignment;
114
+ SourceFile: ts.SourceFile;
115
+ SpreadAssignment: ts.SpreadAssignment;
116
+ SpreadElement: ts.SpreadElement;
117
+ StringLiteral: ts.StringLiteral;
118
+ SwitchStatement: ts.SwitchStatement;
119
+ SyntaxList: ts.SyntaxList;
120
+ SyntheticExpression: ts.SyntheticExpression;
121
+ TaggedTemplateExpression: ts.TaggedTemplateExpression;
122
+ TemplateExpression: ts.TemplateExpression;
123
+ TemplateHead: ts.TemplateHead;
124
+ TemplateLiteralType: ts.TemplateLiteralType;
125
+ TemplateLiteralTypeSpan: ts.TemplateLiteralTypeSpan;
126
+ TemplateMiddle: ts.TemplateMiddle;
127
+ TemplateSpan: ts.TemplateSpan;
128
+ TemplateTail: ts.TemplateTail;
129
+ ThrowStatement: ts.ThrowStatement;
130
+ TryStatement: ts.TryStatement;
131
+ TupleType: ts.TupleType;
132
+ TypeAliasDeclaration: ts.TypeAliasDeclaration;
133
+ TypeOfExpression: ts.TypeOfExpression;
134
+ TypeParameter: ts.TypeParameter;
135
+ TypePredicate: ts.TypePredicate;
136
+ TypeReference: ts.TypeReference;
137
+ UnionType: ts.UnionType;
138
+ VariableDeclaration: ts.VariableDeclaration;
139
+ VariableDeclarationList: ts.VariableDeclarationList;
140
+ VariableStatement: ts.VariableStatement;
141
+ VoidExpression: ts.VoidExpression;
142
+ WhileStatement: ts.WhileStatement;
143
+ WithStatement: ts.WithStatement;
144
+ YieldExpression: ts.YieldExpression;
145
+ }
@@ -0,0 +1,30 @@
1
+ import type * as ts from "typescript";
2
+
3
+ import {
4
+ CharacterReportRange,
5
+ NormalizedReportRangeObject,
6
+ } from "@flint.fyi/core";
7
+
8
+ export function normalizeRange(
9
+ original: CharacterReportRange,
10
+ sourceFile: ts.SourceFile,
11
+ ): NormalizedReportRangeObject {
12
+ const onCharacters = isNode(original)
13
+ ? { begin: original.getStart(), end: original.getEnd() }
14
+ : original;
15
+
16
+ return {
17
+ begin: normalizeRangePosition(onCharacters.begin, sourceFile),
18
+ end: normalizeRangePosition(onCharacters.end, sourceFile),
19
+ };
20
+ }
21
+
22
+ function isNode(value: unknown): value is ts.Node {
23
+ return typeof value === "object" && value !== null && "kind" in value;
24
+ }
25
+
26
+ function normalizeRangePosition(raw: number, sourceFile: ts.SourceFile) {
27
+ const { character, line } = sourceFile.getLineAndCharacterOfPosition(raw);
28
+
29
+ return { column: character, line, raw };
30
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { createPlugin } from "@flint.fyi/core";
2
+
3
+ import consecutiveNonNullAssertions from "./rules/consecutiveNonNullAssertions.js";
4
+ import forInArrays from "./rules/forInArrays.js";
5
+ import namespaceDeclarations from "./rules/namespaceDeclarations.js";
6
+
7
+ export const ts = createPlugin({
8
+ globs: {
9
+ all: ["**/*.{cjs,js,jsx,mjs,ts,tsx}"],
10
+ },
11
+ name: "ts",
12
+ rules: [forInArrays, consecutiveNonNullAssertions, namespaceDeclarations],
13
+ });
@@ -0,0 +1,33 @@
1
+ import rule from "./consecutiveNonNullAssertions.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ declare const outer: { inner: number } | null;
9
+ outer!!.inner;
10
+ `,
11
+ output: `
12
+ declare const outer: { inner: number } | null;
13
+ outer!.inner;
14
+ `,
15
+ snapshot: `
16
+ declare const outer: { inner: number } | null;
17
+ outer!!.inner;
18
+ ~~
19
+ Consecutive non-null assertion operators are unnecessary.
20
+ `,
21
+ },
22
+ ],
23
+ valid: [
24
+ `
25
+ declare const outer: { inner: number } | null;
26
+ outer!.inner;
27
+ `,
28
+ `
29
+ declare const outer: { inner: number } | null;
30
+ outer?.inner!;
31
+ `,
32
+ ],
33
+ });
@@ -0,0 +1,43 @@
1
+ import * as ts from "typescript";
2
+
3
+ import { typescriptLanguage } from "../language.js";
4
+
5
+ export default typescriptLanguage.createRule({
6
+ about: {
7
+ id: "consecutiveNonNullAssertions",
8
+ preset: "logical",
9
+ },
10
+ messages: {
11
+ consecutiveNonNullAssertion: {
12
+ primary: "Consecutive non-null assertion operators are unnecessary.",
13
+ secondary: [
14
+ "The non-null assertion operator (`!`) is used to assert that a value is not null or undefined.",
15
+ "Using it multiple times in a row does not do anything, and just takes up space unnecessarily.",
16
+ ],
17
+ suggestions: ["Remove the redundant non-null assertion operator."],
18
+ },
19
+ },
20
+ setup(context) {
21
+ return {
22
+ NonNullExpression(node) {
23
+ if (node.parent.kind !== ts.SyntaxKind.NonNullExpression) {
24
+ return;
25
+ }
26
+
27
+ const range = {
28
+ begin: node.end,
29
+ end: node.parent.end + 1,
30
+ };
31
+
32
+ context.report({
33
+ fix: {
34
+ range,
35
+ text: "",
36
+ },
37
+ message: "consecutiveNonNullAssertion",
38
+ range,
39
+ });
40
+ },
41
+ };
42
+ },
43
+ });
@@ -0,0 +1,25 @@
1
+ import rule from "./forInArrays.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ declare const array: string[];
9
+ for (const i in array) {}
10
+ `,
11
+ snapshot: `
12
+ declare const array: string[];
13
+ for (const i in array) {}
14
+ ~~~~~~~~~~~~~~~~~~~~~~
15
+ For-in loops over arrays have surprising behavior that often leads to bugs.
16
+ `,
17
+ },
18
+ ],
19
+ valid: [
20
+ `
21
+ declare const array: string[];
22
+ for (const i of array) {}
23
+ `,
24
+ ],
25
+ });
@@ -0,0 +1,67 @@
1
+ import * as tsutils from "ts-api-utils";
2
+ import * as ts from "typescript";
3
+
4
+ import { typescriptLanguage } from "../language.js";
5
+ import { getConstrainedTypeAtLocation } from "./utils/getConstrainedType.js";
6
+ import { isTypeRecursive } from "./utils/isTypeRecursive.js";
7
+
8
+ export default typescriptLanguage.createRule({
9
+ about: {
10
+ id: "forInArrays",
11
+ preset: "logical",
12
+ },
13
+ messages: {
14
+ forIn: {
15
+ primary:
16
+ "For-in loops over arrays have surprising behavior that often leads to bugs.",
17
+ secondary: [
18
+ "A for-in loop (`for (const i in o)`) iterates over all enumerable properties of an object, including those that are not array indices.",
19
+ "This can lead to unexpected behavior when used with arrays, as it may include properties that are not part of the array's numeric indices.",
20
+ "It also returns the index key (`i`) as a string, which is not the expected numeric type for array indices.",
21
+ ],
22
+ suggestions: [
23
+ "Use a construct more suited for arrays, such as a for-of loop (`for (const i of o)`).",
24
+ ],
25
+ },
26
+ },
27
+ setup(context) {
28
+ function hasNumberLikeLength(type: ts.Type): boolean {
29
+ const lengthProperty = type.getProperty("length");
30
+
31
+ if (lengthProperty == null) {
32
+ return false;
33
+ }
34
+
35
+ return tsutils.isTypeFlagSet(
36
+ context.typeChecker.getTypeOfSymbol(lengthProperty),
37
+ ts.TypeFlags.NumberLike,
38
+ );
39
+ }
40
+
41
+ function isArrayLike(type: ts.Type): boolean {
42
+ return isTypeRecursive(
43
+ type,
44
+ (t) => t.getNumberIndexType() != null && hasNumberLikeLength(t),
45
+ );
46
+ }
47
+
48
+ return {
49
+ ForInStatement(node) {
50
+ const type = getConstrainedTypeAtLocation(
51
+ node.expression,
52
+ context.typeChecker,
53
+ );
54
+
55
+ if (isArrayLike(type)) {
56
+ context.report({
57
+ message: "forIn",
58
+ range: {
59
+ begin: node.getStart(),
60
+ end: node.statement.getStart() - 1,
61
+ },
62
+ });
63
+ }
64
+ },
65
+ };
66
+ },
67
+ });
@@ -0,0 +1,41 @@
1
+ import rule from "./namespaceDeclarations.js";
2
+ import { ruleTester } from "./ruleTester.js";
3
+
4
+ ruleTester.describe(rule, {
5
+ invalid: [
6
+ {
7
+ code: `
8
+ namespace name {}
9
+ `,
10
+ snapshot: `
11
+ namespace name {}
12
+ ~~~~~~~~~
13
+ Prefer using ECMAScript modules over legacy TypeScript namespaces.
14
+ `,
15
+ },
16
+ ],
17
+ valid: [
18
+ `declare global {}`,
19
+ `declare module 'name' {}`,
20
+ {
21
+ code: `declare module name {}`,
22
+ options: { allowDeclarations: true },
23
+ },
24
+ {
25
+ code: `declare namespace name {}`,
26
+ options: { allowDeclarations: true },
27
+ },
28
+ {
29
+ code: `
30
+ declare namespace outer {
31
+ namespace inner {}
32
+ }`,
33
+ options: { allowDeclarations: true },
34
+ },
35
+ {
36
+ code: `namespace name {}`,
37
+ fileName: "file.d.ts",
38
+ options: { allowDefinitionFiles: true },
39
+ },
40
+ ],
41
+ });
@@ -0,0 +1,58 @@
1
+ import * as tsutils from "ts-api-utils";
2
+ import * as ts from "typescript";
3
+ import { z } from "zod";
4
+
5
+ import { getNodeRange } from "../getNodeRange.js";
6
+ import { typescriptLanguage } from "../language.js";
7
+
8
+ export default typescriptLanguage.createRule({
9
+ about: {
10
+ id: "namespaceDeclarations",
11
+ preset: "logical",
12
+ },
13
+ messages: {
14
+ preferModules: {
15
+ primary:
16
+ "Prefer using ECMAScript modules over legacy TypeScript namespaces.",
17
+ secondary: [
18
+ "Namespaces are a legacy feature of TypeScript that can lead to confusion and are not compatible with ECMAScript modules.",
19
+ ],
20
+ suggestions: [
21
+ "Modern codebases generally use `export` and `import` statements to define and use ECMAScript modules instead.",
22
+ ],
23
+ },
24
+ },
25
+ options: {
26
+ allowDeclarations: z.boolean().default(false),
27
+ allowDefinitionFiles: z.boolean().default(false),
28
+ },
29
+ setup(context, { allowDeclarations, allowDefinitionFiles }) {
30
+ if (allowDefinitionFiles && context.sourceFile.isDeclarationFile) {
31
+ return;
32
+ }
33
+
34
+ return {
35
+ ModuleDeclaration(node) {
36
+ if (
37
+ node.parent.kind !== ts.SyntaxKind.SourceFile ||
38
+ node.name.kind !== ts.SyntaxKind.Identifier ||
39
+ node.name.text === "global"
40
+ ) {
41
+ return;
42
+ }
43
+
44
+ if (
45
+ allowDeclarations &&
46
+ tsutils.includesModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)
47
+ ) {
48
+ return;
49
+ }
50
+
51
+ context.report({
52
+ message: "preferModules",
53
+ range: getNodeRange(node.getChildAt(0), context.sourceFile),
54
+ });
55
+ },
56
+ };
57
+ },
58
+ });
@@ -0,0 +1,4 @@
1
+ import { RuleTester } from "@flint.fyi/rule-tester";
2
+ import { describe, it } from "vitest";
3
+
4
+ export const ruleTester = new RuleTester({ describe, it });
@@ -0,0 +1,9 @@
1
+ import * as ts from "typescript";
2
+
3
+ export function getConstrainedTypeAtLocation(
4
+ node: ts.Node,
5
+ typeChecker: ts.TypeChecker,
6
+ ) {
7
+ const type = typeChecker.getTypeAtLocation(node);
8
+ return typeChecker.getBaseConstraintOfType(type) ?? type;
9
+ }
@@ -0,0 +1,10 @@
1
+ import * as ts from "typescript";
2
+
3
+ export function isTypeRecursive(
4
+ type: ts.Type,
5
+ predicate: (t: ts.Type) => boolean,
6
+ ): boolean {
7
+ return type.isUnionOrIntersection()
8
+ ? type.types.some((subType) => isTypeRecursive(subType, predicate))
9
+ : predicate(type);
10
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "lib",
4
+ "rootDir": "src"
5
+ },
6
+ "extends": "../../tsconfig.base.json",
7
+ "include": ["src"],
8
+ "references": [{ "path": "../core" }, { "path": "../rule-tester" }]
9
+ }