@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE.md +20 -0
- package/lib/collectReferencedFilePaths.d.ts +2 -0
- package/lib/collectReferencedFilePaths.js +21 -0
- package/lib/createTypeScriptFileFromProgram.d.ts +3 -0
- package/lib/createTypeScriptFileFromProgram.js +56 -0
- package/lib/createTypeScriptFileFromProjectService.d.ts +3 -0
- package/lib/createTypeScriptFileFromProjectService.js +17 -0
- package/lib/formatDiagnostic.d.ts +10 -0
- package/lib/formatDiagnostic.js +121 -0
- package/lib/getNodeRange.d.ts +3 -0
- package/lib/getNodeRange.js +6 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/language.d.ts +7 -0
- package/lib/language.js +62 -0
- package/lib/nodes.d.ts +141 -0
- package/lib/nodes.js +1 -0
- package/lib/normalizeRange.d.ts +3 -0
- package/lib/normalizeRange.js +16 -0
- package/lib/plugin.d.ts +15 -0
- package/lib/plugin.js +11 -0
- package/lib/rules/consecutiveNonNullAssertions.d.ts +5 -0
- package/lib/rules/consecutiveNonNullAssertions.js +39 -0
- package/lib/rules/consecutiveNonNullAssertions.test.d.ts +1 -0
- package/lib/rules/consecutiveNonNullAssertions.test.js +32 -0
- package/lib/rules/forInArrays.d.ts +5 -0
- package/lib/rules/forInArrays.js +50 -0
- package/lib/rules/forInArrays.test.d.ts +1 -0
- package/lib/rules/forInArrays.test.js +24 -0
- package/lib/rules/namespaceDeclarations.d.ts +9 -0
- package/lib/rules/namespaceDeclarations.js +48 -0
- package/lib/rules/namespaceDeclarations.test.d.ts +1 -0
- package/lib/rules/namespaceDeclarations.test.js +40 -0
- package/lib/rules/ruleTester.d.ts +2 -0
- package/lib/rules/ruleTester.js +3 -0
- package/lib/rules/utils/getConstrainedType.d.ts +2 -0
- package/lib/rules/utils/getConstrainedType.js +4 -0
- package/lib/rules/utils/isTypeRecursive.d.ts +2 -0
- package/lib/rules/utils/isTypeRecursive.js +5 -0
- package/package.json +34 -0
- package/src/collectReferencedFilePaths.ts +37 -0
- package/src/createTypeScriptFileFromProgram.ts +77 -0
- package/src/createTypeScriptFileFromProjectService.ts +29 -0
- package/src/formatDiagnostic.ts +166 -0
- package/src/getNodeRange.ts +13 -0
- package/src/index.ts +2 -0
- package/src/language.ts +112 -0
- package/src/nodes.ts +145 -0
- package/src/normalizeRange.ts +30 -0
- package/src/plugin.ts +13 -0
- package/src/rules/consecutiveNonNullAssertions.test.ts +33 -0
- package/src/rules/consecutiveNonNullAssertions.ts +43 -0
- package/src/rules/forInArrays.test.ts +25 -0
- package/src/rules/forInArrays.ts +67 -0
- package/src/rules/namespaceDeclarations.test.ts +41 -0
- package/src/rules/namespaceDeclarations.ts +58 -0
- package/src/rules/ruleTester.ts +4 -0
- package/src/rules/utils/getConstrainedType.ts +9 -0
- package/src/rules/utils/isTypeRecursive.ts +10 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as tsutils from "ts-api-utils";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { typescriptLanguage } from "../language.js";
|
|
4
|
+
import { getConstrainedTypeAtLocation } from "./utils/getConstrainedType.js";
|
|
5
|
+
import { isTypeRecursive } from "./utils/isTypeRecursive.js";
|
|
6
|
+
export default typescriptLanguage.createRule({
|
|
7
|
+
about: {
|
|
8
|
+
id: "forInArrays",
|
|
9
|
+
preset: "logical",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
forIn: {
|
|
13
|
+
primary: "For-in loops over arrays have surprising behavior that often leads to bugs.",
|
|
14
|
+
secondary: [
|
|
15
|
+
"A for-in loop (`for (const i in o)`) iterates over all enumerable properties of an object, including those that are not array indices.",
|
|
16
|
+
"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.",
|
|
17
|
+
"It also returns the index key (`i`) as a string, which is not the expected numeric type for array indices.",
|
|
18
|
+
],
|
|
19
|
+
suggestions: [
|
|
20
|
+
"Use a construct more suited for arrays, such as a for-of loop (`for (const i of o)`).",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
setup(context) {
|
|
25
|
+
function hasNumberLikeLength(type) {
|
|
26
|
+
const lengthProperty = type.getProperty("length");
|
|
27
|
+
if (lengthProperty == null) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return tsutils.isTypeFlagSet(context.typeChecker.getTypeOfSymbol(lengthProperty), ts.TypeFlags.NumberLike);
|
|
31
|
+
}
|
|
32
|
+
function isArrayLike(type) {
|
|
33
|
+
return isTypeRecursive(type, (t) => t.getNumberIndexType() != null && hasNumberLikeLength(t));
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ForInStatement(node) {
|
|
37
|
+
const type = getConstrainedTypeAtLocation(node.expression, context.typeChecker);
|
|
38
|
+
if (isArrayLike(type)) {
|
|
39
|
+
context.report({
|
|
40
|
+
message: "forIn",
|
|
41
|
+
range: {
|
|
42
|
+
begin: node.getStart(),
|
|
43
|
+
end: node.statement.getStart() - 1,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import rule from "./forInArrays.js";
|
|
2
|
+
import { ruleTester } from "./ruleTester.js";
|
|
3
|
+
ruleTester.describe(rule, {
|
|
4
|
+
invalid: [
|
|
5
|
+
{
|
|
6
|
+
code: `
|
|
7
|
+
declare const array: string[];
|
|
8
|
+
for (const i in array) {}
|
|
9
|
+
`,
|
|
10
|
+
snapshot: `
|
|
11
|
+
declare const array: string[];
|
|
12
|
+
for (const i in array) {}
|
|
13
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
14
|
+
For-in loops over arrays have surprising behavior that often leads to bugs.
|
|
15
|
+
`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
valid: [
|
|
19
|
+
`
|
|
20
|
+
declare const array: string[];
|
|
21
|
+
for (const i of array) {}
|
|
22
|
+
`,
|
|
23
|
+
],
|
|
24
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const _default: import("@flint.fyi/core").Rule<{
|
|
3
|
+
readonly id: "namespaceDeclarations";
|
|
4
|
+
readonly preset: "logical";
|
|
5
|
+
}, import("../nodes.js").TSNodesByName, import("../language.js").TypeScriptServices, "preferModules", {
|
|
6
|
+
readonly allowDeclarations: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
readonly allowDefinitionFiles: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as tsutils from "ts-api-utils";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getNodeRange } from "../getNodeRange.js";
|
|
5
|
+
import { typescriptLanguage } from "../language.js";
|
|
6
|
+
export default typescriptLanguage.createRule({
|
|
7
|
+
about: {
|
|
8
|
+
id: "namespaceDeclarations",
|
|
9
|
+
preset: "logical",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
preferModules: {
|
|
13
|
+
primary: "Prefer using ECMAScript modules over legacy TypeScript namespaces.",
|
|
14
|
+
secondary: [
|
|
15
|
+
"Namespaces are a legacy feature of TypeScript that can lead to confusion and are not compatible with ECMAScript modules.",
|
|
16
|
+
],
|
|
17
|
+
suggestions: [
|
|
18
|
+
"Modern codebases generally use `export` and `import` statements to define and use ECMAScript modules instead.",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
allowDeclarations: z.boolean().default(false),
|
|
24
|
+
allowDefinitionFiles: z.boolean().default(false),
|
|
25
|
+
},
|
|
26
|
+
setup(context, { allowDeclarations, allowDefinitionFiles }) {
|
|
27
|
+
if (allowDefinitionFiles && context.sourceFile.isDeclarationFile) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
ModuleDeclaration(node) {
|
|
32
|
+
if (node.parent.kind !== ts.SyntaxKind.SourceFile ||
|
|
33
|
+
node.name.kind !== ts.SyntaxKind.Identifier ||
|
|
34
|
+
node.name.text === "global") {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (allowDeclarations &&
|
|
38
|
+
tsutils.includesModifier(node.modifiers, ts.SyntaxKind.DeclareKeyword)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
context.report({
|
|
42
|
+
message: "preferModules",
|
|
43
|
+
range: getNodeRange(node.getChildAt(0), context.sourceFile),
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import rule from "./namespaceDeclarations.js";
|
|
2
|
+
import { ruleTester } from "./ruleTester.js";
|
|
3
|
+
ruleTester.describe(rule, {
|
|
4
|
+
invalid: [
|
|
5
|
+
{
|
|
6
|
+
code: `
|
|
7
|
+
namespace name {}
|
|
8
|
+
`,
|
|
9
|
+
snapshot: `
|
|
10
|
+
namespace name {}
|
|
11
|
+
~~~~~~~~~
|
|
12
|
+
Prefer using ECMAScript modules over legacy TypeScript namespaces.
|
|
13
|
+
`,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
valid: [
|
|
17
|
+
`declare global {}`,
|
|
18
|
+
`declare module 'name' {}`,
|
|
19
|
+
{
|
|
20
|
+
code: `declare module name {}`,
|
|
21
|
+
options: { allowDeclarations: true },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: `declare namespace name {}`,
|
|
25
|
+
options: { allowDeclarations: true },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: `
|
|
29
|
+
declare namespace outer {
|
|
30
|
+
namespace inner {}
|
|
31
|
+
}`,
|
|
32
|
+
options: { allowDeclarations: true },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: `namespace name {}`,
|
|
36
|
+
fileName: "file.d.ts",
|
|
37
|
+
options: { allowDefinitionFiles: true },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flint.fyi/ts",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "[Experimental] TypeScript language plugin for Flint.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/JoshuaKGoldberg/flint",
|
|
8
|
+
"directory": "packages/ts"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "JoshuaKGoldberg",
|
|
13
|
+
"email": "npm@joshuakgoldberg.com"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./lib/index.js",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@flint.fyi/core": "",
|
|
19
|
+
"@flint.fyi/rule-tester": "",
|
|
20
|
+
"@typescript-eslint/project-service": "^8.35.0",
|
|
21
|
+
"@typescript/vfs": "^1.6.1",
|
|
22
|
+
"cached-factory": "^0.1.0",
|
|
23
|
+
"debug-for-file": "^0.2.0",
|
|
24
|
+
"ts-api-utils": "^2.1.0",
|
|
25
|
+
"typescript": ">=5.8.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=24.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
export function collectReferencedFilePaths(
|
|
5
|
+
program: ts.Program,
|
|
6
|
+
sourceFile: ts.SourceFile,
|
|
7
|
+
) {
|
|
8
|
+
// TODO: Also handle inline import()s
|
|
9
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/115
|
|
10
|
+
return sourceFile.statements
|
|
11
|
+
.filter(isImportDeclarationWithStringLiteral)
|
|
12
|
+
.map((statement) => {
|
|
13
|
+
const resolved = ts.resolveModuleName(
|
|
14
|
+
statement.moduleSpecifier.text,
|
|
15
|
+
sourceFile.fileName,
|
|
16
|
+
program.getCompilerOptions(),
|
|
17
|
+
// TODO: Eventually, the file system should be abstracted
|
|
18
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/73
|
|
19
|
+
ts.sys,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
resolved.resolvedModule?.isExternalLibraryImport === false &&
|
|
24
|
+
path.relative(process.cwd(), resolved.resolvedModule.resolvedFileName)
|
|
25
|
+
);
|
|
26
|
+
})
|
|
27
|
+
.filter((resolvedFileName) => resolvedFileName !== false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isImportDeclarationWithStringLiteral(
|
|
31
|
+
statement: ts.Statement,
|
|
32
|
+
): statement is ts.ImportDeclaration & { moduleSpecifier: ts.StringLiteral } {
|
|
33
|
+
return (
|
|
34
|
+
ts.isImportDeclaration(statement) &&
|
|
35
|
+
ts.isStringLiteral(statement.moduleSpecifier)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LanguageFileDefinition,
|
|
3
|
+
NormalizedRuleReport,
|
|
4
|
+
RuleReport,
|
|
5
|
+
} from "@flint.fyi/core";
|
|
6
|
+
import * as ts from "typescript";
|
|
7
|
+
|
|
8
|
+
import { collectReferencedFilePaths } from "./collectReferencedFilePaths.js";
|
|
9
|
+
import { formatDiagnostic } from "./formatDiagnostic.js";
|
|
10
|
+
import { normalizeRange } from "./normalizeRange.js";
|
|
11
|
+
|
|
12
|
+
export function createTypeScriptFileFromProgram(
|
|
13
|
+
program: ts.Program,
|
|
14
|
+
sourceFile: ts.SourceFile,
|
|
15
|
+
): LanguageFileDefinition {
|
|
16
|
+
return {
|
|
17
|
+
cache: {
|
|
18
|
+
dependencies: [
|
|
19
|
+
// TODO: Add support for multi-TSConfig workspaces.
|
|
20
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/64 & more.
|
|
21
|
+
"tsconfig.json",
|
|
22
|
+
|
|
23
|
+
...collectReferencedFilePaths(program, sourceFile),
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
getDiagnostics() {
|
|
27
|
+
return ts
|
|
28
|
+
.getPreEmitDiagnostics(program, sourceFile)
|
|
29
|
+
.map((diagnostic) => ({
|
|
30
|
+
code: `TS${diagnostic.code}`,
|
|
31
|
+
text: formatDiagnostic({
|
|
32
|
+
...diagnostic,
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
34
|
+
length: diagnostic.length!,
|
|
35
|
+
message: ts.flattenDiagnosticMessageText(
|
|
36
|
+
diagnostic.messageText,
|
|
37
|
+
"\n",
|
|
38
|
+
),
|
|
39
|
+
name: `TS${diagnostic.code}`,
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41
|
+
start: diagnostic.start!,
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
runRule(rule, options) {
|
|
46
|
+
const reports: NormalizedRuleReport[] = [];
|
|
47
|
+
|
|
48
|
+
const context = {
|
|
49
|
+
report: (report: RuleReport) => {
|
|
50
|
+
reports.push({
|
|
51
|
+
...report,
|
|
52
|
+
message: rule.messages[report.message],
|
|
53
|
+
range: normalizeRange(report.range, sourceFile),
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
sourceFile,
|
|
57
|
+
typeChecker: program.getTypeChecker(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const visitors = rule.setup(context, options);
|
|
61
|
+
|
|
62
|
+
if (!visitors) {
|
|
63
|
+
return reports;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const visit = (node: ts.Node) => {
|
|
67
|
+
visitors[ts.SyntaxKind[node.kind]]?.(node);
|
|
68
|
+
|
|
69
|
+
node.forEachChild(visit);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
sourceFile.forEachChild(visit);
|
|
73
|
+
|
|
74
|
+
return reports;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LanguageFileDefinition } from "@flint.fyi/core";
|
|
2
|
+
import { debugForFile } from "debug-for-file";
|
|
3
|
+
import * as ts from "typescript";
|
|
4
|
+
|
|
5
|
+
import { createTypeScriptFileFromProgram } from "./createTypeScriptFileFromProgram.js";
|
|
6
|
+
|
|
7
|
+
const log = debugForFile(import.meta.filename);
|
|
8
|
+
|
|
9
|
+
export function createTypeScriptFileFromProjectService(
|
|
10
|
+
filePathAbsolute: string,
|
|
11
|
+
program: ts.Program,
|
|
12
|
+
service: ts.server.ProjectService,
|
|
13
|
+
): LanguageFileDefinition {
|
|
14
|
+
const sourceFile = program.getSourceFile(filePathAbsolute);
|
|
15
|
+
if (!sourceFile) {
|
|
16
|
+
throw new Error(`Could not retrieve source file for: ${filePathAbsolute}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
log("Retrieved source file and type checker for file %s:", filePathAbsolute);
|
|
20
|
+
|
|
21
|
+
const file = createTypeScriptFileFromProgram(program, sourceFile);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...file,
|
|
25
|
+
[Symbol.dispose]() {
|
|
26
|
+
service.closeClientFile(filePathAbsolute);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Adapted from: https://github.com/ArnaudBarre/tsl/blob/742a6f1a956705239f2149f856b1f572ade79919/src/formatDiagnostic.ts
|
|
2
|
+
// ...which notes:
|
|
3
|
+
// Adapted from: https://github.com/microsoft/TypeScript/blob/78c16795cdee70b9d9f0f248b6dbb6ba50994a59/src/compiler/program.ts#L680-L811
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
7
|
+
|
|
8
|
+
import ts, {
|
|
9
|
+
flattenDiagnosticMessageText,
|
|
10
|
+
getLineAndCharacterOfPosition,
|
|
11
|
+
getPositionOfLineAndCharacter,
|
|
12
|
+
type SourceFile,
|
|
13
|
+
} from "typescript";
|
|
14
|
+
|
|
15
|
+
export interface RawDiagnostic {
|
|
16
|
+
file?: ts.SourceFile;
|
|
17
|
+
length: number;
|
|
18
|
+
message: string;
|
|
19
|
+
name: string;
|
|
20
|
+
relatedInformation?: ts.DiagnosticRelatedInformation[];
|
|
21
|
+
start: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatDiagnostic(diagnostic: RawDiagnostic) {
|
|
25
|
+
let output = "";
|
|
26
|
+
|
|
27
|
+
if (diagnostic.file !== undefined) {
|
|
28
|
+
output += formatLocation(diagnostic.file, diagnostic.start);
|
|
29
|
+
output += " - ";
|
|
30
|
+
}
|
|
31
|
+
output += color(diagnostic.name, COLOR.Grey);
|
|
32
|
+
output += ": ";
|
|
33
|
+
output += diagnostic.message;
|
|
34
|
+
if (diagnostic.file !== undefined) {
|
|
35
|
+
output += "\n";
|
|
36
|
+
output += formatCodeSpan(
|
|
37
|
+
diagnostic.file,
|
|
38
|
+
diagnostic.start,
|
|
39
|
+
diagnostic.length,
|
|
40
|
+
"",
|
|
41
|
+
COLOR.Red,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (diagnostic.relatedInformation) {
|
|
45
|
+
output += "\n";
|
|
46
|
+
for (const {
|
|
47
|
+
file,
|
|
48
|
+
length,
|
|
49
|
+
messageText,
|
|
50
|
+
start,
|
|
51
|
+
} of diagnostic.relatedInformation) {
|
|
52
|
+
const indent = " ";
|
|
53
|
+
if (file) {
|
|
54
|
+
output += "\n";
|
|
55
|
+
output += " " + formatLocation(file, start!);
|
|
56
|
+
output += formatCodeSpan(file, start!, length!, indent, COLOR.Cyan);
|
|
57
|
+
}
|
|
58
|
+
output += "\n";
|
|
59
|
+
output += indent + flattenDiagnosticMessageText(messageText, "\n");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return output;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function color(text: string, formatStyle: string) {
|
|
67
|
+
return formatStyle + text + resetEscapeSequence;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const gutterStyleSequence = "\u001b[7m";
|
|
71
|
+
const ellipsis = "...";
|
|
72
|
+
const gutterSeparator = " ";
|
|
73
|
+
const resetEscapeSequence = "\u001b[0m";
|
|
74
|
+
const COLOR = {
|
|
75
|
+
Blue: "\u001b[94m",
|
|
76
|
+
Cyan: "\u001b[96m",
|
|
77
|
+
Grey: "\u001b[90m",
|
|
78
|
+
Red: "\u001b[91m",
|
|
79
|
+
Yellow: "\u001b[93m",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function displayFilename(name: string) {
|
|
83
|
+
if (name.startsWith("./")) {
|
|
84
|
+
return name.slice(2);
|
|
85
|
+
}
|
|
86
|
+
return name.slice(process.cwd().length + 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatCodeSpan(
|
|
90
|
+
file: SourceFile,
|
|
91
|
+
start: number,
|
|
92
|
+
length: number,
|
|
93
|
+
indent: string,
|
|
94
|
+
squiggleColor: string,
|
|
95
|
+
) {
|
|
96
|
+
const { character: firstLineChar, line: firstLine } =
|
|
97
|
+
getLineAndCharacterOfPosition(file, start);
|
|
98
|
+
const { character: lastLineChar, line: lastLine } =
|
|
99
|
+
getLineAndCharacterOfPosition(file, start + length);
|
|
100
|
+
const lastLineInFile = getLineAndCharacterOfPosition(
|
|
101
|
+
file,
|
|
102
|
+
file.text.length,
|
|
103
|
+
).line;
|
|
104
|
+
const hasMoreThanFiveLines = lastLine - firstLine >= 4;
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
106
|
+
let gutterWidth = (lastLine + 1 + "").length;
|
|
107
|
+
if (hasMoreThanFiveLines) {
|
|
108
|
+
gutterWidth = Math.max(ellipsis.length, gutterWidth);
|
|
109
|
+
}
|
|
110
|
+
let context = "";
|
|
111
|
+
for (let i = firstLine; i <= lastLine; i++) {
|
|
112
|
+
context += "\n";
|
|
113
|
+
if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) {
|
|
114
|
+
context +=
|
|
115
|
+
indent +
|
|
116
|
+
color(ellipsis.padStart(gutterWidth), gutterStyleSequence) +
|
|
117
|
+
gutterSeparator +
|
|
118
|
+
"\n";
|
|
119
|
+
i = lastLine - 1;
|
|
120
|
+
}
|
|
121
|
+
const lineStart = getPositionOfLineAndCharacter(file, i, 0);
|
|
122
|
+
const lineEnd =
|
|
123
|
+
i < lastLineInFile
|
|
124
|
+
? getPositionOfLineAndCharacter(file, i + 1, 0)
|
|
125
|
+
: file.text.length;
|
|
126
|
+
let lineContent = file.text.slice(lineStart, lineEnd);
|
|
127
|
+
lineContent = lineContent.trimEnd();
|
|
128
|
+
lineContent = lineContent.replace(/\t/g, " ");
|
|
129
|
+
context +=
|
|
130
|
+
indent +
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
132
|
+
color((i + 1 + "").padStart(gutterWidth), gutterStyleSequence) +
|
|
133
|
+
gutterSeparator;
|
|
134
|
+
context += lineContent + "\n";
|
|
135
|
+
context +=
|
|
136
|
+
indent +
|
|
137
|
+
color("".padStart(gutterWidth), gutterStyleSequence) +
|
|
138
|
+
gutterSeparator;
|
|
139
|
+
context += squiggleColor;
|
|
140
|
+
if (i === firstLine) {
|
|
141
|
+
const lastCharForLine = i === lastLine ? lastLineChar : void 0;
|
|
142
|
+
context += lineContent.slice(0, firstLineChar).replace(/\S/g, " ");
|
|
143
|
+
context += lineContent
|
|
144
|
+
.slice(firstLineChar, lastCharForLine)
|
|
145
|
+
.replace(/./g, "~");
|
|
146
|
+
} else if (i === lastLine) {
|
|
147
|
+
context += lineContent.slice(0, lastLineChar).replace(/./g, "~");
|
|
148
|
+
} else {
|
|
149
|
+
context += lineContent.replace(/./g, "~");
|
|
150
|
+
}
|
|
151
|
+
context += resetEscapeSequence;
|
|
152
|
+
}
|
|
153
|
+
return context;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatLocation(file: SourceFile, start: number): string {
|
|
157
|
+
const { character, line } = getLineAndCharacterOfPosition(file, start);
|
|
158
|
+
const relativeFileName = displayFilename(file.fileName);
|
|
159
|
+
let output = "";
|
|
160
|
+
output += color(relativeFileName, COLOR.Cyan);
|
|
161
|
+
output += ":";
|
|
162
|
+
output += color(`${line + 1}`, COLOR.Yellow);
|
|
163
|
+
output += ":";
|
|
164
|
+
output += color(`${character + 1}`, COLOR.Yellow);
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import { CharacterReportRange } from "@flint.fyi/core";
|
|
4
|
+
|
|
5
|
+
export function getNodeRange(
|
|
6
|
+
node: ts.Node,
|
|
7
|
+
sourceFile: ts.SourceFile,
|
|
8
|
+
): CharacterReportRange {
|
|
9
|
+
return {
|
|
10
|
+
begin: node.getStart(sourceFile),
|
|
11
|
+
end: node.getEnd(),
|
|
12
|
+
};
|
|
13
|
+
}
|
package/src/index.ts
ADDED
package/src/language.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createLanguage } from "@flint.fyi/core";
|
|
2
|
+
import { createProjectService } from "@typescript-eslint/project-service";
|
|
3
|
+
import {
|
|
4
|
+
createFSBackedSystem,
|
|
5
|
+
createVirtualTypeScriptEnvironment,
|
|
6
|
+
} from "@typescript/vfs";
|
|
7
|
+
import { CachedFactory } from "cached-factory";
|
|
8
|
+
import { debugForFile } from "debug-for-file";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import * as ts from "typescript";
|
|
11
|
+
|
|
12
|
+
import { createTypeScriptFileFromProgram } from "./createTypeScriptFileFromProgram.js";
|
|
13
|
+
import { createTypeScriptFileFromProjectService } from "./createTypeScriptFileFromProjectService.js";
|
|
14
|
+
import { TSNodesByName } from "./nodes.js";
|
|
15
|
+
|
|
16
|
+
const log = debugForFile(import.meta.filename);
|
|
17
|
+
|
|
18
|
+
const projectRoot = path.join(import.meta.dirname, "../..");
|
|
19
|
+
|
|
20
|
+
export interface TypeScriptServices {
|
|
21
|
+
sourceFile: ts.SourceFile;
|
|
22
|
+
typeChecker: ts.TypeChecker;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const typescriptLanguage = createLanguage<
|
|
26
|
+
TSNodesByName,
|
|
27
|
+
TypeScriptServices
|
|
28
|
+
>({
|
|
29
|
+
about: {
|
|
30
|
+
name: "TypeScript",
|
|
31
|
+
},
|
|
32
|
+
prepare: () => {
|
|
33
|
+
const { service } = createProjectService();
|
|
34
|
+
const seenPrograms = new Set<ts.Program>();
|
|
35
|
+
|
|
36
|
+
const environments = new CachedFactory((filePathAbsolute: string) => {
|
|
37
|
+
const system = createFSBackedSystem(
|
|
38
|
+
new Map([[filePathAbsolute, "// ..."]]),
|
|
39
|
+
projectRoot,
|
|
40
|
+
ts,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return createVirtualTypeScriptEnvironment(
|
|
44
|
+
system,
|
|
45
|
+
[filePathAbsolute],
|
|
46
|
+
ts,
|
|
47
|
+
{
|
|
48
|
+
skipLibCheck: true,
|
|
49
|
+
target: ts.ScriptTarget.ESNext,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const servicePrograms = new CachedFactory((filePathAbsolute: string) => {
|
|
55
|
+
log("Opening client file:", filePathAbsolute);
|
|
56
|
+
service.openClientFile(filePathAbsolute);
|
|
57
|
+
|
|
58
|
+
log("Retrieving client services:", filePathAbsolute);
|
|
59
|
+
const scriptInfo = service.getScriptInfo(filePathAbsolute);
|
|
60
|
+
if (!scriptInfo) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Could not find script info for file: ${filePathAbsolute}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const defaultProject = service.getDefaultProjectForFile(
|
|
67
|
+
scriptInfo.fileName,
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
if (!defaultProject) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Could not find default project for file: ${filePathAbsolute}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const program = defaultProject.getLanguageService(true).getProgram();
|
|
77
|
+
if (!program) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Could not retrieve program for file: ${filePathAbsolute}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return program;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
prepareFileOnDisk: (filePathAbsolute) => {
|
|
88
|
+
const program = servicePrograms.get(filePathAbsolute);
|
|
89
|
+
|
|
90
|
+
seenPrograms.add(program);
|
|
91
|
+
|
|
92
|
+
return createTypeScriptFileFromProjectService(
|
|
93
|
+
filePathAbsolute,
|
|
94
|
+
program,
|
|
95
|
+
service,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
prepareFileVirtually: (filePathAbsolute, sourceText) => {
|
|
99
|
+
const environment = environments.get(filePathAbsolute);
|
|
100
|
+
environment.updateFile(filePathAbsolute, sourceText);
|
|
101
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
102
|
+
const sourceFile = environment.getSourceFile(filePathAbsolute)!;
|
|
103
|
+
const program = environment.languageService.getProgram()!;
|
|
104
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
105
|
+
|
|
106
|
+
seenPrograms.add(program);
|
|
107
|
+
|
|
108
|
+
return createTypeScriptFileFromProgram(program, sourceFile);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|