diff --git a/config/patches/oxlint-plugin-anti-slop@0.1.2.patch b/config/patches/oxlint-plugin-anti-slop@0.1.2.patch index 1e7349f8fac..435207f935a 100644 --- a/config/patches/oxlint-plugin-anti-slop@0.1.2.patch +++ b/config/patches/oxlint-plugin-anti-slop@0.1.2.patch @@ -30,3 +30,49 @@ index af5f08a5002fb26f94414be13f1ad848ae73d522..eb95ebc44fcb4b4760b2afe097a114cb environment = createTypeAliasEnvironment( node, context.sourceCode.visitorKeys, +diff --git a/src/rules/no-widen-then-assert.ts b/src/rules/no-widen-then-assert.ts +--- a/src/rules/no-widen-then-assert.ts ++++ b/src/rules/no-widen-then-assert.ts +@@ -325,11 +325,12 @@ + }, + }, + createOnce(context) { +- let scopes: Parameters[0] = []; ++ let scopes: Parameters[0] | null = null; + + const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => { + const expression = assertedExpression(node); + if (expression.type !== "Identifier") return; ++ scopes ??= context.sourceCode.scopeManager.scopes; + + const variable = resolvedVariableForIdentifier(scopes, expression); + if (variable === null) return; +@@ -357,7 +358,7 @@ + + return { + Program() { +- scopes = context.sourceCode.scopeManager.scopes; ++ scopes = null; + }, + TSAsExpression: checkAssertion, + TSTypeAssertion: checkAssertion, +diff --git a/src/rules/no-module-mocking.ts b/src/rules/no-module-mocking.ts +--- a/src/rules/no-module-mocking.ts ++++ b/src/rules/no-module-mocking.ts +@@ -39,7 +39,6 @@ + + function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean { + if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false; +- if (!isTestFrameworkObject(sourceCode, callee.object)) return false; + const property = callee.property; + const method = callee.computed + ? property.type === "Literal" && +@@ -51,7 +50,7 @@ + : property.type === "Identifier" + ? property.name + : null; +- return method !== null && moduleMockMethods.has(method); ++ return method !== null && moduleMockMethods.has(method) && isTestFrameworkObject(sourceCode, callee.object); + } + + /** Ban test framework module mocking in favor of real dependency seams. */ diff --git a/config/scripts/anti-slop-scope-regression.test.mjs b/config/scripts/anti-slop-scope-regression.test.mjs new file mode 100644 index 00000000000..50b75c95e81 --- /dev/null +++ b/config/scripts/anti-slop-scope-regression.test.mjs @@ -0,0 +1,48 @@ +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' + +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({ + pluginName: 'anti-slop', + pluginPath: path.join(directory, 'index.ts'), + rules: { [`anti-slop/${rule}`]: 'error' }, + source, + extension: 'ts' + }) +} + +it.each([ + ['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], + ['const value = {}; let wide: unknown = value; wide as { id: string };', 0], + ['const value = {}; const wide: unknown = value; function read() { return wide as {}; }', 0], + [ + 'const value = {}; const wide: unknown = value; function read(wide: unknown) { return wide as {}; }', + 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([ + ["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], + ["vi['mock']('module');", 1], + ["import { vi } from 'vitest'; vi.mocked(value);", 0], + ["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) +}) diff --git a/config/scripts/mobile-web-app-module-entry-resolution.test.mjs b/config/scripts/mobile-web-app-module-entry-resolution.test.mjs new file mode 100644 index 00000000000..ccdf6936927 --- /dev/null +++ b/config/scripts/mobile-web-app-module-entry-resolution.test.mjs @@ -0,0 +1,34 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { expect, it } from 'vitest' +import { mobileWebAppModuleClosure } from './build-mobile-web-app-bundle.mjs' + +it.each(['extensionless', 'explicit', 'absolute'])( + 'resolves %s census entries against their source tree and prefers web siblings', + async (form) => { + const directory = mkdtempSync(path.join(tmpdir(), 'orca-mobile-entry-')) + const modules = ['app/_layout', 'app/h/_layout', 'app/h/[hostId]/index'] + const webModules = ['app/_layout.web.tsx', 'app/h/_layout.tsx', 'app/h/[hostId]/index.web.tsx'] + try { + for (const file of [...webModules, 'app/_layout.tsx', 'app/h/[hostId]/index.tsx']) { + const target = path.join(directory, file) + mkdirSync(path.dirname(target), { recursive: true }) + writeFileSync(target, 'export const entry = 1') + } + const entries = modules.map((entry) => { + if (form === 'explicit') { + return `${entry}.tsx` + } + if (form === 'absolute') { + return path.join(directory, `${entry}.tsx`) + } + return entry + }) + const { local } = await mobileWebAppModuleClosure(entries, { absWorkingDir: directory }) + expect(local.map((file) => file.replaceAll('\\', '/')).sort()).toEqual(webModules.sort()) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8b7d4f438a..dcf9f1db688 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,7 +177,7 @@ patchedDependencies: lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636 node-pty@1.1.0: c2fe19cde7aef223738a7503ae429dd9b03432130b064aa64a601900b33c151f - oxlint-plugin-anti-slop@0.1.2: c442be3f300c2d47e98af4c6347fc18cad8c13cc18913c4ee50b3720dad85753 + oxlint-plugin-anti-slop@0.1.2: c10c871702365a0c2c59e9f1c5fb92df97cae5755d534c0a3d83c6368f6feafe ws@8.22.0: 9393e04dd9a90b01eb7aa35fcd5b1431b5038ce999fbbd82651136193ba51f14 importers: @@ -514,7 +514,7 @@ importers: version: 1.86.0(oxlint-tsgolint@7.0.2003) oxlint-plugin-anti-slop: specifier: github:dmmulroy/anti-slop#c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b - version: https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b(patch_hash=c442be3f300c2d47e98af4c6347fc18cad8c13cc18913c4ee50b3720dad85753) + version: https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b(patch_hash=c10c871702365a0c2c59e9f1c5fb92df97cae5755d534c0a3d83c6368f6feafe) oxlint-plugin-react-doctor: specifier: 0.9.14 version: 0.9.14 @@ -14171,7 +14171,7 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.71.0 '@oxfmt/binding-win32-x64-msvc': 0.71.0 - oxlint-plugin-anti-slop@https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b(patch_hash=c442be3f300c2d47e98af4c6347fc18cad8c13cc18913c4ee50b3720dad85753): + oxlint-plugin-anti-slop@https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b(patch_hash=c10c871702365a0c2c59e9f1c5fb92df97cae5755d534c0a3d83c6368f6feafe): dependencies: '@oxlint/plugins': 1.78.0