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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ See [README.md](README.md) for requirements and commands.

- Keep comments brief and do not over-explain.
- A line comment belongs directly above the code it describes, inside that logical block. Preserve the separating blank line above the comment; the comment never takes its place.
- Wrap each comment paragraph at the last word that fits within Prettier's `printWidth` in `package.json`. Prettier does not reflow comments. Preserve paragraph and JSDoc tag boundaries, unbreakable text, disable comments and layouts whose spacing carries meaning.
- Document purpose, parameters, results and failures with JSDoc, without repeating TypeScript types.
- Inline callbacks need no JSDoc.
- A function assigned to a local variable needs no JSDoc, as an inline callback needs none. Where a returned function's contract is a documented function type, assign the function to a variable typed with it and return the variable, rather than repeating the type's docs; keep anything specific to the implementation in a plain line comment.
Expand Down
24 changes: 24 additions & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import { defineConfig } from "eslint/config";
import eslint from "@eslint/js";
import stylistic from "@stylistic/eslint-plugin";
import tseslint from "typescript-eslint";
import globals from "globals";
import type { Options } from "prettier";
import manifest from "./package.json" with { type: "json" };
import { documentationConfig } from "./scripts/lint/jsdoc-config.mts";
import { importBoundaries, vitestImportCalls } from "./scripts/lint/import-boundaries.mts";

/** Read optional formatter settings alongside the required print width. */
const prettierOptions: Options = manifest.prettier;

/** Combine type-aware checks with documentation and member-order rules. */
export default defineConfig(
{ ignores: ["dist/", "coverage/", "reports/", ".stryker-tmp/"] },
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
plugins: { "@stylistic": stylistic },
rules: {
"@stylistic/max-len": [
"error",
{
// Prettier owns code width, including lines with trailing comments.
code: Number.MAX_SAFE_INTEGER,
comments: manifest.prettier.printWidth,
// Use Prettier's default when the project does not set a tab width.
tabWidth: prettierOptions.tabWidth ?? 2,
ignoreUrls: true,
ignorePattern: [
String.raw`^\s*(?://\s*eslint-disable-(?:next-line|line)|(?:\{\s*)?/\*\s*eslint-disable(?:-next-line|-line)?)(?:\s|\*/|$)`,
// TypeScript takes everything after this directive's name as its description.
String.raw`^\s*(?:///?|/\*)\s*@ts-expect-error`,
String.raw`^\s*(?://+|(?:\{\s*)?/\*+|\*)?\s*\S+\s*(?:\*/\s*\}?)?\s*$`,
].join("|"),
},
],
"@typescript-eslint/member-ordering": "error",
"arrow-body-style": ["error", "as-needed"],
"one-var": ["error", "never"],
Expand Down
151 changes: 151 additions & 0 deletions scripts/lint/comment-width.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ESLint } from "eslint";
import { format } from "prettier";
import tseslint from "typescript-eslint";
import { expect, test } from "vitest";
import manifest from "../../package.json" with { type: "json" };

const eslint = new ESLint({ overrideConfig: tseslint.configs.disableTypeChecked });
const width = manifest.prettier.printWidth;

function paragraph(length: number) {
return `${"word ".repeat(length).slice(0, length - 1)}.`;
}

async function diagnostics(source: string, filePath = "scripts/lint/comment-width-example.mts", linter = eslint) {
const results = await linter.lintText(source, { filePath });
expect(results).toHaveLength(1);
expect(results[0]?.fatalErrorCount).toBe(0);

return results.flatMap((result) => result.messages.filter((message) => message.ruleId === "@stylistic/max-len"));
}

test.each([
{ name: "line comment", prefix: "// ", suffix: "" },
{ name: "JSDoc line", prefix: "/**\n * ", suffix: "\n */" },
])("limits a $name to the configured print width", async ({ prefix, suffix }) => {
const text = paragraph(width - 3);
expect(await diagnostics(`${prefix}${text}${suffix}`)).toEqual([]);
expect(await diagnostics(`${prefix}${text}.${suffix}`)).toEqual([expect.objectContaining({ messageId: "maxComment" })]);
});

test("counts indentation tabs as Prettier does", async () => {
const formatted = await format("if (ready) { work(); }", {
...manifest.prettier,
parser: "typescript",
useTabs: false,
});
const indentation = /\n( +)work/u.exec(formatted)?.[1] ?? "";
expect(indentation.length).toBeGreaterThan(0);

const text = paragraph(width - indentation.length - 3);
expect(await diagnostics(`\t// ${text}`)).toEqual([]);
expect(await diagnostics(`\t// ${text}.`)).toEqual([expect.objectContaining({ messageId: "maxComment" })]);
});

test("allows a line holding a long URL", async () => {
const source = `// Read https://example.com/${"segment/".repeat(width)} for details.`;
expect(await diagnostics(source)).toEqual([]);
});

test.each([
{ prefix: "// eslint-disable-next-line", suffix: "" },
{ prefix: "// eslint-disable-line", suffix: "" },
{ prefix: "/* eslint-disable", suffix: " */" },
{ prefix: "/* eslint-disable-next-line", suffix: " */" },
{ prefix: "/* eslint-disable-line", suffix: " */" },
])("allows a long reason on $prefix", async ({ prefix, suffix }) => {
const source = `${prefix} no-debugger -- ${paragraph(width)}${suffix}\ndebugger;`;
expect(await diagnostics(source)).toEqual([]);
expect(await diagnostics(`\t${source}`)).toEqual([]);
expect(
await diagnostics(`${prefix.replace("eslint-", "Mention eslint-")} no-debugger -- ${paragraph(width)}${suffix}`),
).toEqual([expect.objectContaining({ messageId: "maxComment" })]);
});

test.each(["", " "])("allows JSX disable comments with spacing %j", async (spacing) => {
const wrap = (comment: string) => `const element = <>\n\t{${spacing}/* ${comment} */}\n</>;`;
const directive = `eslint-disable-next-line no-debugger -- ${paragraph(width)}`;

expect(await diagnostics(wrap(directive), "src/example.tsx")).toEqual([]);
expect(await diagnostics(wrap(`Mention ${directive}`), "src/example.tsx")).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]);
});

test.each([
{ prefix: "//", suffix: "" },
{ prefix: "///", suffix: "" },
{ prefix: "/*", suffix: " */" },
])("allows a described type-error directive in $prefix", async ({ prefix, suffix }) => {
for (const separator of [" -- ", ": ", "-example "]) {
const directive = `@ts-expect-error${separator}${paragraph(width)}`;

expect(await diagnostics(`${prefix} ${directive}${suffix}`)).toEqual([]);
expect(await diagnostics(`${prefix} Mention ${directive}${suffix}`)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]);
}
});

test.each(["@ts-ignore", "@ts-nocheck", "@ts-check"])("does not exempt %s", async (directive) =>
expect(await diagnostics(`// ${directive} -- ${paragraph(width)}`)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]),
);

test.each(["eslint-disable-next-line no-debugger", "@ts-expect-error"])(
"does not mistake %s inside JSDoc prose for a directive",
async (directive) =>
expect(await diagnostics(`/**\n * // ${directive} -- ${paragraph(width)}\n */`)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]),
);

test("registers the width rule for files outside the documentation globs", async () => {
const linter = new ESLint({
overrideConfig: { ...tseslint.configs.disableTypeChecked, files: ["**/*.jsx"] },
});
const text = paragraph(width - 3);

expect(await diagnostics(`// ${text}`, "scripts/lint/example.jsx", linter)).toEqual([]);
expect(await diagnostics(`// ${text}.`, "scripts/lint/example.jsx", linter)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]);
});

test.each([
{ prefix: "// ", suffix: "", path: "scripts/lint/example.mts" },
{ prefix: "/* ", suffix: " */", path: "scripts/lint/example.mts" },
{ prefix: "/**\n * ", suffix: "\n */", path: "scripts/lint/example.mts" },
{ prefix: "/*\n", suffix: "\n*/", path: "scripts/lint/example.mts" },
{ prefix: "const element = <>\n\t{/* ", suffix: " */}\n</>;", path: "src/example.tsx" },
])("preserves a single unbroken token after $prefix", async ({ prefix, suffix, path }) => {
const token = `packages/${"segment/".repeat(width)}/index.ts`;

expect(await diagnostics(`${prefix}${token}${suffix}`, path)).toEqual([]);
expect(await diagnostics(`${prefix}${token} description${suffix}`, path)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]);
});

test.each([
"// Mention eslint-disable-next-line here.",
"// eslint-disable-next-line-example",
"// eslint-disable",
"/* Mention eslint-disable here.",
"/* eslint-disable-example",
"/** eslint-disable",
])("does not exempt ordinary prose starting with %s", async (prefix) => {
const suffix = prefix.startsWith("/*") ? " */" : "";
expect(await diagnostics(`${prefix} ${paragraph(width)}${suffix}`)).toEqual([
expect.objectContaining({ messageId: "maxComment" }),
]);
});

test.each(["", " // A trailing comment.", " /* A trailing comment. */"])(
"leaves long code lines to Prettier with suffix %j",
async (suffix) => {
const source = `const value = ${"1 + ".repeat(width)}1;${suffix}`;
expect(await diagnostics(source)).toEqual([]);
},
);
Loading