1import { execSync } from 'child_process' 2import ts from 'typescript' 7export type BaselineContentMap = Map<string, string | null> // absPath -> HEAD content (null = not in HEAD, i.e. new file) 9export const loadHeadContents = (ptPathsA: string[]): BaselineContentMap => { 10 const map: BaselineContentMap = new Map() 11 for (const ptPath of ptPathsA) { 12 let content: string | null = null 13 try { content = execSync(`git show HEAD:${ptPath}`, {cwd: ptDir, encoding: 'utf8', stdio: 'pipe'}) } catch { content = null } // new file: no HEAD blob 19export const normalizeViolKey = (viol: string) => viol.replace(/^(\S+?):\d+:\d+( - )/, '$1$2') 21export const partitionRatchetViols = (viols: string[], ratchetDirs: string[], excludes: string[]) => { 22 const ratchetViols: string[] = [], lenientViols: string[] = [] 23 for (const viol of viols) { 24 const ptPath = viol.match(/^(\S+?)(?:\s|:\d)/)?.[1] 25 const isRatchet = !!ptPath && ratchetDirs.some(d => ptPath.startsWith(d)) && !excludes.some(d => ptPath.startsWith(d)) 26 ;(isRatchet ? ratchetViols : lenientViols).push(viol) 28 return {ratchetViols, lenientViols} 31export const subtractBaselineViols = (currentViols: string[], baselineViols: string[]) => { 32 const remaining = new Map<string, number>() 33 for (const viol of baselineViols) { 34 const key = normalizeViolKey(viol) 35 remaining.set(key, (remaining.get(key) ?? 0) + 1) 37 const newViols: string[] = [] 38 let preExistingCount = 0 39 for (const viol of currentViols) { 40 const key = normalizeViolKey(viol) 41 const count = remaining.get(key) ?? 0 43 remaining.set(key, count - 1) 49 return {newViols, preExistingCount} 52export const createHeadOverlayHost = (options: ts.CompilerOptions, baselineContent: BaselineContentMap): ts.CompilerHost => { 53 const host = ts.createCompilerHost(options) 54 const origReadFile = host.readFile 55 const origFileExists = host.fileExists 56 host.readFile = (fileName: string) => baselineContent.has(fileName) ? baselineContent.get(fileName) ?? undefined : origReadFile(fileName) 57 host.fileExists = (fileName: string) => baselineContent.has(fileName) ? baselineContent.get(fileName) !== null : origFileExists(fileName) 61export const createBaselineProgram = ( 63 options: ts.CompilerOptions, 64 editedAbsSet: Set<string>, 65 oldProgram?: ts.Program, 67 const baselineContent = loadHeadContents([...editedAbsSet].map(p => toPtRelPath(p))) 68 return ts.createProgram(roots, options, createHeadOverlayHost(options, baselineContent), oldProgram) 71const diagKey = (diag: ts.Diagnostic): string => { 72 const msg = ts.flattenDiagnosticMessageText(diag.messageText, '\n') 73 return `${diag.code}:${diag.start ?? 0}:${msg}` 76export const getBaselineDiagKeys = ( 77 baselineProgram: ts.Program, 78 importerAbsPaths: string[], 79 surfaceCodes: Set<number>, 81 const keys = new Set<string>() 82 for (const absPath of importerAbsPaths) { 83 const sourceFile = baselineProgram.getSourceFile(absPath) 84 if (!sourceFile) continue 86 ...baselineProgram.getSyntacticDiagnostics(sourceFile), 87 ...baselineProgram.getSemanticDiagnostics(sourceFile), 89 for (const diag of diagnostics) { 90 if (diag.category !== ts.DiagnosticCategory.Error) continue 91 if (!surfaceCodes.has(diag.code)) continue 92 if (diag.file && diag.file.fileName !== absPath) continue 93 keys.add(diagKey(diag)) 96 mcpDebugLog(`baseline-diag-keys: ${keys.size} pre-existing importer errors filtered`) 100export const isPreExistingImporterViol = ( 102 baselineDiagKeys: Set<string>, 103): boolean => baselineDiagKeys.has(diagKey(diag))