From 1be833391a47ee0e49f6f5e3d6190f0ede62a4f5 Mon Sep 17 00:00:00 2001 From: natew Date: Sat, 24 Oct 2026 23:51:26 -1000 Subject: [PATCH] fix: preserve explicit compiler diagnostic severity and context --- CHANGELOG.md | 6 ++ .../xcodebuild-domain-results.test.ts | 12 ++- .../__tests__/xcodebuild-event-parser.test.ts | 45 +++++++++ .../__tests__/xcodebuild-line-parsers.test.ts | 26 ++++++ src/utils/xcodebuild-event-parser.ts | 91 ++++++------------- src/utils/xcodebuild-line-parsers.ts | 65 +++++++++---- 6 files changed, 160 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f47e609ad..8c9c2d585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Fixed compiler warnings and their quoted source snippets being reported as build errors; warning and error diagnostics now retain their continuation lines and notes. + ## [2.7.1] ### Changed diff --git a/src/utils/__tests__/xcodebuild-domain-results.test.ts b/src/utils/__tests__/xcodebuild-domain-results.test.ts index 517d567f3..0234fd927 100644 --- a/src/utils/__tests__/xcodebuild-domain-results.test.ts +++ b/src/utils/__tests__/xcodebuild-domain-results.test.ts @@ -205,7 +205,8 @@ describe('xcodebuild-domain-results', () => { rawLine: '/tmp/App.swift:8:17: error: type mismatch', }); const parsedLine = '/tmp/App.swift:8:17: error: type mismatch'; - const unparsedLine = '2026-04-23 12:00:00.000 xcodebuild[123:456] error: IDE operation failed'; + const unparsedLine = + '2026-04-23 12:00:00.000 xcodebuild[123:456] error: IDE operation failed\n additional details'; const result = createBuildDomainResult({ started: createStartedPipelineWithState(runState.finalize(false, 1000)), @@ -213,7 +214,14 @@ describe('xcodebuild-domain-results', () => { target: 'simulator', artifacts: { buildLogPath: '/tmp/build.log' }, request: { scheme: 'App' }, - fallbackErrorMessages: [parsedLine, unparsedLine, 'ordinary progress line'], + fallbackErrorMessages: [ + parsedLine, + unparsedLine, + '/tmp/Bridge.cpp:4:2: warning: quoted execution error: message', + ' 4 | log("execution error: message");', + '/tmp/Bridge.cpp:2:3: note: quoted execution error: message', + 'ordinary progress line', + ], }); expect(result.diagnostics).toMatchObject({ diff --git a/src/utils/__tests__/xcodebuild-event-parser.test.ts b/src/utils/__tests__/xcodebuild-event-parser.test.ts index a9774083b..8c90b0888 100644 --- a/src/utils/__tests__/xcodebuild-event-parser.test.ts +++ b/src/utils/__tests__/xcodebuild-event-parser.test.ts @@ -433,6 +433,51 @@ describe('xcodebuild-event-parser', () => { }); }); + it.each(['stdout', 'stderr'] as const)( + 'preserves diagnostic severity and context across byte chunks on %s', + (source) => { + const warningLines = [ + '/tmp/Bridge.cpp:4:2: warning: quoted execution error: message [-Wconditional-uninitialized]', + ' 4 | log("/tmp/Other.cpp:1: error: message");', + ' | ^', + '/tmp/Bridge.cpp:2:3: note: initialize the variable', + ' 2 | const char *message;', + ]; + const errorLines = [ + ' /tmp/Bridge.cpp:8:2: error: undeclared identifier', + ' 8 | missing();', + ' | ^', + '/tmp/Bridge.cpp:7:2: note: declared here', + ]; + const text = [...warningLines, ...errorLines, 'xcodebuild: error: build failed'].join('\n'); + const events = collectEvents( + 'BUILD', + Array.from(text, (text) => ({ source, text })), + ); + expect(events).toEqual([ + expect.objectContaining({ + severity: 'warning', + location: '/tmp/Bridge.cpp:4', + message: [ + 'quoted execution error: message [-Wconditional-uninitialized]', + ...warningLines.slice(1).map((line) => line.trim()), + ].join('\n'), + rawLine: warningLines.join('\n'), + }), + expect.objectContaining({ + severity: 'error', + location: '/tmp/Bridge.cpp:8', + message: [ + 'undeclared identifier', + ...errorLines.slice(1).map((line) => line.trim()), + ].join('\n'), + rawLine: errorLines.join('\n'), + }), + expect.objectContaining({ severity: 'error', message: 'build failed' }), + ]); + }, + ); + it('emits warning events', () => { const events = collectEvents('BUILD', [ { source: 'stdout', text: '/tmp/App.swift:10:5: warning: variable unused\n' }, diff --git a/src/utils/__tests__/xcodebuild-line-parsers.test.ts b/src/utils/__tests__/xcodebuild-line-parsers.test.ts index 63b39db9d..4c9b5c75f 100644 --- a/src/utils/__tests__/xcodebuild-line-parsers.test.ts +++ b/src/utils/__tests__/xcodebuild-line-parsers.test.ts @@ -42,6 +42,11 @@ describe('parseBuildErrorDiagnostic', () => { renderedLine: 'xcodebuild: error: Unable to find destination', }); + expect(parseBuildErrorDiagnostic('clang++: error: linker command failed')).toEqual({ + message: 'linker command failed', + renderedLine: 'clang++: error: linker command failed', + }); + expect(parseBuildErrorDiagnostic('error: emit-module command failed')).toEqual({ message: 'emit-module command failed', renderedLine: 'error: emit-module command failed', @@ -57,6 +62,27 @@ describe('parseBuildErrorDiagnostic', () => { }); }); + it('uses header severity rather than quoted diagnostic text', () => { + for (const line of [ + '/tmp/Bridge.cpp:4:2: warning: quoted execution error: message', + '/tmp/Bridge.cpp:4:2: note: quoted execution error: message', + '4 | log("execution error: message");', + '4 | log("/tmp/Bridge.cpp:1: error: message");', + 'ld: warning: quoted error: message', + ]) { + expect(isBuildErrorDiagnosticLine(line)).toBe(false); + expect(parseBuildErrorDiagnostic(line)).toBeNull(); + } + expect( + isBuildErrorDiagnosticLine('xcodebuild: error: invalid destination\n available devices'), + ).toBe(true); + expect(parseBuildErrorDiagnostic('/tmp/Bridge.cpp:1:1: fatal error: missing.h')).toEqual({ + location: '/tmp/Bridge.cpp:1', + message: 'missing.h', + renderedLine: '/tmp/Bridge.cpp:1:1: fatal error: missing.h', + }); + }); + it('does not classify Objective-C selector fragments or NSError dump lines as build errors', () => { const selectorLine = 'pid:error:,'; const nserrorLine = diff --git a/src/utils/xcodebuild-event-parser.ts b/src/utils/xcodebuild-event-parser.ts index a91ba22b8..3ba15c232 100644 --- a/src/utils/xcodebuild-event-parser.ts +++ b/src/utils/xcodebuild-event-parser.ts @@ -7,9 +7,8 @@ import { parseTestCaseLine, parseTotalsLine, parseFailureDiagnostic, - parseBuildErrorDiagnostic, + parseBuildDiagnostic, parseDurationMs, - isBuildErrorDiagnosticLine, type ParsedTestCase, } from './xcodebuild-line-parsers.ts'; import { @@ -52,23 +51,6 @@ const stageMessages: Record = { COMPLETED: 'Completed', }; -function parseWarningLine(line: string): { location?: string; message: string } | null { - const locationMatch = line.match(/^(.*?):(\d+)(?::\d+)?:\s+warning:\s+(.+)$/u); - if (locationMatch) { - return { - location: `${locationMatch[1]}:${locationMatch[2]}`, - message: locationMatch[3], - }; - } - - const prefixedMatch = line.match(/^(?:[\w-]+:\s+)?warning:\s+(.+)$/iu); - if (prefixedMatch) { - return { message: prefixedMatch[1] }; - } - - return null; -} - const IGNORED_NOISE_PATTERNS = [ /^Command line invocation:$/u, /^\s*\/Applications\/Xcode[^\s]+\/Contents\/Developer\/usr\/bin\/xcodebuild\b/u, @@ -138,7 +120,8 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb let testCasesFailedSinceSwiftTestingSummary = 0; let detectedXcresultPath: string | null = null; - let pendingError: { + let pendingDiagnostic: { + severity: 'error' | 'warning'; message: string; location?: string; rawLines: string[]; @@ -289,26 +272,26 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb } } - function flushPendingError(): void { - if (!pendingError) { + function flushPendingDiagnostic(): void { + if (!pendingDiagnostic) { return; } onEvent({ kind, fragment: 'compiler-diagnostic', operation, - severity: 'error', - message: pendingError.message, - location: pendingError.location, - rawLine: pendingError.rawLines.join('\n'), + severity: pendingDiagnostic.severity, + message: pendingDiagnostic.message, + location: pendingDiagnostic.location, + rawLine: pendingDiagnostic.rawLines.join('\n'), }); - pendingError = null; + pendingDiagnostic = null; } function processLine(rawLine: string): void { const line = normalizeEventLine(rawLine); if (!line) { - flushPendingError(); + flushPendingDiagnostic(); return; } @@ -321,13 +304,17 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb } } - if (pendingError && /^\s/u.test(rawLine)) { - pendingError.message += `\n${line}`; - pendingError.rawLines.push(rawLine); + const diagnostic = parseBuildDiagnostic(line); + if ( + pendingDiagnostic && + (diagnostic?.severity === 'note' || (!diagnostic && /^\s/u.test(rawLine))) + ) { + pendingDiagnostic.message += `\n${line}`; + pendingDiagnostic.rawLines.push(rawLine); return; } - flushPendingError(); + flushPendingDiagnostic(); const xcresultPath = parseXcresultPathLine(line); if (xcresultPath) { @@ -396,30 +383,16 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb return; } - const buildError = parseBuildErrorDiagnostic(line); - if (buildError) { - pendingError = { - message: buildError.message, - location: buildError.location, - rawLines: [line], + if (diagnostic && diagnostic.severity !== 'note') { + pendingDiagnostic = { + severity: diagnostic.severity, + message: diagnostic.message, + location: diagnostic.location, + rawLines: [rawLine], }; return; } - const warning = parseWarningLine(line); - if (warning) { - onEvent({ - kind, - fragment: 'compiler-diagnostic', - operation, - severity: 'warning', - message: warning.message, - location: warning.location, - rawLine: line, - }); - return; - } - if (/^Test [Ss]uite /u.test(line)) { return; } @@ -428,18 +401,6 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb return; } - if (isBuildErrorDiagnosticLine(line)) { - onEvent({ - kind, - fragment: 'compiler-diagnostic', - operation, - severity: 'error', - message: line, - rawLine: line, - }); - return; - } - if (onUnrecognizedLine) { onUnrecognizedLine(line); } @@ -470,7 +431,7 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb processLine(stderrBuffer); } flushQueuedFailureDiagnostics(); - flushPendingError(); + flushPendingDiagnostic(); stdoutBuffer = ''; stderrBuffer = ''; }, diff --git a/src/utils/xcodebuild-line-parsers.ts b/src/utils/xcodebuild-line-parsers.ts index dbffe476b..f967010cb 100644 --- a/src/utils/xcodebuild-line-parsers.ts +++ b/src/utils/xcodebuild-line-parsers.ts @@ -49,10 +49,12 @@ export interface ParsedBuildError { renderedLine: string; } -const BUILD_ERROR_DIAGNOSTIC_PATTERN = /(?:^|\s)(?:fatal error|error):\s+\S/iu; +export interface ParsedBuildDiagnostic extends ParsedBuildError { + severity: 'error' | 'warning' | 'note'; +} export function isBuildErrorDiagnosticLine(line: string): boolean { - return BUILD_ERROR_DIAGNOSTIC_PATTERN.test(line); + return parseBuildDiagnostic(line.trim().split(/\r?\n/u, 1)[0])?.severity === 'error'; } function normalizeSuiteName(rawSuiteName: string): string { @@ -149,39 +151,66 @@ export function parseDurationMs(durationText?: string): number | undefined { return Math.round(seconds * 1000); } -export function parseBuildErrorDiagnostic(line: string): ParsedBuildError | null { - // File path with line number: /path/to/File.swift:42:10: error: message - const locationMatch = line.match(/^(.*?):(\d+)(?::\d+)?: (?:fatal error|error): (.+)$/u); +export function parseBuildDiagnostic(line: string): ParsedBuildDiagnostic | null { + // numbered source and caret gutters are diagnostic context, not headers. + if (/^\s*(?:\d+\s*)?\|/u.test(line)) { + return null; + } + + // located compiler diagnostics carry the severity before the message. + const locationMatch = line.match( + /^(.*?):(\d+)(?::\d+)?:\s+(fatal error|error|warning|note):\s+(.+)$/iu, + ); if (locationMatch) { - const [, filePath, lineNumber, message] = locationMatch; + const [, filePath, lineNumber, severity, message] = locationMatch; return { + severity: normalizeSeverity(severity), location: `${filePath}:${lineNumber}`, message, renderedLine: line, }; } - // Path-based error without line number: /path/to/Project.xcodeproj: error: message - const pathErrorMatch = line.match(/^(\/[^:]+): (?:fatal error|error): (.+)$/u); - if (pathErrorMatch) { - const [, filePath, message] = pathErrorMatch; + // project paths and tool prefixes have no line number. + const prefixedMatch = line.match( + /^(?:(\/[^:]+|[\w.+-]+):\s+)?(fatal error|error|warning|note):\s+(.+)$/iu, + ); + if (prefixedMatch) { + const [, prefix, severity, message] = prefixedMatch; return { - location: filePath, + severity: normalizeSeverity(severity), + ...(prefix?.startsWith('/') ? { location: prefix } : {}), message, renderedLine: line, }; } - // Prefixed error: xcodebuild: error: message / error: message - const rawMatch = line.match(/^(?:[\w-]+:\s+)?(?:fatal error|error): (.+)$/u); - if (rawMatch) { - const [, message] = rawMatch; - return { message, renderedLine: line }; + // preserve timestamped tool diagnostics as raw messages. + const timestampedMatch = line.match( + /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+[^\s[]+\[[^\]]+\]\s+(fatal error|error|warning|note):\s+\S.*$/iu, + ); + if (timestampedMatch) { + return { + severity: normalizeSeverity(timestampedMatch[1]), + message: line, + renderedLine: line, + }; } - if (!isBuildErrorDiagnosticLine(line)) { + return null; +} + +function normalizeSeverity(severity: string): ParsedBuildDiagnostic['severity'] { + const normalized = severity.toLowerCase(); + return normalized === 'warning' || normalized === 'note' ? normalized : 'error'; +} + +export function parseBuildErrorDiagnostic(line: string): ParsedBuildError | null { + const diagnostic = parseBuildDiagnostic(line); + if (diagnostic?.severity !== 'error') { return null; } - return { message: line, renderedLine: line }; + const { severity: _severity, ...error } = diagnostic; + return error; }