Skip to content

Commit 02ada15

Browse files
authored
Add no-never-initialized-let rule (#55)
### Changelog Added `@foxglove/no-never-initialized-let` (enabled by default). ### Docs Added to readme ### Description Adds a rule to fill a gap when `init-declarations` is not enabled. Related discussions: eslint/eslint#19581 & microsoft/TypeScript#61496 Copying from readme: > ### [`@foxglove/no-never-initialized-let`](./no-never-initialized-let.js) > > Disallow variable declarations that use `let` but have no intitial value and are never assigned. These variables will always be `undefined` and are likely a programmer error. > > The builtin [prefer-const](https://eslint.org/docs/latest/rules/prefer-const) rule doesn't flag these because they lack an initializer. Otherwise, they could be flagged by [init-declarations](https://eslint.org/docs/latest/rules/init-declarations), but this rule is mostly stylistic and has some implications for TypeScript type inference & refinement. (See [eslint/eslint#19581](eslint/eslint#19581) & [microsoft/TypeScript#61496](microsoft/TypeScript#61496) for more discussion.) > > Examples of **incorrect** code for this rule: > > ```ts > let prevX; > let prevY; > if (x !== prevX) { > prevX = x; > } > if (y !== prevY) { > prevX = x; // typo, should have been Y > } > ``` > > Examples of **correct** code for this rule: > > ```ts > let prevX; > let prevY; > if (x !== prevX) { > prevX = x; > } > if (y !== prevY) { > prevY = y; > } > ``` > >
1 parent 6a7e55d commit 02ada15

5 files changed

+148
-2
lines changed

configs/base.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ module.exports = [
7575

7676
// require double equal for null and undefined, triple equal everywhere else
7777
"@foxglove/strict-equality": "error",
78+
"@foxglove/no-never-initialized-let": "error",
7879
"@foxglove/no-return-promise-resolve": "error",
7980
"@foxglove/prefer-hash-private": "error",
8081

plugin.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/** @type {import("eslint").ESLint.Plugin} */
22
module.exports = {
33
rules: {
4-
"strict-equality": require("./rules/strict-equality"),
5-
"no-return-promise-resolve": require("./rules/no-return-promise-resolve"),
64
"no-boolean-parameters": require("./rules/no-boolean-parameters"),
5+
"no-never-initialized-let": require("./rules/no-never-initialized-let"),
76
"no-restricted-imports": require("./rules/no-restricted-imports"),
7+
"no-return-promise-resolve": require("./rules/no-return-promise-resolve"),
88
"prefer-hash-private": require("./rules/prefer-hash-private"),
9+
"strict-equality": require("./rules/strict-equality"),
910
},
1011
};

rules/README.md

+32
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,38 @@ This rule accepts a single object option with the following default configuratio
4040

4141
- `allowLoneParameter: true` will not report an error if a boolean parameter is the **only** parameter to a function.
4242

43+
### [`@foxglove/no-never-initialized-let`](./no-never-initialized-let.js)
44+
45+
Disallow variable declarations that use `let` but have no intitial value and are never assigned. These variables will always be `undefined` and are likely a programmer error.
46+
47+
The builtin [prefer-const](https://eslint.org/docs/latest/rules/prefer-const) rule doesn't flag these because they lack an initializer. Otherwise, they could be flagged by [init-declarations](https://eslint.org/docs/latest/rules/init-declarations), but this rule is mostly stylistic and has some implications for TypeScript type inference & refinement. (See [eslint/eslint#19581](https://github.com/eslint/eslint/issues/19581) & [microsoft/TypeScript#61496](https://github.com/microsoft/TypeScript/issues/61496) for more discussion.)
48+
49+
Examples of **incorrect** code for this rule:
50+
51+
```ts
52+
let prevX;
53+
let prevY;
54+
if (x !== prevX) {
55+
prevX = x;
56+
}
57+
if (y !== prevY) {
58+
prevX = x; // typo, should have been Y
59+
}
60+
```
61+
62+
Examples of **correct** code for this rule:
63+
64+
```ts
65+
let prevX;
66+
let prevY;
67+
if (x !== prevX) {
68+
prevX = x;
69+
}
70+
if (y !== prevY) {
71+
prevY = y;
72+
}
73+
```
74+
4375
### [`@foxglove/no-return-promise-resolve`](./no-return-promise-resolve.js) 🔧
4476

4577
Disallow returning `Promise.resolve(...)` or `Promise.reject(...)` inside an async function. This is redundant since an async function will always return a Promise — use `return` or `throw` directly instead.

rules/no-never-initialized-let.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @type {import("eslint").Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: "problem",
5+
messages: {
6+
letNeverInitialized:
7+
"`let` variable is not initialized and never assigned, so it will always be undefined. Use `const` instead.",
8+
},
9+
},
10+
create: (context) => {
11+
const { sourceCode } = context;
12+
return {
13+
[`VariableDeclaration[kind=let]:not([declare=true]) > VariableDeclarator[id.type=Identifier]:not([init])`](
14+
/** @type {import("estree").VariableDeclarator & { parent: import("eslint").Rule.Node }} */
15+
node
16+
) {
17+
for (const variable of sourceCode.getDeclaredVariables(node)) {
18+
for (const reference of variable.references) {
19+
if (reference.isWrite()) {
20+
return;
21+
}
22+
}
23+
}
24+
context.report({
25+
node,
26+
messageId: "letNeverInitialized",
27+
});
28+
},
29+
};
30+
},
31+
};
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { RuleTester } from "@typescript-eslint/rule-tester";
2+
import { TSESLint } from "@typescript-eslint/utils";
3+
4+
const rule =
5+
// eslint-disable-next-line @typescript-eslint/no-require-imports
6+
require("./no-never-initialized-let") as TSESLint.RuleModule<"letNeverInitialized">;
7+
8+
const ruleTester = new RuleTester({
9+
languageOptions: {
10+
parserOptions: {
11+
project: "./tsconfig.test.json",
12+
},
13+
},
14+
});
15+
16+
ruleTester.run("no-never-initialized-let", rule, {
17+
valid: [
18+
/* ts */ `
19+
const x = undefined;
20+
let y = undefined;
21+
let z: number | undefined = undefined;
22+
let a = x, b = y;
23+
24+
declare let c: string | undefined;
25+
26+
const foo = (two: string): void => {
27+
let one: string | undefined;
28+
if (one !== two) {
29+
one = two;
30+
}
31+
}
32+
`,
33+
],
34+
invalid: [
35+
{
36+
code: /* ts */ `
37+
let x;
38+
let y: number;
39+
let z: number | undefined;
40+
let a = x, b;
41+
`.trim(),
42+
errors: [
43+
{
44+
messageId: "letNeverInitialized",
45+
line: 1,
46+
column: 5,
47+
},
48+
{
49+
messageId: "letNeverInitialized",
50+
line: 2,
51+
column: 5,
52+
},
53+
{
54+
messageId: "letNeverInitialized",
55+
line: 3,
56+
column: 5,
57+
},
58+
{
59+
messageId: "letNeverInitialized",
60+
line: 4,
61+
column: 12,
62+
},
63+
],
64+
},
65+
{
66+
code: /* ts */ `
67+
const foo = (two: string): void => {
68+
let one: string | undefined;
69+
if (one === two) {}
70+
}
71+
`.trim(),
72+
errors: [
73+
{
74+
messageId: "letNeverInitialized",
75+
line: 2,
76+
column: 7,
77+
},
78+
],
79+
},
80+
],
81+
});

0 commit comments

Comments
 (0)