mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
* chore(lint): hold tests to their own max-lines ceiling The flat 400-line cap treated a spec like a component. A spec is a flat list of independent cases, so hitting the cap there produces arbitrary `-2.spec.ts` splits and hides coverage instead of surfacing design debt — 65 of the 138 files over the limit were tests. Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no longer count, so a docblock can't be the reason a file must be split. Both limits now live in tools/eslint/max-lines-config.mjs, imported by eslint.config.mjs and the baseline generator alike. The generator decides who belongs on the list by running ESLint's own max-lines rule instead of counting lines itself — a private reimplementation would disagree with the rule the moment either side changed (a `//` inside a template literal is enough) and yield a baseline that turns CI red while looking correct. The baseline drops 126 -> 68 entries with nothing added, and six now-dead `eslint-disable max-lines` directives are removed. A new eslint-tools test asserts the committed baseline still matches what the generator produces, so a stale entry or a forgotten regeneration fails CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(lint): classify eslint-tools in the coverage policy A project with a `test` target must be assigned a coverage tier, so adding eslint-tools broke `coverage:policy:check` before the unit suite even ran. Tier B alongside packaging and release-tools: these are Node tests over lint tooling, and a coverage percentage across a generated list would not mean anything. CI runs Tier B/C through its own `--run-non-tier-a` step, so the baseline-consistency test executes there rather than being skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
181 lines
6.5 KiB
JavaScript
181 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Regenerates tools/eslint/max-lines-baseline.mjs: the list of TypeScript
|
|
* files that already exceed the max-lines limit enforced in eslint.config.mjs.
|
|
* Baselined files are exempt from the rule; the list should only shrink.
|
|
*
|
|
* The offender check runs ESLint's own `max-lines` rule through `Linter`,
|
|
* against a config mirroring the real one (production vs test ceiling, shared
|
|
* options), rather than counting lines here. Reimplementing the count would
|
|
* silently disagree with the rule the moment either side changed — a `//`
|
|
* inside a template literal is enough — and a baseline that disagrees with the
|
|
* rule turns CI red while looking correct.
|
|
*
|
|
* Files that carry their own file-wide `eslint-disable max-lines` comment are
|
|
* skipped: they are already exempt with a written justification next to the
|
|
* code, which is the preferred escape hatch for a file that genuinely cannot
|
|
* be split (for example a function serialized into another process).
|
|
*
|
|
* Usage: node tools/eslint/generate-max-lines-baseline.mjs
|
|
*/
|
|
|
|
import { Linter } from 'eslint';
|
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import tsParser from '@typescript-eslint/parser';
|
|
|
|
import {
|
|
MAX_LINES_OPTIONS,
|
|
MAX_LINES_PROD,
|
|
MAX_LINES_TEST,
|
|
TEST_FILE_GLOBS,
|
|
} from './max-lines-config.mjs';
|
|
|
|
// Resolved from this file rather than cwd, so importing the module from a test
|
|
// (or running the script from a subdirectory) scans the same tree either way.
|
|
const workspaceRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'..'
|
|
);
|
|
const scanRoots = ['apps', 'libs', 'tools'];
|
|
const skipDirs = new Set(['node_modules', 'dist', '.nx', 'coverage']);
|
|
|
|
/**
|
|
* Mirrors the two max-lines blocks in eslint.config.mjs. The baseline block is
|
|
* deliberately absent — an already-baselined file must still report here, or
|
|
* the list could never be re-derived.
|
|
*/
|
|
const lintConfig = [
|
|
{
|
|
files: ['**/*.ts', '**/*.tsx'],
|
|
ignores: TEST_FILE_GLOBS,
|
|
languageOptions: { parser: tsParser },
|
|
rules: {
|
|
'max-lines': [
|
|
'error',
|
|
{ max: MAX_LINES_PROD, ...MAX_LINES_OPTIONS },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: TEST_FILE_GLOBS,
|
|
languageOptions: { parser: tsParser },
|
|
rules: {
|
|
'max-lines': [
|
|
'error',
|
|
{ max: MAX_LINES_TEST, ...MAX_LINES_OPTIONS },
|
|
],
|
|
},
|
|
},
|
|
];
|
|
|
|
const linter = new Linter();
|
|
|
|
function collectTsFiles(dir, results) {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.isDirectory()) {
|
|
if (!skipDirs.has(entry.name)) {
|
|
collectTsFiles(path.join(dir, entry.name), results);
|
|
}
|
|
} else if (
|
|
(entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) &&
|
|
!entry.name.endsWith('.d.ts')
|
|
) {
|
|
results.push(path.join(dir, entry.name));
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* True when the file already suppresses max-lines with a file-wide
|
|
* `/* eslint-disable *\/` comment. Those files are deliberately exempt with a
|
|
* reviewed justification, so baselining them too would be redundant — and
|
|
* would wrongly imply they merely predate the rule.
|
|
*/
|
|
function hasInlineMaxLinesDisable(content) {
|
|
// Only a file-wide `eslint-disable` block comment can suppress max-lines;
|
|
// the rule reports at the line where the limit is exceeded, so
|
|
// `eslint-disable-next-line` cannot apply to it.
|
|
const disableComments = content.matchAll(
|
|
/\/\*\s*eslint-disable\s*([^*]*?)\*\//g
|
|
);
|
|
for (const [, ruleList = ''] of disableComments) {
|
|
// `/* eslint-disable */` with no rules disables everything.
|
|
const rules = ruleList.split('--')[0].trim();
|
|
if (rules === '') {
|
|
return true;
|
|
}
|
|
if (rules.split(',').some((rule) => rule.trim() === 'max-lines')) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Repo-relative POSIX path — what the flat-config globs are matched against. */
|
|
function toRelativePosix(filePath) {
|
|
return path.relative(workspaceRoot, filePath).split(path.sep).join('/');
|
|
}
|
|
|
|
function exceedsLimit(content, relativePath) {
|
|
const messages = linter.verify(content, lintConfig, relativePath);
|
|
return messages.some((message) => message.ruleId === 'max-lines');
|
|
}
|
|
|
|
/**
|
|
* The repo-relative paths that currently exceed their bucket's limit, sorted.
|
|
* Exported so a test can assert the committed baseline still matches, which is
|
|
* what keeps stale entries and forgotten regenerations out of the list.
|
|
*/
|
|
export function computeOffenders() {
|
|
return scanRoots
|
|
.flatMap((root) => collectTsFiles(path.join(workspaceRoot, root), []))
|
|
.map((filePath) => {
|
|
const content = readFileSync(filePath, 'utf8');
|
|
return { file: toRelativePosix(filePath), content };
|
|
})
|
|
.filter(
|
|
({ content, file }) =>
|
|
!hasInlineMaxLinesDisable(content) &&
|
|
exceedsLimit(content, file)
|
|
)
|
|
.map(({ file }) => file)
|
|
.sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
/** The exact file contents for a given offender list. */
|
|
export function renderBaseline(offenders) {
|
|
const banner = `// Generated by tools/eslint/generate-max-lines-baseline.mjs — do not edit by hand.
|
|
// TypeScript files that predate the max-lines ESLint rule (${MAX_LINES_PROD} for
|
|
// production code, ${MAX_LINES_TEST} for tests; blank lines and comments are not counted).
|
|
// This list should only shrink: split a file below the limit, rerun the
|
|
// generator, and commit the result. Never add new files here.
|
|
// A new file that genuinely cannot be split takes a file-wide
|
|
// \`/* eslint-disable max-lines -- <why> */\` instead; the generator skips those.
|
|
`;
|
|
const body = offenders.map((file) => ` '${file}',`).join('\n');
|
|
return `${banner}export const maxLinesBaseline = [\n${body}\n];\n`;
|
|
}
|
|
|
|
export const BASELINE_PATH = path.join(
|
|
workspaceRoot,
|
|
'tools/eslint/max-lines-baseline.mjs'
|
|
);
|
|
|
|
// Only write when run as a script — importing this module must not mutate the
|
|
// baseline it is used to verify.
|
|
if (
|
|
process.argv[1] &&
|
|
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
) {
|
|
const offenders = computeOffenders();
|
|
writeFileSync(BASELINE_PATH, renderBaseline(offenders));
|
|
console.log(
|
|
`Baselined ${offenders.length} files (prod > ${MAX_LINES_PROD}, test > ${MAX_LINES_TEST}).`
|
|
);
|
|
}
|