@amashukov/eslint-plugin-mess-detector
v0.2.0
Published
ESLint plugin that fails the build on low-signal code patterns: inline narration, suppression directives, defensive nullish guards, tautological JSDoc, banal error wrappers, env branching, direct Date.now, type-only test assertions, TODO markers.
Maintainers
Readme
@amashukov/eslint-plugin-mess-detector
ESLint plugin that fails the build on the low-signal patterns that bloat a TypeScript / JavaScript codebase: inline narration, suppression directives, defensive nullish guards, tautological JSDoc, banal throw new Error(...) wrappers, runtime environment branching, scattered process.env reads, direct Date.now(), type-only test assertions, and TODO / FIXME markers.
Counterpart of go-lint for Go and rector-php-rules for PHP. Same philosophy, different syntax tree.
Why
Low-effort code tends to drift in the same direction every time:
- Each line gets an inline
//comment that paraphrases the line itself. - Every exported function gets a JSDoc that restates its signature in English (
/** Add adds two numbers */). x == nullchecks appear on values that, by their TypeScript type, can never benullorundefined.- Tests assert
toBeDefined(),toBeTruthy(), ortoBeInstanceOf(...)— checks the type system already does — instead of pinning the actual value. - Errors get wrapped in
throw new Error("failed to read: " + err.message)— strictly worse than re-throwing the original because it lengthens the chain and drops the stack. if (process.env.NODE_ENV === "production")branches sneak into production code, creating divergent test- and prod-only paths.// eslint-disable-next-lineappears next to anything the linter complained about.
This plugin is a single hard gate that flags every one of these in one pass. It does not autofix. The point is to make the human re-think the code, not regex it.
Install
npm install --save-dev @amashukov/eslint-plugin-mess-detectorRequires ESLint v9+ and Node 22+. For the type-aware rules (no-dead-nullish-guard, no-redundant-optional-chain) you also need @typescript-eslint/parser with parserOptions.project set.
Prerequisites — parsers
ESLint parses every file into an AST before any plugin rule runs. For source that is not plain JavaScript, the host project must register the matching parser in flat config — otherwise lint exits with Parsing error: Unexpected token and the plugin never gets a chance to inspect the code. This is the standard ESLint contract; mess-detector follows it just like every other plugin in the ecosystem.
The three common setups:
Plain TypeScript. Install @typescript-eslint/parser and register it for .ts files:
import mess from "@amashukov/eslint-plugin-mess-detector";
import tsParser from "@typescript-eslint/parser";
export default [
{ files: ["**/*.ts"], languageOptions: { parser: tsParser } },
{ files: ["**/*.ts"], plugins: { "mess-detector": mess }, rules: mess.configs.recommended.rules },
];Vue single-file components. Install vue-eslint-parser (which delegates <script lang="ts"> blocks to @typescript-eslint/parser):
import mess from "@amashukov/eslint-plugin-mess-detector";
import vueParser from "vue-eslint-parser";
import tsParser from "@typescript-eslint/parser";
export default [
{
files: ["**/*.vue"],
languageOptions: { parser: vueParser, parserOptions: { parser: tsParser } },
},
{ plugins: { "mess-detector": mess }, rules: mess.configs.recommended.rules },
];Nuxt. Add @nuxt/eslint to the modules list in nuxt.config.ts; nuxt prepare then emits .nuxt/eslint.config.mjs with TS + Vue parsers already wired. Wrap your config with withNuxt(...):
import mess from "@amashukov/eslint-plugin-mess-detector";
import withNuxt from "./.nuxt/eslint.config.mjs";
export default withNuxt({
files: ["**/*.{js,mjs,cjs,ts,vue}"],
plugins: { "mess-detector": mess },
rules: mess.configs.recommended.rules,
});If yarn lint reports Parsing error from any rule, the fix is a missing parser, not a plugin bug.
Usage (flat config, ESLint v9)
Plain (no type information)
// eslint.config.js
import mess from "@amashukov/eslint-plugin-mess-detector";
export default [
mess.configs.recommended,
];This enables the 11 non-type-aware rules.
With type information
// eslint.config.js
import mess from "@amashukov/eslint-plugin-mess-detector";
import tseslint from "typescript-eslint";
export default [
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
},
},
mess.configs["recommended-typed"],
];This adds the two type-aware rules on top.
Rules
| # | Rule | Type-aware | Catches |
|---|---|---|---|
| 1 | no-todo | no | TODO / FIXME / XXX / HACK markers (owned or not) |
| 2 | no-suppression-comments | no | // eslint-disable*, // @ts-ignore, // @ts-expect-error, // @ts-nocheck |
| 3 | no-inline-narration | no | comments inside function bodies |
| 4 | no-process-env-outside-config | no | process.env.X outside config/ and *.config.* files |
| 5 | no-env-branch | no | runtime branching on "prod" / "dev" / "test" strings |
| 6 | no-direct-date-now | no | Date.now(), new Date(), performance.now() outside clock/ |
| 7 | no-redundant-bool-return | no | if (c) return true; return false; |
| 8 | no-banal-error-wrap | no | throw new Error("failed to X: " + err.message) |
| 9 | no-catch-rethrow-banal | no | catch (e) { throw new Error(e.message); } |
| 10 | no-type-only-assertion | no | expect(x).toBeDefined() / toBeInstanceOf(...) etc. |
| 11 | no-tautological-jsdoc | no | JSDoc that restates the function name |
| 12 | no-silent-fallback | no | ??, ??=, and \|\| with a literal default — silent fallbacks for missing values |
| 13 | no-dead-nullish-guard | yes | x === null on a type that admits neither null nor undefined |
| 14 | no-redundant-optional-chain | yes | ?. on a type that admits neither null nor undefined |
Configs
Two flat-config presets:
mess.configs.recommended— rules 1–12. Works on plain ESLint withoutparserOptions.project.mess.configs["recommended-typed"]—recommendedplus the two type-aware rules. Requires@typescript-eslint/parserwithparserOptions.project.
Overlap with widely-used plugins
Several of these patterns are partly covered elsewhere. This plugin keeps all 13 anyway because the value is "single hard gate, no plugin sprawl":
| Concern | Overlapping plugin / rule | This plugin |
|---|---|---|
| TODO / FIXME markers | core ESLint no-warning-comments (off by default, configurable) | no-todo (strict, no carve-outs) |
| // eslint-disable* directives | eslint-plugin-eslint-comments/no-use | no-suppression-comments (strict, also @ts-*) |
| Inline comments | core no-inline-comments (only same-line) | no-inline-narration (whole function body) |
| process.env | core no-process-env | no-process-env-outside-config (glob carve-out) |
| if (cond) return true | eslint-plugin-sonarjs/prefer-single-boolean-return | no-redundant-bool-return |
| new Error(...) content | eslint-plugin-unicorn/error-message | no-banal-error-wrap (banal-verb regex) |
| // @ts-ignore | @typescript-eslint/ban-ts-comment | rolled into no-suppression-comments |
| ?. on non-nullable | @typescript-eslint/no-unnecessary-condition | no-redundant-optional-chain |
| catch { throw ... } | @typescript-eslint/no-useless-catch | no-catch-rethrow-banal (also flags message-only wrap) |
Comparison with sibling repos
| Concern | go-lint | rector-php-rules | eslint-plugin-mess-detector |
|---|---|---|---|
| Inline narration | noinlinecomment | NoCommentsOutsideInterfaceMethodDocBlockRector | no-inline-narration |
| Tautological doc | norobotgodoc | — | no-tautological-jsdoc |
| Suppression directives | nolintdirective | NoPhpstanIgnoreRector | no-suppression-comments |
| Env access outside config | nogetenv | NoSuperglobalAccessRector | no-process-env-outside-config |
| Env branching in src | noenvbranch | NoEnvironmentCheckInSrcRector | no-env-branch |
| Real-clock injection | notimenow | RequirePsrClockInterfaceRector | no-direct-date-now |
| Type-only test assertions | notypeonlyassert | NoTypeOnlyAssertionsInTestsRector | no-type-only-assertion |
| Banal error wrapping | noerrorwrapbanality | — | no-banal-error-wrap + no-catch-rethrow-banal |
| TODO / FIXME markers | notodo | NoTodoCommentRector | no-todo |
| if cond return true | noredundantif | — | no-redundant-bool-return |
| Nullish guard on non-nullable | nodeadguard | — | no-dead-nullish-guard |
| Redundant optional chain | — | — | no-redundant-optional-chain |
Design notes
- No configuration file beyond ESLint's own. Each rule is either on or off via the flat-config block. Policy lives in
eslint.config.js, not a side-car YAML. - No autofixers. Most findings need restructuring, not a regex. The point is to make the human re-think the code.
- No per-line waiver. If a rule is wrong for your project, drop it from the config. Per-line
// eslint-disablewaivers turn into silent debt — andno-suppression-commentsflags them anyway. - Self-hosted. The plugin's own source is linted by its own
recommended-typedconfig with zero findings.
Development
make install # docker-driven npm install
make build # tsup → dist/index.{js,cjs,d.ts}
make test # vitest
make lint # self-host gate (eslint on src/)
make typecheck # tsc --noEmitAll targets run inside node:22-alpine via docker — no host Node required.
License
MIT — see LICENSE.
