Skip to content

eslint-plugin ​

Tier 0. No titan dependencies. eslint 9 is a peer dependency.

sh
npm install --save-dev @titan-design/eslint-plugin eslint

Status: 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 T double 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 ​

ExportWhat it does
default exportthe 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
maxFunctionLinesreports 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_LINES30
maxLineLengthreports 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_LENGTH200
noChainedTypeAssertionsreports 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
noCommentedCodereports 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
noInternalModuleMockreports 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
recommendeda flat config registering the plugin as titan and enabling every rule except max-line-length at error
todoNeedsIssuereports 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 ​

js
// 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:

text
`<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:

text
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:

text
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:

text
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")):

text
`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:

text
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:

text
`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:

text
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-length has 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 as anonymous function; export default function main() reports as main.
  • TODO is matched case-sensitively; todo in prose is ignored, and a lowercase key such as tp-123 is not a task id.
  • A comment such as // note: see below passes, but a prose line that happens to be a call, such as // Note (important), is reported. A bare continue, break, return or debugger (and a TypeScript-only shape such as // Map<string, number>) reads as prose under both espree and typescript-eslint; commented JSX is not caught.
  • FIXME is not checked; only TODO needs a task.
  • no-chained-type-assertions reads syntax only. A double cast split across statements (const u: unknown = x; u as T) passes; type-aware @typescript-eslint/no-unsafe-type-assertion reports that one.
  • no-internal-module-mock matches the names vi and jest, not their import bindings, so import { vi as t } from "vitest"; t.mock("./x") passes. A specifier built at run time (vi.mock(path)) is not judged. Another repo sets internalPrefixes to 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.

MIT licensed