Skip to content

Commit 2fe6cc5

Browse files
0xcedAaronontheweb
andauthored
Fix missing project dependency detection (#421)
* Add multiple target frameworks to ProjectA and ProjectB This demonstrates that project dependency detection has a bug and makes ShouldDetectProjectChanges fail. * Fix missing project dependency detection When multiple target frameworks are defined, the dictionary of source file to project must keep all the project ids. Choosing just one with `.First()` can lead to failure because the project id might correspond to a target framework that does not match the target framework of the referencing project. --------- Co-authored-by: Aaron Stannard <aaron@petabridge.com>
1 parent 247e385 commit 2fe6cc5

File tree

7 files changed

+43
-37
lines changed

7 files changed

+43
-37
lines changed

src/Incrementalist.Cmd/Commands/EmitDependencyGraphTask.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,20 @@ private BuildAnalysisResult FilterBuildResult(BuildAnalysisResult original, IRea
115115
}
116116

117117
// Log the breakdown of modified files by type
118-
var modifiedSourceFiles = affectedFiles.Count(x => x.Value.FileType == FileType.Code);
119-
var modifiedProjectFiles = affectedFiles.Count(x => x.Value.FileType == FileType.Project);
120-
var modifiedSolutionFiles = affectedFiles.Count(x => x.Value.FileType == FileType.Solution);
121-
var modifiedScriptFiles = affectedFiles.Count(x => x.Value.FileType == FileType.Script);
122-
var modifiedOtherFiles = affectedFiles.Count(x => x.Value.FileType == FileType.Other);
118+
var modifiedSourceFiles = affectedFiles.Count(x => x.Value.Any(f => f.FileType == FileType.Code));
119+
var modifiedProjectFiles = affectedFiles.Count(x => x.Value.Any(f => f.FileType == FileType.Project));
120+
var modifiedSolutionFiles = affectedFiles.Count(x => x.Value.Any(f => f.FileType == FileType.Solution));
121+
var modifiedScriptFiles = affectedFiles.Count(x => x.Value.Any(f => f.FileType == FileType.Script));
122+
var modifiedOtherFiles = affectedFiles.Count(x => x.Value.Any(f => f.FileType == FileType.Other));
123123
Logger.LogInformation(
124124
"Modified files breakdown: {SourceFiles} source files, {ProjectFiles} project files, {SolutionFiles} solution files, {ScriptFiles} script files, {OtherFiles} other files",
125125
modifiedSourceFiles, modifiedProjectFiles, modifiedSolutionFiles, modifiedScriptFiles,
126126
modifiedOtherFiles);
127127

128128
// Check if any of the affected files require a solution-wide build
129129
Logger.LogInformation("Analyzing solution-wide impact...");
130-
var projectFiles = allFiles.Where(x => x.Value.FileType == FileType.Project)
131-
.Select(pair => new SlnFileWithPath(pair.Key, pair.Value))
130+
var projectFiles = allFiles.Where(x => x.Value is [{ FileType: FileType.Project }])
131+
.Select(pair => new SlnFileWithPath(pair.Key, pair.Value[0]))
132132
.ToList();
133133
var projectImports = ProjectImportsFinder.FindProjectImports(projectFiles);
134134
var importDetector = new SolutionWideChangeDetector(projectImports);
@@ -140,7 +140,7 @@ private BuildAnalysisResult FilterBuildResult(BuildAnalysisResult original, IRea
140140
}
141141

142142
// Get the list of affected project files directly
143-
var directlyAffectedProjects = affectedFiles.Where(x => x.Value.FileType == FileType.Project)
143+
var directlyAffectedProjects = affectedFiles.Where(x => x.Value.Any(f => f.FileType == FileType.Project))
144144
.Select(x => x.Key)
145145
.ToList();
146146

src/Incrementalist.Tests/Dependencies/EmitDependencyGraphSpecs.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,18 @@ private static TestSolutionModel CreateSolution()
4949
.AddFolder("src", f1Builder =>
5050
{
5151
f1Builder.AddProject(ProjectA,
52-
(_, p1Builder) => { p1Builder.WithFile("HelloWorld.cs", CsharpSamples.HelloClass); });
52+
(_, p1Builder) =>
53+
{
54+
p1Builder.WithFile("HelloWorld.cs", CsharpSamples.HelloClass);
55+
p1Builder.WithTargetFrameworks([TargetFramework.Net8, TargetFramework.Net9, TargetFramework.NetStandard2_0, TargetFramework.NetStandard2_1]);
56+
});
5357

5458
f1Builder.AddProject(ProjectB, (otherProjects, p2Builder) =>
5559
{
5660
p2Builder.WithFile("GoodBye.cs", CsharpSamples.GoodbyeClassWithNamespace);
5761
var projectA = otherProjects.First(p => p.NameWithoutExtension == ProjectA);
5862
p2Builder.WithProjectReference(projectA);
63+
p2Builder.WithTargetFrameworks([TargetFramework.Net8, TargetFramework.Net9, TargetFramework.NetStandard2_0, TargetFramework.NetStandard2_1]);
5964
});
6065

6166
// a third project, C, with no references to anyone else
@@ -69,6 +74,7 @@ private static TestSolutionModel CreateSolution()
6974
p3Builder.WithFile("HelloWorldTests.cs", CsharpSamples.BarClass);
7075
var projectB = otherProjects.First(p => p.NameWithoutExtension == ProjectB);
7176
p3Builder.WithProjectReference(projectB);
77+
p3Builder.WithTargetFrameworks([TargetFramework.Net9]);
7278
});
7379
})
7480
.Build();

src/Incrementalist.Tests/Dependencies/ProjectImportsTrackingSpecs.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ public async Task Should_mark_project_as_changed_when_only_imported_file_changed
7575

7676
var cmd = new FilterAffectedProjectFilesCmd(new TestOutputLogger(_outputHelper), CancellationToken.None,
7777
Repository.BasePath, "master");
78-
var solutionFiles = new Dictionary<AbsolutePath, SlnFile>()
78+
var solutionFiles = new Dictionary<AbsolutePath, SlnFile[]>()
7979
{
80-
[projectFilePath] = new SlnFile(FileType.Project, ProjectId.CreateNewId())
80+
[projectFilePath] = [new SlnFile(FileType.Project, ProjectId.CreateNewId())]
8181
};
8282
var filteredAffectedFiles = await cmd.Process(Task.FromResult(solutionFiles));
8383
Assert.Single(filteredAffectedFiles);

src/Incrementalist/ProjectSystem/Cmds/ComputeDependencyGraphCmd.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace Incrementalist.ProjectSystem.Cmds
2020
/// and emits a topologically sorted set of project file names to be used during testing.
2121
/// </summary>
2222
public sealed class
23-
ComputeDependencyGraphCmd : BuildCommandBase<Dictionary<AbsolutePath, SlnFile>,
23+
ComputeDependencyGraphCmd : BuildCommandBase<Dictionary<AbsolutePath, SlnFile[]>,
2424
Dictionary<AbsolutePath, ICollection<AbsolutePath>>>
2525
{
2626
private readonly Solution _solution;
@@ -32,7 +32,7 @@ public ComputeDependencyGraphCmd(ILogger logger, CancellationToken cancellationT
3232
}
3333

3434
protected override async Task<Dictionary<AbsolutePath, ICollection<AbsolutePath>>> ProcessImpl(
35-
Task<Dictionary<AbsolutePath, SlnFile>> previousTask)
35+
Task<Dictionary<AbsolutePath, SlnFile[]>> previousTask)
3636
{
3737
var affectedSlnFiles = await previousTask;
3838

@@ -51,9 +51,9 @@ protected override async Task<Dictionary<AbsolutePath, ICollection<AbsolutePath>
5151
* We have to gather up each unique project file separately in this case.
5252
*/
5353
List<ProjectId> additionalProjectIds = [];
54-
if (affectedSlnFiles.Any(x => x.Value.FileType == FileType.Project))
54+
if (affectedSlnFiles.Any(x => x.Value.Any(f => f.FileType == FileType.Project)))
5555
{
56-
foreach (var proj in affectedSlnFiles.Where(x => x.Value.FileType == FileType.Project))
56+
foreach (var proj in affectedSlnFiles.Where(x => x.Value.Any(f => f.FileType == FileType.Project)))
5757
additionalProjectIds.AddRange(_solution.Projects
5858
.Where(x => x.FilePath != null && x.FilePath.Equals(proj.Key.Path)).Select(x => x.Id));
5959
}
@@ -78,8 +78,8 @@ protected override async Task<Dictionary<AbsolutePath, ICollection<AbsolutePath>
7878
}
7979

8080
var uniqueProjectIds = affectedSlnFiles
81-
.Where(c => c.Value.ProjectId != null)
82-
.Select(x => x.Value.ProjectId!).Concat(additionalProjectIds)
81+
.Where(c => c.Value.All(f => f.ProjectId != null))
82+
.SelectMany(x => x.Value.Select(f => f.ProjectId!)).Concat(additionalProjectIds)
8383
.Distinct().ToList();
8484

8585
Logger.LogDebug("Evaluating {Count} unique project IDs.", uniqueProjectIds.Count);

src/Incrementalist/ProjectSystem/Cmds/FilterAffectedProjectFilesCmd.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ namespace Incrementalist.ProjectSystem.Cmds
2121
/// they were touched via the GitDiff.
2222
/// </summary>
2323
public sealed class
24-
FilterAffectedProjectFilesCmd : BuildCommandBase<Dictionary<AbsolutePath, SlnFile>,
25-
Dictionary<AbsolutePath, SlnFile>>
24+
FilterAffectedProjectFilesCmd : BuildCommandBase<Dictionary<AbsolutePath, SlnFile[]>,
25+
Dictionary<AbsolutePath, SlnFile[]>>
2626
{
2727
private readonly string _targetGitBranch;
2828
private readonly AbsolutePath _workingDirectory;
@@ -35,8 +35,8 @@ public FilterAffectedProjectFilesCmd(ILogger logger, CancellationToken cancellat
3535
_targetGitBranch = targetGitBranch;
3636
}
3737

38-
protected override async Task<Dictionary<AbsolutePath, SlnFile>> ProcessImpl(
39-
Task<Dictionary<AbsolutePath, SlnFile>> previousTask)
38+
protected override async Task<Dictionary<AbsolutePath, SlnFile[]>> ProcessImpl(
39+
Task<Dictionary<AbsolutePath, SlnFile[]>> previousTask)
4040
{
4141
var fileDictObj = await previousTask;
4242

@@ -45,28 +45,28 @@ protected override async Task<Dictionary<AbsolutePath, SlnFile>> ProcessImpl(
4545
{
4646
Logger.LogError("Unable to find Git repository located in {WorkingDirectory}. Shutting down.",
4747
_workingDirectory);
48-
return new Dictionary<AbsolutePath, SlnFile>();
48+
return [];
4949
}
5050

5151
// validate the target branch
5252
if (!DiffHelper.HasBranch(repo, _targetGitBranch))
5353
{
5454
Logger.LogError("Current git repository doesn't have any branch named [{TargetBranch}]. Shutting down.",
5555
_targetGitBranch);
56-
return new Dictionary<AbsolutePath, SlnFile>();
56+
return [];
5757
}
5858

5959
var affectedFiles = DiffHelper.ChangedFiles(repo, _targetGitBranch).ToList();
6060

61-
var projectFiles = fileDictObj.Where(x => x.Value.FileType == FileType.Project).ToList();
61+
var projectFiles = fileDictObj.Where(x => x.Value is [{ FileType: FileType.Project }]).ToList();
6262
var projectFolders = projectFiles.Where(x => Path.GetDirectoryName(x.Key.Path) is not null)
6363
.ToLookup(x => new AbsolutePath(Path.GetDirectoryName(x.Key.Path)!), v => Tuple.Create(v.Key, v.Value));
6464
var projectImports =
6565
ProjectImportsFinder.FindProjectImports(projectFiles.Select(pair =>
66-
new SlnFileWithPath(pair.Key, pair.Value)));
66+
new SlnFileWithPath(pair.Key, pair.Value[0])));
6767

6868
// filter out any files that aren't affected by the diff
69-
var newDict = new Dictionary<AbsolutePath, SlnFile>();
69+
var newDict = new Dictionary<AbsolutePath, SlnFile[]>();
7070
foreach (var file in affectedFiles)
7171
{
7272
Logger.LogDebug("Affected file: {FilePath}", file);
@@ -86,7 +86,7 @@ protected override async Task<Dictionary<AbsolutePath, SlnFile>> ProcessImpl(
8686
if (SolutionWideChangeDetector.IsSolutionWideFile(file))
8787
{
8888
Logger.LogInformation("Adding solution-wide file {File} to the set of affected files.", file);
89-
newDict[file] = new SlnFile(FileType.Other, null);
89+
newDict[file] = [new SlnFile(FileType.Other, null)];
9090
continue;
9191
}
9292

@@ -110,7 +110,7 @@ protected override async Task<Dictionary<AbsolutePath, SlnFile>> ProcessImpl(
110110
// Mark all dependant as affected
111111
foreach (var dependentProject in import.DependentProjects)
112112
{
113-
newDict[dependentProject.Path] = dependentProject.File;
113+
newDict[dependentProject.Path] = [dependentProject.File];
114114
}
115115
}
116116
}

src/Incrementalist/ProjectSystem/Cmds/GatherAllFilesInSolutionCmd.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Incrementalist.ProjectSystem.Cmds
1717
/// <summary>
1818
/// Gathers all of the files in a solution and categorizes them.
1919
/// </summary>
20-
public sealed class GatherAllFilesInSolutionCmd : BuildCommandBase<Solution, Dictionary<AbsolutePath, SlnFile>>
20+
public sealed class GatherAllFilesInSolutionCmd : BuildCommandBase<Solution, Dictionary<AbsolutePath, SlnFile[]>>
2121
{
2222
private readonly AbsolutePath _workingDirectory;
2323

@@ -28,7 +28,7 @@ public GatherAllFilesInSolutionCmd(ILogger logger, CancellationToken cancellatio
2828
_workingDirectory = workingDirectory;
2929
}
3030

31-
protected override async Task<Dictionary<AbsolutePath, SlnFile>> ProcessImpl(Task<Solution> previousTask)
31+
protected override async Task<Dictionary<AbsolutePath, SlnFile[]>> ProcessImpl(Task<Solution> previousTask)
3232
{
3333
var slnObject = await previousTask;
3434
Contract.Assert(slnObject is not null,

src/Incrementalist/ProjectSystem/SolutionAnalyzer.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static class SolutionAnalyzer
4444
/// <param name="sln">The Solution file.</param>
4545
/// <param name="workingFolder"></param>
4646
/// <returns>A flattened list of all files inside the solution.</returns>
47-
public static Dictionary<AbsolutePath, SlnFile> AllSolutionFiles(Solution sln, AbsolutePath workingFolder)
47+
public static Dictionary<AbsolutePath, SlnFile[]> AllSolutionFiles(Solution sln, AbsolutePath workingFolder)
4848
{
4949
// throw if the solution's file path is null
5050
ArgumentNullException.ThrowIfNull(sln.FilePath, nameof(sln.FilePath));
@@ -55,17 +55,17 @@ public static Dictionary<AbsolutePath, SlnFile> AllSolutionFiles(Solution sln, A
5555
document => new SlnFile(
5656
document.SourceCodeKind == SourceCodeKind.Regular ? FileType.Code : FileType.Script,
5757
document.Project.Id))
58-
.ToDictionary(x => new AbsolutePath(Path.GetFullPath(x.Key!)), x => x.First()).ToList()
58+
.ToDictionary(x => new AbsolutePath(Path.GetFullPath(x.Key!)), x => x.ToArray())
5959
.Concat(sln.Projects.Where(x => x.FilePath != null).Select(x =>
60-
new KeyValuePair<AbsolutePath, SlnFile>(new AbsolutePath(Path.GetFullPath(x.FilePath!)),
61-
new SlnFile(FileType.Project, x.Id)))
60+
new KeyValuePair<AbsolutePath, SlnFile[]>(new AbsolutePath(Path.GetFullPath(x.FilePath!)),
61+
[new SlnFile(FileType.Project, x.Id)]))
6262
.Concat([
63-
new KeyValuePair<AbsolutePath, SlnFile>
64-
(new AbsolutePath(Path.GetFullPath(sln.FilePath)), new SlnFile(FileType.Solution, null))
63+
new KeyValuePair<AbsolutePath, SlnFile[]>
64+
(new AbsolutePath(Path.GetFullPath(sln.FilePath)), [new SlnFile(FileType.Solution, null)])
6565
]));
6666

6767
// need to de-duplicate
68-
var finalFiles = new Dictionary<AbsolutePath, SlnFile>();
68+
var finalFiles = new Dictionary<AbsolutePath, SlnFile[]>();
6969
foreach (var file in allPossibleFiles)
7070
{
7171
finalFiles[file.Key] = file.Value;

0 commit comments

Comments
 (0)