diff --git a/config/scripts/typecheck-diagnostic-baseline.test.mjs b/config/scripts/typecheck-diagnostic-baseline.test.mjs index f261a6b05f5..41948342f9a 100644 --- a/config/scripts/typecheck-diagnostic-baseline.test.mjs +++ b/config/scripts/typecheck-diagnostic-baseline.test.mjs @@ -222,9 +222,22 @@ describe('typecheck diagnostic baseline', () => { const stale = runBaseline(paths) expect(stale.status).toBe(1) + expect(stale.stderr).toContain('config/scripts/typecheck-diagnostic-baseline.mjs') expect(stale.stderr).toContain('--write') }) + it('names a command runnable from the directory that printed it', () => { + // Mobile runs its typecheck from mobile/, where a repo-relative path is not runnable. + const paths = createFixture("const value: number = 'bad'\n") + expect(runBaseline(paths, ['--write']).status).toBe(0) + writeFileSync(paths.source, 'const value: number = 1\n') + + const stale = runBaseline(paths, [], { cwd: join(repoRoot, 'mobile') }) + + expect(stale.status).toBe(1) + expect(stale.stderr).toContain('node ../config/scripts/typecheck-diagnostic-baseline.mjs') + }) + it('refuses to regenerate a baseline that grows from the PR base', () => { const paths = createFixture("const first: number = 'bad'\n") expect(runBaseline(paths, ['--write']).status).toBe(0) diff --git a/config/scripts/typecheck-extension-shadowing.test.mjs b/config/scripts/typecheck-extension-shadowing.test.mjs index 8988344761d..d7525f02165 100644 --- a/config/scripts/typecheck-extension-shadowing.test.mjs +++ b/config/scripts/typecheck-extension-shadowing.test.mjs @@ -22,8 +22,18 @@ const IGNORED_DIRECTORIES = new Set([ 'coverage', '.cross-version-checkouts' ]) -// TypeScript resolves these in order, so an earlier entry shadows a later one. -const EXTENSION_PRIORITY = ['.ts', '.tsx', '.mts', '.cts', '.d.ts', '.d.mts', '.d.cts'] +/** + * Resolution groups, not one ranking: `.mts` and `.cts` are separate module formats + * that coexist with `.ts`. Only members of the same group shadow each other, so a + * routine `foo.ts` + `foo.mts` pairing must not be reported. + */ +const EXTENSION_GROUPS = [ + ['.d.ts', '.ts', '.tsx'], + ['.d.mts', '.mts'], + ['.d.cts', '.cts'] +] +// Within a group tsc prefers the implementation file over the declaration. +const PRIORITY = ['.ts', '.tsx', '.mts', '.cts', '.d.ts', '.d.mts', '.d.cts'] function collectFiles(root) { let found = [] @@ -51,37 +61,66 @@ function collectFiles(root) { found = found.concat(collectFiles(fullPath)) continue } - if (EXTENSION_PRIORITY.some((extension) => entry.name.endsWith(extension))) { - found.push(fullPath) - } + found.push(fullPath) } return found } function extensionOf(path) { - return EXTENSION_PRIORITY.filter((extension) => path.endsWith(extension)).sort( + return PRIORITY.filter((extension) => path.endsWith(extension)).sort( (left, right) => right.length - left.length )[0] } -describe('typecheck extension shadowing', () => { - it('has no file hidden from the program by a higher-priority extension', () => { +/** Exported shape kept pure so the grouping rule is testable without touching the tree. */ +export function findShadowedFiles(paths) { + const shadowed = [] + for (const group of EXTENSION_GROUPS) { const byStem = new Map() - for (const file of collectFiles(repoRoot)) { - const relativePath = relative(repoRoot, file).replaceAll('\\', '/') - const extension = extensionOf(relativePath) - const stem = relativePath.slice(0, -extension.length) + for (const path of paths) { + const extension = extensionOf(path) + if (!extension || !group.includes(extension)) { + continue + } + const stem = path.slice(0, -extension.length) byStem.set(stem, [...(byStem.get(stem) ?? []), extension]) } + for (const [stem, extensions] of byStem) { + if (extensions.length < 2) { + continue + } + const ordered = PRIORITY.filter((extension) => extensions.includes(extension)) + shadowed.push(`${stem}${ordered[1]} is shadowed by ${stem}${ordered[0]}`) + } + } + return shadowed.sort() +} - const shadowed = [...byStem.entries()] - .filter(([, extensions]) => extensions.length > 1) - .map(([stem, extensions]) => { - const ordered = EXTENSION_PRIORITY.filter((extension) => extensions.includes(extension)) - return `${stem}${ordered[1]} is shadowed by ${stem}${ordered[0]}` - }) - .sort() +describe('typecheck extension shadowing', () => { + it('has no file hidden from the program by a higher-priority extension', () => { + const paths = collectFiles(repoRoot).map((file) => + relative(repoRoot, file).replaceAll('\\', '/') + ) - expect(shadowed).toEqual([]) + expect(findShadowedFiles(paths)).toEqual([]) + }) + + it('reports a .tsx hidden behind a .ts of the same stem', () => { + expect(findShadowedFiles(['src/a.ts', 'src/a.tsx'])).toEqual([ + 'src/a.tsx is shadowed by src/a.ts' + ]) + }) + + it('leaves .mts and .cts alone, since they are separate module formats', () => { + // Verified against tsc --listFilesOnly: a.ts, a.mts and a.cts all enter the + // program together; only a.tsx is dropped. + expect(findShadowedFiles(['src/a.ts', 'src/a.mts', 'src/a.cts'])).toEqual([]) + }) + + it('still reports shadowing inside the .mts and .cts groups', () => { + expect(findShadowedFiles(['src/a.mts', 'src/a.d.mts', 'src/b.cts', 'src/b.d.cts'])).toEqual([ + 'src/a.d.mts is shadowed by src/a.mts', + 'src/b.d.cts is shadowed by src/b.cts' + ]) }) }) diff --git a/config/tsconfig.e2e.json b/config/tsconfig.e2e.json index 763d8399616..64dbca6ea61 100644 --- a/config/tsconfig.e2e.json +++ b/config/tsconfig.e2e.json @@ -3,6 +3,8 @@ // in typecheck-e2e-diagnostics.json so the backlog only shrinks. Identity is // {file, code, message} without line/column, so a new error is caught unless it exactly // matches one that was fixed in the same file. Regenerate with `pnpm typecheck:e2e:write`. + // Renaming a file, or a type whose name these messages quote, reads as growth against the + // PR base and regenerating cannot clear it; fix those diagnostics instead. "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", // Why: specs import renderer and main modules directly, so the ambient declarations those // modules rely on (vite client types, build-time defines) have to be in the program too.