@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
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
export function collectReferencedFilePaths(program, sourceFile) {
|
|
4
|
+
// TODO: Also handle inline import()s
|
|
5
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/115
|
|
6
|
+
return sourceFile.statements
|
|
7
|
+
.filter(isImportDeclarationWithStringLiteral)
|
|
8
|
+
.map((statement) => {
|
|
9
|
+
const resolved = ts.resolveModuleName(statement.moduleSpecifier.text, sourceFile.fileName, program.getCompilerOptions(),
|
|
10
|
+
// TODO: Eventually, the file system should be abstracted
|
|
11
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/73
|
|
12
|
+
ts.sys);
|
|
13
|
+
return (resolved.resolvedModule?.isExternalLibraryImport === false &&
|
|
14
|
+
path.relative(process.cwd(), resolved.resolvedModule.resolvedFileName));
|
|
15
|
+
})
|
|
16
|
+
.filter((resolvedFileName) => resolvedFileName !== false);
|
|
17
|
+
}
|
|
18
|
+
function isImportDeclarationWithStringLiteral(statement) {
|
|
19
|
+
return (ts.isImportDeclaration(statement) &&
|
|
20
|
+
ts.isStringLiteral(statement.moduleSpecifier));
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { collectReferencedFilePaths } from "./collectReferencedFilePaths.js";
|
|
3
|
+
import { formatDiagnostic } from "./formatDiagnostic.js";
|
|
4
|
+
import { normalizeRange } from "./normalizeRange.js";
|
|
5
|
+
export function createTypeScriptFileFromProgram(program, sourceFile) {
|
|
6
|
+
return {
|
|
7
|
+
cache: {
|
|
8
|
+
dependencies: [
|
|
9
|
+
// TODO: Add support for multi-TSConfig workspaces.
|
|
10
|
+
// https://github.com/JoshuaKGoldberg/flint/issues/64 & more.
|
|
11
|
+
"tsconfig.json",
|
|
12
|
+
...collectReferencedFilePaths(program, sourceFile),
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
getDiagnostics() {
|
|
16
|
+
return ts
|
|
17
|
+
.getPreEmitDiagnostics(program, sourceFile)
|
|
18
|
+
.map((diagnostic) => ({
|
|
19
|
+
code: `TS${diagnostic.code}`,
|
|
20
|
+
text: formatDiagnostic({
|
|
21
|
+
...diagnostic,
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
23
|
+
length: diagnostic.length,
|
|
24
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
|
25
|
+
name: `TS${diagnostic.code}`,
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
27
|
+
start: diagnostic.start,
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
runRule(rule, options) {
|
|
32
|
+
const reports = [];
|
|
33
|
+
const context = {
|
|
34
|
+
report: (report) => {
|
|
35
|
+
reports.push({
|
|
36
|
+
...report,
|
|
37
|
+
message: rule.messages[report.message],
|
|
38
|
+
range: normalizeRange(report.range, sourceFile),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
sourceFile,
|
|
42
|
+
typeChecker: program.getTypeChecker(),
|
|
43
|
+
};
|
|
44
|
+
const visitors = rule.setup(context, options);
|
|
45
|
+
if (!visitors) {
|
|
46
|
+
return reports;
|
|
47
|
+
}
|
|
48
|
+
const visit = (node) => {
|
|
49
|
+
visitors[ts.SyntaxKind[node.kind]]?.(node);
|
|
50
|
+
node.forEachChild(visit);
|
|
51
|
+
};
|
|
52
|
+
sourceFile.forEachChild(visit);
|
|
53
|
+
return reports;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { debugForFile } from "debug-for-file";
|
|
2
|
+
import { createTypeScriptFileFromProgram } from "./createTypeScriptFileFromProgram.js";
|
|
3
|
+
const log = debugForFile(import.meta.filename);
|
|
4
|
+
export function createTypeScriptFileFromProjectService(filePathAbsolute, program, service) {
|
|
5
|
+
const sourceFile = program.getSourceFile(filePathAbsolute);
|
|
6
|
+
if (!sourceFile) {
|
|
7
|
+
throw new Error(`Could not retrieve source file for: ${filePathAbsolute}`);
|
|
8
|
+
}
|
|
9
|
+
log("Retrieved source file and type checker for file %s:", filePathAbsolute);
|
|
10
|
+
const file = createTypeScriptFileFromProgram(program, sourceFile);
|
|
11
|
+
return {
|
|
12
|
+
...file,
|
|
13
|
+
[Symbol.dispose]() {
|
|
14
|
+
service.closeClientFile(filePathAbsolute);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export interface RawDiagnostic {
|
|
3
|
+
file?: ts.SourceFile;
|
|
4
|
+
length: number;
|
|
5
|
+
message: string;
|
|
6
|
+
name: string;
|
|
7
|
+
relatedInformation?: ts.DiagnosticRelatedInformation[];
|
|
8
|
+
start: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function formatDiagnostic(diagnostic: RawDiagnostic): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
// eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
6
|
+
import { flattenDiagnosticMessageText, getLineAndCharacterOfPosition, getPositionOfLineAndCharacter, } from "typescript";
|
|
7
|
+
export function formatDiagnostic(diagnostic) {
|
|
8
|
+
let output = "";
|
|
9
|
+
if (diagnostic.file !== undefined) {
|
|
10
|
+
output += formatLocation(diagnostic.file, diagnostic.start);
|
|
11
|
+
output += " - ";
|
|
12
|
+
}
|
|
13
|
+
output += color(diagnostic.name, COLOR.Grey);
|
|
14
|
+
output += ": ";
|
|
15
|
+
output += diagnostic.message;
|
|
16
|
+
if (diagnostic.file !== undefined) {
|
|
17
|
+
output += "\n";
|
|
18
|
+
output += formatCodeSpan(diagnostic.file, diagnostic.start, diagnostic.length, "", COLOR.Red);
|
|
19
|
+
}
|
|
20
|
+
if (diagnostic.relatedInformation) {
|
|
21
|
+
output += "\n";
|
|
22
|
+
for (const { file, length, messageText, start, } of diagnostic.relatedInformation) {
|
|
23
|
+
const indent = " ";
|
|
24
|
+
if (file) {
|
|
25
|
+
output += "\n";
|
|
26
|
+
output += " " + formatLocation(file, start);
|
|
27
|
+
output += formatCodeSpan(file, start, length, indent, COLOR.Cyan);
|
|
28
|
+
}
|
|
29
|
+
output += "\n";
|
|
30
|
+
output += indent + flattenDiagnosticMessageText(messageText, "\n");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
35
|
+
function color(text, formatStyle) {
|
|
36
|
+
return formatStyle + text + resetEscapeSequence;
|
|
37
|
+
}
|
|
38
|
+
const gutterStyleSequence = "\u001b[7m";
|
|
39
|
+
const ellipsis = "...";
|
|
40
|
+
const gutterSeparator = " ";
|
|
41
|
+
const resetEscapeSequence = "\u001b[0m";
|
|
42
|
+
const COLOR = {
|
|
43
|
+
Blue: "\u001b[94m",
|
|
44
|
+
Cyan: "\u001b[96m",
|
|
45
|
+
Grey: "\u001b[90m",
|
|
46
|
+
Red: "\u001b[91m",
|
|
47
|
+
Yellow: "\u001b[93m",
|
|
48
|
+
};
|
|
49
|
+
function displayFilename(name) {
|
|
50
|
+
if (name.startsWith("./")) {
|
|
51
|
+
return name.slice(2);
|
|
52
|
+
}
|
|
53
|
+
return name.slice(process.cwd().length + 1);
|
|
54
|
+
}
|
|
55
|
+
function formatCodeSpan(file, start, length, indent, squiggleColor) {
|
|
56
|
+
const { character: firstLineChar, line: firstLine } = getLineAndCharacterOfPosition(file, start);
|
|
57
|
+
const { character: lastLineChar, line: lastLine } = getLineAndCharacterOfPosition(file, start + length);
|
|
58
|
+
const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line;
|
|
59
|
+
const hasMoreThanFiveLines = lastLine - firstLine >= 4;
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
61
|
+
let gutterWidth = (lastLine + 1 + "").length;
|
|
62
|
+
if (hasMoreThanFiveLines) {
|
|
63
|
+
gutterWidth = Math.max(ellipsis.length, gutterWidth);
|
|
64
|
+
}
|
|
65
|
+
let context = "";
|
|
66
|
+
for (let i = firstLine; i <= lastLine; i++) {
|
|
67
|
+
context += "\n";
|
|
68
|
+
if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) {
|
|
69
|
+
context +=
|
|
70
|
+
indent +
|
|
71
|
+
color(ellipsis.padStart(gutterWidth), gutterStyleSequence) +
|
|
72
|
+
gutterSeparator +
|
|
73
|
+
"\n";
|
|
74
|
+
i = lastLine - 1;
|
|
75
|
+
}
|
|
76
|
+
const lineStart = getPositionOfLineAndCharacter(file, i, 0);
|
|
77
|
+
const lineEnd = i < lastLineInFile
|
|
78
|
+
? getPositionOfLineAndCharacter(file, i + 1, 0)
|
|
79
|
+
: file.text.length;
|
|
80
|
+
let lineContent = file.text.slice(lineStart, lineEnd);
|
|
81
|
+
lineContent = lineContent.trimEnd();
|
|
82
|
+
lineContent = lineContent.replace(/\t/g, " ");
|
|
83
|
+
context +=
|
|
84
|
+
indent +
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
86
|
+
color((i + 1 + "").padStart(gutterWidth), gutterStyleSequence) +
|
|
87
|
+
gutterSeparator;
|
|
88
|
+
context += lineContent + "\n";
|
|
89
|
+
context +=
|
|
90
|
+
indent +
|
|
91
|
+
color("".padStart(gutterWidth), gutterStyleSequence) +
|
|
92
|
+
gutterSeparator;
|
|
93
|
+
context += squiggleColor;
|
|
94
|
+
if (i === firstLine) {
|
|
95
|
+
const lastCharForLine = i === lastLine ? lastLineChar : void 0;
|
|
96
|
+
context += lineContent.slice(0, firstLineChar).replace(/\S/g, " ");
|
|
97
|
+
context += lineContent
|
|
98
|
+
.slice(firstLineChar, lastCharForLine)
|
|
99
|
+
.replace(/./g, "~");
|
|
100
|
+
}
|
|
101
|
+
else if (i === lastLine) {
|
|
102
|
+
context += lineContent.slice(0, lastLineChar).replace(/./g, "~");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
context += lineContent.replace(/./g, "~");
|
|
106
|
+
}
|
|
107
|
+
context += resetEscapeSequence;
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
function formatLocation(file, start) {
|
|
112
|
+
const { character, line } = getLineAndCharacterOfPosition(file, start);
|
|
113
|
+
const relativeFileName = displayFilename(file.fileName);
|
|
114
|
+
let output = "";
|
|
115
|
+
output += color(relativeFileName, COLOR.Cyan);
|
|
116
|
+
output += ":";
|
|
117
|
+
output += color(`${line + 1}`, COLOR.Yellow);
|
|
118
|
+
output += ":";
|
|
119
|
+
output += color(`${character + 1}`, COLOR.Yellow);
|
|
120
|
+
return output;
|
|
121
|
+
}
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { TSNodesByName } from "./nodes.js";
|
|
3
|
+
export interface TypeScriptServices {
|
|
4
|
+
sourceFile: ts.SourceFile;
|
|
5
|
+
typeChecker: ts.TypeChecker;
|
|
6
|
+
}
|
|
7
|
+
export declare const typescriptLanguage: import("@flint.fyi/core").Language<TSNodesByName, TypeScriptServices>;
|
package/lib/language.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createLanguage } from "@flint.fyi/core";
|
|
2
|
+
import { createProjectService } from "@typescript-eslint/project-service";
|
|
3
|
+
import { createFSBackedSystem, createVirtualTypeScriptEnvironment, } from "@typescript/vfs";
|
|
4
|
+
import { CachedFactory } from "cached-factory";
|
|
5
|
+
import { debugForFile } from "debug-for-file";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import * as ts from "typescript";
|
|
8
|
+
import { createTypeScriptFileFromProgram } from "./createTypeScriptFileFromProgram.js";
|
|
9
|
+
import { createTypeScriptFileFromProjectService } from "./createTypeScriptFileFromProjectService.js";
|
|
10
|
+
const log = debugForFile(import.meta.filename);
|
|
11
|
+
const projectRoot = path.join(import.meta.dirname, "../..");
|
|
12
|
+
export const typescriptLanguage = createLanguage({
|
|
13
|
+
about: {
|
|
14
|
+
name: "TypeScript",
|
|
15
|
+
},
|
|
16
|
+
prepare: () => {
|
|
17
|
+
const { service } = createProjectService();
|
|
18
|
+
const seenPrograms = new Set();
|
|
19
|
+
const environments = new CachedFactory((filePathAbsolute) => {
|
|
20
|
+
const system = createFSBackedSystem(new Map([[filePathAbsolute, "// ..."]]), projectRoot, ts);
|
|
21
|
+
return createVirtualTypeScriptEnvironment(system, [filePathAbsolute], ts, {
|
|
22
|
+
skipLibCheck: true,
|
|
23
|
+
target: ts.ScriptTarget.ESNext,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
const servicePrograms = new CachedFactory((filePathAbsolute) => {
|
|
27
|
+
log("Opening client file:", filePathAbsolute);
|
|
28
|
+
service.openClientFile(filePathAbsolute);
|
|
29
|
+
log("Retrieving client services:", filePathAbsolute);
|
|
30
|
+
const scriptInfo = service.getScriptInfo(filePathAbsolute);
|
|
31
|
+
if (!scriptInfo) {
|
|
32
|
+
throw new Error(`Could not find script info for file: ${filePathAbsolute}`);
|
|
33
|
+
}
|
|
34
|
+
const defaultProject = service.getDefaultProjectForFile(scriptInfo.fileName, true);
|
|
35
|
+
if (!defaultProject) {
|
|
36
|
+
throw new Error(`Could not find default project for file: ${filePathAbsolute}`);
|
|
37
|
+
}
|
|
38
|
+
const program = defaultProject.getLanguageService(true).getProgram();
|
|
39
|
+
if (!program) {
|
|
40
|
+
throw new Error(`Could not retrieve program for file: ${filePathAbsolute}`);
|
|
41
|
+
}
|
|
42
|
+
return program;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
prepareFileOnDisk: (filePathAbsolute) => {
|
|
46
|
+
const program = servicePrograms.get(filePathAbsolute);
|
|
47
|
+
seenPrograms.add(program);
|
|
48
|
+
return createTypeScriptFileFromProjectService(filePathAbsolute, program, service);
|
|
49
|
+
},
|
|
50
|
+
prepareFileVirtually: (filePathAbsolute, sourceText) => {
|
|
51
|
+
const environment = environments.get(filePathAbsolute);
|
|
52
|
+
environment.updateFile(filePathAbsolute, sourceText);
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
54
|
+
const sourceFile = environment.getSourceFile(filePathAbsolute);
|
|
55
|
+
const program = environment.languageService.getProgram();
|
|
56
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
57
|
+
seenPrograms.add(program);
|
|
58
|
+
return createTypeScriptFileFromProgram(program, sourceFile);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|
package/lib/nodes.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
export interface TSNodesByName {
|
|
3
|
+
ArrayBindingPattern: ts.ArrayBindingPattern;
|
|
4
|
+
ArrayLiteralExpression: ts.ArrayLiteralExpression;
|
|
5
|
+
ArrowFunction: ts.ArrowFunction;
|
|
6
|
+
AsExpression: ts.AsExpression;
|
|
7
|
+
AwaitExpression: ts.AwaitExpression;
|
|
8
|
+
BigIntLiteral: ts.BigIntLiteral;
|
|
9
|
+
BinaryExpression: ts.BinaryExpression;
|
|
10
|
+
BindingElement: ts.BindingElement;
|
|
11
|
+
Block: ts.Block;
|
|
12
|
+
BreakStatement: ts.BreakStatement;
|
|
13
|
+
Bundle: ts.Bundle;
|
|
14
|
+
CallExpression: ts.CallExpression;
|
|
15
|
+
CaseBlock: ts.CaseBlock;
|
|
16
|
+
CaseClause: ts.CaseClause;
|
|
17
|
+
CatchClause: ts.CatchClause;
|
|
18
|
+
ClassDeclaration: ts.ClassDeclaration;
|
|
19
|
+
ClassExpression: ts.ClassExpression;
|
|
20
|
+
ClassStaticBlockDeclaration: ts.ClassStaticBlockDeclaration;
|
|
21
|
+
CommaListExpression: ts.CommaListExpression;
|
|
22
|
+
ComputedPropertyName: ts.ComputedPropertyName;
|
|
23
|
+
ConditionalExpression: ts.ConditionalExpression;
|
|
24
|
+
ConditionalType: ts.ConditionalType;
|
|
25
|
+
ContinueStatement: ts.ContinueStatement;
|
|
26
|
+
DebuggerStatement: ts.DebuggerStatement;
|
|
27
|
+
Decorator: ts.Decorator;
|
|
28
|
+
DefaultClause: ts.DefaultClause;
|
|
29
|
+
DeleteExpression: ts.DeleteExpression;
|
|
30
|
+
DoStatement: ts.DoStatement;
|
|
31
|
+
ElementAccessExpression: ts.ElementAccessExpression;
|
|
32
|
+
EmptyStatement: ts.EmptyStatement;
|
|
33
|
+
EnumDeclaration: ts.EnumDeclaration;
|
|
34
|
+
EnumMember: ts.EnumMember;
|
|
35
|
+
ExportAssignment: ts.ExportAssignment;
|
|
36
|
+
ExportDeclaration: ts.ExportDeclaration;
|
|
37
|
+
ExportSpecifier: ts.ExportSpecifier;
|
|
38
|
+
ExpressionStatement: ts.ExpressionStatement;
|
|
39
|
+
ExpressionWithTypeArguments: ts.ExpressionWithTypeArguments;
|
|
40
|
+
ExternalModuleReference: ts.ExternalModuleReference;
|
|
41
|
+
ForInStatement: ts.ForInStatement;
|
|
42
|
+
ForOfStatement: ts.ForOfStatement;
|
|
43
|
+
ForStatement: ts.ForStatement;
|
|
44
|
+
FunctionDeclaration: ts.FunctionDeclaration;
|
|
45
|
+
FunctionExpression: ts.FunctionExpression;
|
|
46
|
+
HeritageClause: ts.HeritageClause;
|
|
47
|
+
Identifier: ts.Identifier;
|
|
48
|
+
IfStatement: ts.IfStatement;
|
|
49
|
+
ImportAttribute: ts.ImportAttribute;
|
|
50
|
+
ImportAttributes: ts.ImportAttributes;
|
|
51
|
+
ImportClause: ts.ImportClause;
|
|
52
|
+
ImportDeclaration: ts.ImportDeclaration;
|
|
53
|
+
ImportEqualsDeclaration: ts.ImportEqualsDeclaration;
|
|
54
|
+
ImportSpecifier: ts.ImportSpecifier;
|
|
55
|
+
IndexedAccessType: ts.IndexedAccessType;
|
|
56
|
+
InterfaceDeclaration: ts.InterfaceDeclaration;
|
|
57
|
+
IntersectionType: ts.IntersectionType;
|
|
58
|
+
JsxAttribute: ts.JsxAttribute;
|
|
59
|
+
JsxAttributes: ts.JsxAttributes;
|
|
60
|
+
JsxClosingElement: ts.JsxClosingElement;
|
|
61
|
+
JsxClosingFragment: ts.JsxClosingFragment;
|
|
62
|
+
JsxElement: ts.JsxElement;
|
|
63
|
+
JsxExpression: ts.JsxExpression;
|
|
64
|
+
JsxFragment: ts.JsxFragment;
|
|
65
|
+
JsxNamespacedName: ts.JsxNamespacedName;
|
|
66
|
+
JsxOpeningElement: ts.JsxOpeningElement;
|
|
67
|
+
JsxOpeningFragment: ts.JsxOpeningFragment;
|
|
68
|
+
JsxSelfClosingElement: ts.JsxSelfClosingElement;
|
|
69
|
+
JsxSpreadAttribute: ts.JsxSpreadAttribute;
|
|
70
|
+
JsxText: ts.JsxText;
|
|
71
|
+
LabeledStatement: ts.LabeledStatement;
|
|
72
|
+
LiteralType: ts.LiteralType;
|
|
73
|
+
MappedTypeNode: ts.MappedTypeNode;
|
|
74
|
+
MetaProperty: ts.MetaProperty;
|
|
75
|
+
MethodDeclaration: ts.MethodDeclaration;
|
|
76
|
+
MethodSignature: ts.MethodSignature;
|
|
77
|
+
MissingDeclaration: ts.MissingDeclaration;
|
|
78
|
+
ModuleBlock: ts.ModuleBlock;
|
|
79
|
+
ModuleDeclaration: ts.ModuleDeclaration;
|
|
80
|
+
NamedExports: ts.NamedExports;
|
|
81
|
+
NamedImports: ts.NamedImports;
|
|
82
|
+
NamedTupleMember: ts.NamedTupleMember;
|
|
83
|
+
NamespaceExport: ts.NamespaceExport;
|
|
84
|
+
NamespaceExportDeclaration: ts.NamespaceExportDeclaration;
|
|
85
|
+
NamespaceImport: ts.NamespaceImport;
|
|
86
|
+
NewExpression: ts.NewExpression;
|
|
87
|
+
NonNullExpression: ts.NonNullExpression;
|
|
88
|
+
NoSubstitutionTemplateLiteral: ts.NoSubstitutionTemplateLiteral;
|
|
89
|
+
NotEmittedStatement: ts.NotEmittedStatement;
|
|
90
|
+
NotEmittedTypeElement: ts.NotEmittedTypeElement;
|
|
91
|
+
NumericLiteral: ts.NumericLiteral;
|
|
92
|
+
ObjectBindingPattern: ts.ObjectBindingPattern;
|
|
93
|
+
ObjectLiteralExpression: ts.ObjectLiteralExpression;
|
|
94
|
+
OmittedExpression: ts.OmittedExpression;
|
|
95
|
+
ParenthesizedExpression: ts.ParenthesizedExpression;
|
|
96
|
+
PartiallyEmittedExpression: ts.PartiallyEmittedExpression;
|
|
97
|
+
PostfixUnaryExpression: ts.PostfixUnaryExpression;
|
|
98
|
+
PrefixUnaryExpression: ts.PrefixUnaryExpression;
|
|
99
|
+
PrivateIdentifier: ts.PrivateIdentifier;
|
|
100
|
+
PropertyAccessExpression: ts.PropertyAccessExpression;
|
|
101
|
+
PropertyAssignment: ts.PropertyAssignment;
|
|
102
|
+
PropertyDeclaration: ts.PropertyDeclaration;
|
|
103
|
+
PropertySignature: ts.PropertySignature;
|
|
104
|
+
QualifiedName: ts.QualifiedName;
|
|
105
|
+
RegularExpressionLiteral: ts.RegularExpressionLiteral;
|
|
106
|
+
ReturnStatement: ts.ReturnStatement;
|
|
107
|
+
SatisfiesExpression: ts.SatisfiesExpression;
|
|
108
|
+
SemicolonClassElement: ts.SemicolonClassElement;
|
|
109
|
+
ShorthandPropertyAssignment: ts.ShorthandPropertyAssignment;
|
|
110
|
+
SourceFile: ts.SourceFile;
|
|
111
|
+
SpreadAssignment: ts.SpreadAssignment;
|
|
112
|
+
SpreadElement: ts.SpreadElement;
|
|
113
|
+
StringLiteral: ts.StringLiteral;
|
|
114
|
+
SwitchStatement: ts.SwitchStatement;
|
|
115
|
+
SyntaxList: ts.SyntaxList;
|
|
116
|
+
SyntheticExpression: ts.SyntheticExpression;
|
|
117
|
+
TaggedTemplateExpression: ts.TaggedTemplateExpression;
|
|
118
|
+
TemplateExpression: ts.TemplateExpression;
|
|
119
|
+
TemplateHead: ts.TemplateHead;
|
|
120
|
+
TemplateLiteralType: ts.TemplateLiteralType;
|
|
121
|
+
TemplateLiteralTypeSpan: ts.TemplateLiteralTypeSpan;
|
|
122
|
+
TemplateMiddle: ts.TemplateMiddle;
|
|
123
|
+
TemplateSpan: ts.TemplateSpan;
|
|
124
|
+
TemplateTail: ts.TemplateTail;
|
|
125
|
+
ThrowStatement: ts.ThrowStatement;
|
|
126
|
+
TryStatement: ts.TryStatement;
|
|
127
|
+
TupleType: ts.TupleType;
|
|
128
|
+
TypeAliasDeclaration: ts.TypeAliasDeclaration;
|
|
129
|
+
TypeOfExpression: ts.TypeOfExpression;
|
|
130
|
+
TypeParameter: ts.TypeParameter;
|
|
131
|
+
TypePredicate: ts.TypePredicate;
|
|
132
|
+
TypeReference: ts.TypeReference;
|
|
133
|
+
UnionType: ts.UnionType;
|
|
134
|
+
VariableDeclaration: ts.VariableDeclaration;
|
|
135
|
+
VariableDeclarationList: ts.VariableDeclarationList;
|
|
136
|
+
VariableStatement: ts.VariableStatement;
|
|
137
|
+
VoidExpression: ts.VoidExpression;
|
|
138
|
+
WhileStatement: ts.WhileStatement;
|
|
139
|
+
WithStatement: ts.WithStatement;
|
|
140
|
+
YieldExpression: ts.YieldExpression;
|
|
141
|
+
}
|
package/lib/nodes.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function normalizeRange(original, sourceFile) {
|
|
2
|
+
const onCharacters = isNode(original)
|
|
3
|
+
? { begin: original.getStart(), end: original.getEnd() }
|
|
4
|
+
: original;
|
|
5
|
+
return {
|
|
6
|
+
begin: normalizeRangePosition(onCharacters.begin, sourceFile),
|
|
7
|
+
end: normalizeRangePosition(onCharacters.end, sourceFile),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function isNode(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && "kind" in value;
|
|
12
|
+
}
|
|
13
|
+
function normalizeRangePosition(raw, sourceFile) {
|
|
14
|
+
const { character, line } = sourceFile.getLineAndCharacterOfPosition(raw);
|
|
15
|
+
return { column: character, line, raw };
|
|
16
|
+
}
|
package/lib/plugin.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ts: import("@flint.fyi/core").Plugin<import("@flint.fyi/core").RuleAbout, {
|
|
2
|
+
all: string[];
|
|
3
|
+
}, (import("@flint.fyi/core").Rule<{
|
|
4
|
+
readonly id: "consecutiveNonNullAssertions";
|
|
5
|
+
readonly preset: "logical";
|
|
6
|
+
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "consecutiveNonNullAssertion", undefined> | import("@flint.fyi/core").Rule<{
|
|
7
|
+
readonly id: "forInArrays";
|
|
8
|
+
readonly preset: "logical";
|
|
9
|
+
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "forIn", undefined> | import("@flint.fyi/core").Rule<{
|
|
10
|
+
readonly id: "namespaceDeclarations";
|
|
11
|
+
readonly preset: "logical";
|
|
12
|
+
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "preferModules", {
|
|
13
|
+
readonly allowDeclarations: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
14
|
+
readonly allowDefinitionFiles: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
15
|
+
}>)[]>;
|
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createPlugin } from "@flint.fyi/core";
|
|
2
|
+
import consecutiveNonNullAssertions from "./rules/consecutiveNonNullAssertions.js";
|
|
3
|
+
import forInArrays from "./rules/forInArrays.js";
|
|
4
|
+
import namespaceDeclarations from "./rules/namespaceDeclarations.js";
|
|
5
|
+
export const ts = createPlugin({
|
|
6
|
+
globs: {
|
|
7
|
+
all: ["**/*.{cjs,js,jsx,mjs,ts,tsx}"],
|
|
8
|
+
},
|
|
9
|
+
name: "ts",
|
|
10
|
+
rules: [forInArrays, consecutiveNonNullAssertions, namespaceDeclarations],
|
|
11
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
+
readonly id: "consecutiveNonNullAssertions";
|
|
3
|
+
readonly preset: "logical";
|
|
4
|
+
}, import("../nodes.js").TSNodesByName, import("../language.js").TypeScriptServices, "consecutiveNonNullAssertion", undefined>;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { typescriptLanguage } from "../language.js";
|
|
3
|
+
export default typescriptLanguage.createRule({
|
|
4
|
+
about: {
|
|
5
|
+
id: "consecutiveNonNullAssertions",
|
|
6
|
+
preset: "logical",
|
|
7
|
+
},
|
|
8
|
+
messages: {
|
|
9
|
+
consecutiveNonNullAssertion: {
|
|
10
|
+
primary: "Consecutive non-null assertion operators are unnecessary.",
|
|
11
|
+
secondary: [
|
|
12
|
+
"The non-null assertion operator (`!`) is used to assert that a value is not null or undefined.",
|
|
13
|
+
"Using it multiple times in a row does not do anything, and just takes up space unnecessarily.",
|
|
14
|
+
],
|
|
15
|
+
suggestions: ["Remove the redundant non-null assertion operator."],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
setup(context) {
|
|
19
|
+
return {
|
|
20
|
+
NonNullExpression(node) {
|
|
21
|
+
if (node.parent.kind !== ts.SyntaxKind.NonNullExpression) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const range = {
|
|
25
|
+
begin: node.end,
|
|
26
|
+
end: node.parent.end + 1,
|
|
27
|
+
};
|
|
28
|
+
context.report({
|
|
29
|
+
fix: {
|
|
30
|
+
range,
|
|
31
|
+
text: "",
|
|
32
|
+
},
|
|
33
|
+
message: "consecutiveNonNullAssertion",
|
|
34
|
+
range,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import rule from "./consecutiveNonNullAssertions.js";
|
|
2
|
+
import { ruleTester } from "./ruleTester.js";
|
|
3
|
+
ruleTester.describe(rule, {
|
|
4
|
+
invalid: [
|
|
5
|
+
{
|
|
6
|
+
code: `
|
|
7
|
+
declare const outer: { inner: number } | null;
|
|
8
|
+
outer!!.inner;
|
|
9
|
+
`,
|
|
10
|
+
output: `
|
|
11
|
+
declare const outer: { inner: number } | null;
|
|
12
|
+
outer!.inner;
|
|
13
|
+
`,
|
|
14
|
+
snapshot: `
|
|
15
|
+
declare const outer: { inner: number } | null;
|
|
16
|
+
outer!!.inner;
|
|
17
|
+
~~
|
|
18
|
+
Consecutive non-null assertion operators are unnecessary.
|
|
19
|
+
`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
valid: [
|
|
23
|
+
`
|
|
24
|
+
declare const outer: { inner: number } | null;
|
|
25
|
+
outer!.inner;
|
|
26
|
+
`,
|
|
27
|
+
`
|
|
28
|
+
declare const outer: { inner: number } | null;
|
|
29
|
+
outer?.inner!;
|
|
30
|
+
`,
|
|
31
|
+
],
|
|
32
|
+
});
|