@flint.fyi/ts 0.14.4 → 0.14.6
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 +15 -0
- package/lib/createTypeScriptFileFromProgram.js +4 -1
- package/lib/formatDiagnostic.js +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/lib/normalizeRange.js +3 -6
- package/lib/plugin.d.ts +17 -1
- package/lib/plugin.js +9 -1
- package/lib/rules/chainedAssignments.js +2 -1
- package/lib/rules/chainedAssignments.test.js +24 -0
- package/lib/rules/debuggerStatements.test.js +4 -4
- package/lib/rules/emptyBlocks.d.ts +6 -0
- package/lib/rules/emptyBlocks.js +66 -0
- package/lib/rules/emptyBlocks.test.d.ts +1 -0
- package/lib/rules/emptyBlocks.test.js +135 -0
- package/lib/rules/forDirections.js +15 -8
- package/lib/rules/forDirections.test.js +25 -0
- package/lib/rules/octalNumbers.test.js +2 -2
- package/lib/rules/shadowedRestrictedNames.d.ts +6 -0
- package/lib/rules/shadowedRestrictedNames.js +95 -0
- package/lib/rules/shadowedRestrictedNames.test.d.ts +1 -0
- package/lib/rules/shadowedRestrictedNames.test.js +147 -0
- package/lib/rules/symbolDescriptions.test.js +4 -4
- package/lib/rules/unicodeBOMs.test.js +1 -0
- package/lib/rules/unnecessaryBlocks.d.ts +6 -0
- package/lib/rules/unnecessaryBlocks.js +75 -0
- package/lib/rules/unnecessaryBlocks.test.d.ts +1 -0
- package/lib/rules/unnecessaryBlocks.test.js +147 -0
- package/lib/rules/unnecessaryConcatenation.d.ts +6 -0
- package/lib/rules/unnecessaryConcatenation.js +38 -0
- package/lib/rules/unnecessaryConcatenation.test.d.ts +1 -0
- package/lib/rules/unnecessaryConcatenation.test.js +87 -0
- package/lib/rules/voidOperator.test.js +2 -2
- package/lib/utils/getDeclarationsIfGlobal.d.ts +2 -0
- package/lib/utils/getDeclarationsIfGlobal.js +7 -0
- package/lib/utils/isGlobalDeclaration.js +2 -3
- package/lib/utils/isGlobalDeclarationOfName.d.ts +1 -1
- package/lib/utils/isGlobalDeclarationOfName.js +4 -0
- package/package.json +3 -3
- package/src/createTypeScriptFileFromProgram.ts +5 -1
- package/src/formatDiagnostic.ts +1 -1
- package/src/index.ts +3 -0
- package/src/normalizeRange.ts +3 -8
- package/src/plugin.ts +9 -1
- package/src/rules/chainedAssignments.test.ts +24 -0
- package/src/rules/chainedAssignments.ts +4 -1
- package/src/rules/debuggerStatements.test.ts +4 -4
- package/src/rules/emptyBlocks.test.ts +136 -0
- package/src/rules/emptyBlocks.ts +73 -0
- package/src/rules/forDirections.test.ts +25 -0
- package/src/rules/forDirections.ts +20 -9
- package/src/rules/octalNumbers.test.ts +2 -2
- package/src/rules/shadowedRestrictedNames.test.ts +148 -0
- package/src/rules/shadowedRestrictedNames.ts +105 -0
- package/src/rules/symbolDescriptions.test.ts +4 -4
- package/src/rules/unicodeBOMs.test.ts +1 -0
- package/src/rules/unnecessaryBlocks.test.ts +148 -0
- package/src/rules/unnecessaryBlocks.ts +89 -0
- package/src/rules/unnecessaryConcatenation.test.ts +88 -0
- package/src/rules/unnecessaryConcatenation.ts +44 -0
- package/src/rules/voidOperator.test.ts +2 -2
- package/src/utils/getDeclarationsIfGlobal.ts +14 -0
- package/src/utils/isGlobalDeclaration.ts +3 -5
- package/src/utils/isGlobalDeclarationOfName.ts +6 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -16,6 +16,10 @@ export function isGlobalDeclarationOfName(node, name, typeChecker) {
|
|
|
16
16
|
declaration.initializer.kind === ts.SyntaxKind.Identifier) {
|
|
17
17
|
return isGlobalDeclarationOfName(declaration.initializer, name, typeChecker);
|
|
18
18
|
}
|
|
19
|
+
// Special case: a property of an interface
|
|
20
|
+
if (ts.isPropertySignature(declaration)) {
|
|
21
|
+
return isGlobalDeclarationOfName(declaration.parent, name, typeChecker);
|
|
22
|
+
}
|
|
19
23
|
return (isDeclarationOfName(declaration, name) &&
|
|
20
24
|
declarationIncludesGlobal(declaration));
|
|
21
25
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flint.fyi/ts",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.6",
|
|
4
4
|
"description": "[Experimental] TypeScript language plugin for Flint.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@flint.fyi/core": "",
|
|
19
19
|
"@flint.fyi/rule-tester": "",
|
|
20
|
-
"@typescript-eslint/project-service": "^8.
|
|
20
|
+
"@typescript-eslint/project-service": "^8.46.2",
|
|
21
21
|
"@typescript/vfs": "^1.6.1",
|
|
22
22
|
"cached-factory": "^0.1.0",
|
|
23
23
|
"debug-for-file": "^0.2.0",
|
|
24
24
|
"ts-api-utils": "^2.1.0",
|
|
25
|
-
"typescript": ">=5.
|
|
25
|
+
"typescript": ">=5.9.3"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=24.0.0"
|
|
@@ -53,6 +53,10 @@ export function createTypeScriptFileFromProgram(
|
|
|
53
53
|
report: (report: RuleReport) => {
|
|
54
54
|
reports.push({
|
|
55
55
|
...report,
|
|
56
|
+
fix:
|
|
57
|
+
report.fix && !Array.isArray(report.fix)
|
|
58
|
+
? [report.fix]
|
|
59
|
+
: report.fix,
|
|
56
60
|
message: rule.messages[report.message],
|
|
57
61
|
range: normalizeRange(report.range, sourceFile),
|
|
58
62
|
});
|
|
@@ -74,7 +78,7 @@ export function createTypeScriptFileFromProgram(
|
|
|
74
78
|
node.forEachChild(visit);
|
|
75
79
|
};
|
|
76
80
|
|
|
77
|
-
sourceFile
|
|
81
|
+
visit(sourceFile);
|
|
78
82
|
|
|
79
83
|
return reports;
|
|
80
84
|
},
|
package/src/formatDiagnostic.ts
CHANGED
|
@@ -138,7 +138,7 @@ function formatCodeSpan(
|
|
|
138
138
|
gutterSeparator;
|
|
139
139
|
context += squiggleColor;
|
|
140
140
|
if (i === firstLine) {
|
|
141
|
-
const lastCharForLine = i === lastLine ? lastLineChar :
|
|
141
|
+
const lastCharForLine = i === lastLine ? lastLineChar : undefined;
|
|
142
142
|
context += lineContent.slice(0, firstLineChar).replace(/\S/g, " ");
|
|
143
143
|
context += lineContent
|
|
144
144
|
.slice(firstLineChar, lastCharForLine)
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { getTSNodeRange } from "./getTSNodeRange.js";
|
|
2
2
|
export * from "./language.js";
|
|
3
3
|
export { ts } from "./plugin.js";
|
|
4
|
+
export { getDeclarationsIfGlobal } from "./utils/getDeclarationsIfGlobal.js";
|
|
4
5
|
export { isGlobalDeclaration } from "./utils/isGlobalDeclaration.js";
|
|
6
|
+
export { isGlobalDeclarationOfName } from "./utils/isGlobalDeclarationOfName.js";
|
|
7
|
+
export { isGlobalVariable } from "./utils/isGlobalVariable.js";
|
package/src/normalizeRange.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type * as ts from "typescript";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
CharacterReportRange,
|
|
5
|
+
getColumnAndLineOfPosition,
|
|
5
6
|
NormalizedReportRangeObject,
|
|
6
7
|
} from "@flint.fyi/core";
|
|
7
8
|
|
|
@@ -14,17 +15,11 @@ export function normalizeRange(
|
|
|
14
15
|
: original;
|
|
15
16
|
|
|
16
17
|
return {
|
|
17
|
-
begin:
|
|
18
|
-
end:
|
|
18
|
+
begin: getColumnAndLineOfPosition(sourceFile, onCharacters.begin),
|
|
19
|
+
end: getColumnAndLineOfPosition(sourceFile, onCharacters.end),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function isNode(value: unknown): value is ts.Node {
|
|
23
24
|
return typeof value === "object" && value !== null && "kind" in value;
|
|
24
25
|
}
|
|
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
CHANGED
|
@@ -13,6 +13,7 @@ import debuggerStatements from "./rules/debuggerStatements.js";
|
|
|
13
13
|
import defaultCaseLast from "./rules/defaultCaseLast.js";
|
|
14
14
|
import duplicateArguments from "./rules/duplicateArguments.js";
|
|
15
15
|
import elseIfDuplicates from "./rules/elseIfDuplicates.js";
|
|
16
|
+
import emptyBlocks from "./rules/emptyBlocks.js";
|
|
16
17
|
import emptyDestructures from "./rules/emptyDestructures.js";
|
|
17
18
|
import emptyStaticBlocks from "./rules/emptyStaticBlocks.js";
|
|
18
19
|
import exceptionAssignments from "./rules/exceptionAssignments.js";
|
|
@@ -36,13 +37,16 @@ import returnAssignments from "./rules/returnAssignments.js";
|
|
|
36
37
|
import selfAssignments from "./rules/selfAssignments.js";
|
|
37
38
|
import selfComparisons from "./rules/selfComparisons.js";
|
|
38
39
|
import sequences from "./rules/sequences.js";
|
|
40
|
+
import shadowedRestrictedNames from "./rules/shadowedRestrictedNames.js";
|
|
39
41
|
import sparseArrays from "./rules/sparseArrays.js";
|
|
40
42
|
import symbolDescriptions from "./rules/symbolDescriptions.js";
|
|
41
43
|
import typeofComparisons from "./rules/typeofComparisons.js";
|
|
42
44
|
import unassignedVariables from "./rules/unassignedVariables.js";
|
|
43
45
|
import undefinedVariables from "./rules/undefinedVariables.js";
|
|
44
46
|
import unicodeBOMs from "./rules/unicodeBOMs.js";
|
|
47
|
+
import unnecessaryBlocks from "./rules/unnecessaryBlocks.js";
|
|
45
48
|
import unnecessaryCatches from "./rules/unnecessaryCatches.js";
|
|
49
|
+
import unnecessaryConcatenation from "./rules/unnecessaryConcatenation.js";
|
|
46
50
|
import unsafeNegations from "./rules/unsafeNegations.js";
|
|
47
51
|
import variableDeletions from "./rules/variableDeletions.js";
|
|
48
52
|
import voidOperator from "./rules/voidOperator.js";
|
|
@@ -51,7 +55,7 @@ export const ts = createPlugin({
|
|
|
51
55
|
files: {
|
|
52
56
|
all: ["**/*.{cjs,js,jsx,mjs,ts,tsx}"],
|
|
53
57
|
},
|
|
54
|
-
name: "
|
|
58
|
+
name: "TypeScript",
|
|
55
59
|
rules: [
|
|
56
60
|
anyReturns,
|
|
57
61
|
asyncPromiseExecutors,
|
|
@@ -66,6 +70,7 @@ export const ts = createPlugin({
|
|
|
66
70
|
defaultCaseLast,
|
|
67
71
|
duplicateArguments,
|
|
68
72
|
elseIfDuplicates,
|
|
73
|
+
emptyBlocks,
|
|
69
74
|
emptyDestructures,
|
|
70
75
|
emptyStaticBlocks,
|
|
71
76
|
exceptionAssignments,
|
|
@@ -89,13 +94,16 @@ export const ts = createPlugin({
|
|
|
89
94
|
selfAssignments,
|
|
90
95
|
selfComparisons,
|
|
91
96
|
sequences,
|
|
97
|
+
shadowedRestrictedNames,
|
|
92
98
|
sparseArrays,
|
|
93
99
|
symbolDescriptions,
|
|
94
100
|
typeofComparisons,
|
|
95
101
|
unassignedVariables,
|
|
96
102
|
undefinedVariables,
|
|
97
103
|
unicodeBOMs,
|
|
104
|
+
unnecessaryBlocks,
|
|
98
105
|
unnecessaryCatches,
|
|
106
|
+
unnecessaryConcatenation,
|
|
99
107
|
unsafeNegations,
|
|
100
108
|
variableDeletions,
|
|
101
109
|
voidOperator,
|
|
@@ -234,6 +234,30 @@ class MyClass {
|
|
|
234
234
|
second = 0;
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
+
`,
|
|
238
|
+
`
|
|
239
|
+
let result = "";
|
|
240
|
+
result += "a" + "b";
|
|
241
|
+
`,
|
|
242
|
+
`
|
|
243
|
+
let result = 0;
|
|
244
|
+
result += 1 + 2;
|
|
245
|
+
`,
|
|
246
|
+
`
|
|
247
|
+
let result = 0;
|
|
248
|
+
result += 1 * 2;
|
|
249
|
+
`,
|
|
250
|
+
`
|
|
251
|
+
let result = 0;
|
|
252
|
+
result += 1 - 2;
|
|
253
|
+
`,
|
|
254
|
+
`
|
|
255
|
+
let result = 0;
|
|
256
|
+
result += 1 / 2;
|
|
257
|
+
`,
|
|
258
|
+
`
|
|
259
|
+
let result = 0;
|
|
260
|
+
result += 1 ** 2;
|
|
237
261
|
`,
|
|
238
262
|
],
|
|
239
263
|
});
|
|
@@ -35,7 +35,10 @@ export default typescriptLanguage.createRule({
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const rightSide = unwrapParenthesizedExpression(node.right);
|
|
38
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
!ts.isBinaryExpression(rightSide) ||
|
|
40
|
+
!tsutils.isAssignmentKind(rightSide.operatorToken.kind)
|
|
41
|
+
) {
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -30,8 +30,8 @@ function test() {
|
|
|
30
30
|
snapshot: `
|
|
31
31
|
function test() {
|
|
32
32
|
debugger;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
~~~~~~~~~
|
|
34
|
+
Debugger statements should not be used in production code.
|
|
35
35
|
}
|
|
36
36
|
`,
|
|
37
37
|
suggestions: [
|
|
@@ -54,8 +54,8 @@ if (condition) {
|
|
|
54
54
|
snapshot: `
|
|
55
55
|
if (condition) {
|
|
56
56
|
debugger;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
~~~~~~~~~
|
|
58
|
+
Debugger statements should not be used in production code.
|
|
59
59
|
}
|
|
60
60
|
`,
|
|
61
61
|
suggestions: [
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import rule from "./emptyBlocks.js";
|
|
2
|
+
import { ruleTester } from "./ruleTester.js";
|
|
3
|
+
|
|
4
|
+
ruleTester.describe(rule, {
|
|
5
|
+
invalid: [
|
|
6
|
+
{
|
|
7
|
+
code: `
|
|
8
|
+
if (condition) {}
|
|
9
|
+
`,
|
|
10
|
+
snapshot: `
|
|
11
|
+
if (condition) {}
|
|
12
|
+
~~
|
|
13
|
+
Empty block statements should be removed or contain code.
|
|
14
|
+
`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: `
|
|
18
|
+
if (condition) {
|
|
19
|
+
} else {}
|
|
20
|
+
`,
|
|
21
|
+
snapshot: `
|
|
22
|
+
if (condition) {
|
|
23
|
+
~
|
|
24
|
+
Empty block statements should be removed or contain code.
|
|
25
|
+
} else {}
|
|
26
|
+
~
|
|
27
|
+
~~
|
|
28
|
+
Empty block statements should be removed or contain code.
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
code: `
|
|
33
|
+
while (condition) {}
|
|
34
|
+
`,
|
|
35
|
+
snapshot: `
|
|
36
|
+
while (condition) {}
|
|
37
|
+
~~
|
|
38
|
+
Empty block statements should be removed or contain code.
|
|
39
|
+
`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: `
|
|
43
|
+
for (let i = 0; i < 10; i++) {}
|
|
44
|
+
`,
|
|
45
|
+
snapshot: `
|
|
46
|
+
for (let i = 0; i < 10; i++) {}
|
|
47
|
+
~~
|
|
48
|
+
Empty block statements should be removed or contain code.
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: `
|
|
53
|
+
switch (value) {}
|
|
54
|
+
`,
|
|
55
|
+
snapshot: `
|
|
56
|
+
switch (value) {}
|
|
57
|
+
~~
|
|
58
|
+
Empty block statements should be removed or contain code.
|
|
59
|
+
`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
code: `
|
|
63
|
+
do {} while (condition);
|
|
64
|
+
`,
|
|
65
|
+
snapshot: `
|
|
66
|
+
do {} while (condition);
|
|
67
|
+
~~
|
|
68
|
+
Empty block statements should be removed or contain code.
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
code: `
|
|
73
|
+
if (x) {
|
|
74
|
+
doSomething();
|
|
75
|
+
} else if (y) {}
|
|
76
|
+
`,
|
|
77
|
+
snapshot: `
|
|
78
|
+
if (x) {
|
|
79
|
+
doSomething();
|
|
80
|
+
} else if (y) {}
|
|
81
|
+
~~
|
|
82
|
+
Empty block statements should be removed or contain code.
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
code: `
|
|
87
|
+
try {
|
|
88
|
+
doSomething();
|
|
89
|
+
} finally {}
|
|
90
|
+
`,
|
|
91
|
+
snapshot: `
|
|
92
|
+
try {
|
|
93
|
+
doSomething();
|
|
94
|
+
} finally {}
|
|
95
|
+
~~
|
|
96
|
+
Empty block statements should be removed or contain code.
|
|
97
|
+
`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
valid: [
|
|
101
|
+
`if (condition) { doSomething(); }`,
|
|
102
|
+
`while (condition) { doSomething(); }`,
|
|
103
|
+
`for (let i = 0; i < 10; i++) { doSomething(); }`,
|
|
104
|
+
`switch (value) { case 1: break; }`,
|
|
105
|
+
`do { doSomething(); } while (condition);`,
|
|
106
|
+
`function test() {}`,
|
|
107
|
+
`const fn = function() {};`,
|
|
108
|
+
`const arrow = () => {};`,
|
|
109
|
+
`class MyClass { method() {} }`,
|
|
110
|
+
`class MyClass { constructor() {} }`,
|
|
111
|
+
`class MyClass { get value() {} }`,
|
|
112
|
+
`class MyClass { set value(v) {} }`,
|
|
113
|
+
`
|
|
114
|
+
if (condition) {
|
|
115
|
+
// Intentionally empty
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
`
|
|
119
|
+
while (condition) {
|
|
120
|
+
/* Do nothing */
|
|
121
|
+
}
|
|
122
|
+
`,
|
|
123
|
+
`
|
|
124
|
+
try {
|
|
125
|
+
doSomething();
|
|
126
|
+
} catch (error) {}
|
|
127
|
+
`,
|
|
128
|
+
`
|
|
129
|
+
try {
|
|
130
|
+
doSomething();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Ignore errors
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
],
|
|
136
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import { getTSNodeRange } from "../getTSNodeRange.js";
|
|
4
|
+
import { typescriptLanguage } from "../language.js";
|
|
5
|
+
|
|
6
|
+
const allowedParents = new Set([
|
|
7
|
+
ts.SyntaxKind.ArrowFunction,
|
|
8
|
+
ts.SyntaxKind.CatchClause,
|
|
9
|
+
ts.SyntaxKind.Constructor,
|
|
10
|
+
ts.SyntaxKind.FunctionDeclaration,
|
|
11
|
+
ts.SyntaxKind.FunctionExpression,
|
|
12
|
+
ts.SyntaxKind.GetAccessor,
|
|
13
|
+
ts.SyntaxKind.MethodDeclaration,
|
|
14
|
+
ts.SyntaxKind.SetAccessor,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export default typescriptLanguage.createRule({
|
|
18
|
+
about: {
|
|
19
|
+
description: "Reports empty block statements that should contain code.",
|
|
20
|
+
id: "emptyBlocks",
|
|
21
|
+
preset: "stylistic",
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
emptyBlock: {
|
|
25
|
+
primary: "Empty block statements should be removed or contain code.",
|
|
26
|
+
secondary: [
|
|
27
|
+
"Empty blocks can indicate incomplete code or areas where logic was removed but the block structure was left behind.",
|
|
28
|
+
"They can also reduce code readability by cluttering the codebase with unnecessary braces.",
|
|
29
|
+
],
|
|
30
|
+
suggestions: [
|
|
31
|
+
"Add a comment explaining why the block is intentionally empty, or remove the empty block entirely.",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
setup(context) {
|
|
36
|
+
function hasComments(block: ts.Block): boolean {
|
|
37
|
+
const fullText = context.sourceFile.getFullText();
|
|
38
|
+
|
|
39
|
+
const openBrace = block.getStart(context.sourceFile);
|
|
40
|
+
const closeBrace = block.getEnd();
|
|
41
|
+
const innerText = fullText.substring(openBrace + 1, closeBrace - 1);
|
|
42
|
+
|
|
43
|
+
// Check if there are any non-whitespace characters (which would be comments)
|
|
44
|
+
// since we already know there are no statements
|
|
45
|
+
return /\S+/.test(innerText.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isEmptyBlock(block: ts.Block): boolean {
|
|
49
|
+
return block.statements.length === 0 && !hasComments(block);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
visitors: {
|
|
54
|
+
Block: (node) => {
|
|
55
|
+
if (!allowedParents.has(node.parent.kind) && isEmptyBlock(node)) {
|
|
56
|
+
context.report({
|
|
57
|
+
message: "emptyBlock",
|
|
58
|
+
range: getTSNodeRange(node, context.sourceFile),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
CaseBlock: (node) => {
|
|
63
|
+
if (node.clauses.length === 0) {
|
|
64
|
+
context.report({
|
|
65
|
+
message: "emptyBlock",
|
|
66
|
+
range: getTSNodeRange(node, context.sourceFile),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -81,6 +81,30 @@ for (let index = 0; index < 10; index += -1) {}
|
|
|
81
81
|
for (let index = 0; index < 10; index += -1) {}
|
|
82
82
|
~~~~~~~~~~~
|
|
83
83
|
The update moves the counter in the wrong direction for this loop condition.
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
code: `
|
|
88
|
+
declare context text: string;
|
|
89
|
+
for (let i = 10; i >= 0 && text[i] !== " "; i++) { }
|
|
90
|
+
`,
|
|
91
|
+
snapshot: `
|
|
92
|
+
declare context text: string;
|
|
93
|
+
for (let i = 10; i >= 0 && text[i] !== " "; i++) { }
|
|
94
|
+
~~~
|
|
95
|
+
The update moves the counter in the wrong direction for this loop condition.
|
|
96
|
+
`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: `
|
|
100
|
+
declare context text: string;
|
|
101
|
+
for (let i = 10; text[i] !== " " && i >= 0; i++) { }
|
|
102
|
+
`,
|
|
103
|
+
snapshot: `
|
|
104
|
+
declare context text: string;
|
|
105
|
+
for (let i = 10; text[i] !== " " && i >= 0; i++) { }
|
|
106
|
+
~~~
|
|
107
|
+
The update moves the counter in the wrong direction for this loop condition.
|
|
84
108
|
`,
|
|
85
109
|
},
|
|
86
110
|
],
|
|
@@ -96,5 +120,6 @@ for (let index = 0; index < 10; index += -1) {}
|
|
|
96
120
|
`for (let index = 10; index > 0; index -= 1) { }`,
|
|
97
121
|
`for (let index = 0; index < 10; index += 2) { }`,
|
|
98
122
|
`for (let index = 10; index > 0; index -= 2) { }`,
|
|
123
|
+
`for (let i = 10; i >= 0 && text[i] === "\\"; i--) { }`,
|
|
99
124
|
],
|
|
100
125
|
});
|
|
@@ -3,7 +3,10 @@ import * as ts from "typescript";
|
|
|
3
3
|
import { getTSNodeRange } from "../getTSNodeRange.js";
|
|
4
4
|
import { typescriptLanguage } from "../language.js";
|
|
5
5
|
|
|
6
|
-
function getConditionDirection(
|
|
6
|
+
function getConditionDirection(
|
|
7
|
+
condition: ts.Expression,
|
|
8
|
+
counterName: string,
|
|
9
|
+
): -1 | 1 | undefined {
|
|
7
10
|
if (!ts.isBinaryExpression(condition)) {
|
|
8
11
|
return undefined;
|
|
9
12
|
}
|
|
@@ -11,6 +14,14 @@ function getConditionDirection(condition: ts.Expression, counterName: string) {
|
|
|
11
14
|
const leftName = getCounterName(condition.left);
|
|
12
15
|
const rightName = getCounterName(condition.right);
|
|
13
16
|
|
|
17
|
+
if (!leftName && !rightName) {
|
|
18
|
+
return condition.operatorToken.kind ===
|
|
19
|
+
ts.SyntaxKind.AmpersandAmpersandToken
|
|
20
|
+
? (getConditionDirection(condition.left, counterName) ??
|
|
21
|
+
getConditionDirection(condition.right, counterName))
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
const isCounterLeft = leftName === counterName;
|
|
15
26
|
const isCounterRight = rightName === counterName;
|
|
16
27
|
|
|
@@ -55,12 +66,12 @@ function getCounterName(node: ts.Expression) {
|
|
|
55
66
|
return ts.isIdentifier(node) ? node.text : undefined;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
function
|
|
69
|
+
function getIncrementorDirection(incrementor: ts.Expression) {
|
|
59
70
|
if (
|
|
60
|
-
ts.isPostfixUnaryExpression(
|
|
61
|
-
ts.isPrefixUnaryExpression(
|
|
71
|
+
ts.isPostfixUnaryExpression(incrementor) ||
|
|
72
|
+
ts.isPrefixUnaryExpression(incrementor)
|
|
62
73
|
) {
|
|
63
|
-
switch (
|
|
74
|
+
switch (incrementor.operator) {
|
|
64
75
|
case ts.SyntaxKind.MinusMinusToken:
|
|
65
76
|
return -1;
|
|
66
77
|
case ts.SyntaxKind.PlusPlusToken:
|
|
@@ -70,8 +81,8 @@ function getUpdateDirection(update: ts.Expression) {
|
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
if (ts.isBinaryExpression(
|
|
74
|
-
const { operatorToken, right } =
|
|
84
|
+
if (ts.isBinaryExpression(incrementor)) {
|
|
85
|
+
const { operatorToken, right } = incrementor;
|
|
75
86
|
|
|
76
87
|
if (
|
|
77
88
|
operatorToken.kind === ts.SyntaxKind.PlusEqualsToken ||
|
|
@@ -140,7 +151,7 @@ export default typescriptLanguage.createRule({
|
|
|
140
151
|
return;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
const updateDirection =
|
|
154
|
+
const updateDirection = getIncrementorDirection(node.incrementor);
|
|
144
155
|
if (!updateDirection) {
|
|
145
156
|
return;
|
|
146
157
|
}
|
|
@@ -150,7 +161,7 @@ export default typescriptLanguage.createRule({
|
|
|
150
161
|
declaration.name.text,
|
|
151
162
|
);
|
|
152
163
|
|
|
153
|
-
if (
|
|
164
|
+
if (conditionDirection && conditionDirection !== updateDirection) {
|
|
154
165
|
context.report({
|
|
155
166
|
message: "wrongDirection",
|
|
156
167
|
range: getTSNodeRange(node.incrementor, context.sourceFile),
|
|
@@ -160,8 +160,8 @@ function calculate() {
|
|
|
160
160
|
snapshot: `
|
|
161
161
|
function calculate() {
|
|
162
162
|
return 077;
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
~~~
|
|
164
|
+
This legacy octal numeric literal evaluates to 63. Did you mean that value, or to use a modern octal syntax such as 0o77?
|
|
165
165
|
}
|
|
166
166
|
`,
|
|
167
167
|
suggestions: [
|