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?