diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ce894d23248..c8b4a15f508 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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; diff --git a/.oxlintrc.json b/.oxlintrc.json index ef1c2a14101..ee88edaa4bb 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -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", diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index bfa526238b7..54485134c60 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -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" - } } ] } diff --git a/config/oxlint-plugins/anti-slop-prefilter.mjs b/config/oxlint-plugins/anti-slop-prefilter.mjs index 388714131a6..008310ce6d8 100644 --- a/config/oxlint-plugins/anti-slop-prefilter.mjs +++ b/config/oxlint-plugins/anti-slop-prefilter.mjs @@ -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|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) diff --git a/config/oxlint-plugins/ime-enter-guard.mjs b/config/oxlint-plugins/ime-enter-guard.mjs index e28cd773e62..443986ca08a 100644 --- a/config/oxlint-plugins/ime-enter-guard.mjs +++ b/config/oxlint-plugins/ime-enter-guard.mjs @@ -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 {} } diff --git a/config/oxlint-plugins/quadratic-buffer-concat.mjs b/config/oxlint-plugins/quadratic-buffer-concat.mjs index 8f60f4b6b65..ee80fbc456d 100644 --- a/config/oxlint-plugins/quadratic-buffer-concat.mjs +++ b/config/oxlint-plugins/quadratic-buffer-concat.mjs @@ -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() diff --git a/config/oxlint-plugins/renderer-scrollbar-style.mjs b/config/oxlint-plugins/renderer-scrollbar-style.mjs index 3fb64ff9099..28de741ee1d 100644 --- a/config/oxlint-plugins/renderer-scrollbar-style.mjs +++ b/config/oxlint-plugins/renderer-scrollbar-style.mjs @@ -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() diff --git a/config/oxlint-react-doctor.json b/config/oxlint-react-doctor.json index c29dc78c46b..231b88bd43e 100644 --- a/config/oxlint-react-doctor.json +++ b/config/oxlint-react-doctor.json @@ -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"] diff --git a/config/scripts/anti-slop-prefilter.test.mjs b/config/scripts/anti-slop-prefilter.test.mjs index e4a790dc0e9..6af7c17e219 100644 --- a/config/scripts/anti-slop-prefilter.test.mjs +++ b/config/scripts/anti-slop-prefilter.test.mjs @@ -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', diff --git a/config/scripts/app-store-performance-plugin.test.mjs b/config/scripts/app-store-performance-plugin.test.mjs index 5e4df531cb4..3e2a110f006 100644 --- a/config/scripts/app-store-performance-plugin.test.mjs +++ b/config/scripts/app-store-performance-plugin.test.mjs @@ -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([]) }) diff --git a/config/scripts/check-changed-code-quality.test.mjs b/config/scripts/check-changed-code-quality.test.mjs index 7af6d338652..9db417b8c7a 100644 --- a/config/scripts/check-changed-code-quality.test.mjs +++ b/config/scripts/check-changed-code-quality.test.mjs @@ -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 } + return <>{rows.map((row, 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'), diff --git a/config/scripts/check-changed-selection.mjs b/config/scripts/check-changed-selection.mjs index e09882a0dd8..90391ceeb3f 100644 --- a/config/scripts/check-changed-selection.mjs +++ b/config/scripts/check-changed-selection.mjs @@ -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'], diff --git a/config/scripts/check-changed.mjs b/config/scripts/check-changed.mjs index 794c4460b28..1cef7a30bd1 100644 --- a/config/scripts/check-changed.mjs +++ b/config/scripts/check-changed.mjs @@ -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', diff --git a/config/scripts/pr-preflight-gates.test.mjs b/config/scripts/pr-preflight-gates.test.mjs index e97578e6b78..3c4095bb60e 100644 --- a/config/scripts/pr-preflight-gates.test.mjs +++ b/config/scripts/pr-preflight-gates.test.mjs @@ -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', () => { diff --git a/config/scripts/quadratic-buffer-concat-plugin.test.mjs b/config/scripts/quadratic-buffer-concat-plugin.test.mjs index 41c672ac845..47cc34afafd 100644 --- a/config/scripts/quadratic-buffer-concat-plugin.test.mjs +++ b/config/scripts/quadratic-buffer-concat-plugin.test.mjs @@ -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]) }', diff --git a/config/scripts/renderer-scrollbar-style-plugin.test.mjs b/config/scripts/renderer-scrollbar-style-plugin.test.mjs index 00b6cea5c55..c21b5c711be 100644 --- a/config/scripts/renderer-scrollbar-style-plugin.test.mjs +++ b/config/scripts/renderer-scrollbar-style-plugin.test.mjs @@ -17,6 +17,11 @@ function lintSources(sources) { } const violations = [ + [ + 'escaped class expression', + String.raw`export const X = () =>
`, + String.raw`'\x6fverflow-y-auto'` + ], [ 'unstyled class', 'export const X = () =>
', diff --git a/config/scripts/run-lint.mjs b/config/scripts/run-lint.mjs index 3466e4b88c0..121e9a4a95d 100644 --- a/config/scripts/run-lint.mjs +++ b/config/scripts/run-lint.mjs @@ -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= 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 + } } diff --git a/package.json b/package.json index f09e24a9cb6..fddb1bb400c 100644 --- a/package.json +++ b/package.json @@ -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" } } } diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-toolbar.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-toolbar.tsx index 165af6ccbe7..f95fcb21d7a 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-toolbar.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-toolbar.tsx @@ -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) => (