Skip to content

Conversation

@bripeticca
Copy link
Contributor

@bripeticca bripeticca commented Nov 11, 2025

This PR refactors diagnostic handling in the Swift build system by introducing a dedicated message handler and per-task output buffering to properly parse and emit compiler diagnostics individually.

Key Changes

SwiftBuildSystemMessageHandler

  • Introduced a new dedicated handler class to process SwiftBuildMessage events from the build operation
  • Moved message handling logic out of inline nested functions for better organization and testability
  • Maintains build state, progress animation, and diagnostic processing in a single cohesive component

Per-Task Data Buffering

  • Added taskDataBuffer struct in BuildState to capture compiler output per task signature
  • New TaskDataBuffer struct allows for using LocationContext or LocationContext2 as a subscript key to fetch the appropriate data buffer for a task, defaulting to the global buffer if no associated task or target can be identified.
  • Task output is accumulated in the buffer as .output messages arrive
  • Buffer contents are processed when tasks complete, ensuring all output is captured before parsing
  • Failed tasks with no useful or apparent message will be demoted to an info log level to avoid creating too much noise on the output.

Per-Task Diagnostic Buffering

  • Added diagnosticBuffer property to the BuildState to track diagnostics to emit once we receive a taskComplete event
  • A check is done to ascertain whether the diagnostic info we receive is a global/target diagnostic, and if so we emit the diagnostic immediately; all other diagnostics are accumulated in the buffer to be emitted once the associated task is completed.

EmittedTasks

  • Helper struct for the message handler to track which task's messages have already been emitted
  • Handles both taskIDs as well as taskSignatures

Test Suite

SwiftBuildSystemMessageHandlerTests

  • New test suite created to assert that the diagnostic output is formatted and emitted as expected.
  • Uses the initializers for the nested SwiftBuildMessage info structs that are exposed for testing purposes only

* Built tentative test class SwiftBuildSystemOutputParser to
  handle the compiler output specifically
* Added a handleDiagnostic method to possibly substitute the
  emitEvent local scope implementation of handling a
  SwiftBuildMessage diagnostic
* the flag `appendToOutputStream` helps us to determine whether
  a diagnostic is to be emitted or whether we'll be emitting
  the compiler output via OutputInfo
* separate the emitEvent method into the SwiftBuildSystemMessageHandler
@bripeticca bripeticca force-pushed the swb/diagnosticcodesnippet branch from a20f020 to f3aaabf Compare November 20, 2025 18:56
@bripeticca bripeticca force-pushed the swb/diagnosticcodesnippet branch from 1dcaaeb to c48e606 Compare November 20, 2025 18:58
@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca bripeticca changed the title [WIP] Capture code snippet from diagnostic compiler output Capture code snippet from diagnostic compiler output Nov 20, 2025
@bripeticca bripeticca marked this pull request as ready for review November 20, 2025 19:21
Copy link
Contributor

@owenv owenv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thie generally lgtm but I have some concerns about the regex-based parsing when we emit the textual compiler output.

  1. Perf - It's important this is fast so that it doesn't block the end of the build if a command produces huge quantities of output. It's hard to say if this will be a real issue without some testing
  2. We're re-parsing information which we're already getting from the compiler in structured form. I see the appeal of not reporting a diagnostic twice if multiple compile jobs report it though

@jakepetroules
Copy link
Contributor

2. We're re-parsing information which we're already getting from the compiler in structured form. I see the appeal of not reporting a diagnostic twice if multiple compile jobs report it though

I also pointed this out inline, though I'm not sure why the re-parsing relates to deduplication? We should be able to deduplicate diagnostics whether we parse them again or use the existing ones.

@bripeticca
Copy link
Contributor Author

bripeticca commented Nov 21, 2025

though I'm not sure why the re-parsing relates to deduplication

(@jakepetroules @owenv -- tagging for visibility, GitHub notifications can be weird)

Re-parsing doesn't affect the de-duplication -- when tracking the data buffer per task, I also track whether we've emitted the associated output for a given task (using its signature) and guard against this before emitting for that task again. We only go down this path (emitting for a given task) once we've received the task completed event.

I mentioned this inline as well but for visibility: the re-parsing just addresses the fact that for a given task signature, we have an accumulated data buffer that contains all possible diagnostic messages coming from the compiler. I find that the user ergonomics aren't great when simply emitting the entire string blob through the observability scope, since these diagnostics can have varying severities and we'll have to decide up-front which severity to choose to emit the entire string of all diagnostics.

I do also maintain a list of the DiagnosticInfo that we omit in favour of emitting the OutputInfo containing the same diagnostic messages, but with the plus of having the pre-formatted code snippet (the DiagnosticInfo is missing the pre-formatted code snippet but contains enough information to recreate it ourselves, and it was suggested that we instead fall back to using the OutputInfo for that reason).

Perhaps some more discussion is needed here. 😄

* Remove taskStarted outputStream emissions
* Propagate exception if unable to find path
@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Contributor Author

@swift-ci please test windows

@bripeticca
Copy link
Contributor Author

Errors seem unrelated to this change, re-triggering tests

@swift-ci please test linux

@bripeticca
Copy link
Contributor Author

@swift-ci please test macOS

@bripeticca
Copy link
Contributor Author

@swift-ci please test linux

@bripeticca
Copy link
Contributor Author

@PhantomInTheWire It looks like this change will definitely affect possible solutions for #8877 , but I am skeptical that it will resolve it entirely. If you're interested in taking a look at it still, I would highly recommend doing so once this PR is merged 😄

@PhantomInTheWire
Copy link

sure i'll take a look once this is merged, @bripeticca

…t stream

Some minor fixes to how task output is handled in the SwiftBuildSystemMessageHandler.
@jakepetroules
Copy link
Contributor

@swift-ci test

@bripeticca
Copy link
Contributor Author

@swift-ci please test

@bripeticca
Copy link
Contributor Author

@swift-ci test windows

@bripeticca
Copy link
Contributor Author

@swift-ci test

@bripeticca
Copy link
Contributor Author

@swift-ci test windows

@bripeticca bripeticca requested a review from bkhouri December 11, 2025 14:40
}

@Test
func testManyDiagnosticsReported() throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: When it comes to "arrays", I tend to want to validate the empty, 1, 2 and many items. It's great to see the 0, 1 and many diagnostics tests added. Could I ask to cover case where 2 diagnostics are emitted?


try expectDiagnostics(observability.diagnostics) { result in
result.check(diagnostic: "Simple diagnostic", severity: .error)
result.check(diagnostic: "Another diagnostic", severity: .debug)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: maybe I'm missing something, but is this diagnosing a .note kind? if so, why do we expect it to be debug?

)

let events: [SwiftBuildMessage] = [
.taskStartedInfo(taskID: 1, taskSignature: "simple-diagnostic"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do events need to start and end with the .taskStartedInfo(...) and .taskCompleteInfo(...) for these tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- the way the message handler works is that it will track when a task is completed to emit the diagnostics/output associated with that task. A task started event is needed before we handle a task completed event, otherwise the handler will throw an error since shouldn't be possible.

let simpleDiagnosticString: String = "[error]: Simple diagnostic\n"
let simpleOutputInfo: SwiftBuildMessage = .outputInfo(
data: data(simpleDiagnosticString),
locationContext: .task(taskID: 1, targetID: 1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do the values of taskID and targetID need to match between all test SwiftBuildMessage that are in the events array? If so, consider storing the values in a let and using said variable name in various places to make the test more readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would need to match if I'm trying to mock out a stream of events for which I want to track its progress per ID. I'll refactor this in a follow-up PR!

}

@Test
func testDiagnosticOutputWhenOnlyWarnings() throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: should we add additional test that verify other diagnostics? e.g.: only .note, .debug and all other possible diagnostic kind?

}

/// SwiftBuildMessage.DiagnosticInfo
package static func diagnosticInfo(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: rename this to diagnostic or diagnosticData as Info may give a false sense that were are creating an info level diagnostics message/event

"log didn't contain expected linker diagnostics. stderr: '\(stderr)",
)
#expect(
!stdout.contains(searchPathRegex),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (possibly-blocking): this should match the string from stderr expectation?

#expect(!stderr.contains(RelativePath("Sources/MyLibrary/library.foo").pathString))
let libraryFooPath = RelativePath("Sources/MyLibrary/library.foo").pathString
#expect(!stderr.components(separatedBy: "\n").contains { $0.contains("warning: ") && $0.contains(libraryFooPath) })
if data.buildSystem == .native {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: It's not related to your change, but since you are modifying this file, can we convert the if data.buildSystem == .native to a switch call, by explicitly adding a case call for each value (ie: do not use default)? this will ensure we handle "new build systems" should we ever add one in the future.

case .swiftbuild:
#expect(stdout.contains("MySourceGenBuildTool-product"), "stdout:\n\(stdout)\nstderr:\n\(stderr)")
#expect(stderr.contains("Creating foo.swift from foo.dat"), "stdout:\n\(stdout)\nstderr:\n\(stderr)")
#expect(stdout.contains("Creating foo.swift from foo.dat"), "stdout:\n\(stdout)\nstderr:\n\(stderr)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: this is great to see!

@bkhouri bkhouri dismissed their stale review December 11, 2025 16:23

The reason for blocking this (missing tests) are included in the PR.

@bripeticca bripeticca merged commit 10af69e into swiftlang:main Dec 11, 2025
43 of 45 checks passed
bripeticca added a commit to bripeticca/swift-package-manager that referenced this pull request Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants