exadev-eslint-config 2.1.0 → 2.1.2
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/README.md +36 -44
- package/dist/index.cjs +14 -2
- package/dist/index.js +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
9
|
-
Multiple ExaDev repos
|
|
10
|
-
|
|
11
|
-
Only the *rules* are centralized here, not a consumer's whole `eslint.config.ts`. A repo's own file-scoping (`files`/`ignores`), tsconfig wiring, and any runtime-isomorphism import bans are genuinely project-specific -- forcing those into one shared config would mean either losing real per-project distinctions or building a heavily-parameterised config just to route around them. Each consumer keeps its own `eslint.config.ts`, importing rule implementations from here instead of a local copy.
|
|
9
|
+
Multiple ExaDev repos carried identical copies of a handful of custom ESLint rules (barrel/index discipline, re-export placement, pointless-alias detection). This package is the single source of truth for those rules. Only the *rules* are centralized -- not a consumer's whole `eslint.config.ts`, since file-scoping, tsconfig wiring, and runtime-isomorphism import bans are genuinely project-specific. Each consumer keeps its own `eslint.config.ts`, importing rule implementations from here.
|
|
12
10
|
|
|
13
11
|
## Getting started
|
|
14
12
|
|
|
15
|
-
Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as peer dependencies
|
|
13
|
+
Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required peer dependencies. Importing anything from this package resolves `typescript-eslint`, since both the default export and `plugin` share the same root module -- ESM/CJS module evaluation runs a module's entire top-level import graph regardless of which export the caller reads (see [Architecture](#architecture)).
|
|
16
14
|
|
|
17
15
|
```sh
|
|
18
16
|
pnpm add -D @exadev/eslint-config typescript-eslint eslint
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
The default export is the full, type-checked ruleset: typescript-eslint's
|
|
19
|
+
The default export is the full, type-checked ruleset: typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` presets, `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, and `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright -- the last two relaxed in test files (see below). Spread it directly into `tseslint.config(...)`:
|
|
22
20
|
|
|
23
21
|
```ts
|
|
24
22
|
// eslint.config.ts
|
|
@@ -36,22 +34,20 @@ export default tseslint.config(
|
|
|
36
34
|
);
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
**A published package whose `src/index.ts` is its package entry point overrides
|
|
37
|
+
**A published package whose `src/index.ts` is its package entry point overrides `banned` to `single` in one line** (flat-config later blocks override earlier rule settings), since deleting its barrel would break every downstream importer:
|
|
40
38
|
|
|
41
39
|
```ts
|
|
42
40
|
...exadev,
|
|
43
41
|
{ rules: { 'exadev/barrel-policy': ['error', { mode: 'single' }] } }, // this package keeps its barrel
|
|
44
42
|
```
|
|
45
43
|
|
|
44
|
+
`recommendedTypeChecked` subsumes typescript-eslint's plain `recommended` outright (all 46 of its rules are a subset of `recommendedTypeChecked`'s 73), and its base config registers the `@typescript-eslint` plugin and sets `languageOptions.parser` itself. That is why **you must remove your own `...tseslint.configs.recommended`/`recommendedTypeChecked`/`stylisticTypeChecked` spreads** -- flat config rejects two different plugin object instances registered under the same namespace. You still supply `languageOptions.parserOptions.project`/`projectService` pointing at your own tsconfig(s).
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
`recommendedTypeChecked` already subsumes typescript-eslint's own plain `recommended` outright -- every one of its 46 rules is a strict subset of `recommendedTypeChecked`'s 73, confirmed by inspecting the actual rule maps. This is a real bundling, not a rule reference that assumes you already have typescript-eslint registered: `recommendedTypeChecked`'s own base config registers the `@typescript-eslint` plugin and sets `languageOptions.parser` itself. That is exactly why **you must remove your own `...tseslint.configs.recommended`/`recommendedTypeChecked`/`stylisticTypeChecked` spreads** rather than keep them alongside this -- ESLint flat config rejects two different plugin object instances registered under the same namespace. What you still supply yourself is `languageOptions.parserOptions.project`/`projectService` pointing at your own tsconfig(s); this bundle's base config never sets that, since it's genuinely project-specific.
|
|
49
|
-
|
|
50
|
-
**Test files (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions above, and only those two.** A compile-time-only `@ts-expect-error` proving a construct genuinely fails to type-check is a well-established, legitimate test pattern -- TypeScript's own "unused `@ts-expect-error` directive" diagnostic already catches one that stops being needed, independent of this rule -- so a test file reverts to the rule's own pre-ban default, `allow-with-description`, rather than the outright ban. `@ts-ignore`/`@ts-nocheck` stay banned even in test files: `@ts-expect-error` is strictly better for both, so there's no legitimate test-specific reason to reach for either. `consistent-type-assertions` relaxes to `assertionStyle: 'as'` in test files -- letting a test construct a partial/stub value with a real `as` assertion where the full type wouldn't otherwise accept it -- while the legacy angle-bracket `<Type>value` form stays banned everywhere, tests included. Nothing inherited from `recommendedTypeChecked`/`stylisticTypeChecked` itself is relaxed in test files; only this package's own two additions are.
|
|
46
|
+
**Test files (`**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`) get two narrow relaxations of this package's own additions, and only those two.** `@ts-expect-error` reverts to `allow-with-description` (a compile-time-only assertion of a type failure is a legitimate test pattern; `@ts-ignore`/`@ts-nocheck` stay banned since `@ts-expect-error` is strictly better). `consistent-type-assertions` relaxes to `assertionStyle: 'as'` (the legacy `<Type>value` form stays banned everywhere). Nothing inherited from the presets is relaxed.
|
|
51
47
|
|
|
52
48
|
### The lighter option: the `plugin` named export
|
|
53
49
|
|
|
54
|
-
For a project that wants only this package's own rules
|
|
50
|
+
For a project that wants only this package's own rules without the full type-checked bundle, import the named `plugin` export and wire rules individually:
|
|
55
51
|
|
|
56
52
|
```ts
|
|
57
53
|
// eslint.config.ts
|
|
@@ -87,7 +83,7 @@ export default defineConfig([
|
|
|
87
83
|
]);
|
|
88
84
|
```
|
|
89
85
|
|
|
90
|
-
`
|
|
86
|
+
`tseslint.config()` does **not** accept string `extends` (only `defineConfig()` does); pass the config value directly instead:
|
|
91
87
|
|
|
92
88
|
```ts
|
|
93
89
|
import { plugin } from '@exadev/eslint-config';
|
|
@@ -103,35 +99,31 @@ export default tseslint.config(
|
|
|
103
99
|
);
|
|
104
100
|
```
|
|
105
101
|
|
|
106
|
-
**`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores`
|
|
107
|
-
|
|
108
|
-
The one thing self-scoping can't know on your behalf is a barrel that lives somewhere other than `src/index.ts`, or a project-specific exception beyond the barrel (an extra file you want exempt from the re-export ban). For either of those, layer an additional override on top of `recommended`/`barrel` -- e.g. `{ files: ['lib/other-legacy-reexport.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }` -- rather than falling back to wiring all four rules individually, which is still fine but no longer required for the common case.
|
|
109
|
-
|
|
110
|
-
`plugin.configs.recommended`/`plugin.configs.barrel` are not usable without `typescript-eslint` installed, even though neither config itself references it: `plugin` is a named export sharing its root module with the default export, so `typescript-eslint` resolves the moment anything is imported from `@exadev/eslint-config` at all -- see [Architecture](#architecture) for the trade-off this reflects.
|
|
102
|
+
**`plugin.configs.recommended`/`plugin.configs.barrel` carry no `files`/`ignores` and are safe unscoped** -- `no-side-effects-in-index` and `no-non-barrel-reexport` each check `context.filename` themselves (self-scoping). For a barrel not at `src/index.ts`, or a project-specific exception, layer an override on top (e.g. `{ files: ['lib/other.ts'], rules: { 'exadev/no-non-barrel-reexport': 'off' } }`) rather than wiring all four rules individually.
|
|
111
103
|
|
|
112
104
|
## Rules
|
|
113
105
|
|
|
114
106
|
| Rule | Fixable | Description |
|
|
115
107
|
| --- | --- | --- |
|
|
116
|
-
| `barrel-policy` | |
|
|
108
|
+
| `barrel-policy` | | Umbrella over the four barrel rules below: one `{ mode }` option selecting a whole index-file policy. See [Barrel policy](#barrel-policy). |
|
|
117
109
|
| `no-index-files` | | Bans any `index.*` file outright (mode 1). The strictest policy. |
|
|
118
|
-
| `no-non-barrel-index` | | Only `src/index.ts` may be named `index.*` -- any other
|
|
119
|
-
| `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel. Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match.
|
|
120
|
-
| `no-side-effects-in-index` | | A barrel
|
|
121
|
-
| `barrel-direct-siblings-only` | | A barrel may re-export only from a direct sibling
|
|
122
|
-
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. |
|
|
110
|
+
| `no-non-barrel-index` | | Only `src/index.ts` may be named `index.*` -- any other `index.ts`/`.js`/etc would be silently selected by a consumer's bare directory import. |
|
|
111
|
+
| `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel. Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match. Autofix deletes the export and the now-pointless import when it was the import's only use. Self-scopes away from any index file. |
|
|
112
|
+
| `no-side-effects-in-index` | | A barrel file may contain only re-export statements -- nothing that could execute at import time. Self-scopes to any index file. |
|
|
113
|
+
| `barrel-direct-siblings-only` | | A barrel may re-export only from a direct sibling (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
114
|
+
| `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. Autofix rewrites every read to the original name and deletes the declaration (including its `export` keyword, when exported). Still reported but deliberately not auto-fixable where collapsing the alias would change meaning: an explicit type annotation (`const exhaustive: never = item` -- the annotation is the point), a read where the original name is shadowed, a read as a shorthand object property, more than one declarator in the statement, or a source that is written to anywhere. |
|
|
123
115
|
|
|
124
116
|
## Barrel policy
|
|
125
117
|
|
|
126
|
-
`exadev/barrel-policy` is the convenience layer
|
|
118
|
+
`exadev/barrel-policy` is the convenience layer: one rule id, one `{ mode }` option selecting a complete index-file policy. Use EITHER this umbrella OR the individual rules (not both -- they double-report).
|
|
127
119
|
|
|
128
|
-
| `mode` | Which files may be barrels | What a barrel may contain | Where
|
|
120
|
+
| `mode` | Which files may be barrels | What a barrel may contain | Where re-exports may come from |
|
|
129
121
|
| --- | --- | --- | --- |
|
|
130
|
-
| `'banned'` (
|
|
122
|
+
| `'banned'` (default/recommended) | none | — | — |
|
|
131
123
|
| `'single'` | exactly `src/index.ts` | only re-exports | anywhere |
|
|
132
124
|
| `'siblings'` | any `index.ts` | only re-exports | a direct sibling only (`./module`) |
|
|
133
125
|
|
|
134
|
-
In every mode, re-exports are banned
|
|
126
|
+
In every mode, re-exports are banned outside a permitted barrel, and a permitted barrel may contain only re-export statements. The umbrella composes the identical predicates the standalone rules use (shared in `src/rules/barrel-helpers.ts`). It is non-fixable -- the autofix lives on `no-non-barrel-reexport`.
|
|
135
127
|
|
|
136
128
|
## Build, test, and lint
|
|
137
129
|
|
|
@@ -143,46 +135,46 @@ pnpm test
|
|
|
143
135
|
pnpm build
|
|
144
136
|
```
|
|
145
137
|
|
|
146
|
-
Each rule has a co-located `*.test.ts`
|
|
138
|
+
Each rule has a co-located `*.test.ts` exercising it with ESLint's `RuleTester` under Vitest. `vitest.setup.ts` wires `RuleTester.describe`/`.it`/`.itOnly` to Vitest's `describe`/`it` explicitly (no `test.globals`). Each test uses typescript-eslint's parser for TypeScript-only fixtures; none need type information.
|
|
147
139
|
|
|
148
|
-
`pnpm test` always measures coverage (
|
|
140
|
+
`pnpm test` always measures coverage (`@vitest/coverage-v8`), scoped to `src/**/*.ts` excluding `*.test.ts`. Text output in terminal; `html`/`lcov` in `coverage/` (gitignored alongside `.eslintcache` and `dist/`).
|
|
149
141
|
|
|
150
|
-
The `lint`/`typecheck`/`test`/`build` npm scripts
|
|
142
|
+
The `lint`/`typecheck`/`test`/`build` npm scripts wrap turbo tasks named `_lint`/`_typecheck`/`_test`/`_build` -- run `pnpm build`, not `turbo run build`.
|
|
151
143
|
|
|
152
|
-
`pnpm build` runs `tsdown` from
|
|
144
|
+
`pnpm build` runs `tsdown` from `src/index.ts`, bundling the whole module graph into ESM + CJS + declarations. `prepublishOnly` re-runs lint, typecheck, `test`, `tsdown`, `publint`, and `attw --pack`.
|
|
153
145
|
|
|
154
146
|
## Architecture
|
|
155
147
|
|
|
156
|
-
`src/plugin.ts` builds an `ESLint.Plugin`
|
|
148
|
+
`src/plugin.ts` builds an `ESLint.Plugin` (ESLint's own type) combining `src/rules/` into a flat `rules` map. `configs.recommended` and `configs.barrel` are getters in the object literal -- each references the fully-built `plugin` (`plugins: { exadev: plugin }`), which a plain property initializer can't do mid-construction. `recommended` ships `barrel-policy` at `mode: 'banned'`; `barrel` at `mode: 'single'`.
|
|
157
149
|
|
|
158
|
-
`src/recommended-type-checked.ts` bundles typescript-eslint's
|
|
150
|
+
`src/recommended-type-checked.ts` bundles typescript-eslint's `recommendedTypeChecked` + `stylisticTypeChecked` alongside this plugin's rules into a flat config array. Its value is typed as `ConfigArrayValue = Extract<ConfigValue, unknown[]>` (the array-only member of ESLint's own config-value union), because annotating with the wider union broke `...exadev` with `TS2488`.
|
|
159
151
|
|
|
160
|
-
`src/index.ts` is the
|
|
152
|
+
`src/index.ts` is the entry point: `export { default } from './recommended-type-checked'; export { default as plugin } from './plugin';`. Both exports share one root module, so importing `{ plugin }` alone still resolves `typescript-eslint` via the sibling re-export -- an accepted trade-off (an earlier separate-subpath split proved more awkward in practice).
|
|
161
153
|
|
|
162
|
-
`pnpm-workspace.yaml`
|
|
154
|
+
`pnpm-workspace.yaml` declares an empty `packages: []` -- not a real workspace, just giving turbo a root for local task caching.
|
|
163
155
|
|
|
164
156
|
## Conventions
|
|
165
157
|
|
|
166
|
-
`eslint.config.ts` dogfoods
|
|
158
|
+
`eslint.config.ts` dogfoods the default export on itself (`import exadev from './src/index'`), spreading it exactly as a real consumer would. `no-side-effects-in-index` and `no-non-barrel-reexport` self-scope to `src/index.ts` internally, so no `files`/`ignores` wiring is needed here. Plugin construction lives in `src/plugin.ts` specifically so `src/index.ts` stays a pure re-export point.
|
|
167
159
|
|
|
168
|
-
`tsconfig.json` enables `verbatimModuleSyntax` (
|
|
160
|
+
`tsconfig.json` enables `verbatimModuleSyntax` (`import type`/`export type` required for type-only imports -- also enforced by `consistent-type-imports`) and `noUncheckedIndexedAccess` (narrow indexed access before use rather than asserting).
|
|
169
161
|
|
|
170
|
-
Conventional commits are enforced by commitlint, restricted to the type-enum defined once in `release.config.ts`'s `commitTypes` -- both commitlint
|
|
162
|
+
Conventional commits are enforced by commitlint, restricted to the type-enum defined once in `release.config.ts`'s `commitTypes` -- both commitlint and semantic-release derive from that single list.
|
|
171
163
|
|
|
172
164
|
## Gotchas and quirks
|
|
173
165
|
|
|
174
|
-
- `.attw.json` ignores
|
|
175
|
-
- `src/index.ts` mixing a default export with a named one
|
|
176
|
-
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint
|
|
177
|
-
- The CI release job sets `HUSKY=0` (
|
|
166
|
+
- `.attw.json` ignores `false-export-default`: tsdown/rolldown's CJS output for this plugin's sole default export doesn't emit the `export =` form `arethetypeswrong` wants under legacy `node10` resolution. The modes ESLint flat config uses (`node16`, `bundler`) are unaffected, so the rule is suppressed rather than changing the default-export shape.
|
|
167
|
+
- `src/index.ts` mixing a default export with a named one triggers rolldown's `MIXED_EXPORTS` warning: a raw CommonJS `require()` would see the raw exports object instead of the default. ESM `import` (the actual consumer path) resolves both correctly; `attw --pack` and `publint` report no problems, so the warning is accepted (see `tsdown.config.ts`).
|
|
168
|
+
- Husky hooks: `pre-commit` runs lint-staged (`eslint --fix` on staged `*.ts`), `commit-msg` runs commitlint, `pre-push` runs typecheck + test + build.
|
|
169
|
+
- The CI release job sets `HUSKY=0` (commit-msg hook skips the automated release commit) and blanks `NPM_TOKEN`/`NODE_AUTH_TOKEN` explicitly so an inherited token can't win over OIDC trusted publishing.
|
|
178
170
|
|
|
179
171
|
## Contributing
|
|
180
172
|
|
|
181
|
-
Conventional commits are enforced by
|
|
173
|
+
Conventional commits are enforced by a husky `commit-msg` hook and re-checked in CI. CI runs commitlint, lint, and typecheck+test+build+attw on every push and pull request; the release job runs only on push to `main`, after all pass.
|
|
182
174
|
|
|
183
175
|
## Release
|
|
184
176
|
|
|
185
|
-
Conventional commits drive [semantic-release](https://semantic-release.gitbook.io/semantic-release) on every push to `main`: version bump, `CHANGELOG.md`, GitHub Release, and
|
|
177
|
+
Conventional commits drive [semantic-release](https://semantic-release.gitbook.io/semantic-release) on every push to `main`: version bump, `CHANGELOG.md`, GitHub Release, and npm publish via OIDC (no stored token). A second CI job republishes the identical build under the unscoped alias `exadev-eslint-config`.
|
|
186
178
|
|
|
187
179
|
## License
|
|
188
180
|
|
package/dist/index.cjs
CHANGED
|
@@ -28,7 +28,7 @@ let typescript_eslint = require("typescript-eslint");
|
|
|
28
28
|
typescript_eslint = __toESM(typescript_eslint, 1);
|
|
29
29
|
let node_path = require("node:path");
|
|
30
30
|
//#region package.json
|
|
31
|
-
var version = "2.1.
|
|
31
|
+
var version = "2.1.2";
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/rules/barrel-helpers.ts
|
|
34
34
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -375,6 +375,15 @@ const noNonBarrelReexport = {
|
|
|
375
375
|
function isIdentifierReference(reference) {
|
|
376
376
|
return reference.identifier.type === "Identifier";
|
|
377
377
|
}
|
|
378
|
+
function hasTypeAnnotation(id) {
|
|
379
|
+
return "typeAnnotation" in id && id.typeAnnotation !== void 0 && id.typeAnnotation !== null;
|
|
380
|
+
}
|
|
381
|
+
function resolveFrom(scope, name) {
|
|
382
|
+
for (let current = scope; current; current = current.upper) {
|
|
383
|
+
const found = current.set.get(name);
|
|
384
|
+
if (found) return found;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
378
387
|
//#endregion
|
|
379
388
|
//#region src/plugin.ts
|
|
380
389
|
const plugin = {
|
|
@@ -405,6 +414,7 @@ const plugin = {
|
|
|
405
414
|
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
406
415
|
const aliasName = node.id.name;
|
|
407
416
|
const originalName = node.init.name;
|
|
417
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
408
418
|
context.report({
|
|
409
419
|
node,
|
|
410
420
|
messageId: "pointlessReassignment",
|
|
@@ -415,6 +425,7 @@ const plugin = {
|
|
|
415
425
|
fix(fixer) {
|
|
416
426
|
const variable = scope.set.get(aliasName);
|
|
417
427
|
if (!variable) return null;
|
|
428
|
+
if (aliasIsAnnotated) return null;
|
|
418
429
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
419
430
|
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
420
431
|
if (readRefs.some((reference) => {
|
|
@@ -430,10 +441,11 @@ const plugin = {
|
|
|
430
441
|
}
|
|
431
442
|
return false;
|
|
432
443
|
})) return null;
|
|
444
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
433
445
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
434
446
|
const declaration = node.parent;
|
|
435
447
|
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
436
|
-
fixes.push(fixer.remove(declaration));
|
|
448
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
437
449
|
return fixes;
|
|
438
450
|
}
|
|
439
451
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import tseslint from "typescript-eslint";
|
|
2
2
|
import { posix } from "node:path";
|
|
3
3
|
//#region package.json
|
|
4
|
-
var version = "2.1.
|
|
4
|
+
var version = "2.1.2";
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/rules/barrel-helpers.ts
|
|
7
7
|
const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
|
|
@@ -348,6 +348,15 @@ const noNonBarrelReexport = {
|
|
|
348
348
|
function isIdentifierReference(reference) {
|
|
349
349
|
return reference.identifier.type === "Identifier";
|
|
350
350
|
}
|
|
351
|
+
function hasTypeAnnotation(id) {
|
|
352
|
+
return "typeAnnotation" in id && id.typeAnnotation !== void 0 && id.typeAnnotation !== null;
|
|
353
|
+
}
|
|
354
|
+
function resolveFrom(scope, name) {
|
|
355
|
+
for (let current = scope; current; current = current.upper) {
|
|
356
|
+
const found = current.set.get(name);
|
|
357
|
+
if (found) return found;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
351
360
|
//#endregion
|
|
352
361
|
//#region src/plugin.ts
|
|
353
362
|
const plugin = {
|
|
@@ -378,6 +387,7 @@ const plugin = {
|
|
|
378
387
|
if (!sourceVariable || sourceVariable.references.some((reference) => reference.isWrite() && !reference.init)) return;
|
|
379
388
|
const aliasName = node.id.name;
|
|
380
389
|
const originalName = node.init.name;
|
|
390
|
+
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
381
391
|
context.report({
|
|
382
392
|
node,
|
|
383
393
|
messageId: "pointlessReassignment",
|
|
@@ -388,6 +398,7 @@ const plugin = {
|
|
|
388
398
|
fix(fixer) {
|
|
389
399
|
const variable = scope.set.get(aliasName);
|
|
390
400
|
if (!variable) return null;
|
|
401
|
+
if (aliasIsAnnotated) return null;
|
|
391
402
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
392
403
|
const readRefs = variable.references.filter((reference) => reference.isRead() && isIdentifierReference(reference));
|
|
393
404
|
if (readRefs.some((reference) => {
|
|
@@ -403,10 +414,11 @@ const plugin = {
|
|
|
403
414
|
}
|
|
404
415
|
return false;
|
|
405
416
|
})) return null;
|
|
417
|
+
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
406
418
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
407
419
|
const declaration = node.parent;
|
|
408
420
|
if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
|
|
409
|
-
fixes.push(fixer.remove(declaration));
|
|
421
|
+
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
410
422
|
return fixes;
|
|
411
423
|
}
|
|
412
424
|
});
|