test: batch lint fixtures and remove repeated runtime scan (#29286)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-15 18:13:10 -07:00
committed by GitHub
co-authored by OrcaWin
parent f06404d7a8
commit fb529aaa30
7 changed files with 112 additions and 52 deletions
@@ -1,25 +1,25 @@
import { cpSync, mkdtempSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { afterAll, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
import { afterAll, beforeAll, expect, it } from 'vitest'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
const directory = mkdtempSync(path.join(import.meta.dirname, '.anti-slop-scope-'))
const source = path.dirname(createRequire(import.meta.url).resolve('oxlint-plugin-anti-slop'))
cpSync(source, directory, { recursive: true })
afterAll(() => rmSync(directory, { recursive: true, force: true }))
function lint(source, rule) {
return runOxlintPluginOnSource({
function lint(sources, rule) {
return runOxlintPluginOnSources({
pluginName: 'anti-slop',
pluginPath: path.join(directory, 'index.ts'),
rules: { [`anti-slop/${rule}`]: 'error' },
source,
sources,
extension: 'ts'
})
}
it.each([
const wideningCases = [
['const value = {}; const wide: unknown = value; wide as { id: string };', 1],
['const value = {}; const alias = value; const wide: unknown = alias; <{ id: string }>wide;', 1],
['function parse(input: unknown) { return input as { id: string }; }', 0],
@@ -30,11 +30,9 @@ it.each([
0
],
['const value = {}; const wide: unknown = value; wide as unknown;', 0]
])('preserves widening evidence and lexical boundaries: %s', (source, count) => {
expect(lint(source, 'no-widen-then-assert')).toHaveLength(count)
})
]
it.each([
const mockCases = [
["import { vi as test } from 'vitest'; test.mock('module');", 1],
["import { jest as test } from '@jest/globals'; test['doMock']('module');", 1],
["jest.unstable_mockModule('module', () => ({}));", 1],
@@ -43,6 +41,27 @@ it.each([
["const unrelated = { mock() {} }; unrelated.mock('module');", 0],
["const vi = { mock() {} }; vi.mock('module');", 0],
["import { vi as test } from 'vitest'; function run(test) { test.mock('module'); }", 0]
])('preserves mock method and owner resolution: %s', (source, count) => {
expect(lint(source, 'no-module-mocking')).toHaveLength(count)
]
const wideningDiagnostics = new Map()
const mockDiagnostics = new Map()
beforeAll(() => {
for (const [cases, rule, results] of [
[wideningCases, 'no-widen-then-assert', wideningDiagnostics],
[mockCases, 'no-module-mocking', mockDiagnostics]
]) {
const sources = cases.map(([source]) => source)
lint(sources, rule).forEach((diagnostics, index) => results.set(sources[index], diagnostics))
}
})
it.each(wideningCases)(
'preserves widening evidence and lexical boundaries: %s',
(source, count) => {
expect(wideningDiagnostics.get(source)).toHaveLength(count)
}
)
it.each(mockCases)('preserves mock method and owner resolution: %s', (source, count) => {
expect(mockDiagnostics.get(source)).toHaveLength(count)
})
@@ -241,10 +241,4 @@ describe('the command-line check', () => {
expect(await main({ plugins: [addElectron] })).toBe(1)
expect(error.mock.calls.join('\n')).toContain('+ src/main/native-chat/transcript-read-cache.ts')
}, 120_000)
// Why real: the value of this gate is the transitive edges, which a fixture cannot model.
it('passes on the tree as it is, with nothing reachable importing electron', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {})
expect(await main()).toBe(0)
}, 120_000)
})
+15 -7
View File
@@ -1,14 +1,14 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
import { beforeAll, describe, expect, it } from 'vitest'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/ime-enter-guard.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
function lintSources(sources) {
return runOxlintPluginOnSources({
pluginName: 'ime-enter-guard',
pluginPath,
source,
sources,
rules: { 'ime-enter-guard/require-ime-guard': 'warn' }
})
}
@@ -118,12 +118,20 @@ const accepted = [
]
]
const diagnosticsBySource = new Map()
beforeAll(() => {
const sources = [...violations, ...accepted].map(([, source]) => source)
lintSources(sources).forEach((diagnostics, index) => {
diagnosticsBySource.set(sources[index], diagnostics)
})
})
describe('IME Enter guard Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source) => {
expect(lintSource(source)).toHaveLength(1)
expect(diagnosticsBySource.get(source)).toHaveLength(1)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
expect(diagnosticsBySource.get(source)).toEqual([])
})
})
@@ -1,14 +1,14 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
import { beforeAll, describe, expect, it } from 'vitest'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/native-chat-design-tokens.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
function lintSources(sources) {
return runOxlintPluginOnSources({
pluginName: 'native-chat-design-tokens',
pluginPath,
source,
sources,
rules: {
'native-chat-design-tokens/no-arbitrary-font-size': 'warn',
'native-chat-design-tokens/no-softened-foreground': 'warn',
@@ -55,12 +55,20 @@ const accepted = [
]
]
const diagnosticsBySource = new Map()
beforeAll(() => {
const sources = [...violations, ...accepted].map(([, source]) => source)
lintSources(sources).forEach((diagnostics, index) => {
diagnosticsBySource.set(sources[index], diagnostics)
})
})
describe('native-chat-design-tokens oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source) => {
expect(lintSource(source)).toHaveLength(1)
expect(diagnosticsBySource.get(source)).toHaveLength(1)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
expect(diagnosticsBySource.get(source)).toEqual([])
})
})
+21 -6
View File
@@ -10,19 +10,25 @@ const oxlintPackageDirectory = path.dirname(
)
const oxlintPath = path.join(oxlintPackageDirectory, 'bin', 'oxlint')
export function runOxlintPluginOnSource({
export function runOxlintPluginOnSource({ source, ...options }) {
return runOxlintPluginOnSources({ ...options, sources: [source] })[0]
}
export function runOxlintPluginOnSources({
pluginName,
pluginPath,
rules,
source,
sources,
extension = 'tsx'
}) {
const directory = mkdtempSync(path.join(tmpdir(), `orca-${pluginName}-lint-`))
const sourcePath = path.join(directory, `sample.${extension}`)
const sourcePaths = sources.map((_, index) =>
path.join(directory, `sample-${index}.${extension}`)
)
const configPath = path.join(directory, 'oxlint.json')
try {
writeFileSync(sourcePath, source)
sources.forEach((source, index) => writeFileSync(sourcePaths[index], source))
writeFileSync(
configPath,
JSON.stringify({
@@ -42,7 +48,7 @@ export function runOxlintPluginOnSource({
)
const result = spawnSync(
process.execPath,
[oxlintPath, '--config', configPath, '--format', 'json', sourcePath],
[oxlintPath, '--config', configPath, '--format', 'json', ...sourcePaths],
{ encoding: 'utf8' }
)
if (result.error) {
@@ -51,7 +57,16 @@ export function runOxlintPluginOnSource({
if (!result.stdout.trim()) {
throw new Error(result.stderr || `${pluginName} did not produce Oxlint output`)
}
return JSON.parse(result.stdout).diagnostics
const diagnostics = JSON.parse(result.stdout).diagnostics
const byFile = new Map(sourcePaths.map((file) => [file, []]))
for (const diagnostic of diagnostics) {
const findings = byFile.get(path.resolve(diagnostic.filename))
if (!findings) {
throw new Error(`Unexpected Oxlint diagnostic file: ${diagnostic.filename}`)
}
findings.push(diagnostic)
}
return sourcePaths.map((file) => byFile.get(file))
} finally {
rmSync(directory, { recursive: true, force: true })
}
@@ -1,14 +1,14 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
import { beforeAll, describe, expect, it } from 'vitest'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/quadratic-buffer-concat.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
function lintSources(sources) {
return runOxlintPluginOnSources({
pluginName: 'quadratic-buffer-concat',
pluginPath,
source,
sources,
extension: 'ts',
rules: {
'quadratic-buffer-concat/no-loop-carried-concat': 'warn'
@@ -99,15 +99,23 @@ const accepted = [
['no Buffer concat', 'export const x = 1']
]
const diagnosticsBySource = new Map()
beforeAll(() => {
const sources = [...violations, ...accepted].map(([, source]) => source)
lintSources(sources).forEach((diagnostics, index) => {
diagnosticsBySource.set(sources[index], diagnostics)
})
})
describe('quadratic Buffer.concat Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source, accumulator) => {
const diagnostics = lintSource(source)
const diagnostics = diagnosticsBySource.get(source)
expect(diagnostics).toHaveLength(1)
expect(diagnostics[0].message).toContain(accumulator)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
expect(diagnosticsBySource.get(source)).toEqual([])
})
})
@@ -1,15 +1,15 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { beforeAll, describe, expect, it } from 'vitest'
import { plainClassName } from '../oxlint-plugins/renderer-scrollbar-style.mjs'
import { runOxlintPluginOnSource } from './oxlint-plugin-test-runner.mjs'
import { runOxlintPluginOnSources } from './oxlint-plugin-test-runner.mjs'
const pluginPath = path.resolve('config/oxlint-plugins/renderer-scrollbar-style.mjs')
function lintSource(source) {
return runOxlintPluginOnSource({
function lintSources(sources) {
return runOxlintPluginOnSources({
pluginName: 'renderer-scrollbar-style',
pluginPath,
source,
sources,
rules: {
'renderer-scrollbar-style/require-styled-vertical-scrollbar': 'warn'
}
@@ -94,13 +94,21 @@ const accepted = [
]
]
const diagnosticsBySource = new Map()
beforeAll(() => {
const sources = [...violations, ...accepted].map(([, source]) => source)
lintSources(sources).forEach((diagnostics, index) => {
diagnosticsBySource.set(sources[index], diagnostics)
})
})
describe('renderer scrollbar style Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source) => {
expect(lintSource(source)).toHaveLength(1)
expect(diagnosticsBySource.get(source)).toHaveLength(1)
})
it.each(accepted)('accepts %s', (_name, source) => {
expect(lintSource(source)).toEqual([])
expect(diagnosticsBySource.get(source)).toEqual([])
})
it.each([