Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/utils/__tests__/xcodebuild-domain-results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,23 @@ 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)),
succeeded: false,
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({
Expand Down
45 changes: 45 additions & 0 deletions src/utils/__tests__/xcodebuild-event-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
26 changes: 26 additions & 0 deletions src/utils/__tests__/xcodebuild-line-parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 =
Expand Down
91 changes: 26 additions & 65 deletions src/utils/xcodebuild-event-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import {
parseTestCaseLine,
parseTotalsLine,
parseFailureDiagnostic,
parseBuildErrorDiagnostic,
parseBuildDiagnostic,
parseDurationMs,
isBuildErrorDiagnosticLine,
type ParsedTestCase,
} from './xcodebuild-line-parsers.ts';
import {
Expand Down Expand Up @@ -52,23 +51,6 @@ const stageMessages: Record<XcodebuildStage, string> = {
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,
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -470,7 +431,7 @@ export function createXcodebuildEventParser(options: EventParserOptions): Xcodeb
processLine(stderrBuffer);
}
flushQueuedFailureDiagnostics();
flushPendingError();
flushPendingDiagnostic();
stdoutBuffer = '';
stderrBuffer = '';
},
Expand Down
65 changes: 47 additions & 18 deletions src/utils/xcodebuild-line-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}