Skip to content
Merged
71 changes: 71 additions & 0 deletions src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2923,6 +2923,77 @@ public void GetTargetListsWithSolutionInvalidTargets(string entryTarget)
}
}

[Fact]
[UseInvariantCulture]
public void InvalidProjectReferenceErrorIncludesReferringProject()
{
using (var env = TestEnvironment.Create())
{
// Create a project that references a non-existent project
TransientTestFile project1 = env.CreateFile("project1.proj", @"
<Project>
<ItemGroup>
<ProjectReference Include=""missing.proj"" />
</ItemGroup>
</Project>");

// Attempt to create a graph should throw an exception
var exception = Should.Throw<AggregateException>(() => new ProjectGraph(project1.Path));

// The exception should be an InvalidProjectFileException
exception.InnerExceptions.ShouldHaveSingleItem();
var innerException = exception.InnerExceptions[0].ShouldBeOfType<InvalidProjectFileException>();

// The error message should mention the referring project
innerException.Message.ShouldContain("project1.proj");
innerException.Message.ShouldContain("Referenced by:");
}
}

[Fact]
[UseInvariantCulture]
public void InvalidProjectReferenceErrorIncludesMultipleReferringProjects()
{
using (var env = TestEnvironment.Create())
{
// Create two projects that both reference a non-existent project
TransientTestFile project1 = env.CreateFile("project1.proj", @"
<Project>
<ItemGroup>
<ProjectReference Include=""missing.proj"" />
</ItemGroup>
</Project>");
Comment thread
baronfel marked this conversation as resolved.
Outdated

TransientTestFile project2 = env.CreateFile("project2.proj", @"
<Project>
<ItemGroup>
<ProjectReference Include=""missing.proj"" />
</ItemGroup>
</Project>");
Comment thread
baronfel marked this conversation as resolved.
Outdated

TransientTestFile main = env.CreateFile("main.proj", @"
<Project>
<ItemGroup>
<ProjectReference Include=""project1.proj"" />
<ProjectReference Include=""project2.proj"" />
</ItemGroup>
</Project>");

// Attempt to create a graph should throw an exception
var exception = Should.Throw<AggregateException>(() => new ProjectGraph(main.Path));

// The exception should be an InvalidProjectFileException
exception.InnerExceptions.ShouldHaveSingleItem();
var innerException = exception.InnerExceptions[0].ShouldBeOfType<InvalidProjectFileException>();

// The error message should mention at least one referring project
innerException.Message.ShouldContain("Referenced by:");
bool hasProject1 = innerException.Message.Contains("project1.proj");
bool hasProject2 = innerException.Message.Contains("project2.proj");
(hasProject1 || hasProject2).ShouldBeTrue();
}
}

public void Dispose()
{
_env.Dispose();
Expand Down
46 changes: 42 additions & 4 deletions src/Build/Graph/GraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ internal class GraphBuilder
private IReadOnlyDictionary<string, IReadOnlyCollection<string>> _solutionDependencies;
private ConcurrentDictionary<ConfigurationMetadata, Lazy<ProjectInstance>> _platformNegotiationInstancesCache = new();

/// <summary>
/// Tracks which projects are referencing each project. Used to provide better error messages
/// when a referenced project fails to load.
/// Key: The project being referenced
/// Value: Set of projects that reference it
/// </summary>
private readonly ConcurrentDictionary<ConfigurationMetadata, ConcurrentBag<string>> _projectReferrers = new();

public GraphBuilder(
IEnumerable<ProjectGraphEntryPoint> entryPoints,
ProjectCollection projectCollection,
Expand Down Expand Up @@ -528,10 +536,36 @@ private ParsedProject ParseProject(ConfigurationMetadata configurationMetadata)
// TODO: ProjectInstance just converts the dictionary back to a PropertyDictionary, so find a way to directly provide it.
var globalProperties = configurationMetadata.GlobalProperties.ToDictionary();

var projectInstance = _projectInstanceFactory(
configurationMetadata.ProjectFullPath,
globalProperties,
_projectCollection);
ProjectInstance projectInstance;
try
{
projectInstance = _projectInstanceFactory(
configurationMetadata.ProjectFullPath,
globalProperties,
_projectCollection);
}
catch (InvalidProjectFileException ex) when (_projectReferrers.TryGetValue(configurationMetadata, out var referrers) && !referrers.IsEmpty)
{
// Enrich the exception with information about which project(s) referenced this project
string referrerList = string.Join(", ", referrers.Distinct().OrderBy(r => r));

string enrichedMessage = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
"ProjectGraphProjectFileCannotBeLoadedWithReferrers",
referrerList,
ex.Message);

throw new InvalidProjectFileException(
ex.ProjectFile ?? configurationMetadata.ProjectFullPath,
ex.LineNumber,
ex.ColumnNumber,
ex.EndLineNumber,
ex.EndColumnNumber,
enrichedMessage,
ex.ErrorSubcategory,
ex.ErrorCode,
ex.HelpKeyword,
ex.InnerException);
}

if (projectInstance == null)
{
Expand Down Expand Up @@ -585,6 +619,10 @@ private void SubmitProjectForParsing(ConfigurationMetadata projectToEvaluate)
referenceInfo.ReferenceConfiguration.ProjectFullPath));
}

// Track that this project is referencing the target project
_projectReferrers.GetOrAdd(referenceInfo.ReferenceConfiguration, _ => new ConcurrentBag<string>())
.Add(parsedProject.ProjectInstance.FullPath);

SubmitProjectForParsing(referenceInfo.ReferenceConfiguration);

referenceInfos.Add(referenceInfo);
Expand Down
8 changes: 8 additions & 0 deletions src/Build/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,14 @@ Utilization: {0} Average Utilization: {1:###.0}</value>
request a target to build itself (perhaps via a chain of other targets)
</comment>
</data>
<data name="ProjectGraphProjectFileCannotBeLoadedWithReferrers" xml:space="preserve">
<value>Referenced by: {0}. {1}</value>
<comment>
This message is prepended to the original error when a referenced project file cannot be loaded during static graph construction.
{0} is a comma-separated list of projects that reference the failed project
{1} is the complete original error message
</comment>
</data>
<data name="CacheMissesNotAllowedInIsolatedGraphBuilds" xml:space="preserve">
<value>MSB4252: Project "{0}" with global properties
({1})
Expand Down
9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Build/Resources/xlf/Strings.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading