diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9d7a4c1..d498e8e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,6 +5,7 @@
17.11.4
+
diff --git a/src/BuildPrediction/Microsoft.Build.Prediction.csproj b/src/BuildPrediction/Microsoft.Build.Prediction.csproj
index 2d9ae00..16cd9ec 100644
--- a/src/BuildPrediction/Microsoft.Build.Prediction.csproj
+++ b/src/BuildPrediction/Microsoft.Build.Prediction.csproj
@@ -9,6 +9,7 @@
A library to predict inputs and outputs of MSBuild projects
+
diff --git a/src/BuildPrediction/Predictors/CodeAnalysisRuleSetPredictor.cs b/src/BuildPrediction/Predictors/CodeAnalysisRuleSetPredictor.cs
index e2b32db..7d4dcd8 100644
--- a/src/BuildPrediction/Predictors/CodeAnalysisRuleSetPredictor.cs
+++ b/src/BuildPrediction/Predictors/CodeAnalysisRuleSetPredictor.cs
@@ -29,7 +29,7 @@ public sealed class CodeAnalysisRuleSetPredictor : IProjectPredictor
private readonly List _emptyList = new List(0);
// Often rulesets are reused across projects, so keep a cache to avoid parsing the same ruleset over and over.
- private readonly ConcurrentDictionary> _cachedInputs = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary> _cachedInputs = new ConcurrentDictionary>();
///
public void PredictInputsAndOutputs(
@@ -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 ruleSetDirectories,
@@ -148,7 +148,8 @@ private HashSet ParseRuleset(
return _emptySet;
}
- if (_cachedInputs.TryGetValue(ruleSetPath, out HashSet cachedResults))
+ RuleSetCacheKey cacheKey = new RuleSetCacheKey(ruleSetPath, ruleSetDirectories);
+ if (_cachedInputs.TryGetValue(cacheKey, out HashSet cachedResults))
{
return cachedResults;
}
@@ -241,7 +242,7 @@ private HashSet ParseRuleset(
// As described above, do not cache intermediate results inside a cycle.
if (!isInCycle)
{
- _cachedInputs.TryAdd(ruleSetPath, results);
+ _cachedInputs.TryAdd(cacheKey, results);
}
return results;
@@ -252,5 +253,53 @@ private HashSet ParseRuleset(
return _emptySet;
}
}
+
+ private readonly record struct RuleSetCacheKey
+ {
+ public RuleSetCacheKey(string ruleSetPath, IReadOnlyList ruleSetDirectories)
+ {
+ RuleSetPath = ruleSetPath;
+ RuleSetDirectories = new List(ruleSetDirectories);
+ }
+
+ public string RuleSetPath { get; }
+
+ public List 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;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/BuildPredictionTests/Predictors/CodeAnalysisRuleSetPredictorTests.cs b/src/BuildPredictionTests/Predictors/CodeAnalysisRuleSetPredictorTests.cs
index 7c7a585..0c1643b 100644
--- a/src/BuildPredictionTests/Predictors/CodeAnalysisRuleSetPredictorTests.cs
+++ b/src/BuildPredictionTests/Predictors/CodeAnalysisRuleSetPredictorTests.cs
@@ -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()
{
@@ -296,7 +318,7 @@ public void RuleSetWithComplexCycle()
AssertInputs("a2.ruleset", expectedResultsInCycleBCD.Union(new[] { rulesetA2.FullPath }).ToList());
}
- private void AssertInputs(string ruleSetPath, IList expectedInputs)
+ private void AssertInputs(string ruleSetPath, IList expectedInputs, IList codeAnalysisRuleSetDirectories = null)
{
ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, "project.proj"));
if (ruleSetPath != null)
@@ -305,6 +327,11 @@ private void AssertInputs(string ruleSetPath, IList expectedInputs)
projectRootElement.AddProperty(CodeAnalysisRuleSetPredictor.CodeAnalysisRuleSetPropertyName, ruleSetPath);
}
+ if (codeAnalysisRuleSetDirectories != null)
+ {
+ projectRootElement.AddProperty(CodeAnalysisRuleSetPredictor.CodeAnalysisRuleSetDirectoriesPropertyName, string.Join(";", codeAnalysisRuleSetDirectories));
+ }
+
var projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);
var expectedInputFiles = expectedInputs?