Skip to content

Commit b0004d3

Browse files
authored
Merge pull request #14 from elaz-applift/fix/json-filtering
Fix: Filter out JSON-like lines to prevent false error detection
2 parents 765b54c + 5666be1 commit b0004d3

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

Sources/OutputParser.swift

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,88 @@ class OutputParser {
294294
return seenTestNames.contains(normalizedTestName)
295295
}
296296

297+
/// Checks if a line looks like JSON output (e.g., from the tool's own output or other JSON sources)
298+
/// This prevents false positives when parsing build output that contains JSON
299+
private func isJSONLikeLine(_ line: String) -> Bool {
300+
let trimmed = line.trimmingCharacters(in: .whitespaces)
301+
302+
// Check for JSON-like patterns:
303+
// 1. Lines that start with quotes and contain colon (JSON key-value pairs)
304+
// 2. Lines containing JSON structure like "key" : "value"
305+
// 3. Lines with escaped quotes and backslashes typical of JSON
306+
// 4. Lines that are indented and contain JSON-like structures (common in formatted JSON)
307+
308+
// Pattern: "key" : "value" or "key" : value
309+
let jsonKeyValuePattern = Regex {
310+
Optionally(OneOrMore(.whitespace))
311+
"\""
312+
OneOrMore(.any, .reluctant)
313+
"\""
314+
Optionally(OneOrMore(.whitespace))
315+
":"
316+
Optionally(OneOrMore(.whitespace))
317+
}
318+
319+
if trimmed.firstMatch(of: jsonKeyValuePattern) != nil {
320+
return true
321+
}
322+
323+
// Check for JSON array/object markers at start
324+
if trimmed.hasPrefix("{") || trimmed.hasPrefix("[") || trimmed.hasPrefix("}") || trimmed.hasPrefix("]") {
325+
return true
326+
}
327+
328+
// Check for lines with multiple escaped characters (common in JSON)
329+
// Pattern like "\\(message)\"" suggests JSON escaping
330+
if line.contains("\\\"") && line.contains("\"") && line.contains(":") {
331+
return true
332+
}
333+
334+
// Check for indented lines that look like JSON (common in formatted JSON output)
335+
// Lines starting with spaces/tabs followed by quotes are likely JSON
336+
if line.hasPrefix(" ") || line.hasPrefix("\t") {
337+
// If it's indented and contains quoted strings with colons, it's likely JSON
338+
if trimmed.firstMatch(of: jsonKeyValuePattern) != nil {
339+
return true
340+
}
341+
// Check for JSON array/object markers in indented lines
342+
if trimmed.hasPrefix("{") || trimmed.hasPrefix("}") || trimmed.hasPrefix("[") || trimmed.hasPrefix("]") {
343+
return true
344+
}
345+
}
346+
347+
// Check for lines that contain "error:" but are clearly JSON (e.g., error messages in JSON)
348+
// Pattern: lines with quotes, colons, and escaped characters that contain "error:"
349+
if line.contains("error:") {
350+
let trimmed = line.trimmingCharacters(in: .whitespaces)
351+
352+
// If line starts with "error:" (even if indented), it's likely a real error, not JSON
353+
// UNLESS it's clearly JSON structure like "error" : "value"
354+
if trimmed.hasPrefix("\"") && trimmed.contains("\"") && trimmed.contains(":") {
355+
// This looks like JSON: "error" : "value" or "errors" : [...]
356+
return true
357+
}
358+
359+
// If it's indented and has JSON-like structure (quoted keys), it's probably JSON
360+
if (line.hasPrefix(" ") || line.hasPrefix("\t")) && trimmed.hasPrefix("\"") {
361+
return true
362+
}
363+
364+
// If it has escaped quotes and looks like JSON structure, but NOT if it starts with "error:"
365+
// (lines starting with "error:" are likely real errors, not JSON)
366+
if !trimmed.hasPrefix("error:") {
367+
let hasQuotedStrings = line.contains("\"") && line.contains(":")
368+
let hasEscapedContent = line.contains("\\") && line.contains("\"")
369+
// If it has escaped quotes and looks like JSON structure (but not a file path)
370+
if hasEscapedContent && hasQuotedStrings && !line.contains("file:") && !line.contains(".swift:") && !line.contains(".m:") && !line.contains(".h:") {
371+
return true
372+
}
373+
}
374+
}
375+
376+
return false
377+
}
378+
297379
private func recordPassedTest(named testName: String) {
298380
let normalizedTestName = normalizeTestName(testName)
299381
guard seenPassedTestNames.insert(normalizedTestName).inserted else {
@@ -303,6 +385,11 @@ class OutputParser {
303385
}
304386

305387
private func parseError(_ line: String) -> BuildError? {
388+
// Skip JSON-like lines (e.g., " \"message\" : \"\\\\(message)\\\"\"")
389+
if isJSONLikeLine(line) {
390+
return nil
391+
}
392+
306393
// Skip visual error lines (e.g., " | `- error: message")
307394
if line.hasPrefix(" ") && (line.contains("|") || line.contains("`")) {
308395
return nil
@@ -417,6 +504,11 @@ class OutputParser {
417504
}
418505

419506
private func parseWarning(_ line: String) -> BuildWarning? {
507+
// Skip JSON-like lines (e.g., " \"message\" : \"\\\\(message)\\\"\"")
508+
if isJSONLikeLine(line) {
509+
return nil
510+
}
511+
420512
// Skip visual warning lines (e.g., " | `- warning: message")
421513
if line.hasPrefix(" ") && (line.contains("|") || line.contains("`")) {
422514
return nil

Tests/OutputParserTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,66 @@ final class OutputParserTests: XCTestCase {
344344
XCTAssertEqual(result.summary.passedTests, 23)
345345
XCTAssertEqual(result.summary.buildTime, "0.031")
346346
}
347+
348+
func testJSONLikeLinesAreFiltered() {
349+
let parser = OutputParser()
350+
// This simulates the actual problematic case: Swift compiler warning/note lines
351+
// with string interpolation patterns that were incorrectly parsed as errors
352+
let input = """
353+
/Path/To/File.swift:79:41: warning: string interpolation produces a debug description for an optional value; did you mean to make this explicit?
354+
355+
return "Encryption error: \\(message)"
356+
357+
^~~~~~~
358+
359+
/Path/To/File.swift:79:41: note: use 'String(describing:)' to silence this warning
360+
361+
return "Encryption error: \\(message)"
362+
363+
^~~~~~~
364+
365+
String(describing: )
366+
367+
/Path/To/File.swift:79:41: note: provide a default value to avoid this warning
368+
369+
return "Encryption error: \\(message)"
370+
371+
^~~~~~~
372+
373+
?? <#default value#>
374+
"""
375+
376+
let result = parser.parse(input: input)
377+
378+
// Should parse the warning correctly, but NOT parse the note lines as errors
379+
// The note lines contain \\(message) pattern which shouldn't be treated as error messages
380+
XCTAssertEqual(result.status, "success") // No actual errors, just warnings
381+
XCTAssertEqual(result.summary.errors, 0)
382+
XCTAssertEqual(result.summary.warnings, 1) // Should parse the warning
383+
XCTAssertEqual(result.errors.count, 0)
384+
}
385+
386+
func testJSONLikeLinesWithActualErrors() {
387+
let parser = OutputParser()
388+
// Mix of compiler note lines (with interpolation patterns) and actual errors
389+
// Should only parse the real errors, not the note lines
390+
let input = """
391+
/Path/To/File.swift:79:41: note: use 'String(describing:)' to silence this warning
392+
return "Encryption error: \\(message)"
393+
^~~~~~~
394+
main.swift:15:5: error: use of undeclared identifier 'unknown'
395+
"""
396+
397+
let result = parser.parse(input: input)
398+
399+
// Should parse the real error but ignore note lines with interpolation patterns
400+
XCTAssertEqual(result.status, "failed")
401+
XCTAssertEqual(result.summary.errors, 1)
402+
XCTAssertEqual(result.errors.count, 1)
403+
XCTAssertEqual(result.errors[0].file, "main.swift")
404+
XCTAssertEqual(result.errors[0].line, 15)
405+
XCTAssertEqual(result.errors[0].message, "use of undeclared identifier 'unknown'")
406+
}
347407

348408
func testCodeCoverageDataStructures() {
349409
// Test that coverage data structures can be created and encoded

0 commit comments

Comments
 (0)