Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Consumers need `eslint >=10.0.0` and `typescript-eslint >=8.0.0` as required pee
pnpm add -D @exadev/eslint-config typescript-eslint eslint
```

The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more -- those are no longer re-listed below), `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/consistent-return` (a function that implicitly returns `undefined` on one path and a real value on another -- a common real bug, not a deliberate design), `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-shadow`, `@typescript-eslint/no-use-before-define` set to `{ functions: false }` (a genuine temporal-dead-zone crash risk for `let`/`const`/`class`/enum bindings, but exempting function declarations, which are fully hoisted and therefore runtime-safe to call before their point of textual declaration -- this codebase's own rule files consistently define their helper functions after the logic that calls them), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/strict-void-return` (not yet in any typescript-eslint preset -- disallows passing a value-returning function where a void-returning one is expected, e.g. `arr.forEach(x => otherArray.push(x))`, which typechecks today under TS's own void-return contravariance leniency), `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), and `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers` is not (see below). Spread it directly into `tseslint.config(...)`:
The default export is the full, type-checked ruleset: typescript-eslint's `strictTypeChecked` + `stylisticTypeChecked` presets (`strictTypeChecked` subsumes `recommendedTypeChecked`, so it already includes `no-deprecated`, `no-misused-spread`, `no-mixed-enums`, `no-unnecessary-condition`, `use-unknown-in-catch-callback-variable`, `return-await`, `related-getter-setter-pairs`, `no-unnecessary-type-parameters`, and more -- those are no longer re-listed below), `exadev/barrel-policy` at `mode: 'banned'` (see [Barrel policy](#barrel-policy)), `exadev/no-object-assign`, `exadev/no-mutable-union-array-param`, `exadev/no-array-isarray-mutation`, `exadev/no-enum-number-widening`, `exadev/no-enum-reverse-lookup-widening`, `exadev/no-map-instanceof-mutation`, `exadev/no-set-instanceof-mutation`, `exadev/prefer-readonly-array-param`, `exadev/prefer-readonly-object-param`, `exadev/prefer-numeric-sort-compare`, `exadev/no-pointless-reassignment`, `linterOptions.noInlineConfig`, `@typescript-eslint/consistent-type-assertions` banning all type assertions, `@typescript-eslint/consistent-type-imports`, `@typescript-eslint/consistent-type-exports`, `@typescript-eslint/consistent-return` (a function that implicitly returns `undefined` on one path and a real value on another -- a common real bug, not a deliberate design), `@typescript-eslint/no-non-null-assertion` banning the `!` operator (the same manual-override escape hatch as a type assertion, under a different spelling), `@typescript-eslint/no-redeclare`, `@typescript-eslint/no-shadow`, `@typescript-eslint/no-use-before-define` set to `{ functions: false }` (a genuine temporal-dead-zone crash risk for `let`/`const`/`class`/enum bindings, but exempting function declarations, which are fully hoisted and therefore runtime-safe to call before their point of textual declaration -- this codebase's own rule files consistently define their helper functions after the logic that calls them), `@typescript-eslint/ban-ts-comment` banning `@ts-expect-error` outright, `@typescript-eslint/method-signature-style` set to `'property'` (method-shorthand signatures are checked bivariantly under `strictFunctionTypes`, which is unsound), `@typescript-eslint/prefer-readonly`, `@typescript-eslint/promise-function-async`, `@typescript-eslint/require-array-sort-compare`, `@typescript-eslint/strict-void-return` (not yet in any typescript-eslint preset -- disallows passing a value-returning function where a void-returning one is expected, e.g. `arr.forEach(x => otherArray.push(x))`, which typechecks today under TS's own void-return contravariance leniency), `@typescript-eslint/switch-exhaustiveness-check`, `@typescript-eslint/strict-boolean-expressions` at the rule's own bare defaults (an unambiguous non-nullable truthy check stays allowed; an ambiguous nullable check does not), `@typescript-eslint/no-magic-numbers` tuned to exempt array indexes, enum members, readonly class properties, default parameter values, and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`), `max-lines` set to `{ max: 800, skipBlankLines: true, skipComments: true }` (counting only real code, so a file isn't pushed over the limit by whitespace or its own WHY-explanation comments), and `no-warning-comments` banning any comment containing `Stryker disable` (Stryker's own mutation-testing suppression directive, invisible to ESLint's `noInlineConfig` above since it isn't an eslint-disable comment at all) -- the type-assertion and ts-comment rules are relaxed in test files, and `no-magic-numbers`/`max-lines`/`no-warning-comments` are not (see below). Spread it directly into `tseslint.config(...)`:

```ts
// eslint.config.ts
Expand Down
43 changes: 43 additions & 0 deletions src/recommended-type-checked.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,46 @@ describe('recommended-type-checked test-file relaxation', () => {
expect(lint('const x = <number>1;\n', 'src/foo.test.ts')).toContain('@typescript-eslint/consistent-type-assertions');
});
});

describe('no-warning-comments (Stryker suppression comments)', () => {
it('bans a Stryker disable-next-line comment', () => {
expect(lint('// Stryker disable next-line all\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments');
});

it('bans a Stryker disable comment scoped to a mutator list', () => {
expect(lint('// Stryker disable all: reason\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments');
});

it('matches case-insensitively', () => {
expect(lint('// stryker DISABLE all\nconst x = 1;\n', 'src/foo.ts')).toContain('no-warning-comments');
});

it('does not flag an unrelated comment', () => {
expect(lint('// a perfectly ordinary comment\nconst x = 1;\n', 'src/foo.ts')).not.toContain('no-warning-comments');
});
});

// Matches the `max: 800` configured on the rule under test above.
const MAX_LINES = 800;
// Comfortably more blank/comment lines than MAX_LINES, to prove they are never counted no matter how many pile up.
const NON_CODE_LINE_COUNT = MAX_LINES * 2;

// Each generated line declares a uniquely-named const -- a repeated `const x = 1;` would itself be a parse error (redeclaration in the same scope), which would mask what these tests actually check.
function generateLinesOfCode(count: number): string {
return Array.from({ length: count }, (_, index) => `const generatedLine${String(index)} = ${String(index)};`).join('\n');
}

describe('max-lines', () => {
it('bans a file over 800 real lines of code', () => {
expect(lint(generateLinesOfCode(MAX_LINES + 1), 'src/foo.ts')).toContain('max-lines');
});

it('allows a file at or under 800 real lines of code', () => {
expect(lint(generateLinesOfCode(MAX_LINES), 'src/foo.ts')).not.toContain('max-lines');
});

it('does not count blank lines or comment-only lines toward the limit', () => {
const code = `${'\n// a comment\n'.repeat(NON_CODE_LINE_COUNT)}${generateLinesOfCode(1)}\n`;
expect(lint(code, 'src/foo.ts')).not.toContain('max-lines');
});
});
4 changes: 4 additions & 0 deletions src/recommended-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ const recommendedTypeChecked: ConfigArrayValue = [
'@typescript-eslint/strict-void-return': 'error',
// A switch over a union or enum missing a member, with no default to fall back on, silently does nothing for the missing case instead of erroring.
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// A file over 800 lines is a signal to split it, not a target to hit exactly -- `skipBlankLines`/`skipComments` count only real code, so a file isn't pushed over the limit by whitespace or the WHY-explanations this codebase's own conventions require.
'max-lines': ['error', { max: 800, skipBlankLines: true, skipComments: true }],
// A Stryker mutation-testing suppression comment (its own `disable`/`disable next-line` directive, always led by the tool's own name) silences coverage for the line(s) it annotates -- unlike an eslint-disable comment (already banned outright by noInlineConfig above), ESLint has no native awareness of Stryker's own comment syntax, so it is otherwise invisible to lint. `location: 'anywhere'` matches the term wherever it falls in the comment (Stryker's own syntax always trails it with a mutator list, `all`, or a reason), not only at the very start.
'no-warning-comments': ['error', { terms: ['stryker disable'], location: 'anywhere' }],
},
},
{
Expand Down