UNPKG

@rushstack/eslint-plugin

Version:

An ESLint plugin providing supplementary rules for use with the @rushstack/eslint-config package

626 lines (435 loc) 22.8 kB
# @rushstack/eslint-plugin This plugin implements supplementary rules for use with the `@rushstack/eslint-config` package, which provides a TypeScript ESLint ruleset tailored for large teams and projects. Please see [that project's documentation](https://www.npmjs.com/package/@rushstack/eslint-config) for details. To learn about Rush Stack, please visit: [https://rushstack.io/](https://rushstack.io/) ## `@rushstack/hoist-jest-mock` Require Jest module mocking APIs to be called before any other statements in their code block. #### Rule Details Jest module mocking APIs such as "jest.mock()" must be called before the associated module is imported, otherwise they will have no effect. Transpilers such as `ts-jest` and `babel-jest` automatically "hoist" these calls, however this can produce counterintuitive behavior. Instead, the `hoist-jest-mocks` lint rule simply requires developers to write the statements in the correct order. The following APIs are affected: 'jest.mock()', 'jest.unmock()', 'jest.enableAutomock()', 'jest.disableAutomock()', 'jest.deepUnmock()'. For technical background, please read the Jest documentation here: https://jestjs.io/docs/en/es6-class-mocks #### Examples The following patterns are considered problems when `@rushstack/hoist-jest-mock` is enabled: ```ts import * as file from './file'; // import statement jest.mock('./file'); // error test("example", () => { jest.mock('./file2'); // error }); ``` ```ts require('./file'); // import statement jest.mock('./file'); // error ``` The following patterns are NOT considered problems: ```ts jest.mock('./file'); // okay, because mock() precedes the import below import * as file from './file'; // import statement ``` ```ts // These statements are not real "imports" because they import compile-time types // without any runtime effects import type { X } from './file'; let y: typeof import('./file'); jest.mock('./file'); // okay ``` ## `@rushstack/import-requires-chunk-name` Require each dynamic `import()` used for code splitting to specify exactly one Webpack chunk name via a magic comment. #### Rule Details When using dynamic `import()` to create separately loaded chunks, Webpack (and compatible bundlers such as Rspack) can assign a deterministic name if a `/* webpackChunkName: 'my-chunk' */` or `// webpackChunkName: "my-chunk"` magic comment is provided. Without an explicit name, the bundler falls back to autogenerated identifiers (often numeric or hashed), which: - Are less stable across refactors, hurting long‑term caching - Make bundle analysis and performance troubleshooting harder - Can produce confusing diffs during code reviews This rule enforces that: 1. Every `import(<specifier>)` expression used for code splitting includes a chunk name magic comment inside its parentheses. 2. Exactly one chunk name is declared. Multiple chunk name comments (or a single comment containing multiple comma‑separated `webpackChunkName` entries) are flagged. The chunk name must appear in either a block or line comment inside the `import()` call. Accepted forms: ```ts import(/* webpackChunkName: 'feature-settings' */ './feature/settings'); import( /* webpackChunkName: "feature-settings" */ './feature/settings' ); import( // webpackChunkName: "feature-settings" './feature/settings' ); ``` No options are currently supported. For background on magic comments, see: https://webpack.js.org/api/module-methods/#magic-comments #### Examples The following patterns are considered problems when `@rushstack/import-requires-chunk-name` is enabled: ```ts // Missing chunk name import('./feature/settings'); // error ``` ```ts // Multiple chunk name comments import( /* webpackChunkName: 'feature-settings' */ /* webpackChunkName: 'feature-settings-alt' */ './feature/settings' ); // error ``` ```ts // Multiple chunk names in a single comment (comma separated) import( /* webpackChunkName: 'feature-settings', webpackChunkName: 'feature-settings-alt' */ './feature/settings' ); // error ``` The following patterns are NOT considered problems: ```ts // Single block comment with one chunk name import(/* webpackChunkName: 'feature-settings' */ './feature/settings'); ``` ```ts // Multiline formatting with a block comment import( /* webpackChunkName: 'feature-settings' */ './feature/settings' ); ``` ```ts // Line comment form import( // webpackChunkName: 'feature-settings' './feature/settings' ); ``` #### Notes - If your bundler does not understand Webpack magic comments (e.g. plain Node ESM loader), disable this rule for that project. - Choose stable, descriptive chunk names—avoid including hashes, timestamps, or environment‑specific tokens. - Chunk names share a global namespace in the final bundle; avoid collisions to keep analysis clear. #### Rationale Explicit chunk naming improves cache hit rates, observability, and maintainability. Enforcing the practice via an ESLint rule prevents missing or duplicate declarations that could lead to unpredictable bundle naming. ## `@rushstack/no-backslash-imports` Prevent import and export specifiers from using Windows-style backslashes in module paths. #### Rule Details JavaScript module specifiers always use POSIX forward slashes. Using backslashes (e.g. `import './src\utils'`) can lead to inconsistent behavior across tools, and may break resolution in some environments. This rule flags any import or export whose source contains a `\` character and provides an autofix that replaces backslashes with `/`. #### Examples The following patterns are considered problems when `@rushstack/no-backslash-imports` is enabled: ```ts import helper from './lib\\helper'; // error (autofix -> './lib/helper') export * from './data\\items'; // error ``` The following patterns are NOT considered problems: ```ts import helper from './lib/helper'; export * from '../data/items'; ``` #### Notes - Works for `import`, dynamic `import()`, and `export ... from` forms. - Loader/query strings (e.g. `raw-loader!./file`) are preserved during the fix; only path separators are changed. #### Rationale Forward slashes are portable and avoid subtle cross-platform inconsistencies. Autofixing reduces churn and enforces a predictable style. ## `@rushstack/no-external-local-imports` Prevent relative imports that reach outside the configured TypeScript `rootDir` (if specified) or outside the package boundary. #### Rule Details Local relative imports should refer only to files that are part of the compiling unit: either under the package directory or (when a `rootDir` is configured) under that root. Reaching outside can accidentally couple a package to sibling projects, untracked build inputs, or files excluded from type checking. This rule resolves each relative import/ export source and ensures the target is contained within the effective root. If not, it is flagged. #### Examples Assume `rootDir` is `src` and the package folder is `/repo/packages/example`: ```ts // In /repo/packages/example/src/components/Button.ts import '../utils/file'; // error if '../utils/file' is outside src import '../../../other-package/src/index'; // error (outside package root) ``` ```ts // In /repo/packages/example/src/index.ts import './utils/file'; // passes (inside rootDir) ``` #### Notes - Only relative specifiers are checked. Package specifiers (`react`, `lodash`) are ignored. - If no `rootDir` is defined, the package directory acts as the boundary. - Useful for enforcing project isolation in monorepos. #### Rationale Prevents accidental dependencies on files that aren’t part of the compilation or publishing surface, improving encapsulation and build reproducibility. ## `@rushstack/no-new-null` Prevent usage of the JavaScript `null` value, while allowing code to access existing APIs that may require `null`. #### Rule Details Most programming languages have a "null" or "nil" value that serves several purposes: 1. the initial value for an uninitialized variable 2. the value of `x.y` or `x["y"]` when `x` has no such key, and 3. a special token that developers can assign to indicate an unknown or empty state. In JavaScript, the `undefined` value fulfills all three roles. JavaScript's `null` value is a redundant secondary token that only fulfills (3), even though its name confusingly implies otherwise. The `null` value was arguably a mistake in the original JavaScript language design, but it cannot be banned entirely because it is returned by some entrenched system APIs such as `JSON.parse()`, and also some popular NPM packages. Thus, this rule aims to tolerate preexisting `null` values while preventing new ones from being introduced. The `@rushstack/no-new-null` rule flags type definitions with `null` that can be exported or used by others. The rule ignores declarations that are local variables, private members, or types that are not exported. If you are designing a new JSON file format, it's a good idea to avoid `null` entirely. In most cases there are better representations that convey more information about an item that is unknown, omitted, or disabled. If you do need to declare types for JSON structures containing `null`, rather than suppressing the lint rule, you can use a specialized [JsonNull](https://api.rushstack.io/pages/node-core-library.jsonnull/) type as provided by [@rushstack/node-core-library](https://www.npmjs.com/package/@rushstack/node-core-library). #### Examples The following patterns are considered problems when `@rushstack/no-new-null` is enabled: ```ts // interface declaration with null field interface IHello { hello: null; } // error // type declaration with null field type Hello = { hello: null; } // error // type function alias type T = (args: string | null) => void; // error // type alias type N = null; // error // type constructor type C = {new (args: string | null)} // error // function declaration with null args function hello(world: string | null): void {}; // error function legacy(callback: (err: Error| null) => void): void { }; // error // function with null return type function hello(): (err: Error | null) => void {}; // error // const with null type const nullType: 'hello' | null = 'hello'; // error // classes with publicly visible properties and methods class PublicNulls { property: string | null; // error propertyFunc: (val: string | null) => void; // error legacyImplicitPublic(hello: string | null): void {} // error public legacyExplicitPublic(hello: string | null): void {} // error } ``` The following patterns are NOT considered problems: ```ts // wrapping an null-API export function ok(hello: string): void { const innerCallback: (err: Error | null) => void = (e) => {}; // passes return innerCallback(null); } // classes where null APIs are used, but are private-only class PrivateNulls { private pField: string | null; // passes private pFunc: (val: string | null) => void; // passes private legacyPrivate(hello: string | null): void { // passes this.pField = hello; this.pFunc(this.pField) this.pFunc('hello') } } ``` ## `@rushstack/no-null` (Deprecated) Prevent usage of JavaScript's `null` keyword. #### Rule Details This rule has been superseded by `@rushstack/no-new-null`, and is maintained to support code that has not migrated to the new rule yet. The `@rushstack/no-null` rule prohibits `null` as a literal value, but allows it in type annotations. Comparisons with `null` are also allowed. #### Examples The following patterns are considered problems when `@rushstack/no-null` is enabled: ```ts let x = null; // error f(null); // error function g() { return null; // error } ``` The following patterns are NOT considered problems: ```ts let x: number | null = f(); // declaring types as possibly "null" is okay if (x === null) { // comparisons are okay x = 0; } ``` ## `@rushstack/no-transitive-dependency-imports` Prevent importing modules from transitive dependencies that are not declared in the package’s direct dependency list. #### Rule Details Packages should only import modules from their own direct dependencies. Importing a transitive dependency (available only because another dependency pulled it in) creates hidden coupling and can break when versions change. This rule detects any import path containing multiple `node_modules` segments (for relative paths) or any direct reference to a nested `node_modules` folder for package specifiers, flagging such usages. Allowed exception: a single relative traversal into `node_modules` (e.g. `import '../node_modules/some-pkg/dist/index.js'`) is tolerated to support bypassing package `exports` fields intentionally. Additional traversals are disallowed. #### Examples The following patterns are considered problems when `@rushstack/no-transitive-dependency-imports` is enabled: ```ts // Transitive dependency via deep relative path import '../../node_modules/some-pkg/node_modules/other-pkg/lib/internal'; // error (multiple node_modules segments) // Direct package import that resolves into nested node_modules (caught via parsing) import 'other-pkg/node_modules/inner-pkg'; // error ``` The following patterns are NOT considered problems: ```ts // Direct dependency import 'react'; // Single bypass to reach a file export import '../node_modules/some-pkg/dist/index.js'; ``` #### Notes - Encourages declaring needed dependencies explicitly in `package.json`. - Reduces breakage due to indirect version changes. #### Rationale Explicit declarations keep dependency graphs understandable and maintainable; avoiding transitive imports prevents fragile build outcomes. ## `@rushstack/no-untyped-underscore` (Opt-in) Prevent TypeScript code from accessing legacy JavaScript members whose name has an underscore prefix. #### Rule Details JavaScript does not provide a straightforward way to restrict access to object members, so API names commonly indicate a private member by using an underscore prefix (e.g. `exampleObject._privateMember`). For inexperienced developers who may be unfamiliar with this convention, in TypeScript we can mark the APIs as `private` or omit them from the typings. However, when migrating a large code base to TypeScript, it may be difficult to declare types for every legacy API. In this situation, the `@rushstack/no-untyped-underscore` rule can help. This rule detects expressions that access a member with an underscore prefix, EXCEPT in cases where: - The object is typed: specifically, `exampleObject` has a TypeScript type that declares `_privateMember`; OR - The object expression uses: the `this` or `super` keywords; OR - The object expression is a variable named `that`. (In older ES5 code, `that` was commonly used as an alias for `this` in unbound contexts.) #### Examples The following patterns are considered problems when `@rushstack/no-untyped-underscore` is enabled: ```ts let x: any; x._privateMember = 123; // error, because x is untyped let x: { [key: string]: number }; x._privateMember = 123; // error, because _privateMember is not a declared member of x's type ``` The following patterns are NOT considered problems: ```ts let x: { _privateMember: any }; x._privateMember = 123; // okay, because _privateMember is declared by x's type let x = { _privateMember: 0 }; x._privateMember = 123; // okay, because _privateMember is part of the inferred type enum E { _PrivateMember } let e: E._PrivateMember = E._PrivateMember; // okay, because _PrivateMember is declared by E ``` ## `@rushstack/normalized-imports` Require relative import paths to be written in a normalized minimal form and autofix unnecessary directory traversals. #### Rule Details Developers sometimes write relative paths with redundant traversals (e.g. `import '../module'` when already in the parent, or `import '././utils'`). This rule computes the shortest relative path between the importing file and target, rewrites it using POSIX separators, and ensures a leading `./` is present when needed. Non-relative (package) imports are ignored. If the provided path differs from the normalized form, the rule reports it and autofixes to the canonical specifier while preserving loader/query suffixes. #### Examples The following patterns are considered problems when `@rushstack/normalized-imports` is enabled: ```ts // Redundant parent traversal import '../currentDir/utils'; // error (autofix -> './utils') // Repeated ./ segments import '././components/Button'; // error (autofix -> './components/Button') ``` The following patterns are NOT considered problems: ```ts import './utils'; import '../shared/types'; ``` #### Notes - Only relative paths (`./` or `../`) are normalized. - Helps produce deterministic diff noise and cleaner refactors. #### Rationale Consistent relative paths improve readability and make large-scale moves/renames less error-prone. ## `@rushstack/pair-react-dom-render-unmount` Require ReactDOM (legacy) render trees created in a file to be explicitly unmounted in that same file to avoid memory leaks. #### Rule Details React 18 introduced `ReactDOM.createRoot()` and `root.unmount()`, but many codebases still use the legacy APIs: - `ReactDOM.render(element, container)` - `ReactDOM.unmountComponentAtNode(container)` If a component tree is rendered and the container node is later discarded without an explicit unmount, detached DOM nodes and event handlers may remain in memory. This rule enforces a simple pairing discipline: the total number of render calls in a file must match the total number of unmount calls. If they differ, every render and unmount in the file is flagged so the developer can reconcile them. The rule detects both namespace invocations (e.g. `ReactDOM.render(...)`) and separately imported named functions (e.g. `import { render, unmountComponentAtNode } from 'react-dom'`). Default or namespace imports (e.g. `import * as ReactDOM from 'react-dom'` or `import ReactDOM from 'react-dom'`) are supported. No configuration options are currently supported. #### Examples The following patterns are considered problems when `@rushstack/pair-react-dom-render-unmount` is enabled: ```ts import * as ReactDOM from 'react-dom'; ReactDOM.render(<App /> , document.getElementById('root')); // Missing matching unmount ``` ```ts import { render } from 'react-dom'; render(<App /> , document.getElementById('root')); // Missing matching unmountComponentAtNode ``` ```ts import { unmountComponentAtNode } from 'react-dom'; // Unmount without a corresponding render in this file unmountComponentAtNode(document.getElementById('root')!); ``` ```ts import { render, unmountComponentAtNode } from 'react-dom'; render(<A /> , a); render(<B /> , b); // Only one unmount unmountComponentAtNode(a); // "b"'s render is not paired ``` The following patterns are NOT considered problems: ```ts import * as ReactDOM from 'react-dom'; const rootEl = document.getElementById('root'); ReactDOM.render(<App /> , rootEl); ReactDOM.unmountComponentAtNode(rootEl!); ``` ```ts import { render, unmountComponentAtNode } from 'react-dom'; render(<A /> , a); render(<B /> , b); unmountComponentAtNode(a); unmountComponentAtNode(b); // All renders paired ``` ```ts // No legacy ReactDOM render/unmount usage in this file // (e.g. uses React 18 createRoot API or just defines components) — rule passes ``` #### Notes - The rule does not attempt dataflow analysis to verify the same container node is passed; it only enforces count parity. - Modern React apps using `createRoot()` should migrate to pairing `root.unmount()`. This legacy rule helps older code until migration is complete. - Multiple files can coordinate unmounting (e.g. via a shared cleanup utility); in that case this rule will flag the imbalance—consider colocating the unmount or disabling the rule for that file. #### Rationale Unpaired legacy renders are a common cause of memory leaks and test pollution. A lightweight count-based heuristic catches most oversights without requiring complex static analysis. ## `@rushstack/typedef-var` Require explicit type annotations for top-level variable declarations, while exempting local variables within function or method scopes. #### Rule Details This rule is implemented to supplement the deprecated `@typescript-eslint/typedef` rule. The `@typescript-eslint/typedef` rule was deprecated based on the judgment that "unnecessary type annotations, where type inference is sufficient, can be cumbersome to maintain and generally reduce code readability." However, we prioritize code reading and maintenance over code authorship. That is, even when the compiler can infer a type, this rule enforces explicit type annotations to ensure that a code reviewer (e.g., when viewing a GitHub Diff) does not have to rely entirely on inference and can immediately ascertain a variable's type. This approach makes writing code harder but significantly improves the more crucial activity of reading and reviewing code. Therefore, the `@rushstack/typedef-var` rule enforces type annotations for all variable declarations outside of local function or class method scopes. This includes the module's top-level scope and any block scopes that do not belong to a function or method. To balance this strictness with code authoring convenience, the rule deliberately relaxes the type annotation requirement for the following local variable declarations: - Variable declarations within a function body. - Variable declarations within a class method. - Variables declared via object or array destructuring assignments. #### Examples The following patterns are considered problems when `@rushstack/typedef-var` is enabled: ```ts // Top-level declarations lack explicit type annotations const x = 123; // error let x = 123; // error var x = 123; // error ``` ```ts // Declaration within a non-function block scope { const x = 123; // error } ``` The following patterns are NOT considered problems: ```ts // Local variables inside function expressions are exempt function f() { const x = 123; } // passes const f = () => { const x = 123; }; // passes const f = function() { const x = 123; } // passes ``` ```ts // Local variables inside class methods are exempt class C { public m(): void { const x = 123; // passes } } class C { public m = (): void => { const x = 123; // passes } } ``` ```ts // Array and Object Destructuring assignments are exempt let { a, b } = { // passes a: 123, b: 234 } ``` ## Links - [CHANGELOG.md]( https://github.com/microsoft/rushstack/blob/main/eslint/eslint-plugin/CHANGELOG.md) - Find out what's new in the latest version `@rushstack/eslint-plugin` is part of the [Rush Stack](https://rushstack.io/) family of projects.