@flint.fyi/ts 0.13.2 → 0.14.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/CHANGELOG.md +20 -0
- package/lib/createTypeScriptFileFromProgram.js +3 -1
- package/lib/createTypeScriptFileFromProjectService.d.ts +9 -2
- package/lib/createTypeScriptFileFromProjectService.js +6 -3
- package/lib/directives/parseDirectivesFromTypeScriptFile.d.ts +5 -0
- package/lib/directives/parseDirectivesFromTypeScriptFile.js +21 -0
- package/lib/directives/parseDirectivesFromTypeScriptFile.test.d.ts +1 -0
- package/lib/directives/parseDirectivesFromTypeScriptFile.test.js +91 -0
- package/lib/getFirstEnumValues.d.ts +35 -0
- package/lib/getFirstEnumValues.js +51 -0
- package/lib/getFirstEnumValues.test.d.ts +1 -0
- package/lib/getFirstEnumValues.test.js +45 -0
- package/lib/language.js +7 -4
- package/lib/plugin.d.ts +3 -0
- package/lib/prepareTypeScriptFile.d.ts +7 -0
- package/lib/prepareTypeScriptFile.js +7 -0
- package/lib/rules/consecutiveNonNullAssertions.d.ts +1 -0
- package/lib/rules/consecutiveNonNullAssertions.js +3 -2
- package/lib/rules/forInArrays.d.ts +1 -0
- package/lib/rules/forInArrays.js +1 -0
- package/lib/rules/namespaceDeclarations.d.ts +1 -0
- package/lib/rules/namespaceDeclarations.js +9 -2
- package/package.json +1 -1
- package/src/createTypeScriptFileFromProgram.ts +6 -3
- package/src/createTypeScriptFileFromProjectService.ts +7 -5
- package/src/directives/parseDirectivesFromTypeScriptFile.test.ts +108 -0
- package/src/directives/parseDirectivesFromTypeScriptFile.ts +31 -0
- package/src/getFirstEnumValues.test.ts +51 -0
- package/src/getFirstEnumValues.ts +59 -0
- package/src/language.ts +17 -8
- package/src/prepareTypeScriptFile.ts +14 -0
- package/src/rules/consecutiveNonNullAssertions.ts +3 -2
- package/src/rules/forInArrays.ts +1 -0
- package/src/rules/namespaceDeclarations.ts +13 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @flint/ts
|
|
2
2
|
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 27280d3: use first names for ts.SyntaxKind visitors, not aliases
|
|
8
|
+
|
|
9
|
+
## 0.14.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 7d0d873: add // flint-\* comment directives
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [b48f4a9]
|
|
18
|
+
- Updated dependencies [7d0d873]
|
|
19
|
+
- Updated dependencies [79f15da]
|
|
20
|
+
- @flint.fyi/core@0.15.0
|
|
21
|
+
- @flint.fyi/rule-tester@0.14.0
|
|
22
|
+
|
|
3
23
|
## 0.13.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { collectReferencedFilePaths } from "./collectReferencedFilePaths.js";
|
|
3
3
|
import { formatDiagnostic } from "./formatDiagnostic.js";
|
|
4
|
+
import { getFirstEnumValues } from "./getFirstEnumValues.js";
|
|
4
5
|
import { normalizeRange } from "./normalizeRange.js";
|
|
6
|
+
const NodeSyntaxKinds = getFirstEnumValues(ts.SyntaxKind);
|
|
5
7
|
export function createTypeScriptFileFromProgram(program, sourceFile) {
|
|
6
8
|
return {
|
|
7
9
|
cache: {
|
|
@@ -47,7 +49,7 @@ export function createTypeScriptFileFromProgram(program, sourceFile) {
|
|
|
47
49
|
}
|
|
48
50
|
const { visitors } = runtime;
|
|
49
51
|
const visit = (node) => {
|
|
50
|
-
visitors[
|
|
52
|
+
visitors[NodeSyntaxKinds[node.kind]]?.(node);
|
|
51
53
|
node.forEachChild(visit);
|
|
52
54
|
};
|
|
53
55
|
sourceFile.forEachChild(visit);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import { LanguageFileDefinition } from "@flint.fyi/core";
|
|
2
1
|
import * as ts from "typescript";
|
|
3
|
-
export declare function createTypeScriptFileFromProjectService(filePathAbsolute: string, program: ts.Program, service: ts.server.ProjectService):
|
|
2
|
+
export declare function createTypeScriptFileFromProjectService(filePathAbsolute: string, program: ts.Program, service: ts.server.ProjectService): {
|
|
3
|
+
languageFile: {
|
|
4
|
+
[Symbol.dispose](): void;
|
|
5
|
+
cache?: import("@flint.fyi/core").LanguageFileCacheImpacts;
|
|
6
|
+
getDiagnostics?(): import("@flint.fyi/core").LanguageDiagnostics;
|
|
7
|
+
runRule<OptionsSchema extends import("@flint.fyi/core").AnyOptionalSchema | undefined = import("@flint.fyi/core").AnyOptionalSchema | undefined>(rule: import("@flint.fyi/core").AnyRuleDefinition<OptionsSchema>, options: import("@flint.fyi/core").InferredObject<OptionsSchema>): import("@flint.fyi/core/lib/types/promises.js").PromiseOrSync<import("@flint.fyi/core").NormalizedReport[]>;
|
|
8
|
+
};
|
|
9
|
+
sourceFile: ts.SourceFile;
|
|
10
|
+
};
|
|
@@ -9,9 +9,12 @@ export function createTypeScriptFileFromProjectService(filePathAbsolute, program
|
|
|
9
9
|
log("Retrieved source file and type checker for file %s:", filePathAbsolute);
|
|
10
10
|
const file = createTypeScriptFileFromProgram(program, sourceFile);
|
|
11
11
|
return {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
languageFile: {
|
|
13
|
+
...file,
|
|
14
|
+
[Symbol.dispose]() {
|
|
15
|
+
service.closeClientFile(filePathAbsolute);
|
|
16
|
+
},
|
|
15
17
|
},
|
|
18
|
+
sourceFile,
|
|
16
19
|
};
|
|
17
20
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DirectivesCollector } from "@flint.fyi/core";
|
|
2
|
+
import * as tsutils from "ts-api-utils";
|
|
3
|
+
import { normalizeRange } from "../normalizeRange.js";
|
|
4
|
+
export function parseDirectivesFromTypeScriptFile(sourceFile) {
|
|
5
|
+
const collector = new DirectivesCollector(sourceFile.statements.at(0)?.getStart(sourceFile) ?? sourceFile.text.length);
|
|
6
|
+
tsutils.forEachComment(sourceFile, (fullText, sourceRange) => {
|
|
7
|
+
const commentText = fullText.slice(sourceRange.pos, sourceRange.end);
|
|
8
|
+
const match = /^\/\/\s*flint-(\S+)(?:\s+(.+))?/.exec(commentText);
|
|
9
|
+
if (!match) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const commentRange = {
|
|
13
|
+
begin: sourceRange.pos,
|
|
14
|
+
end: sourceRange.end,
|
|
15
|
+
};
|
|
16
|
+
const range = normalizeRange(commentRange, sourceFile);
|
|
17
|
+
const [type, selection] = match.slice(1);
|
|
18
|
+
collector.add(range, selection, type);
|
|
19
|
+
});
|
|
20
|
+
return collector.collect();
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { parseDirectivesFromTypeScriptFile } from "./parseDirectivesFromTypeScriptFile.js";
|
|
4
|
+
describe(parseDirectivesFromTypeScriptFile, () => {
|
|
5
|
+
it("returns empty arrays when there are no directives", () => {
|
|
6
|
+
const sourceFile = ts.createSourceFile("test.ts", "// unrelated", ts.ScriptTarget.ESNext, true);
|
|
7
|
+
const actual = parseDirectivesFromTypeScriptFile(sourceFile);
|
|
8
|
+
expect(actual).toEqual({
|
|
9
|
+
directives: [],
|
|
10
|
+
reports: [],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it("returns parsed directives when there are comment directives", () => {
|
|
14
|
+
const sourceFile = ts.createSourceFile("test.ts", `
|
|
15
|
+
// flint-disable-file a
|
|
16
|
+
// flint-disable-next-line b
|
|
17
|
+
// flint-invalid
|
|
18
|
+
`, ts.ScriptTarget.ESNext, true);
|
|
19
|
+
const actual = parseDirectivesFromTypeScriptFile(sourceFile);
|
|
20
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
21
|
+
{
|
|
22
|
+
"directives": [
|
|
23
|
+
{
|
|
24
|
+
"range": {
|
|
25
|
+
"begin": {
|
|
26
|
+
"column": 16,
|
|
27
|
+
"line": 1,
|
|
28
|
+
"raw": 17,
|
|
29
|
+
},
|
|
30
|
+
"end": {
|
|
31
|
+
"column": 39,
|
|
32
|
+
"line": 1,
|
|
33
|
+
"raw": 40,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"selections": [
|
|
37
|
+
"a",
|
|
38
|
+
],
|
|
39
|
+
"type": "disable-file",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"range": {
|
|
43
|
+
"begin": {
|
|
44
|
+
"column": 16,
|
|
45
|
+
"line": 2,
|
|
46
|
+
"raw": 57,
|
|
47
|
+
},
|
|
48
|
+
"end": {
|
|
49
|
+
"column": 44,
|
|
50
|
+
"line": 2,
|
|
51
|
+
"raw": 85,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"selections": [
|
|
55
|
+
"b",
|
|
56
|
+
],
|
|
57
|
+
"type": "disable-next-line",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
"reports": [
|
|
61
|
+
{
|
|
62
|
+
"about": {
|
|
63
|
+
"id": "commentDirectiveUnknown",
|
|
64
|
+
},
|
|
65
|
+
"message": {
|
|
66
|
+
"primary": "Unknown comment directive type: "invalid".",
|
|
67
|
+
"secondary": [
|
|
68
|
+
"TODO",
|
|
69
|
+
],
|
|
70
|
+
"suggestions": [
|
|
71
|
+
"TODO",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
"range": {
|
|
75
|
+
"begin": {
|
|
76
|
+
"column": 16,
|
|
77
|
+
"line": 3,
|
|
78
|
+
"raw": 102,
|
|
79
|
+
},
|
|
80
|
+
"end": {
|
|
81
|
+
"column": 32,
|
|
82
|
+
"line": 3,
|
|
83
|
+
"raw": 118,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes TypeScript enum values that alias earlier values.
|
|
3
|
+
* @param original Any TypeScript enum with numeric values.
|
|
4
|
+
* @returns A version of the enum with only the first occurrence of each value.
|
|
5
|
+
* @example
|
|
6
|
+
* Given the following enum:
|
|
7
|
+
* ```ts
|
|
8
|
+
* enum Example {
|
|
9
|
+
* A = 1,
|
|
10
|
+
* B = 2,
|
|
11
|
+
* C = A
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
* Its original value looks like:
|
|
15
|
+
* ```ts
|
|
16
|
+
* {
|
|
17
|
+
* 1: 'C', // <-- Points to 'C', the alias of 'A'
|
|
18
|
+
* 2: 'B',
|
|
19
|
+
* A: 1,
|
|
20
|
+
* B: 2,
|
|
21
|
+
* C: 1,
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
* Calling `getFirstEnumValues(Example)` would return:
|
|
25
|
+
* ```ts
|
|
26
|
+
* {
|
|
27
|
+
* 1: 'A', // <-- Corrected to the original 'A'
|
|
28
|
+
* 2: 'B',
|
|
29
|
+
* A: 1,
|
|
30
|
+
* B: 2,
|
|
31
|
+
* C: 1,
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function getFirstEnumValues<Keys extends string, Values extends number, Original extends Record<Keys | Values, Keys | Values>>(original: Original): Original;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes TypeScript enum values that alias earlier values.
|
|
3
|
+
* @param original Any TypeScript enum with numeric values.
|
|
4
|
+
* @returns A version of the enum with only the first occurrence of each value.
|
|
5
|
+
* @example
|
|
6
|
+
* Given the following enum:
|
|
7
|
+
* ```ts
|
|
8
|
+
* enum Example {
|
|
9
|
+
* A = 1,
|
|
10
|
+
* B = 2,
|
|
11
|
+
* C = A
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
* Its original value looks like:
|
|
15
|
+
* ```ts
|
|
16
|
+
* {
|
|
17
|
+
* 1: 'C', // <-- Points to 'C', the alias of 'A'
|
|
18
|
+
* 2: 'B',
|
|
19
|
+
* A: 1,
|
|
20
|
+
* B: 2,
|
|
21
|
+
* C: 1,
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
* Calling `getFirstEnumValues(Example)` would return:
|
|
25
|
+
* ```ts
|
|
26
|
+
* {
|
|
27
|
+
* 1: 'A', // <-- Corrected to the original 'A'
|
|
28
|
+
* 2: 'B',
|
|
29
|
+
* A: 1,
|
|
30
|
+
* B: 2,
|
|
31
|
+
* C: 1,
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function getFirstEnumValues(original) {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const key in original) {
|
|
38
|
+
if (typeof original[key] !== "number") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!(key in result)) {
|
|
42
|
+
result[key] = original[key];
|
|
43
|
+
}
|
|
44
|
+
if (!(original[key] in result)) {
|
|
45
|
+
// 🤷 enums are tricky to type.
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
|
47
|
+
result[original[key]] = key;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getFirstEnumValues } from "./getFirstEnumValues.js";
|
|
3
|
+
describe("getFirstEnumValues", () => {
|
|
4
|
+
it("returns an empty object when given an empty enum", () => {
|
|
5
|
+
let Empty;
|
|
6
|
+
(function (Empty) {
|
|
7
|
+
})(Empty || (Empty = {}));
|
|
8
|
+
const result = getFirstEnumValues(Empty);
|
|
9
|
+
expect(result).toEqual({});
|
|
10
|
+
});
|
|
11
|
+
it("returns all original pairs when when given a non-repeating enum", () => {
|
|
12
|
+
let NonRepeating;
|
|
13
|
+
(function (NonRepeating) {
|
|
14
|
+
NonRepeating[NonRepeating["A"] = 1] = "A";
|
|
15
|
+
NonRepeating[NonRepeating["B"] = 2] = "B";
|
|
16
|
+
NonRepeating[NonRepeating["C"] = 3] = "C";
|
|
17
|
+
})(NonRepeating || (NonRepeating = {}));
|
|
18
|
+
const result = getFirstEnumValues(NonRepeating);
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
1: "A",
|
|
21
|
+
2: "B",
|
|
22
|
+
3: "C",
|
|
23
|
+
A: 1,
|
|
24
|
+
B: 2,
|
|
25
|
+
C: 3,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("returns the first key when when given a repeating-value enum", () => {
|
|
29
|
+
let Repeating;
|
|
30
|
+
(function (Repeating) {
|
|
31
|
+
Repeating[Repeating["A"] = 1] = "A";
|
|
32
|
+
Repeating[Repeating["B"] = 2] = "B";
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/prefer-literal-enum-member
|
|
34
|
+
Repeating[Repeating["C"] = 1] = "C";
|
|
35
|
+
})(Repeating || (Repeating = {}));
|
|
36
|
+
const result = getFirstEnumValues(Repeating);
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
1: "A",
|
|
39
|
+
2: "B",
|
|
40
|
+
A: 1,
|
|
41
|
+
B: 2,
|
|
42
|
+
C: 1,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
package/lib/language.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import * as ts from "typescript";
|
|
8
8
|
import { createTypeScriptFileFromProgram } from "./createTypeScriptFileFromProgram.js";
|
|
9
9
|
import { createTypeScriptFileFromProjectService } from "./createTypeScriptFileFromProjectService.js";
|
|
10
|
+
import { prepareTypeScriptFile } from "./prepareTypeScriptFile.js";
|
|
10
11
|
const log = debugForFile(import.meta.filename);
|
|
11
12
|
const projectRoot = path.join(import.meta.dirname, "../..");
|
|
12
13
|
export const typescriptLanguage = createLanguage({
|
|
@@ -42,12 +43,13 @@ export const typescriptLanguage = createLanguage({
|
|
|
42
43
|
return program;
|
|
43
44
|
});
|
|
44
45
|
return {
|
|
45
|
-
|
|
46
|
+
prepareFromDisk: (filePathAbsolute) => {
|
|
46
47
|
const program = servicePrograms.get(filePathAbsolute);
|
|
47
48
|
seenPrograms.add(program);
|
|
48
|
-
|
|
49
|
+
const { languageFile, sourceFile } = createTypeScriptFileFromProjectService(filePathAbsolute, program, service);
|
|
50
|
+
return prepareTypeScriptFile(languageFile, sourceFile);
|
|
49
51
|
},
|
|
50
|
-
|
|
52
|
+
prepareFromVirtual: (filePathAbsolute, sourceText) => {
|
|
51
53
|
const environment = environments.get(filePathAbsolute);
|
|
52
54
|
environment.updateFile(filePathAbsolute, sourceText);
|
|
53
55
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
@@ -55,7 +57,8 @@ export const typescriptLanguage = createLanguage({
|
|
|
55
57
|
const program = environment.languageService.getProgram();
|
|
56
58
|
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
57
59
|
seenPrograms.add(program);
|
|
58
|
-
|
|
60
|
+
const languageFile = createTypeScriptFileFromProgram(program, sourceFile);
|
|
61
|
+
return prepareTypeScriptFile(languageFile, sourceFile);
|
|
59
62
|
},
|
|
60
63
|
};
|
|
61
64
|
},
|
package/lib/plugin.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export declare const ts: import("@flint.fyi/core").Plugin<import("@flint.fyi/core").RuleAbout, string | undefined, [import("@flint.fyi/core").Rule<{
|
|
2
|
+
readonly description: "Reports iterating over an array with a for-in loop.";
|
|
2
3
|
readonly id: "forInArrays";
|
|
3
4
|
readonly preset: "logical";
|
|
4
5
|
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "forIn", undefined>, import("@flint.fyi/core").Rule<{
|
|
6
|
+
readonly description: "Reports unnecessary extra non-null assertions.";
|
|
5
7
|
readonly id: "consecutiveNonNullAssertions";
|
|
6
8
|
readonly preset: "logical";
|
|
7
9
|
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "consecutiveNonNullAssertion", undefined>, import("@flint.fyi/core").Rule<{
|
|
10
|
+
readonly description: "Reports using legacy `namespace` declarations.";
|
|
8
11
|
readonly id: "namespaceDeclarations";
|
|
9
12
|
readonly preset: "logical";
|
|
10
13
|
}, import("./nodes.js").TSNodesByName, import("./language.js").TypeScriptServices, "preferModules", {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { LanguageFileDefinition } from "@flint.fyi/core";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
export declare function prepareTypeScriptFile(languageFile: LanguageFileDefinition, sourceFile: ts.SourceFile): {
|
|
4
|
+
file: LanguageFileDefinition;
|
|
5
|
+
directives: import("@flint.fyi/core").CommentDirective[];
|
|
6
|
+
reports: import("@flint.fyi/core").FileReport[];
|
|
7
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
+
readonly description: "Reports unnecessary extra non-null assertions.";
|
|
2
3
|
readonly id: "consecutiveNonNullAssertions";
|
|
3
4
|
readonly preset: "logical";
|
|
4
5
|
}, import("../nodes.js").TSNodesByName, import("../language.js").TypeScriptServices, "consecutiveNonNullAssertion", undefined>;
|
|
@@ -2,6 +2,7 @@ import * as ts from "typescript";
|
|
|
2
2
|
import { typescriptLanguage } from "../language.js";
|
|
3
3
|
export default typescriptLanguage.createRule({
|
|
4
4
|
about: {
|
|
5
|
+
description: "Reports unnecessary extra non-null assertions.",
|
|
5
6
|
id: "consecutiveNonNullAssertions",
|
|
6
7
|
preset: "logical",
|
|
7
8
|
},
|
|
@@ -23,8 +24,8 @@ export default typescriptLanguage.createRule({
|
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
const range = {
|
|
26
|
-
begin: node.end,
|
|
27
|
-
end: node.parent.end
|
|
27
|
+
begin: node.end - 1,
|
|
28
|
+
end: node.parent.end,
|
|
28
29
|
};
|
|
29
30
|
context.report({
|
|
30
31
|
fix: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
+
readonly description: "Reports iterating over an array with a for-in loop.";
|
|
2
3
|
readonly id: "forInArrays";
|
|
3
4
|
readonly preset: "logical";
|
|
4
5
|
}, import("../nodes.js").TSNodesByName, import("../language.js").TypeScriptServices, "forIn", undefined>;
|
package/lib/rules/forInArrays.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getConstrainedTypeAtLocation } from "./utils/getConstrainedType.js";
|
|
|
5
5
|
import { isTypeRecursive } from "./utils/isTypeRecursive.js";
|
|
6
6
|
export default typescriptLanguage.createRule({
|
|
7
7
|
about: {
|
|
8
|
+
description: "Reports iterating over an array with a for-in loop.",
|
|
8
9
|
id: "forInArrays",
|
|
9
10
|
preset: "logical",
|
|
10
11
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
declare const _default: import("@flint.fyi/core").Rule<{
|
|
3
|
+
readonly description: "Reports using legacy `namespace` declarations.";
|
|
3
4
|
readonly id: "namespaceDeclarations";
|
|
4
5
|
readonly preset: "logical";
|
|
5
6
|
}, import("../nodes.js").TSNodesByName, import("../language.js").TypeScriptServices, "preferModules", {
|
|
@@ -5,6 +5,7 @@ import { getTSNodeRange } from "../getTSNodeRange.js";
|
|
|
5
5
|
import { typescriptLanguage } from "../language.js";
|
|
6
6
|
export default typescriptLanguage.createRule({
|
|
7
7
|
about: {
|
|
8
|
+
description: "Reports using legacy `namespace` declarations.",
|
|
8
9
|
id: "namespaceDeclarations",
|
|
9
10
|
preset: "logical",
|
|
10
11
|
},
|
|
@@ -20,8 +21,14 @@ export default typescriptLanguage.createRule({
|
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
options: {
|
|
23
|
-
allowDeclarations: z
|
|
24
|
-
|
|
24
|
+
allowDeclarations: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe("Whether to allow namespaces declared with the `declare` keyword."),
|
|
28
|
+
allowDefinitionFiles: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.default(false)
|
|
31
|
+
.describe("Whether to allow namespaces in `.d.ts` and other definition files."),
|
|
25
32
|
},
|
|
26
33
|
setup(context, { allowDeclarations, allowDefinitionFiles }) {
|
|
27
34
|
if (allowDefinitionFiles && context.sourceFile.isDeclarationFile) {
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LanguageFileDefinition,
|
|
3
|
-
|
|
3
|
+
NormalizedReport,
|
|
4
4
|
RuleReport,
|
|
5
5
|
} from "@flint.fyi/core";
|
|
6
6
|
import * as ts from "typescript";
|
|
7
7
|
|
|
8
8
|
import { collectReferencedFilePaths } from "./collectReferencedFilePaths.js";
|
|
9
9
|
import { formatDiagnostic } from "./formatDiagnostic.js";
|
|
10
|
+
import { getFirstEnumValues } from "./getFirstEnumValues.js";
|
|
10
11
|
import { normalizeRange } from "./normalizeRange.js";
|
|
11
12
|
|
|
13
|
+
const NodeSyntaxKinds = getFirstEnumValues(ts.SyntaxKind);
|
|
14
|
+
|
|
12
15
|
export function createTypeScriptFileFromProgram(
|
|
13
16
|
program: ts.Program,
|
|
14
17
|
sourceFile: ts.SourceFile,
|
|
@@ -43,7 +46,7 @@ export function createTypeScriptFileFromProgram(
|
|
|
43
46
|
}));
|
|
44
47
|
},
|
|
45
48
|
async runRule(rule, options) {
|
|
46
|
-
const reports:
|
|
49
|
+
const reports: NormalizedReport[] = [];
|
|
47
50
|
|
|
48
51
|
const context = {
|
|
49
52
|
report: (report: RuleReport) => {
|
|
@@ -65,7 +68,7 @@ export function createTypeScriptFileFromProgram(
|
|
|
65
68
|
|
|
66
69
|
const { visitors } = runtime;
|
|
67
70
|
const visit = (node: ts.Node) => {
|
|
68
|
-
visitors[
|
|
71
|
+
visitors[NodeSyntaxKinds[node.kind]]?.(node);
|
|
69
72
|
|
|
70
73
|
node.forEachChild(visit);
|
|
71
74
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { LanguageFileDefinition } from "@flint.fyi/core";
|
|
2
1
|
import { debugForFile } from "debug-for-file";
|
|
3
2
|
import * as ts from "typescript";
|
|
4
3
|
|
|
@@ -10,7 +9,7 @@ export function createTypeScriptFileFromProjectService(
|
|
|
10
9
|
filePathAbsolute: string,
|
|
11
10
|
program: ts.Program,
|
|
12
11
|
service: ts.server.ProjectService,
|
|
13
|
-
)
|
|
12
|
+
) {
|
|
14
13
|
const sourceFile = program.getSourceFile(filePathAbsolute);
|
|
15
14
|
if (!sourceFile) {
|
|
16
15
|
throw new Error(`Could not retrieve source file for: ${filePathAbsolute}`);
|
|
@@ -21,9 +20,12 @@ export function createTypeScriptFileFromProjectService(
|
|
|
21
20
|
const file = createTypeScriptFileFromProgram(program, sourceFile);
|
|
22
21
|
|
|
23
22
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
languageFile: {
|
|
24
|
+
...file,
|
|
25
|
+
[Symbol.dispose]() {
|
|
26
|
+
service.closeClientFile(filePathAbsolute);
|
|
27
|
+
},
|
|
27
28
|
},
|
|
29
|
+
sourceFile,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { parseDirectivesFromTypeScriptFile } from "./parseDirectivesFromTypeScriptFile.js";
|
|
5
|
+
|
|
6
|
+
describe(parseDirectivesFromTypeScriptFile, () => {
|
|
7
|
+
it("returns empty arrays when there are no directives", () => {
|
|
8
|
+
const sourceFile = ts.createSourceFile(
|
|
9
|
+
"test.ts",
|
|
10
|
+
"// unrelated",
|
|
11
|
+
ts.ScriptTarget.ESNext,
|
|
12
|
+
true,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const actual = parseDirectivesFromTypeScriptFile(sourceFile);
|
|
16
|
+
|
|
17
|
+
expect(actual).toEqual({
|
|
18
|
+
directives: [],
|
|
19
|
+
reports: [],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns parsed directives when there are comment directives", () => {
|
|
24
|
+
const sourceFile = ts.createSourceFile(
|
|
25
|
+
"test.ts",
|
|
26
|
+
`
|
|
27
|
+
// flint-disable-file a
|
|
28
|
+
// flint-disable-next-line b
|
|
29
|
+
// flint-invalid
|
|
30
|
+
`,
|
|
31
|
+
ts.ScriptTarget.ESNext,
|
|
32
|
+
true,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const actual = parseDirectivesFromTypeScriptFile(sourceFile);
|
|
36
|
+
|
|
37
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
38
|
+
{
|
|
39
|
+
"directives": [
|
|
40
|
+
{
|
|
41
|
+
"range": {
|
|
42
|
+
"begin": {
|
|
43
|
+
"column": 16,
|
|
44
|
+
"line": 1,
|
|
45
|
+
"raw": 17,
|
|
46
|
+
},
|
|
47
|
+
"end": {
|
|
48
|
+
"column": 39,
|
|
49
|
+
"line": 1,
|
|
50
|
+
"raw": 40,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"selections": [
|
|
54
|
+
"a",
|
|
55
|
+
],
|
|
56
|
+
"type": "disable-file",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"range": {
|
|
60
|
+
"begin": {
|
|
61
|
+
"column": 16,
|
|
62
|
+
"line": 2,
|
|
63
|
+
"raw": 57,
|
|
64
|
+
},
|
|
65
|
+
"end": {
|
|
66
|
+
"column": 44,
|
|
67
|
+
"line": 2,
|
|
68
|
+
"raw": 85,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"selections": [
|
|
72
|
+
"b",
|
|
73
|
+
],
|
|
74
|
+
"type": "disable-next-line",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
"reports": [
|
|
78
|
+
{
|
|
79
|
+
"about": {
|
|
80
|
+
"id": "commentDirectiveUnknown",
|
|
81
|
+
},
|
|
82
|
+
"message": {
|
|
83
|
+
"primary": "Unknown comment directive type: "invalid".",
|
|
84
|
+
"secondary": [
|
|
85
|
+
"TODO",
|
|
86
|
+
],
|
|
87
|
+
"suggestions": [
|
|
88
|
+
"TODO",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
"range": {
|
|
92
|
+
"begin": {
|
|
93
|
+
"column": 16,
|
|
94
|
+
"line": 3,
|
|
95
|
+
"raw": 102,
|
|
96
|
+
},
|
|
97
|
+
"end": {
|
|
98
|
+
"column": 32,
|
|
99
|
+
"line": 3,
|
|
100
|
+
"raw": 118,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
});
|