Decouple Buildalyzer from in-process MSBuild (supersedes #356) by slang25 · Pull Request #359 · Buildalyzer/Buildalyzer · GitHub
Skip to content

Decouple Buildalyzer from in-process MSBuild (supersedes #356) - #359

Open
slang25 wants to merge 102 commits into
Buildalyzer:mainfrom
slang25:slang25/update-buildalyzer-pr-356
Open

slang25 wants to merge 102 commits into
Buildalyzer:mainfrom
slang25:slang25/update-buildalyzer-pr-356

Conversation

@slang25

@slang25 slang25 commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Supersedes #356. That PR swapped MsBuildPipeLogger for XenoAtom.MsBuildPipeLogger; this branch takes it much further — Buildalyzer no longer depends on MSBuild (or Roslyn) in-process at all.

Builds and binary-log replay now run entirely out-of-process (dotnet msbuild / MSBuild.exe), and events stream back over the pipe as XenoAtom's version-independent PipeBuildEventArgs. As a result the core library has zero Microsoft.Build.* runtime dependency — no engine, no MSBuildLocator, no Microsoft.Build.Framework — and no Microsoft.CodeAnalysis.* dependency either. This removes the long-standing binary-log version-coupling (the reader had to be at least as new as the SDK's MSBuild that wrote the log) and makes the library portable.

This is a deliberately breaking release. The intended next major is v10; everything below is the breaking-changes surface for it.

Key changes

  • Out-of-process everything. Live builds and binlog replay both run through dotnet msbuild with the pipe logger attached, handled by the same EventProcessor. No in-process MSBuild → no reader/writer version mismatch, no MSBuildLocator. AnalyzerManager.Analyze(binLogPath) now replays the log by handing it to dotnet msbuild out-of-process, so the MSBuild that reads the log is always the SDK's own. MSBuild.StructuredLogger is dropped. A host that has itself registered MSBuildLocator no longer redirects Buildalyzer's builds to its in-process MSBuild: the inherited MSBUILD_EXE_PATH/MSBuildExtensionsPath/MSBuildSDKsPath are cleared from the child environment unless explicitly set.
  • Compiler inputs from task parameters, not the command line. The compiler task's resolved Sources / References / Analyzers / AdditionalFiles / AnalyzerConfigFiles / EmbeddedFiles arrive as structured task-parameter events (opted in via MSBUILDLOGTASKINPUTS=1, with MSBUILDDISABLENODEREUSE=1 since the trait is read at node startup). Roslyn command-line parsing is retired from core; a small vendored tokenizer remains only to recover preprocessor symbols and the raw command line.
  • Solutions via Microsoft.VisualStudio.SolutionPersistence (both .sln and .slnx), replacing Microsoft.Build.Construction.SolutionFile.
  • TargetsToBuild default changed from ["Clean","Build"] to ["Compile"], mirroring Visual Studio's design-time builds and Roslyn's MSBuildWorkspace (Lighter default TargetsToBuild for design-time / semantic-model use (30–50% cold-load speedup) #344, Default TargetsToBuild to ["Compile"] (fixes #344) #348).
  • Multi-targeted projects build one inner build per target framework, the way VS schedules design-time builds, and are modelled as one Roslyn project per framework in Workspaces, matching MSBuildWorkspace. TFM discovery is MSBuild-evaluated (folded into restore) rather than scraped from project XML. Frameworks, a project's reference closure, and a solution's projects all build in parallel.
  • New Buildalyzer.CompilerLogs package (net9.0;net10.0) that writes compiler log (.complog) files from build results via Basic.CompilerLog.Util, so complog print/rsp/export/replay work on Buildalyzer output.
  • net8.0 minimum for the core library (dropped net6.0); Workspaces targets net8.0;net10.0.

Core runtime dependencies: before → after

  • Before: Microsoft.Build (+ Tasks.Core), Microsoft.CodeAnalysis.CSharp / VisualBasic, Microsoft.VisualStudio.SolutionPersistence, MSBuild.StructuredLogger, MsBuildPipeLogger.Server, NuGet.Frameworks, Microsoft.Extensions.Logging.
  • After: Microsoft.Extensions.Logging.Abstractions, Microsoft.VisualStudio.SolutionPersistence, XenoAtom.MsBuildPipeLogger.

Microsoft.Build.* is no longer even an IL-level assembly reference of Buildalyzer.dll, so new AnalyzerManager() works in a standalone consumer process that has no MSBuild assemblies on disk (previously it threw FileNotFoundException: Could not load 'Microsoft.Build, Version=15.1.0.0').

Breaking changes (for v10)

Dependency & platform

  • Microsoft.Build.* is no longer a (transitive) dependency, and no MSBuild type appears on Buildalyzer's public API. Consumers relying on getting MSBuild transitively must add it themselves.
  • Microsoft.CodeAnalysis.* (Roslyn) is no longer a core dependency, and Roslyn types are removed from core's public surface.
  • MsBuildPipeLogger → XenoAtom.MsBuildPipeLogger. The shipped logger DLL renames from MsBuildPipeLogger.Logger.dll to XenoAtom.MsBuildPipeLogger.Logger.dll (matters if you customize LoggerPathDll).
  • Core TFM floor raised net6.0;net8.0 → net8.0; Workspaces net6.0;net8.0;net10.0 → net8.0;net10.0.
  • Workspaces Roslyn bumped Microsoft.CodeAnalysis.*.Workspaces 4.10.0 → 5.6.0.

Removed / changed members taking or returning MSBuild types

  • Removed IProjectAnalyzer.BuildLoggers, AddBuildLogger(ILogger), RemoveBuildLogger(ILogger) (in-process loggers can't be fed by an out-of-process build).
  • Removed IProjectAnalyzer.ProjectInSolution.
  • IProjectAnalyzer.AddBinaryLogger(...): parameter type Microsoft.Build.Logging.BinaryLogger.ProjectImportsCollectionMode → the new Buildalyzer-owned enum BinaryLogImports (None/Embed/ZipFile); MSBuild writes the log natively via /bl.
  • Removed IAnalyzerResults.BuildEventArguments entirely (it was ImmutableArray<Microsoft.Build.Framework.BuildEventArgs>). The raw events are now XenoAtom pipe types and are an internal implementation detail — they are not re-exposed as PipeBuildEventArgs.
  • IAnalyzerManager.Analyze(string, IEnumerable<Microsoft.Build.Framework.ILogger>) → Analyze(string binLogPath) (the buildLoggers argument is removed).
  • AnalyzerManagerOptions.ProjectFilter: Func<ProjectInSolution, bool> → Func<ProjectInfo, bool>.
  • CompilerItems / CompilerItemsCollection expose Buildalyzer's own IProjectItem, not Microsoft.Build.Framework.ITaskItem.

CompilerCommand reshaped (public record)

  • Removed Errors (ImmutableArray<Microsoft.CodeAnalysis.Diagnostic>).
  • Path collections changed from ImmutableArray<IOPath> to ImmutableArray<string>: SourceFiles, AdditionalFiles, EmbeddedFiles, AnalyzerReferences, AnalyzerConfigPaths.
  • Removed the abstract base RoslynBasedCompilerCommand<TArguments>; CSharpCompilerCommand / VisualBasicCompilerCommand now derive directly from CompilerCommand, so CommandLineArguments (Roslyn's *CommandLineArguments) and VisualBasicCompilerCommand.PreprocessorSymbols are gone.
  • AnalyzerResult.CompilerCommand is now nullable (CompilerCommand?), built lazily.
  • CompilerCommand.SourceFiles is documented as compile order rather than evaluation order (semantic for F#, where Microsoft.FSharp.Targets re-sorts @(Compile) by CompileOrder).

Obsolete-API cleanup (previously [Obsolete], now removed)

  • AnalyzerManager(IOPath) constructor; AnalyzerManager.GetProject(IOPath) and IAnalyzerManager.GetProject(IOPath).
  • IAnalyzerManager.SolutionFile / SolutionFilePath.
  • SolutionInfo.Path and ProjectInfo.Path changed from IOPath to string (the IOPath moved to an internal Location).
  • SolutionInfo.Load signature: Load(string path, Predicate<ProjectInfo>? filter) (was Load(IOPath, Predicate<ProjectInSolution>?)); it now also filters out projects whose file doesn't exist.

Behavioural changes (same API, different results)

  • Analyze(binLogPath) only yields compiler inputs for binary logs that recorded task inputs (produced by a reasonably modern MSBuild), since inputs now come from task parameters rather than the command line. Old/minimal binlogs come back empty.
  • TargetsToBuild default ["Compile"]. This skips BeforeBuild/AfterBuild hooks and the post-CoreCompile closure — a significant speedup. Compile + the design-time properties still runs WPF's MarkupCompilePass1 (so XAML *.g.cs are still surfaced), but files produced only by the full Build closure (e.g. GeneratedInternalTypeHelper.g.cs from MarkupCompilePass2, or BeforeBuild-generated sources) are no longer produced — set TargetsToBuild = ["Build"] if you need them. When setting DesignTime = false, also set TargetsToBuild = ["Build"].
  • Multi-targeted builds: Build() without a TFM schedules one inner build per target framework instead of the outer DispatchToInnerBuilds build, so the extra result with an empty target framework is gone — results[string.Empty] no longer exists (Default TargetsToBuild to ["Compile"] (fixes #344) #348). With a BinaryLogger attached, each per-TFM build writes a TFM-suffixed binlog (project.net8.0.binlog) so they don't overwrite one another.
  • Restore for builds that pin a target framework now runs once, up front, as a separate Restore invocation without TargetFramework (a .restore-suffixed binlog when logging), instead of -restore per pinned build — restore is a per-project operation and pinned restores produced single-framework assets files (Restore runs with TargetFramework set, executing inner-build instead of outer-build restore #346). Unpinned builds still use -restore.
  • Multi-targeted workspaces: AddToWorkspace / GetWorkspace add one Roslyn project per TFM (named Name(tfm), matching MSBuildWorkspace — including when a single flavour of a project that declares TargetFrameworks is added on its own), wiring project references by resolved output-assembly path to the exact flavour MSBuild resolved; AddToWorkspace returns the first TFM's project. TFM discovery is MSBuild-evaluated, honouring OS-conditioned TFMs (this also fixes a phantom $(TargetFrameworks) TFM that previously poisoned the workspace with spurious CS0518/CS0246 errors). Workspaces identifies a Roslyn project by project file plus discriminated name rather than by output path, so projects sharing a TargetPath (or AppendTargetFrameworkToOutputPath=false) are no longer collapsed into one, and matches paths the way the file system does (ordinal on case-sensitive file systems).
  • Workspaces parses documentation comments (DocumentationMode.Parse) for projects without GenerateDocumentationFile, as MSBuildWorkspace does, instead of the compiler's None.
  • Missing-on-disk inputs are surfaced, not dropped (GetDocuments / GetAnalyzerReferences silently drop missing-on-disk paths via File.Exists #345): a source path that doesn't exist becomes a document that loads as empty with Roslyn's standard document-load failure, and a missing analyzer path becomes an UnresolvedAnalyzerReference — exactly as MSBuildWorkspace behaves.
  • F# projects get a real design-time build. SkipCompilerExecution was previously stripped for .fsproj, so analysing F# actually ran fsc and failed whenever the compiler would. Results are unchanged (same sources, references, defines) but fsc no longer runs; consumers that relied on the output assembly being on disk should set TargetsToBuild = ["Build"] and DesignTime = false.

Additive (non-breaking) improvements

  • New IAnalyzerResult members: ReferencesEmbeddingInteropTypes, AnalyzerConfigFiles.
  • New CompilerCommand.EmbedInteropTypes.
  • Workspace reconstruction fallback (Buildalyzer.Workspaces returns an empty workspace when MSBuild fails before Csc runs #341): when MSBuild aborts before CoreCompile invokes the compiler, the project is rebuilt from evaluated Compile items, ResolveAssemblyReference's ReferencePath, Analyzer/AdditionalFiles items, and DefineConstants, instead of returning an empty project. Only activates when the compiler-backed data is empty, so successful builds are unchanged. Applies to every entry point, including IAnalyzerManager.GetWorkspace().
  • Compilation/parse options are derived by parsing the compiler command line with Roslyn's own parser inside Workspaces (the same approach as MSBuildWorkspace), so defines, language version, nullable/unsafe/checked, optimization, platform, warning level, signing and the full diagnostic config (including SDK-injected warnaserror/nowarn defaults) match the compiler; MSBuild-property reconstruction remains as a fallback. Workspace projects also carry MSBuildWorkspace's compilation services, document folders, reference-assembly output path, source encoding and checksum algorithm.
  • New Buildalyzer.Differential.Tests suite that diffs Buildalyzer's workspace against Roslyn's MSBuildWorkspace as the reference: every project is reduced to a full structural shape (identity, output paths, documents with folders, references with aliases and interop flags, analyzers, every compilation and parse option) and compared by full path in one assertion, plus end-to-end diagnostic parity tests. An explicit RealWorld category runs the same comparison against pinned OSS repositories.

Testing

Full suite green across Buildalyzer.Tests, Buildalyzer.Workspaces.Tests, Buildalyzer.CompilerLogs.Tests, and the new Buildalyzer.Differential.Tests (net10 host). Validated end-to-end against real OSS repos (MediatR, Serilog, FluentValidation, Polly).

Before marking ready

  • Depends on XenoAtom.MsBuildPipeLogger 2.0.0 (published on nuget.org) — the branch restores from public feeds only.
  • Reset PackageValidationBaselineVersion (8.0.0) at release — it will otherwise flag the net6.0 TFM removal and the breaking API changes on pack. RELEASE.md is now under a # 10.0.0-preview.1 heading.
  • The EnvironmentFactory change that enables task-input logging on the .NET Framework build path is Windows-only and not exercised by the cross-platform test suite.

🤖 Generated with Claude Code

slang25 and others added 10 commits June 11, 2026 00:48
Swap the unmaintained MsBuildPipeLogger.Server/.Logger packages for the
single XenoAtom.MsBuildPipeLogger fork (1.0.1).

- Buildalyzer references the receiver API; logger DLL is bundled via the
  Buildalyzer.Logger package (excluded from the receiver ref).
- Buildalyzer.Logger compiles BuildalyzerLogger against the bundled logger
  assembly (shipped as copy-to-output content) and packs it alongside.
- Bump Microsoft.Build.Utilities.Core 14.3.0 -> 17.10.46 to match the
  logger's MSBuild version and the patched advisory (GHSA-w3q9-fxm7-j8fq).
- Adapt to the fork's nullable annotations; drop now-unneeded logger
  package refs from the test projects (DLL flows transitively).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion mismatch

XenoAtom.MsBuildPipeLogger's receiver deserializes events with the real
Microsoft.Build BuildEventArgsReader, which must be at least as new as the
MSBuild that wrote them. Buildalyzer pinned Microsoft.Build 17.10.46 at runtime
while the SDK's MSBuild (18.6 on .NET 10) writes newer binlog records, so the
reader threw "unsupported type: 33" on every streamed build (19 failing tests).
Microsoft.Build 18.x only targets net10.0, so it can't simply be referenced from
Buildalyzer's net6.0/net8.0 targets.

Instead, follow XenoAtom's documented guidance: reference the Microsoft.Build.*
assemblies for compile only (ExcludeAssets=runtime, PrivateAssets=all) and load
the matching assemblies from the installed .NET SDK at runtime via
Microsoft.Build.Locator. The reader now always matches the SDK's writer,
verified against both .NET 10 (MSBuild 18.6) and .NET 11 preview (MSBuild 18.8).

- Add MSBuildLocatorBootstrap: a module initializer that registers
  MSBuildLocator before any Microsoft.Build type loads. It restores the
  MSBUILD_EXE_PATH/MSBuildExtensionsPath/MSBuildSDKsPath environment variables
  the locator sets, so they don't override BuildEnvironment's out-of-process
  build paths (which would redirect e.g. .NET Framework builds at the SDK).
- Centralize the compile-only MSBuild references in msbuild-references.props,
  imported by Buildalyzer and the test projects that use its MSBuild-typed API.
- Pin the last net6.0-capable Microsoft.Build.Locator (1.7.8) for net6.0; use
  1.11.2 for net8.0.
- Document the consumer-facing impact in RELEASE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…turedLogger

With MSBuildLocator loading the SDK's MSBuild (18.6) in-process, AddBinaryLogger's
in-process BinaryLogger now writes binlog format v25 instead of the v18 that the
old pinned Microsoft.Build 17.10 produced. MSBuild.StructuredLogger 2.2.158 cannot
read v25 (it throws in ReadPropertyReassignmentEventArgs), so AnalyzerManager.Analyze
silently returned zero results — 19 GetsSourceFilesFromBinaryLog cases failed on CI
(SDK 10.0.302 / MSBuild 18.6.11), while passing locally on an older 18.6.4 patch.

Read the binary log with MSBuild's own BinaryLogReplayEventSource (loaded from the
SDK via MSBuildLocator) instead of MSBuild.StructuredLogger's BinLogReader. The reader
now always matches the SDK's MSBuild that wrote the log, exactly as the live pipe path
already does, removing the separately-versioned-reader mismatch entirely. This makes
MSBuild.StructuredLogger unused, so it is dropped.

Verified: full suite (231 tests) passes in a clean linux .NET 10.0.302 container
(MSBuild 18.6.11), reproducing the CI environment that exposed the failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sing

Buildalyzer now runs all builds and binlog replay out-of-process and reads
their events over the pipe as XenoAtom.MsBuildPipeLogger's version-independent
local event types (PipeBuildEventArgs). This removes the version-coupling that
required a matched in-process MSBuild, so MSBuildLocator is gone.

Key changes:
- Compiler inputs (Sources/References/Analyzers/AdditionalFiles/etc.) now come
  from the compiler task's resolved parameters over the pipe (TaskParameter
  events, opted in via IEventSource4.IncludeTaskInputs + MSBUILDLOGTASKINPUTS),
  not from parsing the csc/vbc command line. Preprocessor symbols still come
  from the command line (scalar params aren't forwarded at normal verbosity).
- Dropped Microsoft.CodeAnalysis.CSharp/VisualBasic, NuGet.Frameworks, and
  Microsoft.Build.Tasks.Core. Roslyn command-line parsing replaced by a vendored
  tokenizer + CompilerCommandBuilder; NuGetFramework TFM check replaced by a
  string heuristic (EnvironmentFactory.IsFrameworkTargetFramework).
- Dropped Microsoft.Build.Utilities.Core: it was only pulled in because
  LoggerConfiguration.DefaultLoggerPath referenced typeof(BuildalyzerLogger),
  forcing its base chain (PipeLogger : Microsoft.Build.Utilities.Logger) to
  load. Path is now derived from this assembly's location instead. Framework
  MSBuild discovery drops ToolLocationHelper for the VS-directory poll.
- Binary logs are written natively by MSBuild via /bl and replayed via
  'dotnet msbuild <binlog> /l:BuildalyzerLogger,...' (out-of-process, version
  independent) rather than an in-process reader.
- BuildalyzerLogger runs in selective mode, forwarding only CoreCompile's
  compiler-input item groups and the Csc/Vbc/Fsc command line.

Core package list is now: Microsoft.Build, Microsoft.Build.Framework (via
Microsoft.Build), Microsoft.Extensions.Logging, SolutionPersistence,
XenoAtom.MsBuildPipeLogger, PolySharp.

SPIKE STATE: NuGet.config points at a machine-local feed (/tmp/xenolocal) for
XenoAtom.MsBuildPipeLogger 1.0.3-local. This must be replaced by a real
prerelease before the branch builds anywhere else.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ence

Buildalyzer's core no longer references the ~30 MB Microsoft.Build engine.
It was only used for three things, all now replaced:

- Legacy .sln parsing (SolutionFile / ProjectInSolution / SolutionProjectType)
  -> Microsoft.VisualStudio.SolutionPersistence (already a dependency), which
  parses classic .sln and .slnx alike. LoadSln/LoadSlnx are unified.
- BinaryLogger.ProjectImportsCollectionMode (only ToString()'d into a
  /bl:ProjectImports=<mode> argument) -> a Buildalyzer-owned BinaryLogImports
  enum (None/Embed/ZipFile).
- ITaskItem -> now sourced from the tiny interface-only Microsoft.Build.Framework.

The core runtime dependency set is now Microsoft.Build.Framework,
Microsoft.Extensions.Logging, Microsoft.VisualStudio.SolutionPersistence,
XenoAtom.MsBuildPipeLogger (+ PolySharp at build time).

Also drops net6.0 from Buildalyzer and Buildalyzer.Workspaces (net8.0 minimum),
which removes the System.Collections.Immutable shim (FrozenDictionary is in-box
on net8.0) and the SolutionPersistence net472-fallback (NU1701) that net6.0
would otherwise have relied on for all solution parsing.

Breaking public API changes (deliberate):
- SolutionInfo.Load / AnalyzerManagerOptions.ProjectFilter now filter on
  Buildalyzer.ProjectInfo instead of Microsoft.Build.Construction.ProjectInSolution
  (and the filter now also applies to .slnx, which previously ignored it).
- Removed the already-[Obsolete] IAnalyzerManager.SolutionFile and
  IProjectAnalyzer.ProjectInSolution (the latter was already dead - the cast
  always returned null).
- AddBinaryLogger's collectProjectImports parameter is now BinaryLogImports.
- ProjectInfo.Reference / SolutionInfo.Reference now hold SolutionPersistence
  types (SolutionProjectModel / SolutionModel).

Note: package validation (baseline 8.0.0) will flag the net6.0 TFM removal on
pack; reset the baseline at release since this is an intentional breaking change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Core Buildalyzer now has NO Microsoft.Build.* runtime dependency at all -
verified: nothing Microsoft.Build.* in Buildalyzer.deps.json and no
Microsoft.Build*.dll in the output. (The only transitive Microsoft.Build.Tasks.Git
is SourceLink build-time tooling, not the engine.)

Microsoft.Build.Framework was doing two unrelated jobs, both removed:

1. Dead MSBuild-ILogger surface. Since builds run out-of-process, the
   in-process ILogger objects attached via AddBuildLogger were never fed any
   events - they only flipped the LogEverything flag. Removed AddBuildLogger,
   RemoveBuildLogger, BuildLoggers, and the unused buildLoggers parameter on
   Analyze(binLogPath). LogEverything is now driven solely by whether a binlog
   was requested (_binaryLogPaths.Count > 0).

2. ITaskItem result model -> replaced with Buildalyzer's own IProjectItem
   (ItemSpec + Metadata), which the code already copied into anyway.
   CompilerItems/CompilerItemsCollection now expose IProjectItem; PipeTaskItem
   (an ITaskItem adapter with six write-members that only existed to satisfy the
   interface) becomes PipeProjectItem implementing IProjectItem; ProjectItem's
   ctor takes IProjectItem.

Breaking public API changes (deliberate):
- Removed IProjectAnalyzer.BuildLoggers / AddBuildLogger / RemoveBuildLogger.
- IAnalyzerManager.Analyze(string, IEnumerable<ILogger>) -> Analyze(string).
- CompilerItems / CompilerItemsCollection expose IProjectItem, not
  Microsoft.Build.Framework.ITaskItem.

Full suite green (212 + 17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the updated wire-format work in the local XenoAtom.MsBuildPipeLogger
(nested wire-count validation, TargetBuiltReason mapping, malformed-DateTime
recovery, target/project fidelity fields, reusable read buffer). Full suite
green (212 + 17).

Still the machine-local /tmp/xenolocal spike feed - to be replaced by a real
prerelease before CI.
… feed

XenoAtom.MsBuildPipeLogger 1.1.0 is now published on nuget.org, so both the
core and logger projects reference it directly and the machine-local
/tmp/xenolocal spike feed is removed from NuGet.config. The branch now restores
from public feeds only (verified: resolves 1.1.0 from nuget.org, full suite
212 + 17 green).
CreateFrameworkEnvironment now also sets MSBUILDLOGTASKINPUTS=1 and
MSBUILDDISABLENODEREUSE=1 (previously only the Core path did), so .NET Framework
builds also emit the compiler task's resolved inputs over the pipe.

RELEASE.md is updated to describe the final out-of-process, Microsoft.Build-free
architecture (removed AddBuildLogger/BuildLoggers, BinaryLogImports enum,
BuildEventArguments as PipeBuildEventArgs, StructuredLogger dropped).

Note: the EnvironmentFactory change is on the Windows/.NET Framework-only build
path, which the cross-platform test suite does not exercise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
slang25 and others added 19 commits July 18, 2026 15:40
…kage

Core only used the logging abstractions (ILogger, ILoggerFactory,
ILoggerProvider, NullLogger, LogLevel) - the sole concrete dependency was
`new LoggerFactory()` in AnalyzerManagerOptions.LogWriter. Replaced that with a
tiny in-house SimpleLoggerFactory (ILoggerFactory that fans a category out to its
providers' loggers), so the reference drops to Microsoft.Extensions.Logging.Abstractions.

Verified: full package absent from Buildalyzer.deps.json; suite green (212 + 17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Major-version cleanup ahead of the next release:

- Drop obsolete public APIs: EmptyDisposable, IAnalyzerManager.SolutionFilePath.
- Keep string-based path APIs (AnalyzerManager(string), GetProject(string),
  SolutionInfo.Load(string)) as the primary, non-obsolete public surface.
- Make IOPath internal. The entire public path surface is now string, in both
  directions (SolutionInfo.Path, ProjectInfo.Path, CompilerCommand.* return
  string; inputs accept string). IOPath survives internally as a normalization
  helper with file-system-aware equality.
- Unify path normalization: add IOPath.Root(), delete AnalyzerManager.NormalizePath.
- Fix latent case-sensitivity bugs: EventProcessor primary-project matching and
  the _projects dictionary now compare paths the way the file system does, so
  paths differing only in case match correctly on Windows/macOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump the Roslyn workspace packages (CSharp/VisualBasic.Workspaces) from
4.10.0 to 5.6.0. Migrate the now-obsolete Workspace.WorkspaceChanged and
WorkspaceFailed events to RegisterWorkspaceChangedHandler and
RegisterWorkspaceFailedHandler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Track the primary project's build context ids and reject compiler
task-input events raised by referenced projects that build in the same
MSBuild invocation, so their Sources/References are not merged into the
primary result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TextWriterLoggerProvider now wraps the caller's TextWriter with
TextWriter.Synchronized. Buildalyzer logs from several threads at once
(most notably the process stdout and stderr readers), so a non-thread-safe
writer such as a StringWriter could previously corrupt its buffer or throw
ArgumentOutOfRangeException. Adds a concurrency regression test.

Also surfaces IAnalyzerResult.AnalyzerConfigFiles (the .editorconfig /
.globalconfig paths passed to the compiler via /analyzerconfig:), which the
data was already captured for on CompilerCommand.AnalyzerConfigPaths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings the Roslyn workspace Buildalyzer produces closer to the one
MSBuildWorkspace produces for the same project:

- Include analyzer config documents (.editorconfig/.globalconfig, including
  the SDK-generated GeneratedMSBuildEditorConfig that source generators read).
- Honour AssemblyName and RootNamespace (Project.AssemblyName /
  DefaultNamespace) instead of always using the project file name.
- Derive compilation and parse options by parsing the compiler command line
  with Roslyn's own parser - the same approach MSBuildWorkspace uses - so
  defines, language version, unsafe/checked/nullable, optimization, platform,
  warning level, documentation mode and the full diagnostic configuration
  (including SDK-injected warnaserror/nowarn defaults) all match the compiler.
  Reconstruction from MSBuild properties remains as a fallback for when no
  command line was captured (e.g. the build failed before the compiler ran).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Authors projects with MSBuild.ProjectCreation, loads each one with both
Buildalyzer and Roslyn's MSBuildWorkspace (the reference), and asserts the
two Roslyn projects agree. MSBuildLocator resolves a single SDK MSBuild for
the in-process reference loader while Buildalyzer builds out-of-process; a
pinned global.json keeps both on the same SDK.

Covers class libraries, console apps, VB, multi-targeting (per framework),
compilation/parse options, language version, package references, package
source generators, implicit usings, additional/analyzer-config/linked
documents, project references (incl. transitive), document contents, and
assembly identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The command-line parser leaves the CompilationOptions resolvers/providers
null, whereas MSBuildWorkspace attaches an XML reference resolver, a source
reference resolver, a DesktopStrongNameProvider and a
DesktopAssemblyIdentityComparer to every project. Match those four (the
metadata reference resolver it uses is internal to Roslyn and only affects
#r, so it is left at its default).

The most visible effect: a strong-named assembly can now be emitted from the
workspace - without a StrongNameProvider, Compilation.Emit fails. Adds a
differential test that generates a key, signs, and emits from both workspaces,
plus a test asserting the resolver/provider types match the reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exercises the whole pipeline (compilation/parse options, analyzer references,
analyzer options and analyzer config documents) by comparing the diagnostics
each workspace actually produces:

- An .editorconfig that promotes CS0219 to an error must flow through to
  Compilation.GetDiagnostics identically on both sides.
- An SDK analyzer (CA1822) enabled via .editorconfig must run and honour that
  severity, producing the same analyzer diagnostics as MSBuildWorkspace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Project_reference_resolves_cross_project_symbols: a design-time build never
  produces the referenced DLL, so App can only bind Library.Widget if the
  project reference is wired into the workspace as a compilation reference.
  Asserts the app compiles without errors on both sides.
- Xml_documentation_include_resolves_from_workspace: a doc-comment <include>
  only resolves with an XmlReferenceResolver (otherwise CS1589), functionally
  exercising the resolver now attached to the workspace's compilation options.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MSBuildWorkspace records each document's logical folder path (Document.Folders)
- the directory of the file relative to the project directory - which some
features (e.g. the namespace-folder analyzer) rely on. Buildalyzer left it
empty. Derive it the same way MSBuildWorkspace does for source, additional and
analyzer-config documents. A differential test (which failed before the fix)
covers nested folders and generated files under obj.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MSBuildWorkspace sets Project.OutputRefFilePath from TargetRefPath; Buildalyzer
left it null. Set it too. Adds differential tests for the ref-assembly path,
additional-file folders (confirming the folder fix covers additional documents)
and nullable diagnostics flowing through end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The differential suite showed that a linked file above the project directory
gets empty folders in MSBuildWorkspace (the SDK auto-links outside-cone files),
whereas the relative-path computation produced ["..']. Return no folders when
the relative path escapes the project directory, and add a linked-file folder
test covering it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Buildalyzer created document SourceText with the default SHA1 hash, while the
modern SDK (and MSBuildWorkspace) use SHA256. Read the ChecksumAlgorithm
property and pass it to SourceText.From so document checksums match the
compiler - important for deterministic builds and #pragma checksum. Adds a
differential test (which failed before the fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Buildalyzer created document SourceText with a hard-coded UTF-16 encoding;
MSBuildWorkspace and the compiler keep the file's own encoding (UTF-8 by
default). Read the file through a stream so the BOM is detected and the
encoding preserved. Adds a differential encoding test (which failed before the
fix) plus behavioral end-to-end tests for unsafe code, checked overflow and
cross-project InternalsVisibleTo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirms Option Strict On flows through to diagnostics (BC30512 on both sides).
The test also documents a case where Buildalyzer is more faithful than the
reference: for a .NET VB class library MSBuildWorkspace injects the VB "My"
template and emits spurious BC30002 errors for unreferenced
Microsoft.VisualBasic.ApplicationServices/Devices types, which the real compiler
and Buildalyzer do not produce. The assertions are scoped accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loads a .slnx with both Buildalyzer (via AnalyzerManager, which parses the
solution with SolutionPersistence) and MSBuildWorkspace.OpenSolutionAsync, and
asserts the same set of projects is loaded - covering the PR's solution-parsing
rewrite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a reference's EmbedInteropTypes metadata is true, MSBuildWorkspace sets
MetadataReferenceProperties.EmbedInteropTypes on that reference; Buildalyzer did
not. Capture the EmbedInteropTypes reference metadata on CompilerCommand, expose
it via IAnalyzerResult.ReferencesEmbeddingInteropTypes, and apply it when
building the workspace's metadata references.

The metadata flows through the design-time build without the compiler running
(which is where a non-PIA would be rejected), so the differential test exercises
this cross-platform by referencing a real assembly marked EmbedInteropTypes; it
failed before the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IAnalyzerResults.BuildEventArguments exposed
ImmutableArray<XenoAtom.MsBuildPipeLogger.PipeBuildEventArgs> - a third-party
implementation-detail type - on the public API. Remove it from IAnalyzerResults
and make the backing property on AnalyzerResults internal (still used internally
to carry restore events across). Every other XenoAtom.MsBuildPipeLogger usage is
already confined to internal types or internal members.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

File-based app materialization has lifecycle races and timeout leakage, and path handling still contains correctness defects.

4 open findings
Previously missed (1)

In code that hasn't changed since last review

Medium severity Deduplicate project references using filesystem-aware path comparison

src/​Buildalyzer/​AnalyzerResult.cs:117

The project references are normalized with file-system-aware paths only after the existing case-insensitive Distinct, so on Linux references such as ../Lib/Lib.csproj and ../lib/Lib.csproj are still collapsed even though they can identify different projects. Deduplicate item specs with IOPath.Comparer, matching the path-key convention established in IOPath.cs:24-33 and AnalyzerManager.cs:13-16.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

- Ref-count materialized file-based app projects per path across all
  analyzers, so one analyzer's build can't delete the project while
  another's is still using it.
- Kill `dotnet run-api` (and its process tree) when it times out rather
  than leaving it running.
- Treat only a leading `..` segment as escaping the project directory, so
  a folder like `..generated` keeps its document folders.
- Reject binlog paths containing `;` or `"` up front. MSBuild splits /bl
  parameters on `;` regardless of quotes and doesn't unescape %3B in the
  log file name, so there is no way to pass them through.
- De-duplicate ProjectReferences on the resolved path with the
  file-system-aware comparer rather than case-insensitively on item spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Binlog replay currently merges platform-specific TFMs, and the new logging and compiler-log paths retain unresolved correctness and scalability issues.

1 open finding
4 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Support third-party analyzer results with compiler metadata

src/​Buildalyzer.CompilerLogs/​AnalyzerResultsExtensions.cs:60

This public extension accepts IEnumerable<IAnalyzerResult>, but the concrete cast treats every third-party IAnalyzerResult implementation as having no command, even when its CompilerArguments and CompilerFilePath are populated. Such results can never produce a compiler log. Build the CompilerCall from the interface members (inferring C#/VB from the project extension), or expose the required command metadata on the interface.

Medium severity Disable duplicate event logging for live builds

src/​Buildalyzer/​ProjectAnalyzer.cs:515

AddBinaryLogger now attaches MSBuild's native /bl logger, so the pipe logger no longer needs to forward every event. Keeping LogEverything enabled duplicates the complete event stream over the pipe and retains it in BuildEventArgsCollector; large builds/binlogs can therefore consume substantially more memory and pipe bandwidth. Keep this false for live builds (binlog replay can still use true in AnalyzerManager.Analyze).

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/Buildalyzer/Logging/EventProcessor.cs Outdated
…nce dedup

Results were keyed by TargetFrameworkMoniker, which net8.0 and
net8.0-windows share (.NETCoreApp,Version=v8.0), so replaying a binlog of
a project targeting both folded the two inner builds into one result.
Key by the short TargetFramework instead, falling back to the moniker for
legacy projects that have none.

Also restore case-insensitive de-duplication of ProjectReferences (still
on the resolved path). The file-system-aware comparer from the previous
commit broke Duplicate_project_references on Linux, which pins down that
references differing only by case are one project.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Virtual-project ownership can cause user file loss, and process output collection contains a concurrency race.

1 open finding
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Synchronize process output collection before reading diagnostics

src/​Buildalyzer/​Environment/​ProcessRunner.cs:122

This now returns while the asynchronous output handlers may still be appending to ProcessDataCollector's List<string> buffers. Analyze() subsequently reads processRunner.Data on the empty-pipe error path, so an inherited writer that keeps the streams open can race the list snapshot and yield partial diagnostics or throw during enumeration. Keep the nonblocking exit behavior, but synchronize collector writes/snapshots (or otherwise stop collection before exposing Data).

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/Buildalyzer/Construction/VirtualProject.cs
FileBasedProgram is the SDK's property, so a hand-written or converted
project at the SDK-chosen path could carry it too, and Buildalyzer would
then overwrite it and delete it after the build. Append a Buildalyzer
comment after the root element when writing the project instead, and only
replace or delete a file that carries it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Solution framework metadata and project-reference correlation still have correctness issues.

0 open findings

1 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Restore target framework parsing for classic solution projects

src/​Buildalyzer/​ProjectInfo.cs:46

Classic .sln entries do not contain TargetFramework/TargetFrameworks, so FindProperties returns no values and ProjectInfo.TargetFrameworks is now empty for every normal solution project. The previous .sln loader populated this public property from the project file. Fall back to parsing the project when the solution model has no framework properties.

Medium severity Disambiguate project references before selecting by target framework

src/​Buildalyzer.Workspaces/​AnalyzerResultExtensions.cs:510

Matching only the target framework still selects the wrong project when two unrelated same-TFM projects share a TargetPath: both candidates match, so whichever appears first becomes the project reference even if the consumer references the other project. Restrict candidates using the consumer's evaluated ProjectReference paths (and keep a metadata reference when no unique project matches) before applying TFM selection.

Medium severity Use file-system-aware comparer for project reference de-duplication

src/​Buildalyzer/​AnalyzerResult.cs:119

This unconditional case-insensitive de-duplication collapses distinct project references on a case-sensitive file system (for example, Lib/Foo.csproj and lib/Foo.csproj). That contradicts the new file-system-aware path handling and can omit one project from workspace closure discovery. Use IOPath.Comparer, as the workspace reference traversal already does.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

…safe

The test projects build the same fixture projects under tests/projects
in place, and `dotnet test buildalyzer.slnx` runs them concurrently: on
Windows, LoadsSolution read an obj/ file another suite's build was
rewriting and failed with "access denied". Build once, then test each
project in turn. (Not `dotnet test -m:1`, which runs the test host from
the CLI's own MSBuild process and made the Differential suite ~3x slower.)

Running them in order surfaced a second failure: Detects_failing_build
threw from StringBuilder.ToString() while dumping Context.Log on dispose,
as build output was still being written on another thread. Context.Log is
now the SafeStringWriter the Differential suite already used, moved into
TestTools so both share it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Solution project framework metadata, case-sensitive project references, and explicit global aliases are currently handled incorrectly.

1 open finding
Previously missed (2)

In code that hasn't changed since last review

Medium severity Fall back to project files for target frameworks

src/​Buildalyzer/​ProjectInfo.cs:46

SolutionProjectModel.FindProperties reads solution property bags; normal .sln project entries do not contain TargetFramework(s). The removed ProjectInSolution path loaded ProjectFile.TargetFrameworks, so this change makes ProjectInfo.TargetFrameworks empty for ordinary .sln projects. Fall back to the project file when the solution model has no such property bag.

Medium severity Use file-system-aware path comparison for deduplication

src/​Buildalyzer/​AnalyzerResult.cs:119

This unconditional case-insensitive deduplication collapses two valid project references whose paths differ only by case on a case-sensitive file system. It also contradicts the file-system-aware path handling used by AnalyzerManager and Workspaces; use IOPath.Comparer here as well.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/Buildalyzer/Compiler/CompilerCommandBuilder.cs Outdated
slang25 and others added 8 commits October 8, 2026 15:08
A reference with Aliases="global,Lib" is visible both in the global
namespace and through `extern alias Lib` - the Csc task passes it as a
plain /reference and an aliased one. Dropping 'global' left only Lib, so
ReferenceAliases and the reconstructed workspace lost the global-namespace
binding. Keep it; 'global' on its own is still the ordinary unaliased
reference and stays out of the map.

In the workspace, such a metadata reference becomes a plain reference
plus one carrying the other aliases, matching MSBuildWorkspace (and the
command-line fallback). Project references keep the merged aliases,
which is also what MSBuildWorkspace does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous commit rewrote the file with LF endings by accident; no
content change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ProjectInfo.TargetFrameworks only looked in the solution's property bag
for the project, which an ordinary .sln or .slnx entry doesn't have, so
it was always empty - and a ProjectFilter on target frameworks matched
nothing. Fall back to the project file, as the ProjectInSolution path did
for .sln before; a missing or unreadable project just has none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AddTaskParameterInput appended, and the WPF "*_wpftmp" project's context
is mapped onto the primary result. When both CoreCompiles log their
inputs into that result - a WPF project built with TargetsToBuild=Build
and DesignTime=false, the configuration the release notes point at for
MarkupCompilePass2-generated sources - every source, reference and
analyzer was listed twice, and the workspace got two documents per file.
Each invocation passes the complete set, so the latest replaces the
earlier, as the old command-line-based capture had it. The WPF test now
also asserts the source list is unique.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The parameterless ProcessRunner.WaitForExit() deliberately no longer
waits for the redirected output readers, but Analyze read the process
output straight after it to explain a replay that raised no events.
That is the one path where the text matters: MSBuild exits 0 from a
failed replay and only prints the reason to stdout, and since the logger
was initialised and shut down the pipe read ends at once, so the drain's
settle window did not cover it either. Give the handlers a bounded
moment to deliver the tail, and take the collector's lock around both
the appends and the snapshot copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When the build never reaches CoreCompile (issue Buildalyzer#341) the references
came from the ReferencePath items' specs alone, and the alias and
embed-interop lookups that followed read maps that are empty without a
CompilerCommand. The items carry both as metadata - ResolveAssemblyReference
forwards them from the Reference/ProjectReference items, and the compiler
task reads them off the same items - so a project using `extern alias`
whose build failed early was reconstructed with the reference but no
alias, and interop assemblies were referenced rather than embedded. Read
the metadata off the items, merged per path; a lone 'global' alias is
the ordinary unaliased reference, as the compiler-side map has it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The evaluated TargetFrameworks value was split but not de-duplicated
before the per-framework builds were scheduled, where the XML scan it
replaced applied Distinct() and MSBuild's own _ComputeTargetFrameworkItems
does too. A framework listed twice - a props file composing
"$(TargetFrameworks);net8.0" over a project that already lists net8.0,
say - built twice, concurrently, into the same obj directory, and a
case-variant duplicate produced two result keys. Distinct, ignoring
case, as MSBuild does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AddBinaryLogger stored the path as given and handed it to MSBuild as
/bl:LogFile, which resolves a relative path in the build process - in
the project directory, or EnvironmentOptions.WorkingDirectory. When the
BinaryLogger ran in-process it resolved against Buildalyzer's own
current directory, so `AddBinaryLogger("out.binlog")` from a tool
running in /work used to write /work/out.binlog and had started writing
<project dir>/out.binlog instead. Root it here, after the character
validation, as Analyze already roots its input.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Analyzer-config fallback, log-reader synchronization, and case-sensitive test path matching still have correctness gaps.

0 open findings

1 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Restore analyzer configuration in pre-compiler workspace fallback

src/​Buildalyzer.Workspaces/​AnalyzerResultExtensions.cs:744

The pre-compiler workspace fallback still loses analyzer configuration. When no compiler invocation is captured, both analyzerConfigFiles and commandLine are empty, so this returns no analyzer-config documents even when evaluated EditorConfigFiles/GlobalAnalyzerConfigFiles items are available. That can change diagnostics and source-generator behavior in exactly the failed-build projects the new item fallback reconstructs; add the same ShouldFallBackToItems item fallback used for sources and additional files.

Medium severity Ensure synchronized log reads remain safe after process exit

src/​Buildalyzer/​Logging/​TextWriterLoggerProvider.cs:12

TextWriter.Synchronized only serializes operations performed through the wrapper; it does not synchronize a caller reading the original StringWriter via ToString(). Since ProcessRunner.WaitForExit() intentionally does not wait for redirected-stream callbacks to finish, a caller can read the configured LogWriter while one of these writes is still running and hit the same corruption/exception this change aims to prevent. The test also reads only after all writers finish. Please either guarantee callbacks are quiescent before returning from a build or keep/document the requirement for a writer whose reads are independently thread-safe.

Medium severity Use file-system-aware project path comparison

tests/​Buildalyzer.Differential.Tests/​WorkspaceComparison.cs:133

This unconditional case-insensitive comparison can select the wrong project on a case-sensitive file system when two project paths differ only by case, invalidating the differential comparison. It also conflicts with the repository's explicit file-system-aware path convention (src/Buildalyzer/IO/IOPath.cs:24-33, used by workspace path indexes). Use the same file-system-aware comparison here rather than OrdinalIgnoreCase.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

…piler runs

The pre-compiler workspace fallback (issue Buildalyzer#341) reconstructed sources,
additional files, references and analyzers from the evaluated items but
left the analyzer configuration empty, so a project recovered from a
failed build lost its .editorconfig severities and source-generator
options.

The SDK computes EditorConfigFiles at evaluation time - every
.editorconfig and .globalconfig above a Compile item plus the project's
own GlobalAnalyzerConfigFiles - and hands exactly that item group to the
compiler task as AnalyzerConfigFiles, so a failed build still has the
set. Fall back to it like the other inputs do. Only the build-generated
GeneratedMSBuildEditorConfig.editorconfig is missing, as the generated
sources are; MSBuildWorkspace reads the same items in this case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚨 Breaking Change Potentially breaks existing users General Enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants