eslint-plugin
Tier 0. No titan dependencies. eslint 9 is a peer dependency.
npm install --save-dev @titan-design/eslint-plugin eslintStatus: unpublished. The package is private until its first publish.
The problem it solves
The titan standards cap functions at about 30 lines and forbid a TODO without a tracking task. Before this package those rules lived only in prose, so nothing caught a breach until review. This package adds both as ESLint rules whose messages name the fix.
When to reach for it
- You want CI to fail on a function longer than 30 non-blank lines.
- You want CI to fail on an egregiously long code line (opt-in
max-line-length). - You want every TODO comment to carry a task id.
- You want commented-out code deleted.
- You want
as unknown as Tdouble casts reported. - You want tests to mock only external dependencies, not the repo's own modules.
To run ESLint against a style profile and get normalized diagnostics, use style-checker.
Public API
| Export | What it does |
|---|---|
| default export | the plugin: meta.name and rules with max-function-lines, max-line-length, no-chained-type-assertions, no-commented-code, no-internal-module-mock and todo-needs-issue |
maxFunctionLines | reports a function, arrow function or method with more than max (default 30) non-blank lines, naming it by its id, variable, key or as anonymous function |
DEFAULT_MAX_LINES | 30 |
maxLineLength | reports a line longer than max (default 200) characters unless no code token touches it (a comment line), it lies in an import or re-export declaration, or string, template, regex and JSX text tokens cover at least half its characters after the indent; opt-in, not in recommended |
DEFAULT_MAX_LINE_LENGTH | 200 |
noChainedTypeAssertions | reports a type assertion (as or <T>) whose operand is another type assertion, once per chain at the outermost one; a chain made only of as const passes |
noCommentedCode | reports a comment whose text parses as statements and carries a code signal: any statement other than an expression statement, or an expression that is an assignment, call, new, await, ++/-- or optional chain, or text ending in ;. A bare continue, break, return or debugger and a lone expression without a signal (read-only, 100 - 75, a, b, this) and a manual page reference such as sudo(8) read as prose. Consecutive whole-line // comments are parsed together; trailing comments never join. Leading * on block comment lines is stripped. JSDoc, eslint-*, @ts-*, istanbul/c8/v8 ignore, prettier-ignore, #! and /// <reference comments always pass |
noInternalModuleMock | reports vi.mock, vi.doMock, jest.mock, jest.doMock or jest.unstable_mockModule whose static specifier (a string, a template without expressions, or vitest's import("…") form) is relative, absolute, a # subpath, or starts with an internal prefix; option internalPrefixes (default ["@titan-design/"]) replaces the prefix list |
recommended | a flat config registering the plugin as titan and enabling every rule except max-line-length at error |
todoNeedsIssue | reports each TODO in a comment that is not immediately followed by a tracker key (TODO(TP-123), TODO: TP-123) or an issue number (TODO(#123), TODO #123); a bare token must end the sentence, so TODO UTF-8 support is reported, as is any bare key with a standards prefix (UTF, ES, ISO, RFC, SHA, MD, TLS, SSL, IPV, ECMA, HTTP) |
Example
// eslint.config.js
import titan from "@titan-design/eslint-plugin";
export default [
{
plugins: { titan },
rules: {
"titan/max-function-lines": "error",
"titan/no-chained-type-assertions": "error",
"titan/no-commented-code": "error",
"titan/no-internal-module-mock": "error",
"titan/todo-needs-issue": "error",
},
},
];Or enable all but the opt-in max-line-length with import { recommended } from "@titan-design/eslint-plugin" and export default [recommended].
Rules and messages
max-function-lines:
`<name>` is <n> lines; the limit is 30. Extract one step into a named function in this file. Do not add eslint-disable and do not edit the suppressions file.max-line-length:
This line is <n> characters; the limit is 200. Name an intermediate value with a const, or break the expression across lines. Do not add eslint-disable and do not edit the suppressions file.no-chained-type-assertions:
Chained type assertion: `as unknown as T` hides a type error from the checker. Parse the value (zod or a type guard), give it its real type, or in a test build the fake with `partialFake<T>()` from @titan-design/test-kit.no-commented-code:
This comment is code. Delete it; git history keeps it. If it explains why, rewrite it as a sentence.no-internal-module-mock (for vi.mock("./host.js")):
`vi.mock("./host.js")` mocks this repo's own code, so the test stops exercising it. Mock only external dependencies (node: builtins, third-party packages): pass the dependency in as a parameter, or use the package's own fake.todo-needs-issue:
TODO needs a task id, for example `TODO(TP-123): ...`. File the task with `active-work task add`, or delete the comment.A 31-line function reports:
`loadAll` is 31 lines; the limit is 30. Extract one step into a named function in this file. Do not add eslint-disable and do not edit the suppressions file.A bare // TODO: later reports:
TODO needs a task id, for example `TODO(TP-123): ...`. File the task with `active-work task add`, or delete the comment.What it deliberately does not do
- It has no baseline of its own. Existing long functions are grandfathered by the consuming repo's suppressions, not by the rule.
max-line-lengthhas no formatter and no autofix, and no baseline of its own. It is opt-in so a plugin bump never breaks a consumer that has not baselined its long lines.
Gotchas
- Blank lines are skipped but comment lines count, so a long doc comment inside a function body counts toward the limit.
- An outer function's count includes the lines of any function nested inside it, as in core
max-lines-per-function; the nested function is also checked on its own. - Names come from the declaration id, variable, property or class key. An assigned
obj.m = function () {}and an anonymous default-export function report asanonymous function;export default function main()reports asmain. TODOis matched case-sensitively;todoin prose is ignored, and a lowercase key such astp-123is not a task id.- A comment such as
// note: see belowpasses, but a prose line that happens to be a call, such as// Note (important), is reported. A barecontinue,break,returnordebugger(and a TypeScript-only shape such as// Map<string, number>) reads as prose under both espree and typescript-eslint; commented JSX is not caught. FIXMEis not checked; onlyTODOneeds a task.no-chained-type-assertionsreads syntax only. A double cast split across statements (const u: unknown = x; u as T) passes; type-aware@typescript-eslint/no-unsafe-type-assertionreports that one.no-internal-module-mockmatches the namesviandjest, not their import bindings, soimport { vi as t } from "vitest"; t.mock("./x")passes. A specifier built at run time (vi.mock(path)) is not judged. Another repo setsinternalPrefixesto its own scopes, for example["@codewatch/"].
Where it came from
New, built for the TP-897 code-quality enforcement plan. no-chained-type-assertions is rewritten on ESLint's rule API from the rule of the same name in anti-slop (MIT); see the repo NOTICE. no-internal-module-mock adapts anti-slop's no-module-mocking: upstream bans every module mock, while this rule bans only mocks of the repo's own code.