perf(lint): skip JS plugin rules on files that cannot match (#33813)

Each custom oxlint JS rule now checks the file text first and returns no
visitors when it cannot report, so oxlint skips the AST transfer to JS.
app-store-performance only resolves scopes for tracked hook names.
anti-slop rules are wrapped with per-rule text prefilters. react-doctor
rules are off for folders without React (the old cloud/** ignore never
matched: config/ globs need a leading **/).

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-26 00:13:01 -07:00
committed by GitHub
co-authored by OrcaWin
parent 04ba0aec20
commit df4d4824c6
8 changed files with 160 additions and 12 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"jsPlugins": [
{
"name": "anti-slop",
"specifier": "../.anti-slop-plugin/index.ts"
"specifier": "./oxlint-plugins/anti-slop-prefilter.mjs"
}
],
"categories": {
@@ -0,0 +1,45 @@
// Skips each anti-slop rule on files whose text cannot match it; when every rule skips, oxlint never transfers the AST to JS.
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')
const REQUIRED_TEXT = {
'no-module-mocking': (text) => /mock/i.test(text),
'no-object-parameters': (text) => text.includes('object'),
'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)
}
function withPrefilter(rule, mayMatch) {
return {
meta: rule.meta,
createOnce(context) {
const { before, ...visitor } = rule.createOnce(context)
return {
...visitor,
before() {
const text = context.sourceCode.text
if (!mayMatch(text) && !hasEscape(text)) {
return false
}
return before?.()
}
}
}
}
}
export default {
meta: antiSlop.meta,
rules: Object.fromEntries(
Object.entries(antiSlop.rules).map(([name, rule]) => [
name,
REQUIRED_TEXT[name] ? withPrefilter(rule, REQUIRED_TEXT[name]) : rule
])
)
}
+21 -10
View File
@@ -82,6 +82,7 @@ function unwrapShallowSelector(selector, state) {
if (
selector?.type === 'CallExpression' &&
selector.callee.type === 'Identifier' &&
state.hookNames.has(selector.callee.name) &&
state.shallowHooks.has(variableFor(selector.callee, state))
) {
return { selector: selector.arguments[0], shallow: true }
@@ -201,6 +202,7 @@ function createRuleState(sourceCode) {
return {
appStoreHooks: new Set(),
shallowHooks: new Set(),
hookNames: new Set(),
sourceCode,
deferredCalls: []
}
@@ -213,20 +215,22 @@ function recordImports(node, state) {
continue
}
const imported = identifierName(specifier.imported)
const variable = variableFor(specifier.local, state)
if (!imported || !variable) {
continue
}
if (source === 'zustand/react/shallow' && imported === 'useShallow') {
state.shallowHooks.add(variable)
}
const shallow = source === 'zustand/react/shallow' && imported === 'useShallow'
// useAppStore is the app store wherever it is re-exported from; sibling
// stores are trusted by naming convention only when they come from this codebase.
if (
STORE_HOOK_NAME.test(imported) &&
const store =
STORE_HOOK_NAME.test(imported ?? '') &&
!NON_STORE_HOOKS.has(imported) &&
(imported === 'useAppStore' || isLocalModuleSource(source))
) {
// Why: getScope builds the whole scope tree, so only resolve bindings this rule tracks.
const variable = shallow || store ? variableFor(specifier.local, state) : null
if (!variable) {
continue
}
state.hookNames.add(specifier.local.name)
if (shallow) {
state.shallowHooks.add(variable)
} else {
state.appStoreHooks.add(variable)
}
}
@@ -235,6 +239,7 @@ function recordImports(node, state) {
function isAppStoreCall(node, state) {
return (
node.callee.type === 'Identifier' &&
state.hookNames.has(node.callee.name) &&
state.appStoreHooks.has(variableFor(node.callee, state)) &&
node.optional !== true
)
@@ -349,8 +354,14 @@ function noNestedFreshUnderShallowRule(sourceCode) {
})
}
// Files that never name a use<Name>Store hook cannot match; returning no visitors skips AST transfer.
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)) {
return {}
}
const visitors = createVisitors(context.sourceCode)
for (const [nodeType, visit] of Object.entries(visitors)) {
visitors[nodeType] = visit.bind(context)
@@ -89,6 +89,10 @@ function testsEnter(text) {
}
function createImeEnterGuardRule(context) {
// Reports need an Enter test somewhere in the file; returning no visitors skips AST transfer.
if (!testsEnter(context.sourceCode.text)) {
return {}
}
const localFunctionTexts = new Map()
const textElementNames = new Set()
const candidates = []
@@ -207,6 +207,11 @@ 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')) {
return {}
}
const declarations = new Map()
const assignedRootsByLoop = new WeakMap()
const recordBindings = (pattern, owner) => {
@@ -265,6 +265,10 @@ 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')) {
return {}
}
const visitors = createVisitors()
for (const [nodeType, visit] of Object.entries(visitors)) {
visitors[nodeType] = visit.bind(context)
+36 -1
View File
@@ -25,5 +25,40 @@
"react-doctor/zustand-no-mutating-state": "warn",
"react-doctor/zustand-no-whole-store-destructure": "warn"
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/out", "cloud/**"]
"overrides": [
{
// These processes have no React; linting them only pays AST transfer to JS.
// Paths outside config/ are matched absolute, so globs need a leading **/.
"files": [
"**/src/main/**",
"**/src/shared/**",
"**/src/preload/**",
"**/src/cli/**",
"**/src/wsl-guest/**",
"**/cloud/**"
],
"rules": {
"react-doctor/effect-needs-cleanup": "off",
"react-doctor/no-array-index-as-key": "off",
"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"]
}
@@ -0,0 +1,44 @@
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import path from 'node:path'
import { beforeAll, expect, it } from 'vitest'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
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-object-parameters', 'export function read(value: object): void {}'],
['no-reduce-accumulator-copy', 'export const all = [[1]].reduce((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',
'const value = {}; const wide: unknown = value; export const id = wide as { id: string };'
]
]
let diagnostics
beforeAll(() => {
if (!existsSync(path.resolve('.anti-slop-plugin/index.ts'))) {
execFileSync(process.execPath, ['config/scripts/sync-anti-slop-plugin.mjs'])
}
diagnostics = runOxlintPluginOnSources({
pluginName: 'anti-slop',
pluginPath,
rules: Object.fromEntries(violations.map(([rule]) => [`anti-slop/${rule}`, 'error'])),
sources: violations.map(([, source]) => source),
extension: 'ts'
})
})
it.each(violations.map((violation, index) => [...violation, index]))(
'%s still reports through the prefilter: %s',
(rule, _source, index) => {
expect(diagnostics[index].map((diagnostic) => diagnostic.code)).toContain(`anti-slop(${rule})`)
}
)