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>
This commit is contained in:
OrcaWin
2026-10-26 03:50:42 -07:00
committed by GitHub
co-authored by OrcaWin
parent 5ee6e42a3b
commit 819d273e27
6 changed files with 85 additions and 10 deletions
+1 -1
View File
@@ -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
+66
View File
@@ -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))
}
+2 -1
View File
@@ -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}`],
@@ -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: [
+4 -3
View File
@@ -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) {
+3 -1
View File
@@ -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) {