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) =>