mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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
|
||||
])
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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})`)
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user