mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17029d364a | |||
| f458278e88 | |||
| fca42d46d9 |
@@ -257,6 +257,15 @@ jobs:
|
|||||||
# migration that changes a column regenerates it in the same change.
|
# migration that changes a column regenerates it in the same change.
|
||||||
run: cd server && npm run check:db-types
|
run: cd server && npm run check:db-types
|
||||||
|
|
||||||
|
- name: Fetch the base the warnings are compared with
|
||||||
|
# lint:warnings names the file behind a grown count by linting it as
|
||||||
|
# it is on the base: the pushed-over commit on a push, the target
|
||||||
|
# branch on a pull request. A miss only drops that hint.
|
||||||
|
run: >-
|
||||||
|
git fetch --no-tags --depth=1 origin
|
||||||
|
"+${{ github.event_name == 'push' && github.event.before || format('refs/heads/{0}', github.base_ref || 'dev') }}:refs/remotes/lint-base"
|
||||||
|
|| true
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
# Errors fail, and the warnings per rule may not pass
|
# Errors fail, and the warnings per rule may not pass
|
||||||
# scripts/eslint-baseline.json (src, tests and eslint-disable'd
|
# scripts/eslint-baseline.json (src, tests and eslint-disable'd
|
||||||
@@ -264,6 +273,7 @@ jobs:
|
|||||||
# program, which outgrows Node's default ~4 GB heap on the runner.
|
# program, which outgrows Node's default ~4 GB heap on the runner.
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
|
TREK_LINT_BASE: refs/remotes/lint-base
|
||||||
run: cd server && npm run lint:warnings
|
run: cd server && npm run lint:warnings
|
||||||
|
|
||||||
- name: Formatting check
|
- name: Formatting check
|
||||||
@@ -395,6 +405,15 @@ jobs:
|
|||||||
# one included, has to be clean (scripts/strict-lint.mjs).
|
# one included, has to be clean (scripts/strict-lint.mjs).
|
||||||
run: cd client && npm run lint:strict
|
run: cd client && npm run lint:strict
|
||||||
|
|
||||||
|
- name: Fetch the base the warnings are compared with
|
||||||
|
# lint:warnings names the file behind a grown count by linting it as
|
||||||
|
# it is on the base: the pushed-over commit on a push, the target
|
||||||
|
# branch on a pull request. A miss only drops that hint.
|
||||||
|
run: >-
|
||||||
|
git fetch --no-tags --depth=1 origin
|
||||||
|
"+${{ github.event_name == 'push' && github.event.before || format('refs/heads/{0}', github.base_ref || 'dev') }}:refs/remotes/lint-base"
|
||||||
|
|| true
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
# Fails on any ESLint error, like lint:check did, and on any rule
|
# Fails on any ESLint error, like lint:check did, and on any rule
|
||||||
# whose warning count rises above scripts/eslint-baseline.json.
|
# whose warning count rises above scripts/eslint-baseline.json.
|
||||||
@@ -402,6 +421,7 @@ jobs:
|
|||||||
# whole TypeScript program, so it gets the heap the server lint gets.
|
# whole TypeScript program, so it gets the heap the server lint gets.
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
|
TREK_LINT_BASE: refs/remotes/lint-base
|
||||||
run: cd client && npm run lint:warnings
|
run: cd client && npm run lint:warnings
|
||||||
|
|
||||||
- name: Page pattern check
|
- name: Page pattern check
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* The tally and the comparison behind lint:warnings (scripts/eslint-ratchet.mjs).
|
* The tally and the comparison behind lint:warnings (scripts/eslint-ratchet.mjs).
|
||||||
*/
|
*/
|
||||||
import { ESLint } from 'eslint';
|
import { ESLint } from 'eslint';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import ts from 'typescript';
|
import ts from 'typescript';
|
||||||
import {
|
import {
|
||||||
@@ -137,6 +138,78 @@ export async function lintClient(root) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The ref new warnings are measured against: TREK_LINT_BASE (the CI sets it), else origin/dev. */
|
||||||
|
const baseRef = () => process.env.TREK_LINT_BASE || 'origin/dev';
|
||||||
|
|
||||||
|
/** Git in the workspace: its output, or null when it fails (no repository, no such ref). */
|
||||||
|
function gitIn(root, args) {
|
||||||
|
try {
|
||||||
|
return execFileSync('git', args, {
|
||||||
|
cwd: root,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
maxBuffer: 1 << 28,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The messages of one rule in one area of a single file's lint result. */
|
||||||
|
function ofRule(results, rule, area) {
|
||||||
|
const result = results[0];
|
||||||
|
if (!result) return [];
|
||||||
|
const pool =
|
||||||
|
area === 'suppressed' ? (result.suppressedMessages ?? []) : result.messages.filter((m) => m.severity === 1);
|
||||||
|
return pool.filter((m) => ruleOf(m) === rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a grown rule's new warnings sit: every file among `keys` (the files
|
||||||
|
* holding the rule now) that differs from `ref` and holds more of the rule
|
||||||
|
* than it did there, each of its warnings of the rule as `file:line message`.
|
||||||
|
* The count alone names no file, and the files holding the most are old ones;
|
||||||
|
* this names the file that tipped it. Null when `ref` is not at hand (a clone
|
||||||
|
* without it), and the report then keeps to the files holding the most.
|
||||||
|
*/
|
||||||
|
export async function newWarningsOf({ root, rule, area, keys, lintText, ref = baseRef(), git = gitIn }) {
|
||||||
|
const changed = git(root, ['diff', '--name-only', '--relative', ref, '--', '.']);
|
||||||
|
if (changed === null) return null;
|
||||||
|
const differs = new Set(changed.split('\n').filter(Boolean));
|
||||||
|
const found = await Promise.all(
|
||||||
|
keys
|
||||||
|
.filter((key) => differs.has(key))
|
||||||
|
.map(async (key) => {
|
||||||
|
const path = join(root, key);
|
||||||
|
const now = ofRule(await lintText(readText(path), path), rule, area);
|
||||||
|
const before = git(root, ['show', `${ref}:./${key}`]);
|
||||||
|
const was = before === null ? [] : ofRule(await lintText(before, path), rule, area);
|
||||||
|
return now.length > was.length ? now.map((m) => `${key}:${m.line ?? 0} ${m.message}`) : [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return found.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prints, under the grown counts, the files and lines behind each one (see newWarningsOf). */
|
||||||
|
async function reportNew(grown, { root, files, lintText, error }) {
|
||||||
|
if (!grown.length || !lintText) return;
|
||||||
|
const found = await Promise.all(
|
||||||
|
grown.map(({ area, rule }) => newWarningsOf({ root, rule, area, keys: Object.keys(files[area][rule]), lintText }))
|
||||||
|
);
|
||||||
|
grown.forEach(({ area, rule }, i) => {
|
||||||
|
const lines = found[i];
|
||||||
|
if (!lines?.length) return;
|
||||||
|
error(`New ${rule} warning(s) in ${area}, against ${baseRef()}:`);
|
||||||
|
for (const line of lines) error(` ${line}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lints one file's text with the client's own config, for newWarningsOf. */
|
||||||
|
function lintTextClient(root) {
|
||||||
|
const eslint = new ESLint({ cwd: root });
|
||||||
|
return (text, filePath) => eslint.lintText(text, { filePath });
|
||||||
|
}
|
||||||
|
|
||||||
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +221,7 @@ export async function check({
|
|||||||
baselinePath = join(root, 'scripts/eslint-baseline.json'),
|
baselinePath = join(root, 'scripts/eslint-baseline.json'),
|
||||||
update = false,
|
update = false,
|
||||||
lint = lintClient,
|
lint = lintClient,
|
||||||
|
lintText = lint === lintClient ? lintTextClient : null,
|
||||||
log = console.log,
|
log = console.log,
|
||||||
error = console.error,
|
error = console.error,
|
||||||
}) {
|
}) {
|
||||||
@@ -160,13 +234,13 @@ export async function check({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const line of errors) error(`FAIL ${line}`);
|
for (const line of errors) error(`FAIL ${line}`);
|
||||||
let grown = 0;
|
const grown = [];
|
||||||
let stale = 0;
|
let stale = 0;
|
||||||
for (const area of AREAS) {
|
for (const area of AREAS) {
|
||||||
for (const [rule, n] of Object.entries(counts[area])) {
|
for (const [rule, n] of Object.entries(counts[area])) {
|
||||||
const allowed = baseline[area][rule] ?? 0;
|
const allowed = baseline[area][rule] ?? 0;
|
||||||
if (n <= allowed) continue;
|
if (n <= allowed) continue;
|
||||||
grown++;
|
grown.push({ area, rule });
|
||||||
const top = Object.entries(files[area][rule])
|
const top = Object.entries(files[area][rule])
|
||||||
.sort(([, a], [, b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
@@ -182,7 +256,8 @@ export async function check({
|
|||||||
});
|
});
|
||||||
stale += fallen.length;
|
stale += fallen.length;
|
||||||
}
|
}
|
||||||
if (grown) {
|
await reportNew(grown, { root, files, lintText: lintText && lintText(root), error });
|
||||||
|
if (grown.length) {
|
||||||
error(
|
error(
|
||||||
'Fix the new warning rather than silencing it: an eslint-disable comment is counted under "suppressed". ' +
|
'Fix the new warning rather than silencing it: an eslint-disable comment is counted under "suppressed". ' +
|
||||||
'Run npx eslint <file> to see each one.'
|
'Run npx eslint <file> to see each one.'
|
||||||
@@ -191,5 +266,5 @@ export async function check({
|
|||||||
log(
|
log(
|
||||||
`eslint: ${errors.length} error(s); warnings ${AREAS.map((area) => `${area} ${sum(counts[area])}/${sum(baseline[area])}`).join(', ')} (now/baseline)`
|
`eslint: ${errors.length} error(s); warnings ${AREAS.map((area) => `${area} ${sum(counts[area])}/${sum(baseline[area])}`).join(', ')} (now/baseline)`
|
||||||
);
|
);
|
||||||
return errors.length || grown || stale ? 1 : 0;
|
return errors.length || grown.length || stale ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import MIconBtn from '../../components/MIconBtn'
|
|||||||
import MToggle from '../../components/MToggle'
|
import MToggle from '../../components/MToggle'
|
||||||
import ConfirmDialog from '../../../components/shared/ConfirmDialog'
|
import ConfirmDialog from '../../../components/shared/ConfirmDialog'
|
||||||
import { useTranslation } from '../../../i18n'
|
import { useTranslation } from '../../../i18n'
|
||||||
import type { JourneyDetail } from '../../../store/journeyStore'
|
import type { JourneyDetail, JourneyTripAccess } from '../../../store/journeyStore'
|
||||||
import { useJourneySettings, useJourneyTripLinking } from '../../../components/Journey/useJourneySettings'
|
import { useJourneySettings, useJourneyTripLinking } from '../../../components/Journey/useJourneySettings'
|
||||||
import { useJourneyShareLink } from '../../../components/Journey/useJourneyShareLink'
|
import { useJourneyShareLink } from '../../../components/Journey/useJourneyShareLink'
|
||||||
import { pickGradient } from '../../../pages/journeyDetail/JourneyDetailPage.helpers'
|
import { pickGradient } from '../../../pages/journeyDetail/JourneyDetailPage.helpers'
|
||||||
@@ -234,25 +234,7 @@ export default function MJourneySettingsSheet({
|
|||||||
{t('journey.contributors.invite')}
|
{t('journey.contributors.invite')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Through a trip's linked journal (#2683): shown, but changed in that trip. */}
|
<MTripAccessList access={journey.trip_access} eyebrow={eyebrow} />
|
||||||
{!!journey.trip_access?.length && (
|
|
||||||
<>
|
|
||||||
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.contributors.viaTrips')}</div>
|
|
||||||
<p className="m-0 mb-[6px] text-caption text-m-faint">{t('journey.contributors.viaTripsHint')}</p>
|
|
||||||
{journey.trip_access.map(a => (
|
|
||||||
<div key={`${a.user_id}-${a.trip_id}`} className="flex items-center gap-[10px] px-[2px] py-1">
|
|
||||||
<Link2 size={15} className="flex-none text-m-muted" />
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span className="block truncate text-caption font-bold">{a.username}</span>
|
|
||||||
<span className="block truncate text-caption text-m-faint">{t('journey.contributors.viaTrip', { trip: a.trip_title })}</span>
|
|
||||||
</span>
|
|
||||||
<span className="rounded-full bg-[color:var(--m-ic)] px-[10px] py-[3px] font-geist text-caption font-extrabold text-m-muted">
|
|
||||||
{a.role}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Public share */}
|
{/* Public share */}
|
||||||
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.share.publicShare')}</div>
|
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.share.publicShare')}</div>
|
||||||
@@ -350,3 +332,27 @@ export default function MJourneySettingsSheet({
|
|||||||
</MSheet>
|
</MSheet>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Who a trip's linked journal lets in (#2683): shown under the contributors, but changed in that trip. */
|
||||||
|
function MTripAccessList({ access, eyebrow }: { access: JourneyTripAccess[] | undefined; eyebrow: string }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
if (!access?.length) return null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.contributors.viaTrips')}</div>
|
||||||
|
<p className="m-0 mb-[6px] text-caption text-m-faint">{t('journey.contributors.viaTripsHint')}</p>
|
||||||
|
{access.map(a => (
|
||||||
|
<div key={`${a.user_id}-${a.trip_id}`} className="flex items-center gap-[10px] px-[2px] py-1">
|
||||||
|
<Link2 size={15} className="flex-none text-m-muted" />
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-caption font-bold">{a.username}</span>
|
||||||
|
<span className="block truncate text-caption text-m-faint">{t('journey.contributors.viaTrip', { trip: a.trip_title })}</span>
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-[color:var(--m-ic)] px-[10px] py-[3px] font-geist text-caption font-extrabold text-m-muted">
|
||||||
|
{a.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
import { areaCounts, check, inlineConfigsOf, lintClient, tally } from '../../../scripts/lib/eslint-warnings.mjs';
|
import {
|
||||||
|
areaCounts,
|
||||||
|
check,
|
||||||
|
inlineConfigsOf,
|
||||||
|
lintClient,
|
||||||
|
newWarningsOf,
|
||||||
|
tally,
|
||||||
|
} from '../../../scripts/lib/eslint-warnings.mjs';
|
||||||
import { RatchetError } from '../../../scripts/lib/ratchet.mjs';
|
import { RatchetError } from '../../../scripts/lib/ratchet.mjs';
|
||||||
import { ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture';
|
import { ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture';
|
||||||
|
|
||||||
@@ -183,4 +191,33 @@ describe('lint:warnings', () => {
|
|||||||
expect(byKey['b.js'].messages.map((m) => m.ruleId)).toEqual(['no-empty']);
|
expect(byKey['b.js'].messages.map((m) => m.ruleId)).toEqual(['no-empty']);
|
||||||
expect(byKey['b.js'].inlineConfigs).toEqual([]);
|
expect(byKey['b.js'].inlineConfigs).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('names the file and line behind a grown count, measured against the base, and nothing without one', async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'lint-new-'));
|
||||||
|
try {
|
||||||
|
mkdirSync(join(root, 'src'));
|
||||||
|
writeFileSync(join(root, 'src/new.ts'), 'two');
|
||||||
|
writeFileSync(join(root, 'src/old.ts'), 'one');
|
||||||
|
// new.ts held none of the rule on the base, old.ts already held its one
|
||||||
|
const git = (_root: string, args: string[]) =>
|
||||||
|
args[0] === 'diff' ? 'src/new.ts\nsrc/old.ts\n' : args[1]?.endsWith('old.ts') ? 'one' : 'none';
|
||||||
|
const counts: Record<string, number> = { two: 2, one: 1, none: 0 };
|
||||||
|
const lintText = async (text: string) => [
|
||||||
|
{
|
||||||
|
messages: Array.from({ length: counts[text] ?? 0 }, (_, i) => ({
|
||||||
|
ruleId: 'no-empty',
|
||||||
|
severity: 1,
|
||||||
|
message: `w${i}`,
|
||||||
|
line: i + 1,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const keys = ['src/new.ts', 'src/old.ts', 'src/untouched.ts'];
|
||||||
|
const lines = await newWarningsOf({ root, rule: 'no-empty', area: 'src', keys, lintText, ref: 'base', git });
|
||||||
|
expect(lines).toEqual(['src/new.ts:1 w0', 'src/new.ts:2 w1']);
|
||||||
|
expect(await newWarningsOf({ root, rule: 'no-empty', area: 'src', keys, lintText, git: () => null })).toBeNull();
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* own major and has to lint with the one its eslint.config.mjs was written
|
* own major and has to lint with the one its eslint.config.mjs was written
|
||||||
* for. This file never imports eslint, so it resolves no version of its own.
|
* for. This file never imports eslint, so it resolves no version of its own.
|
||||||
*/
|
*/
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import ts from 'typescript';
|
import ts from 'typescript';
|
||||||
import {
|
import {
|
||||||
@@ -145,6 +146,77 @@ export function lintWith(ESLint) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The ref new warnings are measured against: TREK_LINT_BASE (the CI sets it), else origin/dev. */
|
||||||
|
const baseRef = () => process.env.TREK_LINT_BASE || 'origin/dev';
|
||||||
|
|
||||||
|
/** Git in the workspace: its output, or null when it fails (no repository, no such ref). */
|
||||||
|
function gitIn(root, args) {
|
||||||
|
try {
|
||||||
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 });
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The messages of one rule in one area of a single file's lint result. */
|
||||||
|
function ofRule(results, rule, area) {
|
||||||
|
const result = results[0];
|
||||||
|
if (!result) return [];
|
||||||
|
const pool =
|
||||||
|
area === 'suppressed' ? (result.suppressedMessages ?? []) : result.messages.filter((m) => m.severity === 1);
|
||||||
|
return pool.filter((m) => ruleOf(m) === rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a grown rule's new warnings sit: every file among `keys` (the files
|
||||||
|
* holding the rule now) that differs from `ref` and holds more of the rule
|
||||||
|
* than it did there, each of its warnings of the rule as `file:line message`.
|
||||||
|
* The count alone names no file, and the files holding the most are old ones;
|
||||||
|
* this names the file that tipped it. Null when `ref` is not at hand (a clone
|
||||||
|
* without it), and the report then keeps to the files holding the most.
|
||||||
|
*/
|
||||||
|
export async function newWarningsOf({ root, rule, area, keys, lintText, ref = baseRef(), git = gitIn }) {
|
||||||
|
const changed = git(root, ['diff', '--name-only', '--relative', ref, '--', '.']);
|
||||||
|
if (changed === null) return null;
|
||||||
|
const differs = new Set(changed.split('\n').filter(Boolean));
|
||||||
|
const found = await Promise.all(
|
||||||
|
keys
|
||||||
|
.filter((key) => differs.has(key))
|
||||||
|
.map(async (key) => {
|
||||||
|
const path = join(root, key);
|
||||||
|
const now = ofRule(await lintText(readText(path), path), rule, area);
|
||||||
|
const before = git(root, ['show', `${ref}:./${key}`]);
|
||||||
|
const was = before === null ? [] : ofRule(await lintText(before, path), rule, area);
|
||||||
|
return now.length > was.length ? now.map((m) => `${key}:${m.line ?? 0} ${m.message}`) : [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return found.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prints, under the grown counts, the files and lines behind each one (see newWarningsOf). */
|
||||||
|
async function reportNew(grown, { root, files, lintText, error }) {
|
||||||
|
if (!grown.length || !lintText) return;
|
||||||
|
const found = await Promise.all(
|
||||||
|
grown.map(({ area, rule }) =>
|
||||||
|
newWarningsOf({ root, rule, area, keys: Object.keys(files[area][rule]), lintText })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
grown.forEach(({ area, rule }, i) => {
|
||||||
|
const lines = found[i];
|
||||||
|
if (!lines?.length) return;
|
||||||
|
error(`New ${rule} warning(s) in ${area}, against ${baseRef()}:`);
|
||||||
|
for (const line of lines) error(` ${line}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A one-file lint for newWarningsOf, with the ESLint the workspace imports (see lintWith). */
|
||||||
|
export function lintTextWith(ESLint) {
|
||||||
|
return (root) => {
|
||||||
|
const eslint = new ESLint({ cwd: root });
|
||||||
|
return (text, filePath) => eslint.lintText(text, { filePath });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,6 +228,8 @@ export async function check({
|
|||||||
baselinePath = join(root, 'scripts/eslint-baseline.json'),
|
baselinePath = join(root, 'scripts/eslint-baseline.json'),
|
||||||
update = false,
|
update = false,
|
||||||
lint,
|
lint,
|
||||||
|
/** lintTextWith(ESLint): names the files behind a grown count; left out, only the counts are told. */
|
||||||
|
lintText = null,
|
||||||
log = console.log,
|
log = console.log,
|
||||||
error = console.error,
|
error = console.error,
|
||||||
}) {
|
}) {
|
||||||
@@ -169,13 +243,13 @@ export async function check({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const line of errors) error(`FAIL ${line}`);
|
for (const line of errors) error(`FAIL ${line}`);
|
||||||
let grown = 0;
|
const grown = [];
|
||||||
let stale = 0;
|
let stale = 0;
|
||||||
for (const area of AREAS) {
|
for (const area of AREAS) {
|
||||||
for (const [rule, n] of Object.entries(counts[area])) {
|
for (const [rule, n] of Object.entries(counts[area])) {
|
||||||
const allowed = baseline[area][rule] ?? 0;
|
const allowed = baseline[area][rule] ?? 0;
|
||||||
if (n <= allowed) continue;
|
if (n <= allowed) continue;
|
||||||
grown++;
|
grown.push({ area, rule });
|
||||||
const top = Object.entries(files[area][rule])
|
const top = Object.entries(files[area][rule])
|
||||||
.sort(([, a], [, b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
@@ -191,7 +265,8 @@ export async function check({
|
|||||||
});
|
});
|
||||||
stale += fallen.length;
|
stale += fallen.length;
|
||||||
}
|
}
|
||||||
if (grown) {
|
await reportNew(grown, { root, files, lintText: lintText && lintText(root), error });
|
||||||
|
if (grown.length) {
|
||||||
error(
|
error(
|
||||||
'Fix the new warning rather than silencing it: an eslint-disable comment is counted under "suppressed". ' +
|
'Fix the new warning rather than silencing it: an eslint-disable comment is counted under "suppressed". ' +
|
||||||
'Run npx eslint <file> to see each one.'
|
'Run npx eslint <file> to see each one.'
|
||||||
@@ -200,5 +275,5 @@ export async function check({
|
|||||||
log(
|
log(
|
||||||
`eslint: ${errors.length} error(s); warnings ${AREAS.map((area) => `${area} ${sum(counts[area])}/${sum(baseline[area])}`).join(', ')} (now/baseline)`
|
`eslint: ${errors.length} error(s); warnings ${AREAS.map((area) => `${area} ${sum(counts[area])}/${sum(baseline[area])}`).join(', ')} (now/baseline)`
|
||||||
);
|
);
|
||||||
return errors.length || grown || stale ? 1 : 0;
|
return errors.length || grown.length || stale ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ESLint } from 'eslint';
|
import { ESLint } from 'eslint';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { afterEach, describe, it } from 'node:test';
|
import { afterEach, describe, it } from 'node:test';
|
||||||
import { areaCounts, check, inlineConfigsOf, lintWith, tally } from './eslint-warnings.mjs';
|
import { areaCounts, check, inlineConfigsOf, lintWith, newWarningsOf, tally } from './eslint-warnings.mjs';
|
||||||
import { RatchetError } from './ratchet.mjs';
|
import { RatchetError } from './ratchet.mjs';
|
||||||
import { ratchetTree } from './tree-fixture.mjs';
|
import { ratchetTree } from './tree-fixture.mjs';
|
||||||
|
|
||||||
@@ -92,6 +93,26 @@ describe('scripts/lib/eslint-warnings', () => {
|
|||||||
assert.match(out, /Fix the new warning rather than silencing it/);
|
assert.match(out, /Fix the new warning rather than silencing it/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('names the file and line behind a grown count, measured against the base, and nothing without one', async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'lint-new-'));
|
||||||
|
try {
|
||||||
|
mkdirSync(join(root, 'src'));
|
||||||
|
writeFileSync(join(root, 'src/new.ts'), 'two');
|
||||||
|
writeFileSync(join(root, 'src/old.ts'), 'one');
|
||||||
|
// new.ts held none of the rule on the base, old.ts already held its one
|
||||||
|
const git = (_root, args) => (args[0] === 'diff' ? 'src/new.ts\nsrc/old.ts\n' : args[1].endsWith('old.ts') ? 'one' : 'none');
|
||||||
|
const lintText = async (text) => [
|
||||||
|
{ messages: Array.from({ length: { two: 2, one: 1, none: 0 }[text] }, (_, i) => ({ ...warn('no-empty', `w${i}`), line: i + 1 })) },
|
||||||
|
];
|
||||||
|
const keys = ['src/new.ts', 'src/old.ts', 'src/untouched.ts'];
|
||||||
|
const lines = await newWarningsOf({ root, rule: 'no-empty', area: 'src', keys, lintText, ref: 'base', git });
|
||||||
|
assert.deepEqual(lines, ['src/new.ts:1 w0', 'src/new.ts:2 w1']);
|
||||||
|
assert.equal(await newWarningsOf({ root, rule: 'no-empty', area: 'src', keys, lintText, git: () => null }), null);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('counts a message an eslint-disable comment silenced', async () => {
|
it('counts a message an eslint-disable comment silenced', async () => {
|
||||||
assert.equal(await run(empty, (root) => [result(root, 'src/a.ts', [], [warn('no-console')])]), 1);
|
assert.equal(await run(empty, (root) => [result(root, 'src/a.ts', [], [warn('no-console')])]), 1);
|
||||||
assert.match(tree.error.join('\n'), /suppressed: 1 no-console warning/);
|
assert.match(tree.error.join('\n'), /suppressed: 1 no-console warning/);
|
||||||
|
|||||||
@@ -23,11 +23,13 @@
|
|||||||
* ESLint comes from this workspace, so the server lints with the major its
|
* ESLint comes from this workspace, so the server lints with the major its
|
||||||
* config was written for.
|
* config was written for.
|
||||||
*/
|
*/
|
||||||
import { check, lintWith } from '../../scripts/lib/eslint-warnings.mjs';
|
import { check, lintTextWith, lintWith } from '../../scripts/lib/eslint-warnings.mjs';
|
||||||
import { runCli } from '../../scripts/lib/ratchet.mjs';
|
import { runCli } from '../../scripts/lib/ratchet.mjs';
|
||||||
|
|
||||||
import { ESLint } from 'eslint';
|
import { ESLint } from 'eslint';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const root = fileURLToPath(new URL('..', import.meta.url));
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
||||||
await runCli('lint:warnings', (args) => check({ root, update: args.includes('--update'), lint: lintWith(ESLint) }));
|
await runCli('lint:warnings', (args) =>
|
||||||
|
check({ root, update: args.includes('--update'), lint: lintWith(ESLint), lintText: lintTextWith(ESLint) }),
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user