Buildalyzer.Workspaces returns an empty workspace when MSBuild fails before Csc runs · Issue #341 · Buildalyzer/Buildalyzer · GitHub
Skip to content

Buildalyzer.Workspaces returns an empty workspace when MSBuild fails before Csc runs #341

Open

Description

@slang25

Filed as part of the analysis tracked in #340.

Symptom

IAnalyzerResult.GetWorkspace() returns an AdhocWorkspace containing the project but with zero documents and zero metadata references, despite the underlying evaluation having succeeded. The caller has no easy way to detect this — Succeeded/OverallSuccess may both be true (the top-level evaluation succeeded) or false (build failed) and the workspace surface looks the same shape either way.

Reproduces deterministically on, e.g., a shallow git clone of any project that uses Nerdbank.GitVersioning: NBGV's GetBuildVersion task throws in BeforeCompile's dependency closure, MSBuild aborts before CoreCompile invokes Csc, and the resulting workspace is empty.

Root cause

EventProcessor captures evaluation-time properties and items on ProjectStarted and stores them via AnalyzerResult.ProcessProject:

// src/Buildalyzer/Logging/EventProcessor.cs:75-106
private void ProjectStarted(object sender, ProjectStartedEventArgs e)
{
    ...
    var propertiesAndItems = PropertiesAndItems.TryCreate(e, _evalulationResults);
    ...
    result.ProcessProject(propertiesAndItems);
}

Those properties and items end up on IAnalyzerResult.Properties and IAnalyzerResult.Items regardless of whether the build later fails.

But the workspace-building public surface (IAnalyzerResult.SourceFiles, .References, .AnalyzerReferences, .PreprocessorSymbols, .AdditionalFiles) is sourced exclusively from CompilerCommand:

// src/Buildalyzer/AnalyzerResult.cs:68-83
public string[] SourceFiles =>
    CompilerCommand?.SourceFiles.Select(f => f.ToString()).ToArray() ?? [];

public string[] References =>
    CompilerCommand?.MetadataReferences.ToArray() ?? [];

public string[] AnalyzerReferences =>
    CompilerCommand?.AnalyzerReferences.Select(r => r.ToString()).ToArray() ?? [];

CompilerCommand is only populated when the Csc task fires its TaskCommandLineEventArgs:

// src/Buildalyzer/Logging/EventProcessor.cs:149-152
if (e is TaskCommandLineEventArgs cmd && cmd.TaskName.IsMatch("Csc"))
{
    result.ProcessCscCommandLine(cmd.CommandLine, _targetStack.Any(...));
}

If CoreCompile never reaches the Csc invocation, CompilerCommand stays null, every accessor returns an empty array, and AnalyzerResultExtensions.AddToWorkspace happily produces an empty workspace from those empty arrays.

Why MSBuildWorkspace doesn't hit this

Roslyn's MSBuildWorkspace BuildHost queries the in-process Build engine API directly for workspace state and streams it back, regardless of whether Build() returned true. Buildalyzer is reconstructing the workspace from build-event log output, so anything that gates on a specific later task firing is fragile.

Suggested fix

When CompilerCommand is null, fall back to evaluation-time data on result.Items / result.Properties:

  • Documents: result.Items["Compile"] (each IProjectItem has the source path as ItemSpec relative to the project directory).
  • Preprocessor symbols: result.GetProperty("DefineConstants"), semicolon-split.
  • LangVersion: already done via result.GetProperty("LangVersion") in CreateParseOptions (AnalyzerResultExtensions.cs:166).
  • Metadata references: harder — Csc's reference list is the resolved closure from ResolveAssemblyReferences. The closest evaluation-time approximation is result.Items["Reference"] plus result.Items["ReferencePath"] if the latter ran. A degraded reference set is better than zero (callers can still get source-level semantic models).

Recommended shape: a private BuildFromItems(...) path inside AnalyzerResultExtensions.GetDocuments / AnalyzerResultExtensions.GetMetadataReferences that activates only when the CompilerCommand-backed path returns empty and result.Items["Compile"] is non-empty. We ship this as an out-of-tree workaround in a playground built around #340 (the "items-fallback" mode) — it recovers ~99% of documents on shallow-clone NBGV failures and is visibly correct when measured against MSBuildWorkspace.

Workaround consumers should know about today

Always check the workspace contents after GetWorkspace():

var workspace = result.GetWorkspace();
var project = workspace.CurrentSolution.Projects.Single();
if (!project.Documents.Any())
{
    // Don't trust this workspace.
}

result.Items["Compile"] and result.Properties are still populated and can be used to construct a workspace directly.

Verified against

  • src/Buildalyzer/Logging/EventProcessor.cs (line refs above)
  • src/Buildalyzer/AnalyzerResult.cs (line refs above)
  • src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs:259-296

Related: #340.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions