From 819d273e27409d5cb18002bc843e4fac70aa28ee Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 26 Oct 2026 03:50:42 -0700 Subject: [PATCH] perf(lint): one ast-grep scan feeds both the lint gate and the flaky-test ratchet (#33903) lint:ast-grep and check-flaky-test-pattern-ratchet each parsed the whole tree. ast-grep-gates.mjs scans once; the lint gate re-runs ast-grep only on files with warning-or-error findings (ast-grep's own report and exit code), and the flaky ratchet counts test-* matches from the shared scan. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .github/workflows/pr.yml | 2 +- config/scripts/ast-grep-gates.mjs | 66 +++++++++++++++++++ config/scripts/check-changed-selection.mjs | 3 +- .../check-flaky-test-pattern-ratchet.mjs | 13 ++-- config/scripts/run-lint.mjs | 7 +- config/scripts/run-ratchets.mjs | 4 +- 6 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 config/scripts/ast-grep-gates.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 846a6e29fbf..30cacfa5520 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -287,7 +287,7 @@ jobs: if: needs.code_paths.outputs.static_analysis == 'true' run: | node_modules/.bin/ast-grep test --config config/sgconfig.yml --skip-snapshot-tests - node_modules/.bin/ast-grep scan --config config/sgconfig.yml --min-severity warning --format github src config tests mobile + node config/scripts/ast-grep-gates.mjs --format=github # Why the full tree: it takes ~2s, and the pre-commit hook is skippable. - name: Check formatting diff --git a/config/scripts/ast-grep-gates.mjs b/config/scripts/ast-grep-gates.mjs new file mode 100644 index 00000000000..e604c6e5dbd --- /dev/null +++ b/config/scripts/ast-grep-gates.mjs @@ -0,0 +1,66 @@ +// One ast-grep scan for both gates: the lint gate (warning and up) and the flaky-test ratchet (`test-*` rules). +import { spawnSync } from 'node:child_process' +import process from 'node:process' +import { pathToFileURL } from 'node:url' +import { + AST_GREP_CONFIG, + AST_GREP_SCAN_PATHS, + resolveAstGrepBinary, + scanAstGrep +} from './ast-grep-scan.mjs' +import { main as checkFlakyTestPatterns } from './check-flaky-test-pattern-ratchet.mjs' + +const LINT_SEVERITIES = new Set(['error', 'warning']) +// Windows caps a command line near 32K characters; past this, rescan the whole tree. +const MAX_PATH_ARGUMENT_CHARS = 24_000 + +export function lintFindingFiles(matches) { + return [ + ...new Set( + matches + .filter((match) => LINT_SEVERITIES.has(match.severity)) + .map((match) => match.file.split('\\').join('/')) + ) + ].sort() +} + +// Rules match file by file, so rescanning only the reported files gives ast-grep's own report and exit status. +function reportLintFindings(root, files, format) { + const paths = + files.reduce((total, file) => total + file.length + 1, 0) <= MAX_PATH_ARGUMENT_CHARS + ? files + : AST_GREP_SCAN_PATHS + const result = spawnSync( + resolveAstGrepBinary(), + [ + 'scan', + '--config', + AST_GREP_CONFIG, + '--min-severity', + 'warning', + ...(format ? ['--format', format] : []), + '--', + ...paths + ], + { cwd: root, stdio: 'inherit', windowsHide: true } + ) + if (result.error) { + throw result.error + } + return result.status ?? 1 +} + +export function main(root = process.cwd(), argv = []) { + const format = argv + .find((argument) => argument.startsWith('--format=')) + ?.slice('--format='.length) + const matches = scanAstGrep(root) + const files = lintFindingFiles(matches) + const lint = files.length > 0 ? reportLintFindings(root, files, format) : 0 + const flaky = checkFlakyTestPatterns(root, undefined, matches) + return Math.max(lint, flaky) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = main(process.cwd(), process.argv.slice(2)) +} diff --git a/config/scripts/check-changed-selection.mjs b/config/scripts/check-changed-selection.mjs index b12bcb55301..e09882a0dd8 100644 --- a/config/scripts/check-changed-selection.mjs +++ b/config/scripts/check-changed-selection.mjs @@ -174,8 +174,9 @@ export function selectTypecheckProjects(files) { function listRatchetChecks(scriptsDir) { const scripts = new Set(listRatchetScripts(scriptsDir)) - // `pnpm lint` runs it although CI gives it a dedicated step outside run-ratchets.mjs. + // `pnpm lint` runs these although CI checks them in their own steps outside run-ratchets.mjs. scripts.add('check-main-sync-spawn-ratchet.mjs') + scripts.add('check-flaky-test-pattern-ratchet.mjs') return [...scripts].sort().map((script) => ({ name: script.replace(/^check-|\.mjs$/g, ''), args: [`config/scripts/${script}`], diff --git a/config/scripts/check-flaky-test-pattern-ratchet.mjs b/config/scripts/check-flaky-test-pattern-ratchet.mjs index d37b8303b76..900d18fa67a 100644 --- a/config/scripts/check-flaky-test-pattern-ratchet.mjs +++ b/config/scripts/check-flaky-test-pattern-ratchet.mjs @@ -9,6 +9,7 @@ import { listTrackedFiles, runCountRatchet } from './ratchet-baseline.mjs' const BASELINE_PATH = 'config/flaky-test-pattern-baseline.txt' const RULE_FILTER = '^test-' +const FLAKY_RULE = new RegExp(RULE_FILTER) const HEADER = [ '# Flaky-test pattern findings per rule and file (config/ast-grep/rules/flaky-tests).', '# This is a RATCHET: counts may only go DOWN. Run `pnpm lint:ast-grep:flaky-tests` to list them.', @@ -28,10 +29,14 @@ export function countFindings(matches, isTracked = () => true) { return counts } -export function collectFlakyTestPatternCounts(root = process.cwd()) { +/** `matches` may come from an unfiltered scan another gate shares; only `test-*` rules count. */ +export function collectFlakyTestPatternCounts(root = process.cwd(), matches = undefined) { // Tracked files only, so untracked scratch never changes the baseline. const tracked = new Set(listTrackedFiles(root, [])) - return countFindings(scanAstGrep(root, { filter: RULE_FILTER }), (file) => tracked.has(file)) + const flaky = (matches ?? scanAstGrep(root, { filter: RULE_FILTER })).filter((match) => + FLAKY_RULE.test(match.ruleId) + ) + return countFindings(flaky, (file) => tracked.has(file)) } function git(root, args) { @@ -74,13 +79,13 @@ export function changedFileScope(changed) { return (key) => changed.has(key.slice(key.indexOf(':') + 1)) } -export function main(root = process.cwd(), arg = undefined) { +export function main(root = process.cwd(), arg = undefined, matches = undefined) { return runCountRatchet( { name: 'flaky-test-pattern', baselinePath: BASELINE_PATH, header: HEADER, - collect: collectFlakyTestPatternCounts, + collect: (dir) => collectFlakyTestPatternCounts(dir, matches), scope: (dir) => changedFileScope(listChangedFiles(dir)), unit: 'flaky-test finding(s)', fix: [ diff --git a/config/scripts/run-lint.mjs b/config/scripts/run-lint.mjs index fd1034309a9..3466e4b88c0 100644 --- a/config/scripts/run-lint.mjs +++ b/config/scripts/run-lint.mjs @@ -15,8 +15,7 @@ export const LINT_STEPS = [ 'check:test-file-runners', 'verify:localization-catalogs', 'verify:localization-coverage', - 'check:flaky-test-pattern-ratchet', - 'lint:ast-grep', + 'lint:ast-grep-gates', 'check:main-sync-spawn-ratchet', 'lint:js-plugins', 'oxlint', @@ -50,7 +49,9 @@ export const LINT_STEPS = [ const BARE_COMMANDS = { oxlint: 'oxlint', // process-host and compat-boundary imports from one TypeScript parse, as CI runs them. - 'check:import-boundaries': 'bun config/scripts/check-import-boundaries.mjs' + 'check:import-boundaries': 'bun config/scripts/check-import-boundaries.mjs', + // lint:ast-grep and the flaky-test ratchet from one scan, as CI runs them. + 'lint:ast-grep-gates': 'node config/scripts/ast-grep-gates.mjs' } function parseMaxWorkers(argv) { diff --git a/config/scripts/run-ratchets.mjs b/config/scripts/run-ratchets.mjs index eadbf28b3a1..10a17b0bda2 100644 --- a/config/scripts/run-ratchets.mjs +++ b/config/scripts/run-ratchets.mjs @@ -11,7 +11,9 @@ const RUN_ELSEWHERE = new Set([ // Needs the PR diff base; the cross-version-wire job runs it. 'check-runtime-launcher-protocol-ratchet.mjs', // Its own background preflight step, so it overlaps lint and typecheck. - 'check-main-sync-spawn-ratchet.mjs' + 'check-main-sync-spawn-ratchet.mjs', + // ast-grep-gates.mjs checks it from the ast-grep lint scan instead of scanning again. + 'check-flaky-test-pattern-ratchet.mjs' ]) export function listRatchetScripts(scriptsDir) {