Refine lint performance and safe process execution (#34147)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-26 17:16:23 -07:00
committed by GitHub
co-authored by OrcaWin
parent 653908a652
commit 875d89af50
20 changed files with 147 additions and 145 deletions
+3
View File
@@ -175,6 +175,9 @@ pnpm test
pnpm build
```
`pnpm lint` runs independent checks in a bounded pool. Use
`pnpm lint --maxWorkers=2 --timings` to limit concurrent checks and print their durations.
For repeated local typechecks, use `pnpm tc` (or `pnpm tc:node`, `pnpm tc:web`,
and `pnpm tc:cli`). On Apple Silicon macOS and x64/arm64 Linux, these use the pinned
`tsrs` compiler with incremental caches. Keep `.cache/typecheck/` between checks;
+1
View File
@@ -42,6 +42,7 @@
"react/jsx-no-useless-fragment": ["error", { "allowExpressions": true }],
"react/jsx-pascal-case": "error",
"react/no-object-type-as-default-prop": "error",
"react/no-unstable-nested-components": ["warn", { "allowAsProps": true }],
"react/self-closing-comp": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
+2 -61
View File
@@ -35,7 +35,8 @@
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "off",
"anti-slop/no-shape-in-symbol-names": "error",
// Naming preferences do not justify a visitor on every identifier.
"anti-slop/no-shape-in-symbol-names": "off",
"anti-slop/no-unknown-parameters": "off",
"anti-slop/no-unknown-returns": "off",
"anti-slop/no-unknown-type-aliases": "error",
@@ -84,66 +85,6 @@
"rules": {
"anti-slop/no-module-mocking": "off"
}
},
// The exemptions below are file-scoped rather than inline `oxlint-disable` comments
// because the root lint scan does not load this plugin, so an inline directive naming
// an anti-slop rule always reads back as an unused directive there.
//
// In the screenshot annotator a "shape" is the drawn geometry -- pen, arrow, rect,
// ellipse, highlight. A domain noun, and it pervades every symbol in the module.
// In mobile/src/test-support/rpc-recording a "shape" is the container a recorded value is
// interned as (list, map or whole) and the structure a recorder fixture must match -- a
// domain noun across the engine, as in the annotator.
{
"files": ["**/test-support/rpc-recording/**"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
{
"files": [
"**/browser-pane/annotate/**",
"**/browser-pane/ClientHostedBrowserPagePane.markup.test.tsx"
],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// The markup tests outside annotate/ drive its API, whose payload key is `shapes`
// (`MarkupOverlay.onComplete({ imageElement, shapes })`): the domain term, not a structure.
{
"files": ["**/browser-pane/*.markup.test.tsx"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// lucide exports the icon component as `Shapes`, and the matching REPO_LUCIDE_ICONS key
// is the persisted icon name shared by the desktop picker and mobile.
{
"files": [
"**/components/repo/repo-icon.tsx",
"**/worktree-list/rows/repo-header-project-actions.tsx",
"**/components/MobileRepoIcon.tsx"
],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// `shapedSidebar` is a persisted onboarding-checklist field and a telemetry enum member;
// renaming it would orphan saved state.
{
"files": ["**/src/shared/onboarding-defaults.ts", "**/src/shared/onboarding-state-types.ts"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// Matching zod's own literal `shape` property is what selects the ZodObject branch of
// RpcSendInput's conditional type.
{
"files": ["**/rpc-contract/rpc-send-params.ts"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
}
]
}
@@ -1,8 +1,8 @@
// Skips each anti-slop rule on files whose text cannot match it; when every rule skips, oxlint never transfers the AST to JS.
// Returning false before traversal avoids deserializing the JS AST when all rules skip.
import antiSlop from '../../.anti-slop-plugin/index.ts'
// Identifiers may be written with \u escapes, which hide them from a plain text search.
const hasEscape = (text) => text.includes('\\u')
// Escaped identifiers and computed string keys can hide required text.
const hasEscape = (text) => text.includes('\\')
const REQUIRED_TEXT = {
'no-module-mocking': (text) => /mock/i.test(text),
@@ -10,7 +10,6 @@ const REQUIRED_TEXT = {
'no-reduce-accumulator-copy': (text) => text.includes('reduce'),
'no-reflect-apply': (text) => text.includes('Reflect'),
'no-reflect-get': (text) => text.includes('Reflect'),
'no-shape-in-symbol-names': (text) => /shape/i.test(text),
'no-unknown-type-aliases': (text) => text.includes('unknown') && /\btype\b/.test(text),
'no-widen-then-assert': (text) => /\b(?:unknown|any|object)\b/.test(text) && /\bas\b|</.test(text)
}
@@ -354,12 +354,12 @@ function noNestedFreshUnderShallowRule(sourceCode) {
})
}
// Files that never name a use<Name>Store hook cannot match; returning no visitors skips AST transfer.
// Skip JS AST deserialization in files that cannot name a store hook.
const STORE_HOOK_TEXT = /use[A-Z][A-Za-z0-9]*Store/
function bindContext(createVisitors) {
return (context) => {
if (!STORE_HOOK_TEXT.test(context.sourceCode.text)) {
if (!STORE_HOOK_TEXT.test(context.sourceCode.text) && !context.sourceCode.text.includes('\\')) {
return {}
}
const visitors = createVisitors(context.sourceCode)
+1 -1
View File
@@ -89,7 +89,7 @@ function testsEnter(text) {
}
function createImeEnterGuardRule(context) {
// Reports need an Enter test somewhere in the file; returning no visitors skips AST transfer.
// Reports need an Enter test; empty visitors avoid JS AST deserialization.
if (!testsEnter(context.sourceCode.text)) {
return {}
}
@@ -208,8 +208,8 @@ function quadraticAccumulator(context, call, loop, declarations, assignedRoots)
function createRule(context) {
const text = context.sourceCode.text
// Returning no visitors when Buffer.concat cannot appear skips AST transfer to JS.
if (!text.includes('Buffer') || !text.includes('concat')) {
// Empty visitors avoid JS AST deserialization when Buffer.concat cannot appear.
if ((!text.includes('Buffer') || !text.includes('concat')) && !text.includes('\\')) {
return {}
}
const declarations = new Map()
@@ -265,8 +265,8 @@ function jsxElementReports(node) {
function bindContext(createVisitors) {
return (context) => {
// Every vertical scroll class and style names overflow; returning no visitors skips AST transfer.
if (!context.sourceCode.text.includes('overflow')) {
// Every vertical scroll class and style names overflow; empty visitors skip JS AST deserialization.
if (!context.sourceCode.text.includes('overflow') && !context.sourceCode.text.includes('\\')) {
return {}
}
const visitors = createVisitors()
+4 -11
View File
@@ -17,9 +17,11 @@
"react-doctor/no-adjust-state-on-prop-change": "warn",
"react-doctor/no-create-store-in-render": "warn",
"react-doctor/no-derived-state-effect": "warn",
"react-doctor/no-initialize-state": "warn",
// Derived-state checks already catch mount copies; hydration sentinels are intentional.
"react-doctor/no-initialize-state": "off",
// The native counterpart runs in the default scan and allows render props.
"react-doctor/no-unstable-nested-components": "off",
"react-doctor/no-side-effect-in-state-updater-function": "warn",
"react-doctor/no-unstable-nested-components": "warn",
"react-doctor/zustand-no-fresh-selector-result": "warn",
"react-doctor/zustand-no-get-during-initialization": "warn",
"react-doctor/zustand-no-mutating-state": "warn",
@@ -43,21 +45,12 @@
"react-doctor/no-adjust-state-on-prop-change": "off",
"react-doctor/no-create-store-in-render": "off",
"react-doctor/no-derived-state-effect": "off",
"react-doctor/no-initialize-state": "off",
"react-doctor/no-side-effect-in-state-updater-function": "off",
"react-doctor/no-unstable-nested-components": "off",
"react-doctor/zustand-no-fresh-selector-result": "off",
"react-doctor/zustand-no-get-during-initialization": "off",
"react-doctor/zustand-no-mutating-state": "off",
"react-doctor/zustand-no-whole-store-destructure": "off"
}
},
{
// react-doctor tags this rule react-jsx-only; it visits every identifier, so skip files without JSX.
"files": ["**/*.{ts,mts,cts,js,mjs,cjs}"],
"rules": {
"react-doctor/no-unstable-nested-components": "off"
}
}
],
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
+5 -2
View File
@@ -9,12 +9,15 @@ const pluginPath = path.resolve('config/oxlint-plugins/anti-slop-prefilter.mjs')
// Each source must still reach its rule through the text prefilter.
const violations = [
['no-module-mocking', "import { vi } from 'vitest'; vi.doMock('module');"],
['no-module-mocking', String.raw`import { vi } from 'vitest'; vi['m\x6fck']('module');`],
['no-object-parameters', 'export function read(value: object): void {}'],
['no-reduce-accumulator-copy', 'export const all = [[1]].reduce((acc, x) => acc.concat(x), []);'],
[
'no-reduce-accumulator-copy',
String.raw`export const all = [[1]]['r\x65duce']((acc, x) => acc.concat(x), []);`
],
['no-reflect-apply', 'export const r = Reflect.apply(Math.max, null, [1]);'],
['no-reflect-get', 'export const r = Reflect.get({ a: 1 }, "a");'],
['no-shape-in-symbol-names', 'export const rowShape = 1;'],
['no-shape-in-symbol-names', 'export const row\\u0053hape = 1;'],
['no-unknown-type-aliases', 'export type Payload = unknown;'],
[
'no-widen-then-assert',
@@ -19,6 +19,10 @@ function lintSources(sources) {
}
const sources = {
escapedImport: String.raw`
import { use\u0041ppStore as useStore } from '@/store'
export const WholeStore = () => useStore()
`,
subscriptions: `
import { useAppStore as useStore } from '@/store'
const WholeStore = () => useStore()
@@ -117,6 +121,12 @@ beforeAll(() => {
})
describe('app store performance Oxlint plugin', () => {
it('resolves an escaped store import through the text prefilter', () => {
expect(
diagnosticsBySource.get(sources.escapedImport).map((diagnostic) => diagnostic.code)
).toEqual(['app-store-performance(require-selector)'])
})
it.each(['lexicalBindings', 'shadowedImports'])('respects lexical bindings in %s', (name) => {
expect(diagnosticsBySource.get(sources[name])).toEqual([])
})
@@ -121,6 +121,31 @@ describe('changed-code quality line matching', () => {
expect(isRootCodeQualityPath('src/main/index.ts')).toBe(true)
})
it.each(['src/renderer/src', 'mobile/src'])(
'checks nested components natively and keeps the selective index-key check in %s',
(sourceRoot) => {
const root = path.resolve(import.meta.dirname, '../..')
const directory = mkdtempSync(path.join(root, sourceRoot, 'native-react-lint-'))
try {
const file = path.join(directory, 'List.tsx')
writeFileSync(
file,
`export function List({ rows }) {
function Row() { return <input /> }
return <>{rows.map((row, index) => <Row key={index} />)}</>
}`
)
const codes = scanFixture('code quality', file).map((diagnostic) => diagnostic.code)
expect(codes).toContain('react(no-unstable-nested-components)')
expect(scanFixture('React Doctor', file).map((diagnostic) => diagnostic.code)).toEqual([
'react-doctor(no-array-index-as-key)'
])
} finally {
rmSync(directory, { recursive: true, force: true })
}
}
)
it.each([
['Counter.tsx', []],
['Counter.txt', [{ start: 1, end: 2 }]],
@@ -409,6 +434,22 @@ describe('unloaded plugin directive unused warning', () => {
})
})
it('keeps mount-time state copies covered after disabling the overlapping rule', () => {
const source = `
import { useEffect, useState } from 'react'
export function Title({ title }) {
const [value, setValue] = useState('')
useEffect(() => { setValue(title) }, [])
return value
}
`
withFixture(source, ({ filename }) => {
expect(scanFixture('React Doctor', filename).map((diagnostic) => diagnostic.code)).toEqual([
'react-doctor(no-derived-state-effect)'
])
})
})
it('does not hide unused native rules in a mixed directive', () => {
withFixture(
directive('react-doctor/no-derived-state-effect, unicorn/no-array-reduce'),
+3 -7
View File
@@ -106,15 +106,11 @@ export const VERIFY_CHECKS = [
inputs: ['README.md', 'docs/readme/']
},
{
name: 'process-host imports',
args: ['config/scripts/check-process-host-imports.mjs'],
name: 'import boundaries',
runtime: 'bun',
args: ['config/scripts/check-import-boundaries.mjs'],
inputs: [...SOURCE_ROOTS, 'config/process-host-direct-import-baseline.txt']
},
{
name: 'compat boundary imports',
args: ['config/scripts/check-compat-boundary-imports.mjs'],
inputs: ['src/']
},
{
name: 'dead design-system classes',
oxlint: ['--config', 'config/oxlint-dead-classes.json', 'src/renderer'],
+2 -1
View File
@@ -203,7 +203,8 @@ function buildTasks(root, plan, { files, mergeBase, maxWorkers, bun, warmRecords
...(antiSlop.length > 0
? [
step(node, ['config/scripts/sync-anti-slop-plugin.mjs']),
step(oxlint.command, [
step(bun(), [
'--smol',
...oxlint.prefixArgs,
'--config',
'config/oxlint-anti-slop.json',
+1 -7
View File
@@ -80,13 +80,7 @@ it('keeps the Bun anti-slop scan aligned with the local audit', () => {
const manifest = JSON.parse(readFileSync('package.json', 'utf8'))
const localCommand = manifest.scripts['audit:anti-slop'].split('&&').at(-1).trim()
expect(audit.run.trim().split('\n')[0]).toBe('node config/scripts/sync-anti-slop-plugin.mjs')
expect(
audit.run
.trim()
.split('\n')
.at(-1)
.replace('bun --smol node_modules/oxlint/bin/oxlint', 'oxlint')
).toBe(localCommand)
expect(audit.run.trim().split('\n').at(-1)).toBe(localCommand)
})
it('requires physical preflight success before publishing shards and admitting consumers', () => {
@@ -17,6 +17,11 @@ function lintSources(sources) {
}
const violations = [
[
'escaped computed concat',
String.raw`let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer['con\x63at']([acc, chunk]) }`,
'acc'
],
[
'self accumulator',
'let acc = Buffer.alloc(0); for (const chunk of chunks) { acc = Buffer.concat([acc, chunk]) }',
@@ -17,6 +17,11 @@ function lintSources(sources) {
}
const violations = [
[
'escaped class expression',
String.raw`export const X = () => <div className={'\x6fverflow-y-auto'} />`,
String.raw`'\x6fverflow-y-auto'`
],
[
'unstyled class',
'export const X = () => <div className="max-h-64 overflow-y-auto" />',
+48 -41
View File
@@ -1,10 +1,12 @@
// `pnpm lint`: runs the independent lint steps in a bounded pool, printing output only for failures.
import { spawn } from 'node:child_process'
import { runProcess } from '@orca/process-host'
import { readFileSync } from 'node:fs'
import { availableParallelism } from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import { resolveOxlintInvocation } from './oxlint-cli-invocation.mjs'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
// package.json script names, slowest first so short steps fill in around them.
export const LINT_STEPS = [
@@ -46,56 +48,54 @@ export const LINT_STEPS = [
]
// Steps with no package script of their own run as the bare command.
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',
// 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) {
const argument = argv.find((value) => /^--max-?workers=\d+$/i.test(value))
const requested = argument ? Number(argument.split('=')[1]) : 0
// Most steps are multi-threaded themselves, so a quarter of the cores keeps the host responsive.
return requested > 0 ? requested : Math.max(2, Math.floor(availableParallelism() / 4))
}
export function resolveLintCommands(scripts, steps = LINT_STEPS) {
return steps.map((name) => {
const command = scripts[name] ?? BARE_COMMANDS[name]
if (!command) {
function resolveLintCommands(scripts, root) {
const pnpm = resolvePnpmCliInvocation()
return LINT_STEPS.map((name) => {
if (name === 'oxlint') {
const { command, prefixArgs } = resolveOxlintInvocation(root)
return { name, program: command, args: prefixArgs }
}
if (!scripts[name]) {
throw new Error(`Lint step ${name} has no package.json script.`)
}
return { name, command }
return { name, program: pnpm.command, args: [...pnpm.prefixArgs, 'run', name] }
})
}
function runStep({ name, command }, root) {
async function runStep({ name, program, args }, root) {
const startedAt = performance.now()
return new Promise((resolve) => {
let output = ''
// Why a shell: package scripts are shell commands (`a && b`), as pnpm would run them.
const child = spawn(command, { cwd: root, shell: true, windowsHide: true })
child.stdout.on('data', (chunk) => (output += chunk))
child.stderr.on('data', (chunk) => (output += chunk))
const finish = (ok, extra = '') =>
resolve({
name,
ok,
output: output + extra,
elapsed: `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
})
child.on('error', (error) => finish(false, String(error.message ?? error)))
child.on('close', (code) => finish(code === 0))
})
try {
const result = await runProcess({ program, args, cwd: root, timeoutMs: null }, 'tail')
return {
name,
ok: result.code === 0 && !result.signal && !result.timedOut,
output: result.stdout + result.stderr,
elapsed: `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
}
} catch (error) {
return { name, ok: false, output: error.message ?? String(error), elapsed: 'failed to start' }
}
}
async function main(root, argv) {
const startedAt = performance.now()
const { scripts } = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
const steps = resolveLintCommands(scripts)
const concurrency = parseMaxWorkers(argv)
// Individual linters also use multiple cores.
let concurrency = Math.max(2, Math.min(4, Math.floor(availableParallelism() / 4)))
let timings = false
for (const argument of argv) {
if (argument === '--') {
continue
}
if (argument === '--timings') {
timings = true
} else if (/^--max-?workers=[1-9]\d*$/i.test(argument)) {
concurrency = Number(argument.split('=')[1])
} else {
throw new Error(`Unknown option ${argument}. Use --maxWorkers=<n> or --timings.`)
}
}
const steps = resolveLintCommands(scripts, root)
const results = []
let next = 0
await Promise.all(
@@ -104,6 +104,8 @@ async function main(root, argv) {
const result = await runStep(steps[next++], root)
if (!result.ok) {
console.error(`\n✗ ${result.name} (${result.elapsed})\n${result.output.trimEnd()}`)
} else if (timings) {
console.log(`✓ ${result.name} (${result.elapsed})`)
}
results.push(result)
}
@@ -122,5 +124,10 @@ async function main(root, argv) {
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = await main(path.resolve(import.meta.dirname, '../..'), process.argv.slice(2))
try {
process.exitCode = await main(path.resolve(import.meta.dirname, '../..'), process.argv.slice(2))
} catch (error) {
console.error(`lint: ${error.message ?? error}`)
process.exitCode = 1
}
}
+7 -3
View File
@@ -26,6 +26,7 @@
"check:code-quality:changed": "node config/scripts/check-changed-code-quality.mjs",
"lint:js-plugins": "oxlint --config config/oxlint-js-plugins.json src config tests mobile native .github",
"lint:ast-grep": "ast-grep scan --config config/sgconfig.yml --min-severity warning src config tests mobile",
"lint:ast-grep-gates": "node config/scripts/ast-grep-gates.mjs",
"lint:ast-grep:flaky-tests": "ast-grep scan --config config/sgconfig.yml --filter \"^test-\" src config tests mobile",
"test:ast-grep": "ast-grep test --config config/sgconfig.yml --skip-snapshot-tests",
"check:dead-classes": "oxlint --config config/oxlint-dead-classes.json src/renderer",
@@ -202,12 +203,13 @@
"test:e2e:remote-bulk-open-freeze": "pnpm run ensure:electron-runtime && pnpm exec playwright test tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"repro:live-remote-bulk-open-freeze": "node config/scripts/live-remote-bulk-open-freeze-repro.mjs",
"repro:live-remote-realistic-freeze": "node config/scripts/live-remote-realistic-freeze-repro.mjs",
"audit:anti-slop": "node config/scripts/sync-anti-slop-plugin.mjs && oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings",
"audit:anti-slop": "node config/scripts/sync-anti-slop-plugin.mjs && bun --smol node_modules/oxlint/bin/oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings",
"sync:anti-slop-plugin": "node config/scripts/sync-anti-slop-plugin.mjs",
"build:packages": "node config/scripts/build-workspace-packages.mjs",
"check:process-host-imports": "node config/scripts/check-process-host-imports.mjs",
"check:main-sync-spawn-ratchet": "node config/scripts/check-main-sync-spawn-ratchet.mjs",
"check:compat-boundary-imports": "node config/scripts/check-compat-boundary-imports.mjs"
"check:compat-boundary-imports": "node config/scripts/check-compat-boundary-imports.mjs",
"check:import-boundaries": "bun config/scripts/check-import-boundaries.mjs"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.3.284",
@@ -417,7 +419,9 @@
]
},
"rules": {
"react-doctor/js-combine-iterations": "off"
"react-doctor/js-combine-iterations": "off",
"react-doctor/no-initialize-state": "off",
"react-doctor/no-unstable-nested-components": "off"
}
}
}
@@ -242,7 +242,6 @@ export function BrowserPageToolbar({
),
disabled: !externalUrl
}}
// oxlint-disable-next-line react-doctor/no-unstable-nested-components -- BrowserChromeToolbar calls this as a function, so BrowserToolbarMenu stays one stable component.
overflowMenu={(overflow) => (
<BrowserToolbarMenu
currentProfileId={sessionProfileId}