Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<MSBuildPackageVersion>17.11.4</MSBuildPackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' " />
<PackageVersion Include="Microsoft.Build" Version="$(MSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.Build.Framework" Version="$(MSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="$(MSBuildPackageVersion)" />
Expand Down
1 change: 1 addition & 0 deletions src/BuildPrediction/Microsoft.Build.Prediction.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Description>A library to predict inputs and outputs of MSBuild projects</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.HashCode" Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' " />
<PackageReference Include="Microsoft.Build" />
<PackageReference Include="Microsoft.Build.Framework" />
<PackageReference Include="Microsoft.Build.Utilities.Core" />
Expand Down
57 changes: 53 additions & 4 deletions src/BuildPrediction/Predictors/CodeAnalysisRuleSetPredictor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed class CodeAnalysisRuleSetPredictor : IProjectPredictor
private readonly List<string> _emptyList = new List<string>(0);

// Often rulesets are reused across projects, so keep a cache to avoid parsing the same ruleset over and over.
private readonly ConcurrentDictionary<string, HashSet<string>> _cachedInputs = new ConcurrentDictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<RuleSetCacheKey, HashSet<string>> _cachedInputs = new ConcurrentDictionary<RuleSetCacheKey, HashSet<string>>();

/// <inheritdoc/>
public void PredictInputsAndOutputs(
Expand Down Expand Up @@ -60,7 +60,7 @@ public void PredictInputsAndOutputs(
}
}

// Based on resolution logic from: https://github.com/Microsoft/msbuild/blob/master/src/Tasks/ResolveCodeAnalysisRuleSet.cs
// Based on resolution logic from: https://github.com/Microsoft/msbuild/blob/main/src/Tasks/ResolveCodeAnalysisRuleSet.cs
private static string GetResolvedRuleSetPath(
string ruleSet,
List<string> ruleSetDirectories,
Expand Down Expand Up @@ -148,7 +148,8 @@ private HashSet<string> ParseRuleset(
return _emptySet;
}

if (_cachedInputs.TryGetValue(ruleSetPath, out HashSet<string> cachedResults))
RuleSetCacheKey cacheKey = new RuleSetCacheKey(ruleSetPath, ruleSetDirectories);
if (_cachedInputs.TryGetValue(cacheKey, out HashSet<string> cachedResults))
{
return cachedResults;
}
Expand Down Expand Up @@ -241,7 +242,7 @@ private HashSet<string> ParseRuleset(
// As described above, do not cache intermediate results inside a cycle.
if (!isInCycle)
{
_cachedInputs.TryAdd(ruleSetPath, results);
_cachedInputs.TryAdd(cacheKey, results);
}

return results;
Expand All @@ -252,5 +253,53 @@ private HashSet<string> ParseRuleset(
return _emptySet;
}
}

private readonly record struct RuleSetCacheKey
{
public RuleSetCacheKey(string ruleSetPath, IReadOnlyList<string> ruleSetDirectories)
{
RuleSetPath = ruleSetPath;
RuleSetDirectories = new List<string>(ruleSetDirectories);
}

public string RuleSetPath { get; }

public List<string> RuleSetDirectories { get; }

public bool Equals(RuleSetCacheKey other)
{
if (!PathComparer.Instance.Equals(RuleSetPath, other.RuleSetPath))
{
return false;
}

if (RuleSetDirectories.Count != other.RuleSetDirectories.Count)
{
return false;
}

for (int i = 0; i < RuleSetDirectories.Count; i++)
{
if (!PathComparer.Instance.Equals(RuleSetDirectories[i], other.RuleSetDirectories[i]))
{
return false;
}
}

return true;
}

public override int GetHashCode()
{
int hash = PathComparer.Instance.GetHashCode(RuleSetPath);

foreach (string directory in RuleSetDirectories)
{
hash = HashCode.Combine(hash, PathComparer.Instance.GetHashCode(directory));
}

return hash;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ public void RuleSetIsCached()
AssertInputs("a.ruleset", expectedInputs);
}

[Fact]
public void RuleSetIsCacheMissOnDifferentCodeAnalysisDirectories()
{
var rulesetA = CreateRuleSetFile("a.ruleset")
.WithInclude("b.ruleset")
.WriteToDisk();
var rulesetB = CreateRuleSetFile("foo\\b.ruleset")
.WriteToDisk();

var expectedInputs = new[]
{
rulesetA.FullPath,
};
AssertInputs("a.ruleset", expectedInputs);

expectedInputs = [
rulesetA.FullPath,
rulesetB.FullPath,
];
AssertInputs("a.ruleset", expectedInputs, [Path.GetDirectoryName(rulesetB.FullPath)]);
}

[Fact]
public void RuleSetWithRuleHintPaths()
{
Expand Down Expand Up @@ -296,7 +318,7 @@ public void RuleSetWithComplexCycle()
AssertInputs("a2.ruleset", expectedResultsInCycleBCD.Union(new[] { rulesetA2.FullPath }).ToList());
}

private void AssertInputs(string ruleSetPath, IList<string> expectedInputs)
private void AssertInputs(string ruleSetPath, IList<string> expectedInputs, IList<string> codeAnalysisRuleSetDirectories = null)
{
ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, "project.proj"));
if (ruleSetPath != null)
Expand All @@ -305,6 +327,11 @@ private void AssertInputs(string ruleSetPath, IList<string> expectedInputs)
projectRootElement.AddProperty(CodeAnalysisRuleSetPredictor.CodeAnalysisRuleSetPropertyName, ruleSetPath);
}

if (codeAnalysisRuleSetDirectories != null)
{
projectRootElement.AddProperty(CodeAnalysisRuleSetPredictor.CodeAnalysisRuleSetDirectoriesPropertyName, string.Join(";", codeAnalysisRuleSetDirectories));
}

var projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);

var expectedInputFiles = expectedInputs?
Expand Down
Loading