Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Failing after 4m0s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Failing after 13m52s
Conventional commits / commits (pull_request) Successful in 33s
Enable no-unsanitized recommended rules and verify TypeScript DOM sink diagnostics.
188 lines
7.1 KiB
JavaScript
188 lines
7.1 KiB
JavaScript
import react from "@eslint-react/eslint-plugin";
|
|
import stylistic from "@stylistic/eslint-plugin";
|
|
import query from "@tanstack/eslint-plugin-query";
|
|
import vitest from "@vitest/eslint-plugin";
|
|
import { ESLint } from "eslint";
|
|
import { importX } from "eslint-plugin-import-x";
|
|
import jestDom from "eslint-plugin-jest-dom";
|
|
import noUnsanitized from "eslint-plugin-no-unsanitized";
|
|
import perfectionist from "eslint-plugin-perfectionist";
|
|
import hooks from "eslint-plugin-react-hooks";
|
|
import refresh from "eslint-plugin-react-refresh";
|
|
import regexp from "eslint-plugin-regexp";
|
|
import security from "eslint-plugin-security";
|
|
import sonarjs from "eslint-plugin-sonarjs";
|
|
import storybook from "eslint-plugin-storybook";
|
|
import testingLibrary from "eslint-plugin-testing-library";
|
|
import unicorn from "eslint-plugin-unicorn";
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import tseslint from "typescript-eslint";
|
|
|
|
test("ESLint enforces the frontend profile and excludes generated artifacts", async () => {
|
|
const eslint = new ESLint();
|
|
for (const [filePath, code, rule] of [
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export function run() { Promise.resolve(); }",
|
|
"@typescript-eslint/no-floating-promises",
|
|
],
|
|
[
|
|
"packages/ui/src/EntryCard/EntryCard.tsx",
|
|
"import { useState } from \"react\"; export function Example({ active }: { active: boolean }) { if (active) useState(0); return null; }",
|
|
"react-hooks/rules-of-hooks",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export const values = Array(3).fill({});",
|
|
"unicorn/no-array-fill-with-reference-type",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export default 1;",
|
|
"import-x/no-default-export",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"new Error(\"forgot to throw\");",
|
|
"sonarjs/no-unthrown-error",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export const pattern = /[aab]/;",
|
|
"regexp/no-dupe-characters-character-class",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export type Status = \"z\" | \"a\";",
|
|
"perfectionist/sort-union-types",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"//missing space\nexport const ready = true;",
|
|
"@stylistic/spaced-comment",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export function pattern(input: string) { return new RegExp(input); }",
|
|
"security/detect-non-literal-regexp",
|
|
],
|
|
[
|
|
"scripts/check-openapi.mjs",
|
|
"import { exec } from \"node:child_process\"; exec(process.argv[2]);",
|
|
"security/detect-child-process",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export function renderHtml(element: HTMLElement, html: string) { element.innerHTML = html; }",
|
|
"no-unsanitized/property",
|
|
],
|
|
[
|
|
"src/shared/api/problem-details.ts",
|
|
"export function renderHtml(element: HTMLElement, html: string) { element.insertAdjacentHTML(\"beforeend\", html); }",
|
|
"no-unsanitized/method",
|
|
],
|
|
[
|
|
"packages/ui/src/EntryCard/EntryCard.test.tsx",
|
|
"import { expect } from \"vitest\"; expect(document.activeElement).toBe(document.body);",
|
|
"jest-dom/prefer-focus",
|
|
],
|
|
[
|
|
"src/shared/api/client.test.ts",
|
|
"import { it } from \"vitest\"; it.only(\"focused\", () => {});",
|
|
"vitest/no-focused-tests",
|
|
],
|
|
[
|
|
"packages/ui/src/EntryCard/EntryCard.test.tsx",
|
|
"import { screen } from \"@testing-library/react\"; screen.findByText(\"ready\");",
|
|
"testing-library/await-async-queries",
|
|
],
|
|
[
|
|
"packages/ui/src/EntryCard/EntryCard.tsx",
|
|
"import { QueryClient } from \"@tanstack/react-query\"; export function Example() { const client = new QueryClient(); return <div>{String(client)}</div>; }",
|
|
"@tanstack/query/stable-query-client",
|
|
],
|
|
]) {
|
|
const [result] = await eslint.lintText(code, { filePath });
|
|
assert.equal(result.fatalErrorCount, 0, filePath);
|
|
assert.ok(
|
|
result.messages.some(message => message.ruleId === rule),
|
|
rule,
|
|
);
|
|
}
|
|
for (const file of [
|
|
"src/shared/api/generated/schema.d.ts",
|
|
"packages/ui/dist/index.js",
|
|
"storybook-static/index.js",
|
|
"coverage/index.js",
|
|
])
|
|
assert.equal(await eslint.isPathIgnored(file), true, file);
|
|
const story = await eslint.calculateConfigForFile(
|
|
"src/shared/ui/Button.stories.tsx",
|
|
);
|
|
assert.equal(story.rules["import-x/no-default-export"][0], 0);
|
|
assert.ok(story.rules["storybook/default-exports"][0] > 0);
|
|
assert.equal(story.rules["react-refresh/only-export-components"], undefined);
|
|
});
|
|
|
|
test("all recommended plugin rules remain enabled in their intended scopes", async () => {
|
|
const eslint = new ESLint();
|
|
const source = "src/shared/api/problem-details.ts";
|
|
const component = "packages/ui/src/EntryCard/EntryCard.tsx";
|
|
const unit = "packages/ui/src/EntryCard/EntryCard.test.tsx";
|
|
for (const [preset, filePath] of [
|
|
[tseslint.configs.recommendedTypeChecked, source],
|
|
[[importX.flatConfigs.recommended, importX.flatConfigs.typescript], source],
|
|
[unicorn.configs.recommended, source],
|
|
[sonarjs.configs.recommended, source],
|
|
[regexp.configs.recommended, source],
|
|
[security.configs.recommended, source],
|
|
[noUnsanitized.configs.recommended, source],
|
|
[perfectionist.configs["recommended-natural"], source],
|
|
[stylistic.configs.recommended, component],
|
|
[react.configs["recommended-type-checked"], component],
|
|
[hooks.configs.flat.recommended, component],
|
|
[refresh.configs.vite, component],
|
|
[query.configs["flat/recommended"], component],
|
|
[vitest.configs.recommended, unit],
|
|
[testingLibrary.configs["flat/react"], unit],
|
|
[jestDom.configs["flat/recommended"], unit],
|
|
...storybook.configs["flat/recommended"].filter(config => config.rules).map(config => [config, config.name.endsWith("main-rules") ? ".storybook/main.ts" : "src/shared/ui/Button.stories.tsx"]),
|
|
]) {
|
|
const configs = [preset].flat();
|
|
const expected = Object.assign({}, ...configs.map(config => config.rules));
|
|
const config = await eslint.calculateConfigForFile(filePath);
|
|
const actual = new Map(Object.entries(config.rules));
|
|
for (const [rule, setting] of Object.entries(expected)) {
|
|
const severity = Array.isArray(setting) ? setting[0] : setting;
|
|
if (severity === "off" || severity === 0 || !rule.includes("/")) continue;
|
|
assert.ok(actual.get(rule)?.[0] > 0, `${filePath}: ${rule}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("ESLint formatting is idempotent and preserves TSX generic syntax", async () => {
|
|
const eslint = new ESLint({ fix: true });
|
|
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";
|
|
const [fixed] = await eslint.lintText(
|
|
"//identity\nconst identity = <T,>(value: T): T => value;\nexport {identity};",
|
|
{ filePath },
|
|
);
|
|
assert.equal(fixed.fatalErrorCount, 0);
|
|
assert.equal(fixed.errorCount, 0);
|
|
assert.ok(fixed.output);
|
|
const [again] = await eslint.lintText(fixed.output, { filePath });
|
|
assert.equal(again.errorCount, 0);
|
|
assert.equal(again.output, undefined);
|
|
});
|
|
|
|
test("ESLint formatting preserves spaces around JSX text and interpolations", async () => {
|
|
const eslint = new ESLint({ fix: true });
|
|
const [fixed] = await eslint.lintText(
|
|
"const Example = () => {\nconst value = 'value';\nreturn <span>Amount: {value} / {value}</span>;\n};\nexport { Example };",
|
|
{ filePath: "packages/ui/src/EntryCard/EntryCard.tsx" },
|
|
);
|
|
assert.equal(fixed.errorCount, 0);
|
|
assert.ok(fixed.output?.includes("Amount: {value} / {value}"));
|
|
});
|