From 20db91ae4e13db7bd82f02a6f1d4b0645df09620 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Fri, 1 Mar 2024 15:46:23 +0100 Subject: [PATCH 01/52] Initial codechanges of editorconfig support --- src/Analyzers.UnitTests/EndToEndTests.cs | 36 +- .../API/BuildAnalyzerConfiguration.cs | 36 + .../Analyzers/SharedOutputPathAnalyzer.cs | 2 +- .../Infrastructure/ConfigurationProvider.cs | 70 +- .../EditorConfig/EditorConfigFile.cs | 201 ++++++ .../EditorConfig/EditorConfigGlobsMatcher.cs | 615 ++++++++++++++++++ .../EditorConfig/EditorConfigParser.cs | 86 +++ .../EditorConfig/IEditorConfigParser.cs | 16 + src/Build/Microsoft.Build.csproj | 4 + 9 files changed, 993 insertions(+), 73 deletions(-) create mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs create mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs create mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs create mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs diff --git a/src/Analyzers.UnitTests/EndToEndTests.cs b/src/Analyzers.UnitTests/EndToEndTests.cs index 7a573b23b3f..a09a9c83924 100644 --- a/src/Analyzers.UnitTests/EndToEndTests.cs +++ b/src/Analyzers.UnitTests/EndToEndTests.cs @@ -80,36 +80,26 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode) - """; TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); TransientTestFile projectFile = _env.CreateFile(workFolder, "FooBar.csproj", contents); TransientTestFile projectFile2 = _env.CreateFile(workFolder, "FooBar-Copy.csproj", contents2); + TransientTestFile config = _env.CreateFile(workFolder, ".editorconfig", + """ + root=true - // var cache = new SimpleProjectRootElementCache(); - // ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile.Path, /*unused*/null, /*unused*/null, cache, false /*Not explicitly loaded - unused*/); + [*.csproj] + msbuild_analyzer.BC0101.IsEnabled=false + msbuild_analyzer.BC0101.severity=warning + msbuild_analyzer.COND0543.IsEnabled=false + msbuild_analyzer.COND0543.severity=Error + msbuild_analyzer.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly + msbuild_analyzer.COND0543.CustomSwitch=QWERTY - TransientTestFile config = _env.CreateFile(workFolder, "editorconfig.json", - /*lang=json,strict*/ - """ - { - "BC0101": { - "IsEnabled": true, - "Severity": "Error" - }, - "COND0543": { - "IsEnabled": false, - "Severity": "Error", - "EvaluationAnalysisScope": "AnalyzedProjectOnly", - "CustomSwitch": "QWERTY" - }, - "BLA": { - "IsEnabled": false - } - } - """); + msbuild_analyzer.BLA.IsEnabled=false + """); // OSX links /var into /private, which makes Path.GetTempPath() return "/var..." but Directory.GetCurrentDirectory return "/private/var...". // This discrepancy breaks path equality checks in analyzers if we pass to MSBuild full path to the initial project. @@ -123,7 +113,7 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode) _env.Output.WriteLine(output); success.ShouldBeTrue(); // The conflicting outputs warning appears - output.ShouldContain("BC0101"); + output.ShouldContain("warning : BC0101"); } } } diff --git a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs index 19ab210e097..e5927374c2f 100644 --- a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System; + namespace Microsoft.Build.Experimental.BuildCop; /// @@ -42,4 +45,37 @@ public class BuildAnalyzerConfiguration /// If some rules are enabled and some are not, the analyzer will be run and reports will be post-filtered. /// public bool? IsEnabled { get; internal init; } + + public static BuildAnalyzerConfiguration Create(Dictionary configDictionary) + { + return new() + { + EvaluationAnalysisScope = TryExtractValue("EvaluationAnalysisScope", configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, + Severity = TryExtractValue("severity", configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, + IsEnabled = TryExtractValue("IsEnabled", configDictionary, out bool test) ? test : null, + }; + } + + private static bool TryExtractValue(string key, Dictionary config, out T value) where T : struct + { + value = default; + if (!config.ContainsKey(key)) + { + return false; + } + + if (typeof(T) == typeof(bool)) + { + if (bool.TryParse(config[key], out bool boolValue)) + { + value = (T)(object)boolValue; + return true; + } + } + else if(typeof(T).IsEnum) + { + return Enum.TryParse(config[key], true, out value); + } + return false; + } } diff --git a/src/Build/BuildCop/Analyzers/SharedOutputPathAnalyzer.cs b/src/Build/BuildCop/Analyzers/SharedOutputPathAnalyzer.cs index 59eb865169b..ba6b6700a1b 100644 --- a/src/Build/BuildCop/Analyzers/SharedOutputPathAnalyzer.cs +++ b/src/Build/BuildCop/Analyzers/SharedOutputPathAnalyzer.cs @@ -19,7 +19,7 @@ internal sealed class SharedOutputPathAnalyzer : BuildAnalyzer public static BuildAnalyzerRule SupportedRule = new BuildAnalyzerRule("BC0101", "ConflictingOutputPath", "Two projects should not share their OutputPath nor IntermediateOutputPath locations", "Configuration", "Projects {0} and {1} have conflicting output paths: {2}.", - new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Warning, IsEnabled = true }); + new BuildAnalyzerConfiguration() { Severity = BuildAnalyzerResultSeverity.Info, IsEnabled = true }); public override string FriendlyName => "MSBuild.SharedOutputPathAnalyzer"; diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 48db5678d98..8a61b2c42c7 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -11,6 +11,7 @@ using System.Text.Json; using Microsoft.Build.Experimental.BuildCop; using System.Configuration; +using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; namespace Microsoft.Build.BuildCop.Infrastructure; @@ -19,56 +20,10 @@ namespace Microsoft.Build.BuildCop.Infrastructure; // Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) internal static class ConfigurationProvider { + private static IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) - private static readonly Dictionary _editorConfig = LoadConfiguration(); - - // This is just a testing implementation for quicker unblock of testing. - // Real implementation will use .editorconfig file. - // Sample json: - /////*lang=json,strict*/ - ////""" - //// { - //// "ABC123": { - //// "IsEnabled": true, - //// "Severity": "Info" - //// }, - //// "COND0543": { - //// "IsEnabled": false, - //// "Severity": "Error", - //// "EvaluationAnalysisScope": "AnalyzedProjectOnly", - //// "CustomSwitch": "QWERTY" - //// }, - //// "BLA": { - //// "IsEnabled": false - //// } - //// } - //// """ - // - // Plus there will need to be a mechanism of distinguishing different configs in different folders - // - e.g. - what to do if we analyze two projects (not sharing output path) and they have different .editorconfig files? - private static Dictionary LoadConfiguration() - { - const string configFileName = "editorconfig.json"; - string configPath = configFileName; - - if (!File.Exists(configPath)) - { - // TODO: pass the current project path - var dir = Environment.CurrentDirectory; - configPath = Path.Combine(dir, configFileName); - - if (!File.Exists(configPath)) - { - return new Dictionary(); - } - } - - var json = File.ReadAllText(configPath); - var DeserializationOptions = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; - return JsonSerializer.Deserialize>(json, DeserializationOptions) ?? - new Dictionary(); - } + private static readonly Dictionary _editorConfig = new Dictionary(); /// /// Gets the user specified unrecognized configuration for the given analyzer rule. @@ -120,7 +75,7 @@ public static BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( for (int idx = 0; idx < userConfigs.Length; idx++) { - configurations[idx] = ConfigurationProvider.MergeConfiguration( + configurations[idx] = MergeConfiguration( analyzer.SupportedRules[idx].Id, analyzer.SupportedRules[idx].DefaultConfiguration, userConfigs[idx]); @@ -157,6 +112,23 @@ public static BuildAnalyzerConfiguration GetUserConfiguration(string projectFull editorConfig = BuildAnalyzerConfiguration.Null; } + var config = s_editorConfigParser.Parse(projectFullPath); + var keyTosearch = $"msbuild_analyzer.{ruleId}."; + var dictionaryConfig = new Dictionary(); + + foreach (var kv in config) + { + if (kv.Key.StartsWith(keyTosearch, StringComparison.OrdinalIgnoreCase)) + { + dictionaryConfig[kv.Key.Replace(keyTosearch.ToLower(), "")] = kv.Value; + } + } + + if (dictionaryConfig.Any()) + { + return BuildAnalyzerConfiguration.Create(dictionaryConfig); + } + return editorConfig; } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs new file mode 100644 index 00000000000..043969129fb --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Note: +// Code and logic is copied from the https://github.com/dotnet/roslyn/blob/06d3f153ed6af6f2b78028a1e1e6ecc55c8ff101/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.cs +// with slight changes like: +// 1. Remove dependency from Source text. +// 2. Remove support of globalconfig + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +{ + internal partial class EditorConfigFile + { + // Matches EditorConfig section header such as "[*.{js,py}]", see https://editorconfig.org for details + private const string s_sectionMatcherPattern = @"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"; + + // Matches EditorConfig property such as "indent_style = space", see https://editorconfig.org for details + private const string s_propertyMatcherPattern = @"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"; + +#if NETCOREAPP + + [GeneratedRegex(s_sectionMatcherPattern)] + private static partial Regex GetSectionMatcherRegex(); + + [GeneratedRegex(s_propertyMatcherPattern)] + private static partial Regex GetPropertyMatcherRegex(); + +#else + private static readonly Regex s_sectionMatcher = new Regex(s_sectionMatcherPattern, RegexOptions.Compiled); + + private static readonly Regex s_propertyMatcher = new Regex(s_propertyMatcherPattern, RegexOptions.Compiled); + + private static Regex GetSectionMatcherRegex() => s_sectionMatcher; + + private static Regex GetPropertyMatcherRegex() => s_propertyMatcher; + +#endif + + internal Section GlobalSection { get; } + + /// + /// The path passed to during construction. + /// + internal string PathToFile { get; } + + internal ImmutableArray
NamedSections { get; } + + /// + /// Gets whether this editorconfig is a topmost editorconfig. + /// + internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val == "true"; + + private EditorConfigFile( + Section globalSection, + ImmutableArray
namedSections, + string pathToFile) + { + GlobalSection = globalSection; + NamedSections = namedSections; + PathToFile = pathToFile; + } + + /// + /// Parses an editor config file text located at the given path. No parsing + /// errors are reported. If any line contains a parse error, it is dropped. + /// + internal static EditorConfigFile Parse(string pathToFile) + { + if (pathToFile is null || !Path.IsPathRooted(pathToFile) || string.IsNullOrEmpty(Path.GetFileName(pathToFile)) || !File.Exists(pathToFile)) + { + throw new ArgumentException("Must be an absolute path to an editorconfig file", nameof(pathToFile)); + } + + Section? globalSection = null; + var namedSectionBuilder = ImmutableArray.CreateBuilder
(); + + // N.B. The editorconfig documentation is quite loose on property interpretation. + // Specifically, it says: + // Currently all properties and values are case-insensitive. + // They are lowercased when parsed. + // To accommodate this, we use a lower case Unicode mapping when adding to the + // dictionary, but we also use a case-insensitive key comparer when doing lookups + var activeSectionProperties = ImmutableDictionary.CreateBuilder(); + string activeSectionName = ""; + + using (StreamReader sr = new StreamReader(pathToFile)) + { + while (sr.Peek() >= 0) + { + string line = sr.ReadLine(); + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + if (IsComment(line)) + { + continue; + } + + var sectionMatches = GetSectionMatcherRegex().Matches(line); + if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) + { + addNewSection(); + + var sectionName = sectionMatches[0].Groups[1].Value; + Debug.Assert(!string.IsNullOrEmpty(sectionName)); + + activeSectionName = sectionName; + activeSectionProperties = ImmutableDictionary.CreateBuilder(); + continue; + } + + var propMatches = GetPropertyMatcherRegex().Matches(line); + if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) + { + var key = propMatches[0].Groups[1].Value.ToLower(); + var value = propMatches[0].Groups[2].Value.ToLower(); + + Debug.Assert(!string.IsNullOrEmpty(key)); + Debug.Assert(key == key.Trim()); + Debug.Assert(value == value?.Trim()); + + activeSectionProperties[key] = value ?? ""; + continue; + } + } + } + + // Add the last section + addNewSection(); + + return new EditorConfigFile(globalSection!, namedSectionBuilder.ToImmutable(), pathToFile); + + void addNewSection() + { + // Close out the previous section + var previousSection = new Section(activeSectionName, activeSectionProperties.ToImmutable()); + if (activeSectionName == "") + { + // This is the global section + globalSection = previousSection; + } + else + { + namedSectionBuilder.Add(previousSection); + } + } + } + + private static bool IsComment(string line) + { + foreach (char c in line) + { + if (!char.IsWhiteSpace(c)) + { + return c == '#' || c == ';'; + } + } + + return false; + } + + /// + /// Represents a named section of the editorconfig file, which consists of a name followed by a set + /// of key-value pairs. + /// + internal sealed class Section + { + public Section(string name, ImmutableDictionary properties) + { + Name = name; + Properties = properties; + } + + /// + /// For regular files, the name as present directly in the section specification of the editorconfig file. For sections in + /// global configs, this is the unescaped full file path. + /// + public string Name { get; } + + /// + /// Keys and values for this section. All keys are lower-cased according to the + /// EditorConfig specification and keys are compared case-insensitively. Otherwise, + /// the values are the literal values present in the source. + /// + public ImmutableDictionary Properties { get; } + } + } +} diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs new file mode 100644 index 00000000000..267d3fbd904 --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -0,0 +1,615 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Note: +// Copied from https://github.com/dotnet/roslyn/blob/06d3f153ed6af6f2b78028a1e1e6ecc55c8ff101/src/Compilers/Core/Portable/CommandLine/AnalyzerConfig.SectionNameMatching.cs +// with some changes to make it quicker to integrate into the MSBuild. +// Changes: +// 1. ArrayBuilder was replaced with List. +// 2. Exceptions. TODO: Wrap in try/catch blocks for proper reporting + + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +{ + internal class EditorConfigGlobsMatcher + { + internal readonly struct SectionNameMatcher + { + private readonly ImmutableArray<(int minValue, int maxValue)> _numberRangePairs; + // public for testing + public Regex Regex { get; } + + public SectionNameMatcher( + Regex regex, + ImmutableArray<(int minValue, int maxValue)> numberRangePairs) + { + Debug.Assert(regex.GetGroupNumbers().Length - 1 == numberRangePairs.Length); + Regex = regex; + _numberRangePairs = numberRangePairs; + } + + internal bool IsMatch(string s) + { + if (_numberRangePairs.IsEmpty) + { + return Regex.IsMatch(s); + } + + var match = Regex.Match(s); + if (!match.Success) + { + return false; + } + + Debug.Assert(match.Groups.Count - 1 == _numberRangePairs.Length); + for (int i = 0; i < _numberRangePairs.Length; i++) + { + var (minValue, maxValue) = _numberRangePairs[i]; + // Index 0 is the whole regex + if (!int.TryParse(match.Groups[i + 1].Value, out int matchedNum) || + matchedNum < minValue || + matchedNum > maxValue) + { + return false; + } + } + return true; + } + } + + /// + /// Takes a and creates a matcher that + /// matches the given language. Returns null if the section name is + /// invalid. + /// + internal static SectionNameMatcher? TryCreateSectionNameMatcher(string sectionName) + { + // An editorconfig section name is a language for recognizing file paths + // defined by the following grammar: + // + // ::= + // ::= | + // ::= "*" | "**" | "?" | | | + // ::= any unicode character + // ::= "{" "}" + // ::= | "," + // ::= "{" ".." "}" + // ::= "-" | + // ::= | + // ::= 0-9 + + var sb = new StringBuilder(); + sb.Append('^'); + + // EditorConfig matching depends on the whether or not there are + // directory separators and where they are located in the section + // name. Specifically, the editorconfig core parser says: + // https://github.com/editorconfig/editorconfig-core-c/blob/5d3996811e962a717a7d7fdd0a941192382241a7/src/lib/editorconfig.c#L231 + // + // Pattern would be: + // /dir/of/editorconfig/file[double_star]/[section] if section does not contain '/', + // /dir/of/editorconfig/file[section] if section starts with a '/', or + // /dir/of/editorconfig/file/[section] if section contains '/' but does not start with '/'. + + if (!sectionName.Contains("/")) + { + sb.Append(".*/"); + } + else if (sectionName[0] != '/') + { + sb.Append('/'); + } + + var lexer = new SectionNameLexer(sectionName); + var numberRangePairs = new List<(int minValue, int maxValue)>(); + if (!TryCompilePathList(ref lexer, sb, parsingChoice: false, numberRangePairs)) + { + numberRangePairs.Clear(); + return null; + } + sb.Append('$'); + + + var imArray = ImmutableArray.CreateBuilder<(int, int)>(numberRangePairs is null ? 0 : numberRangePairs.Count); + if (numberRangePairs?.Count > 0) + { + imArray.AddRange(numberRangePairs); + } + + return new SectionNameMatcher( + new Regex(sb.ToString(), RegexOptions.Compiled), + imArray.ToImmutableArray()); + } + + internal static string UnescapeSectionName(string sectionName) + { + var sb = new StringBuilder(); + SectionNameLexer lexer = new SectionNameLexer(sectionName); + while (!lexer.IsDone) + { + var tokenKind = lexer.Lex(); + if (tokenKind == TokenKind.SimpleCharacter) + { + sb.Append(lexer.EatCurrentCharacter()); + } + else + { + // We only call this on strings that were already passed through IsAbsoluteEditorConfigPath, so + // we shouldn't have any other token kinds here. + throw new Exception("my new exception"); + } + } + return sb.ToString(); + } + + internal static bool IsAbsoluteEditorConfigPath(string sectionName) + { + // NOTE: editorconfig paths must use '/' as a directory separator character on all OS. + + // on all unix systems this is thus a simple test: does the path start with '/' + // and contain no special chars? + + // on windows, a path can be either drive rooted or not (e.g. start with 'c:' or just '') + // in addition to being absolute or relative. + // for example c:myfile.cs is a relative path, but rooted on drive c: + // /myfile2.cs is an absolute path but rooted to the current drive. + + // in addition there are UNC paths and volume guids (see https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats) + // but these start with \\ (and thus '/' in editor config terminology) + + // in this implementation we choose to ignore the drive root for the purposes of + // determining validity. On windows c:/file.cs and /file.cs are both assumed to be + // valid absolute paths, even though the second one is technically relative to + // the current drive of the compiler working directory. + + // Note that this check has no impact on config correctness. Files on windows + // will still be compared using their full path (including drive root) so it's + // not possible to target the wrong file. It's just possible that the user won't + // receive a warning that this section is ignored on windows in this edge case. + + SectionNameLexer nameLexer = new SectionNameLexer(sectionName); + bool sawStartChar = false; + int logicalIndex = 0; + while (!nameLexer.IsDone) + { + if (nameLexer.Lex() != TokenKind.SimpleCharacter) + { + return false; + } + var simpleChar = nameLexer.EatCurrentCharacter(); + + // check the path starts with '/' + if (logicalIndex == 0) + { + if (simpleChar == '/') + { + sawStartChar = true; + } + else if (Path.DirectorySeparatorChar == '/') + { + return false; + } + } + // on windows we get a second chance to find the start char + else if (!sawStartChar && Path.DirectorySeparatorChar == '\\') + { + if (logicalIndex == 1 && simpleChar != ':') + { + return false; + } + else if (logicalIndex == 2) + { + if (simpleChar != '/') + { + return false; + } + else + { + sawStartChar = true; + } + } + } + logicalIndex++; + } + return sawStartChar; + } + + + /// + /// ::= | + /// ::= "*" | "**" | "?" | | | + /// ::= any unicode character + /// ::= "{" "}" + /// ::= | "," + /// ]]> + /// + private static bool TryCompilePathList( + ref SectionNameLexer lexer, + StringBuilder sb, + bool parsingChoice, + List<(int minValue, int maxValue)> numberRangePairs) + { + while (!lexer.IsDone) + { + var tokenKind = lexer.Lex(); + switch (tokenKind) + { + case TokenKind.BadToken: + // Parsing failure + return false; + case TokenKind.SimpleCharacter: + // Matches just this character + sb.Append(Regex.Escape(lexer.EatCurrentCharacter().ToString())); + break; + case TokenKind.Question: + // '?' matches any single character + sb.Append('.'); + break; + case TokenKind.Star: + // Matches any string of characters except directory separator + // Directory separator is defined in editorconfig spec as '/' + sb.Append("[^/]*"); + break; + case TokenKind.StarStar: + // Matches any string of characters + sb.Append(".*"); + break; + case TokenKind.OpenCurly: + // Back up token stream. The following helpers all expect a '{' + lexer.Position--; + // This is ambiguous between {num..num} and {item1,item2} + // We need to look ahead to disambiguate. Looking for {num..num} + // is easier because it can't be recursive. + (string numStart, string numEnd)? rangeOpt = TryParseNumberRange(ref lexer); + if (rangeOpt is null) + { + // Not a number range. Try a choice expression + if (!TryCompileChoice(ref lexer, sb, numberRangePairs)) + { + return false; + } + // Keep looping. There may be more after the '}'. + break; + } + else + { + (string numStart, string numEnd) = rangeOpt.GetValueOrDefault(); + if (int.TryParse(numStart, out var intStart) && int.TryParse(numEnd, out var intEnd)) + { + var pair = intStart < intEnd ? (intStart, intEnd) : (intEnd, intStart); + numberRangePairs.Add(pair); + // Group allowing any digit sequence. The validity will be checked outside of the regex + sb.Append("(-?[0-9]+)"); + // Keep looping + break; + } + return false; + } + case TokenKind.CloseCurly: + // Either the end of a choice, or a failed parse + return parsingChoice; + case TokenKind.Comma: + // The end of a choice section, or a failed parse + return parsingChoice; + case TokenKind.OpenBracket: + sb.Append('['); + if (!TryCompileCharacterClass(ref lexer, sb)) + { + return false; + } + break; + default: + throw new Exception("Exception from Matcher"); + } + } + // If we're parsing a choice we should not exit without a closing '}' + return !parsingChoice; + } + + /// + /// Compile a globbing character class of the form [...]. Returns true if + /// the character class was successfully compiled. False if there was a syntax + /// error. The starting character is expected to be directly after the '['. + /// + private static bool TryCompileCharacterClass(ref SectionNameLexer lexer, StringBuilder sb) + { + // [...] should match any of the characters in the brackets, with special + // behavior for four characters: '!' immediately after the opening bracket + // implies the negation of the character class, '-' implies matching + // between the locale-dependent range of the previous and next characters, + // '\' escapes the following character, and ']' ends the range + if (!lexer.IsDone && lexer.CurrentCharacter == '!') + { + sb.Append('^'); + lexer.Position++; + } + while (!lexer.IsDone) + { + var currentChar = lexer.EatCurrentCharacter(); + switch (currentChar) + { + case '-': + // '-' means the same thing in regex as it does in the glob, so + // put it in verbatim + sb.Append(currentChar); + break; + + case '\\': + // Escape the next char + if (lexer.IsDone) + { + return false; + } + sb.Append('\\'); + sb.Append(lexer.EatCurrentCharacter()); + break; + + case ']': + sb.Append(currentChar); + return true; + + default: + sb.Append(Regex.Escape(currentChar.ToString())); + break; + } + } + // Stream ended without a closing bracket + return false; + } + + /// + /// Parses choice defined by the following grammar: + /// ::= "{" "}" + /// ::= | "," + /// ]]> + /// + private static bool TryCompileChoice( + ref SectionNameLexer lexer, + StringBuilder sb, + List<(int, int)> numberRangePairs) + { + if (lexer.Lex() != TokenKind.OpenCurly) + { + return false; + } + + // Start a non-capturing group for the choice + sb.Append("(?:"); + + // We start immediately after a '{' + // Try to compile the nested + while (TryCompilePathList(ref lexer, sb, parsingChoice: true, numberRangePairs)) + { + // If we've successfully compiled a the last token should + // have been a ',' or a '}' + char lastChar = lexer[lexer.Position - 1]; + if (lastChar == ',') + { + // Another option + sb.Append('|'); + } + else if (lastChar == '}') + { + // Close out the capture group + sb.Append(')'); + return true; + } + else + { + throw new Exception("Exception another one"); + } + } + + // Propagate failure + return false; + } + + /// + /// Parses range defined by the following grammar. + /// ::= "{" ".." "}" + /// ::= "-" | + /// ::= | + /// ::= 0-9 + /// ]]> + /// + private static (string numStart, string numEnd)? TryParseNumberRange(ref SectionNameLexer lexer) + { + var saved = lexer.Position; + if (lexer.Lex() != TokenKind.OpenCurly) + { + lexer.Position = saved; + return null; + } + + var numStart = lexer.TryLexNumber(); + if (numStart is null) + { + // Not a number + lexer.Position = saved; + return null; + } + + // The next two characters must be ".." + if (!lexer.TryEatCurrentCharacter(out char c) || c != '.' || + !lexer.TryEatCurrentCharacter(out c) || c != '.') + { + lexer.Position = saved; + return null; + } + + // Now another number + var numEnd = lexer.TryLexNumber(); + if (numEnd is null || lexer.IsDone || lexer.Lex() != TokenKind.CloseCurly) + { + // Not a number or no '}' + lexer.Position = saved; + return null; + } + + return (numStart, numEnd); + } + + private struct SectionNameLexer + { + private readonly string _sectionName; + + public int Position { get; set; } + + public SectionNameLexer(string sectionName) + { + _sectionName = sectionName; + Position = 0; + } + + public bool IsDone => Position >= _sectionName.Length; + + public TokenKind Lex() + { + int lexemeStart = Position; + switch (_sectionName[Position]) + { + case '*': + { + int nextPos = Position + 1; + if (nextPos < _sectionName.Length && + _sectionName[nextPos] == '*') + { + Position += 2; + return TokenKind.StarStar; + } + else + { + Position++; + return TokenKind.Star; + } + } + + case '?': + Position++; + return TokenKind.Question; + + case '{': + Position++; + return TokenKind.OpenCurly; + + case ',': + Position++; + return TokenKind.Comma; + + case '}': + Position++; + return TokenKind.CloseCurly; + + case '[': + Position++; + return TokenKind.OpenBracket; + + case '\\': + { + // Backslash escapes the next character + Position++; + if (IsDone) + { + return TokenKind.BadToken; + } + + return TokenKind.SimpleCharacter; + } + + default: + // Don't increment position, since caller needs to fetch the character + return TokenKind.SimpleCharacter; + } + } + + public char CurrentCharacter => _sectionName[Position]; + + /// + /// Call after getting from + /// + public char EatCurrentCharacter() => _sectionName[Position++]; + + /// + /// Returns false if there are no more characters in the lex stream. + /// Otherwise, produces the next character in the stream and returns true. + /// + public bool TryEatCurrentCharacter(out char nextChar) + { + if (IsDone) + { + nextChar = default; + return false; + } + else + { + nextChar = EatCurrentCharacter(); + return true; + } + } + + public char this[int position] => _sectionName[position]; + + /// + /// Returns the string representation of a decimal integer, or null if + /// the current lexeme is not an integer. + /// + public string? TryLexNumber() + { + bool start = true; + var sb = new StringBuilder(); + + while (!IsDone) + { + char currentChar = CurrentCharacter; + if (start && currentChar == '-') + { + Position++; + sb.Append('-'); + } + else if (char.IsDigit(currentChar)) + { + Position++; + sb.Append(currentChar); + } + else + { + break; + } + start = false; + } + + var str = sb.ToString(); + return str.Length == 0 || str == "-" + ? null + : str; + } + } + + private enum TokenKind + { + BadToken, + SimpleCharacter, + Star, + StarStar, + Question, + OpenCurly, + CloseCurly, + Comma, + DoubleDot, + OpenBracket, + } + } +} diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs new file mode 100644 index 00000000000..8e1c77fb82b --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing.Design; +using System.IO; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Shared; +using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +{ + public class EditorConfigParser : IEditorConfigParser + { + private const string EditorconfigFile = ".editorconfig"; + private Dictionary> filePathConfigCache; + + internal EditorConfigParser() + { + filePathConfigCache = new Dictionary>(); + } + + public Dictionary Parse(string filePath) + { + if (filePathConfigCache.ContainsKey(filePath)) + { + return filePathConfigCache[filePath]; + } + + var editorConfigDataFromFilesList = new List(); + var directoryOfTheProject = Path.GetDirectoryName(filePath); + var editorConfigFile = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); + + while (editorConfigFile != string.Empty) + { + var editorConfigData = EditorConfigFile.Parse(editorConfigFile); + editorConfigDataFromFilesList.Add(editorConfigData); + + if (editorConfigData.IsRoot) + { + break; + } + else + { + editorConfigFile = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFile))); + } + } + + var resultingDictionary = new Dictionary(); + + if (editorConfigDataFromFilesList.Any()) + { + editorConfigDataFromFilesList.Reverse(); + + foreach (var configData in editorConfigDataFromFilesList) + { + foreach (var section in configData.NamedSections) + { + SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); + if (sectionNameMatcher != null) + { + if (sectionNameMatcher.Value.IsMatch(NormalizeWithForwardSlash(filePath))) + { + foreach (var property in section.Properties) + { + resultingDictionary[property.Key] = property.Value; + } + } + } + } + } + } + + filePathConfigCache[filePath] = resultingDictionary; + return resultingDictionary; + } + + private string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); + } + +} diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs new file mode 100644 index 00000000000..c0c3c510897 --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +{ + public interface IEditorConfigParser + { + Dictionary Parse(string filePath); + } +} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 024368af5f9..79128da19aa 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -176,6 +176,10 @@ + + + + From e010c367a3e2324ca83200d6cad88884c11ed91f Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 4 Mar 2024 10:52:11 +0100 Subject: [PATCH 02/52] Build error fixes --- src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs | 2 +- .../BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs | 4 ++-- .../Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs | 2 +- .../Infrastructure/EditorConfig/EditorConfigParser.cs | 3 +-- .../Infrastructure/EditorConfig/IEditorConfigParser.cs | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 8a61b2c42c7..9b15433cc72 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -17,7 +17,7 @@ namespace Microsoft.Build.BuildCop.Infrastructure; // TODO: https://github.com/dotnet/msbuild/issues/9628 -// Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) +// Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) internal static class ConfigurationProvider { private static IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs index 043969129fb..c42c8a82f98 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -49,7 +49,7 @@ internal partial class EditorConfigFile internal Section GlobalSection { get; } /// - /// The path passed to during construction. + /// The path passed to during construction. /// internal string PathToFile { get; } @@ -97,7 +97,7 @@ internal static EditorConfigFile Parse(string pathToFile) { while (sr.Peek() >= 0) { - string line = sr.ReadLine(); + string? line = sr.ReadLine(); if (string.IsNullOrWhiteSpace(line)) { diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index 267d3fbd904..801496b1965 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -68,7 +68,7 @@ internal bool IsMatch(string s) } /// - /// Takes a and creates a matcher that + /// Takes a and creates a matcher that /// matches the given language. Returns null if the section name is /// invalid. /// diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index 8e1c77fb82b..b496991461a 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -15,7 +15,7 @@ namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig { - public class EditorConfigParser : IEditorConfigParser + internal class EditorConfigParser : IEditorConfigParser { private const string EditorconfigFile = ".editorconfig"; private Dictionary> filePathConfigCache; @@ -82,5 +82,4 @@ public Dictionary Parse(string filePath) private string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); } - } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs index c0c3c510897..c40685f5524 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs @@ -9,8 +9,8 @@ namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig { - public interface IEditorConfigParser + internal interface IEditorConfigParser { - Dictionary Parse(string filePath); + public Dictionary Parse(string filePath); } } From 44983c20724bbf335c71aaa3895d92c2ef2c082f Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 4 Mar 2024 11:27:15 +0100 Subject: [PATCH 03/52] fix the configuration --- src/Analyzers.UnitTests/EndToEndTests.cs | 6 +++--- src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Analyzers.UnitTests/EndToEndTests.cs b/src/Analyzers.UnitTests/EndToEndTests.cs index a09a9c83924..0a4e1b96438 100644 --- a/src/Analyzers.UnitTests/EndToEndTests.cs +++ b/src/Analyzers.UnitTests/EndToEndTests.cs @@ -90,11 +90,11 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode) root=true [*.csproj] - msbuild_analyzer.BC0101.IsEnabled=false - msbuild_analyzer.BC0101.severity=warning + msbuild_analyzer.BC0101.IsEnabled=true + msbuild_analyzer.BC0101.Severity=warning msbuild_analyzer.COND0543.IsEnabled=false - msbuild_analyzer.COND0543.severity=Error + msbuild_analyzer.COND0543.Severity=Error msbuild_analyzer.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly msbuild_analyzer.COND0543.CustomSwitch=QWERTY diff --git a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs index e5927374c2f..9eb2ae16ad1 100644 --- a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs @@ -46,13 +46,19 @@ public class BuildAnalyzerConfiguration ///
public bool? IsEnabled { get; internal init; } + /// + /// Creates a object based on the provided configuration dictionary. + /// If key, equals to the name of the property in lowercase, exists in the dictionary => the value is parsed and assigned to the instance property value. + /// + /// The configuration dictionary containing the settings for the build analyzer. + /// A new instance of with the specified settings. public static BuildAnalyzerConfiguration Create(Dictionary configDictionary) { return new() { - EvaluationAnalysisScope = TryExtractValue("EvaluationAnalysisScope", configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, - Severity = TryExtractValue("severity", configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, - IsEnabled = TryExtractValue("IsEnabled", configDictionary, out bool test) ? test : null, + EvaluationAnalysisScope = TryExtractValue(nameof(EvaluationAnalysisScope).ToLower(), configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, + Severity = TryExtractValue(nameof(Severity).ToLower(), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, + IsEnabled = TryExtractValue(nameof(IsEnabled).ToLower(), configDictionary, out bool test) ? test : null, }; } From b2a05ad8cbfa3769ffc93f7eb57e87f1f1b41364 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 4 Mar 2024 14:32:23 +0100 Subject: [PATCH 04/52] Share the visibility of the Microsoft.Build for unit test purposes. Add Roslyn editorconfig tests for section matcher --- src/Analyzers.UnitTests/EditorConfig_Tests.cs | 591 ++++++++++++++++++ ...Microsoft.Build.Analyzers.UnitTests.csproj | 32 +- src/Build/AssemblyInfo.cs | 2 + .../API/BuildAnalyzerConfiguration.cs | 2 +- .../EditorConfig/EditorConfigParser.cs | 3 +- 5 files changed, 597 insertions(+), 33 deletions(-) create mode 100644 src/Analyzers.UnitTests/EditorConfig_Tests.cs diff --git a/src/Analyzers.UnitTests/EditorConfig_Tests.cs b/src/Analyzers.UnitTests/EditorConfig_Tests.cs new file mode 100644 index 00000000000..ea4188c5f35 --- /dev/null +++ b/src/Analyzers.UnitTests/EditorConfig_Tests.cs @@ -0,0 +1,591 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.UnitTests; +using Xunit; +using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +#nullable disable + +namespace Microsoft.Build.Analyzers.UnitTests +{ + public class EditorConfig_Tests + { + // Section Matchin Test cases: https://github.com/dotnet/roslyn/blob/ba163e712b01358a217065eec8a4a82f94a7efd5/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs#L337 + #region Section Matching Tests + [Fact] + public void SimpleNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc").Value; + Assert.Equal("^.*/abc$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc")); + Assert.False(matcher.IsMatch("/aabc")); + Assert.False(matcher.IsMatch("/ abc")); + Assert.False(matcher.IsMatch("/cabc")); + } + + [Fact] + public void StarOnlyMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*").Value; + Assert.Equal("^.*/[^/]*$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc")); + Assert.True(matcher.IsMatch("/123")); + Assert.True(matcher.IsMatch("/abc/123")); + } + + [Fact] + public void StarNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.cs").Value; + Assert.Equal("^.*/[^/]*\\.cs$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/123.cs")); + Assert.True(matcher.IsMatch("/dir/subpath.cs")); + // Only '/' is defined as a directory separator, so the caller + // is responsible for converting any other machine directory + // separators to '/' before matching + Assert.True(matcher.IsMatch("/dir\\subpath.cs")); + + Assert.False(matcher.IsMatch("/abc.vb")); + } + + [Fact] + public void StarStarNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("**.cs").Value; + Assert.Equal("^.*/.*\\.cs$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/dir/subpath.cs")); + } + + [Fact] + public void EscapeDot() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("...").Value; + Assert.Equal("^.*/\\.\\.\\.$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/...")); + Assert.True(matcher.IsMatch("/subdir/...")); + Assert.False(matcher.IsMatch("/aaa")); + Assert.False(matcher.IsMatch("/???")); + Assert.False(matcher.IsMatch("/abc")); + } + + [Fact] + public void EndBackslashMatch() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc\\"); + Assert.Null(matcher); + } + + [Fact] + public void QuestionMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab?def").Value; + Assert.Equal("^.*/ab.def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abcdef")); + Assert.True(matcher.IsMatch("/ab?def")); + Assert.True(matcher.IsMatch("/abzdef")); + Assert.True(matcher.IsMatch("/ab/def")); + Assert.True(matcher.IsMatch("/ab\\def")); + } + + [Fact] + public void LiteralBackslash() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab\\\\c").Value; + Assert.Equal("^.*/ab\\\\c$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ab\\c")); + Assert.False(matcher.IsMatch("/ab/c")); + Assert.False(matcher.IsMatch("/ab\\\\c")); + } + + [Fact] + public void LiteralStars() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\***\\*\\**").Value; + Assert.Equal("^.*/\\*.*\\*\\*[^/]*$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/*ab/cd**efg*")); + Assert.False(matcher.IsMatch("/ab/cd**efg*")); + Assert.False(matcher.IsMatch("/*ab/cd*efg*")); + Assert.False(matcher.IsMatch("/*ab/cd**ef/gh")); + } + + [Fact] + public void LiteralQuestions() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\??\\?*\\??").Value; + Assert.Equal("^.*/\\?.\\?[^/]*\\?.$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/?a?cde?f")); + Assert.True(matcher.IsMatch("/???????f")); + Assert.False(matcher.IsMatch("/aaaaaaaa")); + Assert.False(matcher.IsMatch("/aa?cde?f")); + Assert.False(matcher.IsMatch("/?a?cdexf")); + Assert.False(matcher.IsMatch("/?axcde?f")); + } + + [Fact] + public void LiteralBraces() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\{\\}def").Value; + Assert.Equal(@"^.*/abc\{}def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc{}def")); + Assert.True(matcher.IsMatch("/subdir/abc{}def")); + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/abc}{def")); + } + + [Fact] + public void LiteralComma() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\,def").Value; + Assert.Equal("^.*/abc,def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc,def")); + Assert.True(matcher.IsMatch("/subdir/abc,def")); + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/abc\\,def")); + Assert.False(matcher.IsMatch("/abc`def")); + } + + [Fact] + public void SimpleChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.{cs,vb,fs}").Value; + Assert.Equal("^.*/[^/]*\\.(?:cs|vb|fs)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.vb")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/subdir/abc.cs")); + Assert.True(matcher.IsMatch("/subdir/abc.vb")); + Assert.True(matcher.IsMatch("/subdir/abc.fs")); + + Assert.False(matcher.IsMatch("/abcxcs")); + Assert.False(matcher.IsMatch("/abcxvb")); + Assert.False(matcher.IsMatch("/abcxfs")); + Assert.False(matcher.IsMatch("/subdir/abcxcs")); + Assert.False(matcher.IsMatch("/subdir/abcxcb")); + Assert.False(matcher.IsMatch("/subdir/abcxcs")); + } + + [Fact] + public void OneChoiceHasSlashes() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs,subdir/test.vb}").Value; + // This is an interesting case that may be counterintuitive. A reasonable understanding + // of the section matching could interpret the choice as generating multiple identical + // sections, so [{a, b, c}] would be equivalent to [a] ... [b] ... [c] with all of the + // same properties in each section. This is somewhat true, but the rules of how the matching + // prefixes are constructed violate this assumption because they are defined as whether or + // not a section contains a slash, not whether any of the choices contain a slash. So while + // [*.cs] usually translates into '**/*.cs' because it contains no slashes, the slashes in + // the second choice make this into '/*.cs', effectively matching only files in the root + // directory of the match, instead of all subdirectories. + Assert.Equal("^/(?:[^/]*\\.cs|subdir/test\\.vb)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.vb")); + + Assert.False(matcher.IsMatch("/subdir/test.cs")); + Assert.False(matcher.IsMatch("/subdir/subdir/test.vb")); + Assert.False(matcher.IsMatch("/test.vb")); + } + + [Fact] + public void EmptyChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{}").Value; + Assert.Equal("^.*/(?:)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/")); + Assert.True(matcher.IsMatch("/subdir/")); + Assert.False(matcher.IsMatch("/.")); + Assert.False(matcher.IsMatch("/anything")); + } + + [Fact] + public void SingleChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs}").Value; + Assert.Equal("^.*/(?:[^/]*\\.cs)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.cs")); + Assert.False(matcher.IsMatch("test.vb")); + Assert.False(matcher.IsMatch("testxcs")); + } + + [Fact] + public void UnmatchedBraces() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("{{{{}}"); + Assert.Null(matcher); + } + + [Fact] + public void CommaOutsideBraces() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc,def"); + Assert.Null(matcher); + } + + [Fact] + public void RecursiveChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{test{.cs,.vb},other.{a{bb,cc}}}").Value; + Assert.Equal("^.*/(?:test(?:\\.cs|\\.vb)|other\\.(?:a(?:bb|cc)))$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/test.vb")); + Assert.True(matcher.IsMatch("/subdir/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.vb")); + Assert.True(matcher.IsMatch("/other.abb")); + Assert.True(matcher.IsMatch("/other.acc")); + + Assert.False(matcher.IsMatch("/test.fs")); + Assert.False(matcher.IsMatch("/other.bbb")); + Assert.False(matcher.IsMatch("/other.ccc")); + Assert.False(matcher.IsMatch("/subdir/other.bbb")); + Assert.False(matcher.IsMatch("/subdir/other.ccc")); + } + + [Fact] + public void DashChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{-}cd{-,}ef").Value; + Assert.Equal("^.*/ab(?:-)cd(?:-|)ef$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ab-cd-ef")); + Assert.True(matcher.IsMatch("/ab-cdef")); + + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/ab--cd-ef")); + Assert.False(matcher.IsMatch("/ab--cd--ef")); + } + + [Fact] + public void MiddleMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{cs,vb,fs}cd").Value; + Assert.Equal("^.*/ab(?:cs|vb|fs)cd$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abcscd")); + Assert.True(matcher.IsMatch("/abvbcd")); + Assert.True(matcher.IsMatch("/abfscd")); + + Assert.False(matcher.IsMatch("/abcs")); + Assert.False(matcher.IsMatch("/abcd")); + Assert.False(matcher.IsMatch("/vbcd")); + } + + private static IEnumerable<(string, string)> RangeAndInverse(string s1, string s2) + { + yield return (s1, s2); + yield return (s2, s1); + } + + [Fact] + public void NumberMatch() + { + foreach (var (i1, i2) in RangeAndInverse("0", "10")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/10")); + Assert.True(matcher.IsMatch("/5")); + Assert.True(matcher.IsMatch("/000005")); + Assert.False(matcher.IsMatch("/-1")); + Assert.False(matcher.IsMatch("/-00000001")); + Assert.False(matcher.IsMatch("/11")); + } + } + + [Fact] + public void NumberMatchNegativeRange() + { + foreach (var (i1, i2) in RangeAndInverse("-10", "0")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/-10")); + Assert.True(matcher.IsMatch("/-5")); + Assert.False(matcher.IsMatch("/1")); + Assert.False(matcher.IsMatch("/-11")); + Assert.False(matcher.IsMatch("/--0")); + } + } + + [Fact] + public void NumberMatchNegToPos() + { + foreach (var (i1, i2) in RangeAndInverse("-10", "10")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/-5")); + Assert.True(matcher.IsMatch("/5")); + Assert.True(matcher.IsMatch("/-10")); + Assert.True(matcher.IsMatch("/10")); + Assert.False(matcher.IsMatch("/-11")); + Assert.False(matcher.IsMatch("/11")); + Assert.False(matcher.IsMatch("/--0")); + } + } + + [Fact] + public void MultipleNumberRanges() + { + foreach (var matchString in new[] { "a{-10..0}b{0..10}", "a{0..-10}b{10..0}" }) + { + var matcher = TryCreateSectionNameMatcher(matchString).Value; + + Assert.True(matcher.IsMatch("/a0b0")); + Assert.True(matcher.IsMatch("/a-5b0")); + Assert.True(matcher.IsMatch("/a-5b5")); + Assert.True(matcher.IsMatch("/a-5b10")); + Assert.True(matcher.IsMatch("/a-10b10")); + Assert.True(matcher.IsMatch("/a-10b0")); + Assert.True(matcher.IsMatch("/a-0b0")); + Assert.True(matcher.IsMatch("/a-0b-0")); + + Assert.False(matcher.IsMatch("/a-11b10")); + Assert.False(matcher.IsMatch("/a-11b10")); + Assert.False(matcher.IsMatch("/a-10b11")); + } + } + + [Fact] + public void BadNumberRanges() + { + var matcherOpt = TryCreateSectionNameMatcher("{0.."); + + Assert.Null(matcherOpt); + + var matcher = TryCreateSectionNameMatcher("{0..}").Value; + + Assert.True(matcher.IsMatch("/0..")); + Assert.False(matcher.IsMatch("/0")); + Assert.False(matcher.IsMatch("/0.")); + Assert.False(matcher.IsMatch("/0abc")); + + matcher = TryCreateSectionNameMatcher("{0..A}").Value; + Assert.True(matcher.IsMatch("/0..A")); + Assert.False(matcher.IsMatch("/0")); + Assert.False(matcher.IsMatch("/0abc")); + + // The reference implementation uses atoi here so we can presume + // numbers out of range of Int32 are not well supported + matcherOpt = TryCreateSectionNameMatcher($"{{0..{UInt32.MaxValue}}}"); + + Assert.Null(matcherOpt); + } + + [Fact] + public void CharacterClassSimple() + { + var matcher = TryCreateSectionNameMatcher("*.[cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[cf]s$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.False(matcher.IsMatch("/abc.vs")); + } + + [Fact] + public void CharacterClassNegative() + { + var matcher = TryCreateSectionNameMatcher("*.[!cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[^cf]s$", matcher.Regex.ToString()); + + Assert.False(matcher.IsMatch("/abc.cs")); + Assert.False(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/abc.vs")); + Assert.True(matcher.IsMatch("/abc.xs")); + Assert.False(matcher.IsMatch("/abc.vxs")); + } + + [Fact] + public void CharacterClassCaret() + { + var matcher = TryCreateSectionNameMatcher("*.[^cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[\^cf]s$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/abc.^s")); + Assert.False(matcher.IsMatch("/abc.vs")); + Assert.False(matcher.IsMatch("/abc.xs")); + Assert.False(matcher.IsMatch("/abc.vxs")); + } + + [Fact] + public void CharacterClassRange() + { + var matcher = TryCreateSectionNameMatcher("[0-9]x").Value; + Assert.Equal("^.*/[0-9]x$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/0x")); + Assert.True(matcher.IsMatch("/1x")); + Assert.True(matcher.IsMatch("/9x")); + Assert.False(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/00x")); + } + + [Fact] + public void CharacterClassNegativeRange() + { + var matcher = TryCreateSectionNameMatcher("[!0-9]x").Value; + Assert.Equal("^.*/[^0-9]x$", matcher.Regex.ToString()); + + Assert.False(matcher.IsMatch("/0x")); + Assert.False(matcher.IsMatch("/1x")); + Assert.False(matcher.IsMatch("/9x")); + Assert.True(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/00x")); + } + + [Fact] + public void CharacterClassRangeAndChoice() + { + var matcher = TryCreateSectionNameMatcher("[ab0-9]x").Value; + Assert.Equal("^.*/[ab0-9]x$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ax")); + Assert.True(matcher.IsMatch("/bx")); + Assert.True(matcher.IsMatch("/0x")); + Assert.True(matcher.IsMatch("/1x")); + Assert.True(matcher.IsMatch("/9x")); + Assert.False(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/0ax")); + } + + [Fact] + public void CharacterClassOpenEnded() + { + var matcher = TryCreateSectionNameMatcher("["); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassEscapedOpenEnded() + { + var matcher = TryCreateSectionNameMatcher(@"[\]"); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassEscapeAtEnd() + { + var matcher = TryCreateSectionNameMatcher(@"[\"); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassOpenBracketInside() + { + var matcher = TryCreateSectionNameMatcher(@"[[a]bc").Value; + + Assert.True(matcher.IsMatch("/abc")); + Assert.True(matcher.IsMatch("/[bc")); + Assert.False(matcher.IsMatch("/ab")); + Assert.False(matcher.IsMatch("/[b")); + Assert.False(matcher.IsMatch("/bc")); + Assert.False(matcher.IsMatch("/ac")); + Assert.False(matcher.IsMatch("/[c")); + + Assert.Equal(@"^.*/[\[a]bc$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassStartingDash() + { + var matcher = TryCreateSectionNameMatcher(@"[-ac]bd").Value; + + Assert.True(matcher.IsMatch("/abd")); + Assert.True(matcher.IsMatch("/cbd")); + Assert.True(matcher.IsMatch("/-bd")); + Assert.False(matcher.IsMatch("/bbd")); + Assert.False(matcher.IsMatch("/-cd")); + Assert.False(matcher.IsMatch("/bcd")); + + Assert.Equal(@"^.*/[-ac]bd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEndingDash() + { + var matcher = TryCreateSectionNameMatcher(@"[ac-]bd").Value; + + Assert.True(matcher.IsMatch("/abd")); + Assert.True(matcher.IsMatch("/cbd")); + Assert.True(matcher.IsMatch("/-bd")); + Assert.False(matcher.IsMatch("/bbd")); + Assert.False(matcher.IsMatch("/-cd")); + Assert.False(matcher.IsMatch("/bcd")); + + Assert.Equal(@"^.*/[ac-]bd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEndBracketAfter() + { + var matcher = TryCreateSectionNameMatcher(@"[ab]]cd").Value; + + Assert.True(matcher.IsMatch("/a]cd")); + Assert.True(matcher.IsMatch("/b]cd")); + Assert.False(matcher.IsMatch("/acd")); + Assert.False(matcher.IsMatch("/bcd")); + Assert.False(matcher.IsMatch("/acd")); + + Assert.Equal(@"^.*/[ab]]cd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEscapeBackslash() + { + var matcher = TryCreateSectionNameMatcher(@"[ab\\]cd").Value; + + Assert.True(matcher.IsMatch("/acd")); + Assert.True(matcher.IsMatch("/bcd")); + Assert.True(matcher.IsMatch("/\\cd")); + Assert.False(matcher.IsMatch("/dcd")); + Assert.False(matcher.IsMatch("/\\\\cd")); + Assert.False(matcher.IsMatch("/cd")); + + Assert.Equal(@"^.*/[ab\\]cd$", matcher.Regex.ToString()); + } + + [Fact] + public void EscapeOpenBracket() + { + var matcher = TryCreateSectionNameMatcher(@"ab\[cd").Value; + + Assert.True(matcher.IsMatch("/ab[cd")); + Assert.False(matcher.IsMatch("/ab[[cd")); + Assert.False(matcher.IsMatch("/abc")); + Assert.False(matcher.IsMatch("/abd")); + + Assert.Equal(@"^.*/ab\[cd$", matcher.Regex.ToString()); + } + #endregion + } +} diff --git a/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj b/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj index 876d03d2e07..97111737da9 100644 --- a/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj +++ b/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj @@ -1,8 +1,5 @@ - - - $(LatestDotNetCoreForMSBuild) @@ -39,35 +36,8 @@ - - Shared\FileUtilities.cs - - - Shared\TempFileUtilities.cs - - - Shared\ErrorUtilities.cs - - - Shared\EscapingUtilities.cs - - - Shared\BuildEnvironmentHelper.cs - - - Shared\ProcessExtensions.cs - - - Shared\ResourceUtilities.cs - - - Shared\ExceptionHandling.cs - - - Shared\FileUtilitiesRegex.cs - - Shared\AssemblyResources.cs + SharedUtilities\AssemblyResources.cs diff --git a/src/Build/AssemblyInfo.cs b/src/Build/AssemblyInfo.cs index 6e57337863d..62a5e403943 100644 --- a/src/Build/AssemblyInfo.cs +++ b/src/Build/AssemblyInfo.cs @@ -23,6 +23,8 @@ [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("Microsoft.Build.Analyzers.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] + // DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, diff --git a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs index 9eb2ae16ad1..589a6305806 100644 --- a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs @@ -58,7 +58,7 @@ public static BuildAnalyzerConfiguration Create(Dictionary confi { EvaluationAnalysisScope = TryExtractValue(nameof(EvaluationAnalysisScope).ToLower(), configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, Severity = TryExtractValue(nameof(Severity).ToLower(), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, - IsEnabled = TryExtractValue(nameof(IsEnabled).ToLower(), configDictionary, out bool test) ? test : null, + IsEnabled = TryExtractValue(nameof(IsEnabled).ToLower(), configDictionary, out bool isEnabled) ? isEnabled : null, }; } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index b496991461a..1b74ee3da56 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -38,6 +38,7 @@ public Dictionary Parse(string filePath) while (editorConfigFile != string.Empty) { + // TODO: Change the API of EditorconfigFile Parse to accept the text value instead of file path. var editorConfigData = EditorConfigFile.Parse(editorConfigFile); editorConfigDataFromFilesList.Add(editorConfigData); @@ -80,6 +81,6 @@ public Dictionary Parse(string filePath) return resultingDictionary; } - private string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); + private static string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); } } From af05c7f2048549cb3e370de1d686cf7b85b1a0ef Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 4 Mar 2024 16:10:33 +0100 Subject: [PATCH 05/52] remote static from configurationProvider --- .../Infrastructure/BuildCopCentralContext.cs | 8 ++++- .../Infrastructure/BuildCopManagerProvider.cs | 14 +++++---- .../Infrastructure/ConfigurationProvider.cs | 30 +++++++++---------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/BuildCopCentralContext.cs b/src/Build/BuildCop/Infrastructure/BuildCopCentralContext.cs index 7ad82c37c1f..5ad6765b799 100644 --- a/src/Build/BuildCop/Infrastructure/BuildCopCentralContext.cs +++ b/src/Build/BuildCop/Infrastructure/BuildCopCentralContext.cs @@ -15,6 +15,12 @@ namespace Microsoft.Build.BuildCop.Infrastructure; /// internal sealed class BuildCopCentralContext { + private readonly ConfigurationProvider _configurationProvider; + internal BuildCopCentralContext(ConfigurationProvider configurationProvider) + { + _configurationProvider = configurationProvider; + } + private record CallbackRegistry( List<(BuildAnalyzerWrapper, Action>)> EvaluatedPropertiesActions, List<(BuildAnalyzerWrapper, Action>)> ParsedItemsActions) @@ -112,7 +118,7 @@ private void RunRegisteredActions( else { configPerRule = - ConfigurationProvider.GetMergedConfigurations(projectFullPath, + _configurationProvider.GetMergedConfigurations(projectFullPath, analyzerCallback.Item1.BuildAnalyzer); if (configPerRule.All(c => !c.IsEnabled)) { diff --git a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs index c826f3854bc..41e2c6fa01e 100644 --- a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs +++ b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs @@ -67,7 +67,8 @@ public void InitializeComponent(IBuildComponentHost host) private sealed class BuildCopManager : IBuildCopManager { private readonly TracingReporter _tracingReporter = new TracingReporter(); - private readonly BuildCopCentralContext _buildCopCentralContext = new(); + private readonly ConfigurationProvider _configurationProvider = new ConfigurationProvider(); + private readonly BuildCopCentralContext _buildCopCentralContext; private readonly ILoggingService _loggingService; private readonly List _analyzersRegistry =[]; private readonly bool[] _enabledDataSources = new bool[(int)BuildCopDataSource.ValuesCount]; @@ -115,6 +116,7 @@ public void ProcessAnalyzerAcquisition(AnalyzerAcquisitionData acquisitionData) internal BuildCopManager(ILoggingService loggingService) { _loggingService = loggingService; + _buildCopCentralContext = new(_configurationProvider); _buildEventsProcessor = new(_buildCopCentralContext); } @@ -180,7 +182,7 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont if (analyzerFactoryContext.MaterializedAnalyzer == null) { BuildAnalyzerConfiguration[] userConfigs = - ConfigurationProvider.GetUserConfigurations(projectFullPath, analyzerFactoryContext.RuleIds); + _configurationProvider.GetUserConfigurations(projectFullPath, analyzerFactoryContext.RuleIds); if (userConfigs.All(c => !(c.IsEnabled ?? analyzerFactoryContext.IsEnabledByDefault))) { @@ -189,7 +191,7 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont } CustomConfigurationData[] customConfigData = - ConfigurationProvider.GetCustomConfigurations(projectFullPath, analyzerFactoryContext.RuleIds); + _configurationProvider.GetCustomConfigurations(projectFullPath, analyzerFactoryContext.RuleIds); ConfigurationContext configurationContext = ConfigurationContext.FromDataEnumeration(customConfigData); @@ -208,7 +210,7 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont $"The analyzer '{analyzer.FriendlyName}' exposes rules '{analyzer.SupportedRules.Select(r => r.Id).ToCsvString()}', but different rules were declared during registration: '{analyzerFactoryContext.RuleIds.ToCsvString()}'"); } - configurations = ConfigurationProvider.GetMergedConfigurations(userConfigs, analyzer); + configurations = _configurationProvider.GetMergedConfigurations(userConfigs, analyzer); // technically all analyzers rules could be disabled, but that would mean // that the provided 'IsEnabledByDefault' value wasn't correct - the only @@ -223,9 +225,9 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont { wrapper = analyzerFactoryContext.MaterializedAnalyzer; - configurations = ConfigurationProvider.GetMergedConfigurations(projectFullPath, wrapper.BuildAnalyzer); + configurations = _configurationProvider.GetMergedConfigurations(projectFullPath, wrapper.BuildAnalyzer); - ConfigurationProvider.CheckCustomConfigurationDataValidity(projectFullPath, + _configurationProvider.CheckCustomConfigurationDataValidity(projectFullPath, analyzerFactoryContext.RuleIds[0]); // Update the wrapper diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 9b15433cc72..3c05da1b245 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -18,12 +18,12 @@ namespace Microsoft.Build.BuildCop.Infrastructure; // TODO: https://github.com/dotnet/msbuild/issues/9628 // Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) -internal static class ConfigurationProvider +internal class ConfigurationProvider { - private static IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); + private IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) - private static readonly Dictionary _editorConfig = new Dictionary(); + private readonly Dictionary _editorConfig = new Dictionary(); /// /// Gets the user specified unrecognized configuration for the given analyzer rule. @@ -35,7 +35,7 @@ internal static class ConfigurationProvider /// /// /// - public static CustomConfigurationData GetCustomConfiguration(string projectFullPath, string ruleId) + public CustomConfigurationData GetCustomConfiguration(string projectFullPath, string ruleId) { return CustomConfigurationData.Null; } @@ -47,27 +47,27 @@ public static CustomConfigurationData GetCustomConfiguration(string projectFullP /// /// If CustomConfigurationData differs in a build for a same ruleId /// - public static void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) + public void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { // TBD } - public static BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( + public BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( string projectFullPath, BuildAnalyzer analyzer) => FillConfiguration(projectFullPath, analyzer.SupportedRules, GetMergedConfiguration); - public static BuildAnalyzerConfiguration[] GetUserConfigurations( + public BuildAnalyzerConfiguration[] GetUserConfigurations( string projectFullPath, IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetUserConfiguration); - public static CustomConfigurationData[] GetCustomConfigurations( + public CustomConfigurationData[] GetCustomConfigurations( string projectFullPath, IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetCustomConfiguration); - public static BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( + public BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( BuildAnalyzerConfiguration[] userConfigs, BuildAnalyzer analyzer) { @@ -84,7 +84,7 @@ public static BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( return configurations; } - private static TConfig[] FillConfiguration(string projectFullPath, IReadOnlyList ruleIds, Func configurationProvider) + private TConfig[] FillConfiguration(string projectFullPath, IReadOnlyList ruleIds, Func configurationProvider) { TConfig[] configurations = new TConfig[ruleIds.Count]; for (int i = 0; i < ruleIds.Count; i++) @@ -105,7 +105,7 @@ private static TConfig[] FillConfiguration(string projectFullPat /// /// /// - public static BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) + public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) { if (!_editorConfig.TryGetValue(ruleId, out BuildAnalyzerConfiguration? editorConfig)) { @@ -139,10 +139,10 @@ public static BuildAnalyzerConfiguration GetUserConfiguration(string projectFull /// /// /// - public static BuildAnalyzerConfigurationInternal GetMergedConfiguration(string projectFullPath, BuildAnalyzerRule analyzerRule) + public BuildAnalyzerConfigurationInternal GetMergedConfiguration(string projectFullPath, BuildAnalyzerRule analyzerRule) => GetMergedConfiguration(projectFullPath, analyzerRule.Id, analyzerRule.DefaultConfiguration); - public static BuildAnalyzerConfigurationInternal MergeConfiguration( + public BuildAnalyzerConfigurationInternal MergeConfiguration( string ruleId, BuildAnalyzerConfiguration defaultConfig, BuildAnalyzerConfiguration editorConfig) @@ -152,13 +152,13 @@ public static BuildAnalyzerConfigurationInternal MergeConfiguration( isEnabled: GetConfigValue(editorConfig, defaultConfig, cfg => cfg.IsEnabled), severity: GetConfigValue(editorConfig, defaultConfig, cfg => cfg.Severity)); - private static BuildAnalyzerConfigurationInternal GetMergedConfiguration( + private BuildAnalyzerConfigurationInternal GetMergedConfiguration( string projectFullPath, string ruleId, BuildAnalyzerConfiguration defaultConfig) => MergeConfiguration(ruleId, defaultConfig, GetUserConfiguration(projectFullPath, ruleId)); - private static T GetConfigValue( + private T GetConfigValue( BuildAnalyzerConfiguration editorConfigValue, BuildAnalyzerConfiguration defaultValue, Func propertyGetter) where T : struct From 80620149bbbcd946e21ee22cc73749a28aa7258e Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 6 Mar 2024 12:49:17 +0100 Subject: [PATCH 06/52] Copy the tests for config file parsing and adjust based on the current implementetion. --- src/Analyzers.UnitTests/EditorConfig_Tests.cs | 499 ++++++++++++++++++ .../Infrastructure/ConfigurationProvider.cs | 16 +- .../EditorConfig/EditorConfigFile.cs | 102 ++-- .../EditorConfig/EditorConfigParser.cs | 9 +- src/Shared/FileUtilities.cs | 2 +- 5 files changed, 563 insertions(+), 65 deletions(-) diff --git a/src/Analyzers.UnitTests/EditorConfig_Tests.cs b/src/Analyzers.UnitTests/EditorConfig_Tests.cs index ea4188c5f35..e02c87a21c3 100644 --- a/src/Analyzers.UnitTests/EditorConfig_Tests.cs +++ b/src/Analyzers.UnitTests/EditorConfig_Tests.cs @@ -2,10 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; +using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Xunit; using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; @@ -587,5 +590,501 @@ public void EscapeOpenBracket() Assert.Equal(@"^.*/ab\[cd$", matcher.Regex.ToString()); } #endregion + + #region AssertEqualityComparer + + private class AssertEqualityComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); + + private static bool CanBeNull() + { + var type = typeof(T); + return !type.GetTypeInfo().IsValueType || + (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + } + + public static bool IsNull(T @object) + { + if (!CanBeNull()) + { + return false; + } + + return object.Equals(@object, default(T)); + } + + public static bool Equals(T left, T right) + { + return Instance.Equals(left, right); + } + + bool IEqualityComparer.Equals(T x, T y) + { + if (CanBeNull()) + { + if (object.Equals(x, default(T))) + { + return object.Equals(y, default(T)); + } + + if (object.Equals(y, default(T))) + { + return false; + } + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + if (x is IEquatable equatable) + { + return equatable.Equals(y); + } + + if (x is IComparable comparableT) + { + return comparableT.CompareTo(y) == 0; + } + + if (x is IComparable comparable) + { + return comparable.CompareTo(y) == 0; + } + + var enumerableX = x as IEnumerable; + var enumerableY = y as IEnumerable; + + if (enumerableX != null && enumerableY != null) + { + var enumeratorX = enumerableX.GetEnumerator(); + var enumeratorY = enumerableY.GetEnumerator(); + + while (true) + { + bool hasNextX = enumeratorX.MoveNext(); + bool hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + { + return hasNextX == hasNextY; + } + + if (!Equals(enumeratorX.Current, enumeratorY.Current)) + { + return false; + } + } + } + + return object.Equals(x, y); + } + + int IEqualityComparer.GetHashCode(T obj) + { + throw new NotImplementedException(); + } + } + + #endregion + + + #region Parsing Tests + + public static void SetEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, string message = null, string itemSeparator = "\r\n", Func itemInspector = null) + { + var expectedSet = new HashSet(expected, comparer); + var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual); + Assert.True(result, message); + } + + public static void Equal( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer = null, + string message = null, + string itemSeparator = null, + Func itemInspector = null, + string expectedValueSourcePath = null, + int expectedValueSourceLine = 0) + { + if (expected == null) + { + Assert.Null(actual); + } + else + { + Assert.NotNull(actual); + } + + if (SequenceEqual(expected, actual, comparer)) + { + return; + } + + Assert.True(false); + } + + private static bool SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null) + { + if (ReferenceEquals(expected, actual)) + { + return true; + } + + var enumerator1 = expected.GetEnumerator(); + var enumerator2 = actual.GetEnumerator(); + + while (true) + { + var hasNext1 = enumerator1.MoveNext(); + var hasNext2 = enumerator2.MoveNext(); + + if (hasNext1 != hasNext2) + { + return false; + } + + if (!hasNext1) + { + break; + } + + var value1 = enumerator1.Current; + var value2 = enumerator2.Current; + + if (!(comparer != null ? comparer.Equals(value1, value2) : AssertEqualityComparer.Equals(value1, value2))) + { + return false; + } + } + + return true; + } + + public static KeyValuePair Create(K key, V value) + { + return new KeyValuePair(key, value); + } + + [Fact] + public void SimpleCase() + { + var config = EditorConfigFile.Parse(""" +root = true + +# Comment1 +# Comment2 +################################## + +my_global_prop = my_global_val + +[*.cs] +my_prop = my_val +"""); + Assert.Equal("", config.GlobalSection.Name); + var properties = config.GlobalSection.Properties; + + SetEqual( + new[] { Create("my_global_prop", "my_global_val") , + Create("root", "true") }, + properties); + + var namedSections = config.NamedSections; + Assert.Equal("*.cs", namedSections[0].Name); + SetEqual( + new[] { Create("my_prop", "my_val") }, + namedSections[0].Properties); + + Assert.True(config.IsRoot); + } + + + [Fact] + //[WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] + public void ConfigWithEscapedValues() + { + var config = EditorConfigFile.Parse(@"is_global = true + +[c:/\{f\*i\?le1\}.cs] +build_metadata.Compile.ToRetrieve = abc123 + +[c:/f\,ile\#2.cs] +build_metadata.Compile.ToRetrieve = def456 + +[c:/f\;i\!le\[3\].cs] +build_metadata.Compile.ToRetrieve = ghi789 +"); + + var namedSections = config.NamedSections; + Assert.Equal("c:/\\{f\\*i\\?le1\\}.cs", namedSections[0].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "abc123") }, + namedSections[0].Properties + ); + + Assert.Equal("c:/f\\,ile\\#2.cs", namedSections[1].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "def456") }, + namedSections[1].Properties + ); + + Assert.Equal("c:/f\\;i\\!le\\[3\\].cs", namedSections[2].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "ghi789") }, + namedSections[2].Properties + ); + } + + /* + [Fact] + [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] + public void CanGetSectionsWithSpecialCharacters() + { + var config = ParseConfigFile(@"is_global = true + +[/home/foo/src/\{releaseid\}.cs] +build_metadata.Compile.ToRetrieve = abc123 + +[/home/foo/src/Pages/\#foo/HomePage.cs] +build_metadata.Compile.ToRetrieve = def456 +"); + + var set = AnalyzerConfigSet.Create(ImmutableArray.Create(config)); + + var sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/{releaseid}.cs"); + Assert.Equal("abc123", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); + + sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/Pages/#foo/HomePage.cs"); + Assert.Equal("def456", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); + }*/ + + [Fact] + public void MissingClosingBracket() + { + var config = EditorConfigFile.Parse(@" +[*.cs +my_prop = my_val"); + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop", "my_val") }, + properties); + + Assert.Equal(0, config.NamedSections.Length); + } + + + [Fact] + public void EmptySection() + { + var config = EditorConfigFile.Parse(@" +[] +my_prop = my_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_val") }, properties); + Assert.Equal(0, config.NamedSections.Length); + } + + + [Fact] + public void CaseInsensitivePropKey() + { + var config = EditorConfigFile.Parse(@" +my_PROP = my_VAL"); + var properties = config.GlobalSection.Properties; + + Assert.True(properties.TryGetValue("my_PrOp", out var val)); + Assert.Equal("my_VAL", val); + Assert.Equal("my_prop", properties.Keys.Single()); + } + + // there is no reversed keys support for msbuild + /*[Fact] + public void NonReservedKeyPreservedCaseVal() + { + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedKeys.Select(k => "MY_" + k + " = MY_VAL"))); + AssertEx.SetEqual( + AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create("my_" + k, "MY_VAL")).ToList(), + config.GlobalSection.Properties); + }*/ + + + [Fact] + public void DuplicateKeys() + { + var config = EditorConfigFile.Parse(@" +my_prop = my_val +my_prop = my_other_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); + } + + + [Fact] + public void DuplicateKeysCasing() + { + var config = EditorConfigFile.Parse(@" +my_prop = my_val +my_PROP = my_other_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); + } + + + [Fact] + public void MissingKey() + { + var config = EditorConfigFile.Parse(@" += my_val1 +my_prop = my_val2"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop", "my_val2") }, + properties); + } + + + + [Fact] + public void MissingVal() + { + var config = EditorConfigFile.Parse(@" +my_prop1 = +my_prop2 = my_val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop1", ""), + Create("my_prop2", "my_val") }, + properties); + } + + + [Fact] + public void SpacesInProperties() + { + var config = EditorConfigFile.Parse(@" +my prop1 = my_val1 +my_prop2 = my val2"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop2", "my val2") }, + properties); + } + + + [Fact] + public void EndOfLineComments() + { + var config = EditorConfigFile.Parse(@" +my_prop2 = my val2 # Comment"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop2", "my val2") }, + properties); + } + + [Fact] + public void SymbolsStartKeys() + { + var config = EditorConfigFile.Parse(@" +@!$abc = my_val1 +@!$\# = my_val2"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(0, properties.Count); + } + + + [Fact] + public void EqualsAndColon() + { + var config = EditorConfigFile.Parse(@" +my:key1 = my_val +my_key2 = my:val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my", "key1 = my_val"), + Create("my_key2", "my:val")}, + properties); + } + + [Fact] + public void SymbolsInProperties() + { + var config = EditorConfigFile.Parse(@" +my@key1 = my_val +my_key2 = my@val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_key2", "my@val") }, + properties); + } + + [Fact] + public void LongLines() + { + // This example is described in the Python ConfigParser as allowing + // line continuation via the RFC 822 specification, section 3.1.1 + // LONG HEADER FIELDS. The VS parser does not accept this as a + // valid parse for an editorconfig file. We follow similarly. + var config = EditorConfigFile.Parse(@" +long: this value continues + in the next line"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("long", "this value continues") }, + properties); + } + + + [Fact] + public void CaseInsensitiveRoot() + { + var config = EditorConfigFile.Parse(@" +RoOt = TruE"); + Assert.True(config.IsRoot); + } + + + /* + Reserved values are not supported at the moment + [Fact] + public void ReservedValues() + { + int index = 0; + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedValues.Select(v => "MY_KEY" + (index++) + " = " + v.ToUpperInvariant()))); + index = 0; + AssertEx.SetEqual( + AnalyzerConfig.ReservedValues.Select(v => KeyValuePair.Create("my_key" + (index++), v)).ToList(), + config.GlobalSection.Properties); + } + */ + + /* + [Fact] + public void ReservedKeys() + { + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedKeys.Select(k => k + " = MY_VAL"))); + AssertEx.SetEqual( + AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create(k, "my_val")).ToList(), + config.GlobalSection.Properties); + } + */ + #endregion } } diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 3c05da1b245..e4492b65610 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -112,7 +112,21 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s editorConfig = BuildAnalyzerConfiguration.Null; } - var config = s_editorConfigParser.Parse(projectFullPath); + var config = new Dictionary(); + + try + { + Console.WriteLine("Config are fetching"); + config = s_editorConfigParser.Parse(projectFullPath); + Console.WriteLine("Config are fetched"); + } + catch (Exception ex) + { + // do not break the build because of the failed editor config parsing + Console.WriteLine(ex.ToString()); + Debug.WriteLine(ex); + } + var keyTosearch = $"msbuild_analyzer.{ruleId}."; var dictionaryConfig = new Dictionary(); diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs index c42c8a82f98..6ef8cc957a6 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -6,6 +6,7 @@ // with slight changes like: // 1. Remove dependency from Source text. // 2. Remove support of globalconfig +// 3. Remove the FilePath and receive only the text using System; using System.Collections.Generic; @@ -48,39 +49,27 @@ internal partial class EditorConfigFile internal Section GlobalSection { get; } - /// - /// The path passed to during construction. - /// - internal string PathToFile { get; } - internal ImmutableArray
NamedSections { get; } /// /// Gets whether this editorconfig is a topmost editorconfig. /// - internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val == "true"; + internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val?.ToLower() == "true"; private EditorConfigFile( Section globalSection, - ImmutableArray
namedSections, - string pathToFile) + ImmutableArray
namedSections) { GlobalSection = globalSection; NamedSections = namedSections; - PathToFile = pathToFile; } /// /// Parses an editor config file text located at the given path. No parsing /// errors are reported. If any line contains a parse error, it is dropped. /// - internal static EditorConfigFile Parse(string pathToFile) + internal static EditorConfigFile Parse(string text) { - if (pathToFile is null || !Path.IsPathRooted(pathToFile) || string.IsNullOrEmpty(Path.GetFileName(pathToFile)) || !File.Exists(pathToFile)) - { - throw new ArgumentException("Must be an absolute path to an editorconfig file", nameof(pathToFile)); - } - Section? globalSection = null; var namedSectionBuilder = ImmutableArray.CreateBuilder
(); @@ -90,58 +79,54 @@ internal static EditorConfigFile Parse(string pathToFile) // They are lowercased when parsed. // To accommodate this, we use a lower case Unicode mapping when adding to the // dictionary, but we also use a case-insensitive key comparer when doing lookups - var activeSectionProperties = ImmutableDictionary.CreateBuilder(); + var activeSectionProperties = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); string activeSectionName = ""; + var lines = string.IsNullOrEmpty(text) ? Array.Empty() : text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); - using (StreamReader sr = new StreamReader(pathToFile)) + foreach(var line in lines) { - while (sr.Peek() >= 0) + if (string.IsNullOrWhiteSpace(line)) { - string? line = sr.ReadLine(); - - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - if (IsComment(line)) - { - continue; - } - - var sectionMatches = GetSectionMatcherRegex().Matches(line); - if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) - { - addNewSection(); - - var sectionName = sectionMatches[0].Groups[1].Value; - Debug.Assert(!string.IsNullOrEmpty(sectionName)); - - activeSectionName = sectionName; - activeSectionProperties = ImmutableDictionary.CreateBuilder(); - continue; - } - - var propMatches = GetPropertyMatcherRegex().Matches(line); - if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) - { - var key = propMatches[0].Groups[1].Value.ToLower(); - var value = propMatches[0].Groups[2].Value.ToLower(); - - Debug.Assert(!string.IsNullOrEmpty(key)); - Debug.Assert(key == key.Trim()); - Debug.Assert(value == value?.Trim()); - - activeSectionProperties[key] = value ?? ""; - continue; - } + continue; + } + + if (IsComment(line)) + { + continue; + } + + var sectionMatches = GetSectionMatcherRegex().Matches(line); + if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) + { + addNewSection(); + + var sectionName = sectionMatches[0].Groups[1].Value; + Debug.Assert(!string.IsNullOrEmpty(sectionName)); + + activeSectionName = sectionName; + activeSectionProperties = ImmutableDictionary.CreateBuilder(); + continue; + } + + var propMatches = GetPropertyMatcherRegex().Matches(line); + if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) + { + var key = propMatches[0].Groups[1].Value.ToLower(); + var value = propMatches[0].Groups[2].Value; + + Debug.Assert(!string.IsNullOrEmpty(key)); + Debug.Assert(key == key.Trim()); + Debug.Assert(value == value?.Trim()); + + activeSectionProperties[key] = value ?? ""; + continue; } } // Add the last section addNewSection(); - return new EditorConfigFile(globalSection!, namedSectionBuilder.ToImmutable(), pathToFile); + return new EditorConfigFile(globalSection!, namedSectionBuilder.ToImmutable()); void addNewSection() { @@ -192,8 +177,7 @@ public Section(string name, ImmutableDictionary properties) /// /// Keys and values for this section. All keys are lower-cased according to the - /// EditorConfig specification and keys are compared case-insensitively. Otherwise, - /// the values are the literal values present in the source. + /// EditorConfig specification and keys are compared case-insensitively. /// public ImmutableDictionary Properties { get; } } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index 1b74ee3da56..c0034659b79 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -33,16 +33,17 @@ public Dictionary Parse(string filePath) } var editorConfigDataFromFilesList = new List(); + var directoryOfTheProject = Path.GetDirectoryName(filePath); var editorConfigFile = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); while (editorConfigFile != string.Empty) { - // TODO: Change the API of EditorconfigFile Parse to accept the text value instead of file path. - var editorConfigData = EditorConfigFile.Parse(editorConfigFile); - editorConfigDataFromFilesList.Add(editorConfigData); + var editorConfigfileContent = File.ReadAllText(editorConfigFile); + var editorConfig = EditorConfigFile.Parse(editorConfigfileContent); + editorConfigDataFromFilesList.Add(editorConfig); - if (editorConfigData.IsRoot) + if (editorConfig.IsRoot) { break; } diff --git a/src/Shared/FileUtilities.cs b/src/Shared/FileUtilities.cs index b1cee220d8b..5b43208d2af 100644 --- a/src/Shared/FileUtilities.cs +++ b/src/Shared/FileUtilities.cs @@ -1458,7 +1458,7 @@ internal static string GetDirectoryNameOfFileAbove(string startingDirectory, str while (lookInDirectory != null); // When we didn't find the location, then return an empty string - return String.Empty; + return string.Empty; } /// From 20433af0b9da1bfc921436d97647f4769b83503b Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 6 Mar 2024 15:54:24 +0100 Subject: [PATCH 07/52] style fixes --- src/Analyzers.UnitTests/EditorConfig_Tests.cs | 222 +++++++++--------- 1 file changed, 107 insertions(+), 115 deletions(-) diff --git a/src/Analyzers.UnitTests/EditorConfig_Tests.cs b/src/Analyzers.UnitTests/EditorConfig_Tests.cs index e02c87a21c3..64f4e9d2832 100644 --- a/src/Analyzers.UnitTests/EditorConfig_Tests.cs +++ b/src/Analyzers.UnitTests/EditorConfig_Tests.cs @@ -19,6 +19,105 @@ namespace Microsoft.Build.Analyzers.UnitTests { public class EditorConfig_Tests { + + #region AssertEqualityComparer + private sealed class AssertEqualityComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); + + private static bool CanBeNull() + { + var type = typeof(T); + return !type.GetTypeInfo().IsValueType || + (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + } + + public static bool IsNull(T @object) + { + if (!CanBeNull()) + { + return false; + } + + return object.Equals(@object, default(T)); + } + + public static bool Equals(T left, T right) + { + return Instance.Equals(left, right); + } + + bool IEqualityComparer.Equals(T x, T y) + { + if (CanBeNull()) + { + if (object.Equals(x, default(T))) + { + return object.Equals(y, default(T)); + } + + if (object.Equals(y, default(T))) + { + return false; + } + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + if (x is IEquatable equatable) + { + return equatable.Equals(y); + } + + if (x is IComparable comparableT) + { + return comparableT.CompareTo(y) == 0; + } + + if (x is IComparable comparable) + { + return comparable.CompareTo(y) == 0; + } + + var enumerableX = x as IEnumerable; + var enumerableY = y as IEnumerable; + + if (enumerableX != null && enumerableY != null) + { + var enumeratorX = enumerableX.GetEnumerator(); + var enumeratorY = enumerableY.GetEnumerator(); + + while (true) + { + bool hasNextX = enumeratorX.MoveNext(); + bool hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + { + return hasNextX == hasNextY; + } + + if (!Equals(enumeratorX.Current, enumeratorY.Current)) + { + return false; + } + } + } + + return object.Equals(x, y); + } + + int IEqualityComparer.GetHashCode(T obj) + { + throw new NotImplementedException(); + } + } + + #endregion + // Section Matchin Test cases: https://github.com/dotnet/roslyn/blob/ba163e712b01358a217065eec8a4a82f94a7efd5/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs#L337 #region Section Matching Tests [Fact] @@ -591,124 +690,20 @@ public void EscapeOpenBracket() } #endregion - #region AssertEqualityComparer - - private class AssertEqualityComparer : IEqualityComparer - { - public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); - - private static bool CanBeNull() - { - var type = typeof(T); - return !type.GetTypeInfo().IsValueType || - (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); - } - - public static bool IsNull(T @object) - { - if (!CanBeNull()) - { - return false; - } - - return object.Equals(@object, default(T)); - } - - public static bool Equals(T left, T right) - { - return Instance.Equals(left, right); - } - - bool IEqualityComparer.Equals(T x, T y) - { - if (CanBeNull()) - { - if (object.Equals(x, default(T))) - { - return object.Equals(y, default(T)); - } - - if (object.Equals(y, default(T))) - { - return false; - } - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - if (x is IEquatable equatable) - { - return equatable.Equals(y); - } - - if (x is IComparable comparableT) - { - return comparableT.CompareTo(y) == 0; - } - - if (x is IComparable comparable) - { - return comparable.CompareTo(y) == 0; - } - - var enumerableX = x as IEnumerable; - var enumerableY = y as IEnumerable; - - if (enumerableX != null && enumerableY != null) - { - var enumeratorX = enumerableX.GetEnumerator(); - var enumeratorY = enumerableY.GetEnumerator(); - - while (true) - { - bool hasNextX = enumeratorX.MoveNext(); - bool hasNextY = enumeratorY.MoveNext(); - - if (!hasNextX || !hasNextY) - { - return hasNextX == hasNextY; - } - - if (!Equals(enumeratorX.Current, enumeratorY.Current)) - { - return false; - } - } - } - - return object.Equals(x, y); - } - - int IEqualityComparer.GetHashCode(T obj) - { - throw new NotImplementedException(); - } - } - - #endregion - - #region Parsing Tests - public static void SetEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, string message = null, string itemSeparator = "\r\n", Func itemInspector = null) + private static void SetEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, string message = null) { var expectedSet = new HashSet(expected, comparer); var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual); Assert.True(result, message); } - public static void Equal( + private static void Equal( IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, - string message = null, - string itemSeparator = null, - Func itemInspector = null, - string expectedValueSourcePath = null, - int expectedValueSourceLine = 0) + string message = null) { if (expected == null) { @@ -724,7 +719,7 @@ public static void Equal( return; } - Assert.True(false); + Assert.True(false, message); } private static bool SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null) @@ -803,7 +798,7 @@ public void SimpleCase() [Fact] - //[WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] + // [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] public void ConfigWithEscapedValues() { var config = EditorConfigFile.Parse(@"is_global = true @@ -822,20 +817,17 @@ public void ConfigWithEscapedValues() Assert.Equal("c:/\\{f\\*i\\?le1\\}.cs", namedSections[0].Name); Equal( new[] { Create("build_metadata.compile.toretrieve", "abc123") }, - namedSections[0].Properties - ); + namedSections[0].Properties); Assert.Equal("c:/f\\,ile\\#2.cs", namedSections[1].Name); Equal( new[] { Create("build_metadata.compile.toretrieve", "def456") }, - namedSections[1].Properties - ); + namedSections[1].Properties); Assert.Equal("c:/f\\;i\\!le\\[3\\].cs", namedSections[2].Name); Equal( new[] { Create("build_metadata.compile.toretrieve", "ghi789") }, - namedSections[2].Properties - ); + namedSections[2].Properties); } /* From d94d09037a930c40e2be37423ef4d4dcbd8587c4 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 6 Mar 2024 15:55:49 +0100 Subject: [PATCH 08/52] Remove the comments --- src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index e4492b65610..f1a8bd5dd1a 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -116,14 +116,10 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s try { - Console.WriteLine("Config are fetching"); config = s_editorConfigParser.Parse(projectFullPath); - Console.WriteLine("Config are fetched"); } catch (Exception ex) { - // do not break the build because of the failed editor config parsing - Console.WriteLine(ex.ToString()); Debug.WriteLine(ex); } From 1e6386076ceda2053e70e46622b194062789a5ca Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 6 Mar 2024 16:34:07 +0100 Subject: [PATCH 09/52] Handle caching properly --- .../Infrastructure/ConfigurationProvider.cs | 9 ++-- .../EditorConfig/EditorConfigParser.cs | 42 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index f1a8bd5dd1a..3af3c50d59a 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -107,13 +107,14 @@ private TConfig[] FillConfiguration(string projectFullPath, IRea /// public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) { - if (!_editorConfig.TryGetValue(ruleId, out BuildAnalyzerConfiguration? editorConfig)) + var cacheKey = $"{ruleId}-projectFullPath "; + + if (!_editorConfig.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? editorConfig)) { editorConfig = BuildAnalyzerConfiguration.Null; } var config = new Dictionary(); - try { config = s_editorConfigParser.Parse(projectFullPath); @@ -136,9 +137,11 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s if (dictionaryConfig.Any()) { - return BuildAnalyzerConfiguration.Create(dictionaryConfig); + editorConfig = BuildAnalyzerConfiguration.Create(dictionaryConfig); } + _editorConfig[cacheKey] = editorConfig; + return editorConfig; } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index c0034659b79..f2fff4b9e32 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -18,20 +18,21 @@ namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig internal class EditorConfigParser : IEditorConfigParser { private const string EditorconfigFile = ".editorconfig"; - private Dictionary> filePathConfigCache; + private Dictionary editorConfigFileCache; internal EditorConfigParser() { - filePathConfigCache = new Dictionary>(); + editorConfigFileCache = new Dictionary(); } public Dictionary Parse(string filePath) { - if (filePathConfigCache.ContainsKey(filePath)) - { - return filePathConfigCache[filePath]; - } + var editorConfigs = EditorConfigFileDiscovery(filePath); + return MergeEditorConfigFiles(editorConfigs, filePath); + } + public IList EditorConfigFileDiscovery(string filePath) + { var editorConfigDataFromFilesList = new List(); var directoryOfTheProject = Path.GetDirectoryName(filePath); @@ -39,8 +40,19 @@ public Dictionary Parse(string filePath) while (editorConfigFile != string.Empty) { - var editorConfigfileContent = File.ReadAllText(editorConfigFile); - var editorConfig = EditorConfigFile.Parse(editorConfigfileContent); + EditorConfigFile editorConfig; + + if (editorConfigFileCache.ContainsKey(editorConfigFile)) + { + editorConfig = editorConfigFileCache[editorConfigFile]; + } + else + { + var editorConfigfileContent = File.ReadAllText(editorConfigFile); + editorConfig = EditorConfigFile.Parse(editorConfigfileContent); + editorConfigFileCache[editorConfigFile] = editorConfig; + } + editorConfigDataFromFilesList.Add(editorConfig); if (editorConfig.IsRoot) @@ -53,13 +65,18 @@ public Dictionary Parse(string filePath) } } + return editorConfigDataFromFilesList; + } + + public Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) + { var resultingDictionary = new Dictionary(); - if (editorConfigDataFromFilesList.Any()) + if (editorConfigFiles.Any()) { - editorConfigDataFromFilesList.Reverse(); - - foreach (var configData in editorConfigDataFromFilesList) + editorConfigFiles.Reverse(); + + foreach (var configData in editorConfigFiles) { foreach (var section in configData.NamedSections) { @@ -78,7 +95,6 @@ public Dictionary Parse(string filePath) } } - filePathConfigCache[filePath] = resultingDictionary; return resultingDictionary; } From 8d8c1e9bc8d408299b03f65558666b1349b36b5a Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 6 Mar 2024 17:07:42 +0100 Subject: [PATCH 10/52] Add custom configuration data implementation --- .../Infrastructure/ConfigurationProvider.cs | 58 +++++++++++-------- .../Infrastructure/CustomConfigurationData.cs | 16 ++++- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 3af3c50d59a..494960d6e5b 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -17,7 +17,6 @@ namespace Microsoft.Build.BuildCop.Infrastructure; // TODO: https://github.com/dotnet/msbuild/issues/9628 -// Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) internal class ConfigurationProvider { private IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); @@ -37,7 +36,13 @@ internal class ConfigurationProvider /// public CustomConfigurationData GetCustomConfiguration(string projectFullPath, string ruleId) { - return CustomConfigurationData.Null; + var configuration = GetConfiguration(projectFullPath, ruleId); + + if (configuration is null || !configuration.Any()) + { + return CustomConfigurationData.Null; + } + return new CustomConfigurationData(ruleId, configuration); } /// @@ -95,25 +100,8 @@ private TConfig[] FillConfiguration(string projectFullPath, IRea return configurations; } - /// - /// Gets effective user specified (or default) configuration for the given analyzer rule. - /// The configuration values CAN be null upon this operation. - /// - /// The configuration module should as well check that BuildAnalyzerConfigurationInternal.EvaluationAnalysisScope - /// for all rules is equal - otherwise it should error out. - /// - /// - /// - /// - public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) + internal Dictionary GetConfiguration(string projectFullPath, string ruleId) { - var cacheKey = $"{ruleId}-projectFullPath "; - - if (!_editorConfig.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? editorConfig)) - { - editorConfig = BuildAnalyzerConfiguration.Null; - } - var config = new Dictionary(); try { @@ -123,7 +111,7 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s { Debug.WriteLine(ex); } - + var keyTosearch = $"msbuild_analyzer.{ruleId}."; var dictionaryConfig = new Dictionary(); @@ -135,9 +123,33 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s } } - if (dictionaryConfig.Any()) + return dictionaryConfig; + } + + /// + /// Gets effective user specified (or default) configuration for the given analyzer rule. + /// The configuration values CAN be null upon this operation. + /// + /// The configuration module should as well check that BuildAnalyzerConfigurationInternal.EvaluationAnalysisScope + /// for all rules is equal - otherwise it should error out. + /// + /// + /// + /// + public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) + { + var cacheKey = $"{ruleId}-projectFullPath "; + + if (!_editorConfig.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? editorConfig)) + { + editorConfig = BuildAnalyzerConfiguration.Null; + } + + var config = GetConfiguration(projectFullPath, ruleId); + + if (config.Any()) { - editorConfig = BuildAnalyzerConfiguration.Create(dictionaryConfig); + editorConfig = BuildAnalyzerConfiguration.Create(config); } _editorConfig[cacheKey] = editorConfig; diff --git a/src/Build/BuildCop/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCop/Infrastructure/CustomConfigurationData.cs index f6ecf0b91cf..928d7c12eba 100644 --- a/src/Build/BuildCop/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCop/Infrastructure/CustomConfigurationData.cs @@ -15,17 +15,27 @@ namespace Microsoft.Build.Experimental.BuildCop; /// that were attribute to a particular rule, but were not recognized by the infrastructure. /// The configuration data that is recognized by the infrastructure is passed as . /// -/// -public class CustomConfigurationData(string ruleId) +public class CustomConfigurationData { public static CustomConfigurationData Null { get; } = new(string.Empty); public static bool NotNull(CustomConfigurationData data) => !Null.Equals(data); + public CustomConfigurationData(string ruleId) + { + RuleId = ruleId; + } + + public CustomConfigurationData(string ruleId, Dictionary properties) + { + RuleId = ruleId; + ConfigurationData = properties; + } + /// /// Identifier of the rule that the configuration data is for. /// - public string RuleId { get; init; } = ruleId; + public string RuleId { get; init; } /// /// Key-value pairs of unstructured data from .editorconfig file. From 68a563e0483b32a81ac13aed565b0b1860317198 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 7 Mar 2024 11:10:03 +0100 Subject: [PATCH 11/52] check for null ref in BuildAnalyzerConfiguration creation and cover logic by tests --- .../BuildAnalyzerConfiguration_Test.cs | 105 ++++++++++++++++++ .../API/BuildAnalyzerConfiguration.cs | 4 +- .../Infrastructure/ConfigurationProvider.cs | 16 +++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs diff --git a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs new file mode 100644 index 00000000000..42aee43c5e0 --- /dev/null +++ b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Experimental.BuildCop; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.Analyzers.UnitTests +{ + public class BuildAnalyzerConfiguration_Test + { + [Fact] + public void CreateWithNull_ReturnsObjectWithNullValues() + { + var buildConfig = BuildAnalyzerConfiguration.Create(null); + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBeNull(); + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Fact] + public void CreateWithEmpty_ReturnsObjectWithNullValues() + { + var buildConfig = BuildAnalyzerConfiguration.Create(new Dictionary()); + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBeNull(); + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("error", BuildAnalyzerResultSeverity.Error)] + [InlineData("info", BuildAnalyzerResultSeverity.Info)] + [InlineData("warning", BuildAnalyzerResultSeverity.Warning)] + [InlineData("WARNING", BuildAnalyzerResultSeverity.Warning)] + [InlineData("non-existing-option", null)] + public void CreateBuildAnalyzerConfiguration_Severity(string parameter, BuildAnalyzerResultSeverity? expected) + { + var config = new Dictionary() + { + { "severity" , parameter }, + }; + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBe(expected); + + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("false", false)] + [InlineData("FALSE", false)] + [InlineData("", null)] + public void CreateBuildAnalyzerConfiguration_IsEnabled(string parameter, bool? expected) + { + var config = new Dictionary() + { + { "isenabled" , parameter }, + }; + + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.IsEnabled.ShouldBe(expected); + + buildConfig.Severity.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("AnalyzedProjectOnly", EvaluationAnalysisScope.AnalyzedProjectOnly)] + [InlineData("AnalyzedProjectWithImportsFromCurrentWorkTree", EvaluationAnalysisScope.AnalyzedProjectWithImportsFromCurrentWorkTree)] + [InlineData("AnalyzedProjectWithImportsWithoutSdks", EvaluationAnalysisScope.AnalyzedProjectWithImportsWithoutSdks)] + [InlineData("AnalyzedProjectWithAllImports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] + [InlineData("analyzedprojectwithallimports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] + [InlineData("non existing value", null)] + public void CreateBuildAnalyzerConfiguration_EvaluationAnalysisScope(string parameter, EvaluationAnalysisScope? expected) + { + var config = new Dictionary() + { + { "evaluationanalysisscope" , parameter }, + }; + + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBe(expected); + + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.Severity.ShouldBeNull(); + } + } +} diff --git a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs index 589a6305806..136d396218c 100644 --- a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs @@ -65,7 +65,8 @@ public static BuildAnalyzerConfiguration Create(Dictionary confi private static bool TryExtractValue(string key, Dictionary config, out T value) where T : struct { value = default; - if (!config.ContainsKey(key)) + + if (config == null || !config.ContainsKey(key)) { return false; } @@ -82,6 +83,7 @@ private static bool TryExtractValue(string key, Dictionary co { return Enum.TryParse(config[key], true, out value); } + return false; } } diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 494960d6e5b..24ed7476d52 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -24,6 +24,11 @@ internal class ConfigurationProvider // (disabled rules and analyzers that need to run in different node) private readonly Dictionary _editorConfig = new Dictionary(); + private readonly List _infrastructureConfigurationKeys = new List() { + nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), + nameof(BuildAnalyzerConfiguration.IsEnabled).ToLower(), + nameof(BuildAnalyzerConfiguration.Severity).ToLower() + }; /// /// Gets the user specified unrecognized configuration for the given analyzer rule. /// @@ -42,6 +47,16 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st { return CustomConfigurationData.Null; } + + // remove the infrastructure owned key names + foreach(var infraConfigurationKey in _infrastructureConfigurationKeys) + { + if (configuration.ContainsKey(infraConfigurationKey)) + { + configuration.Remove(infraConfigurationKey); + } + } + return new CustomConfigurationData(ruleId, configuration); } @@ -109,6 +124,7 @@ internal Dictionary GetConfiguration(string projectFullPath, str } catch (Exception ex) { + // Note: catch any exception, we do not want to break because of the failed operation with parsing the editorconfig. Debug.WriteLine(ex); } From 88160f1a6c97e6bb2d08d63c84d169a171d05483 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 11 Mar 2024 10:27:48 +0100 Subject: [PATCH 12/52] remove interface, add tests on editorconfig discovery --- .../EditorConfigParser_Tests.cs | 128 ++++++++++++++++++ .../Infrastructure/ConfigurationProvider.cs | 10 +- .../EditorConfig/EditorConfigParser.cs | 28 ++-- .../EditorConfig/IEditorConfigParser.cs | 16 --- src/Build/Microsoft.Build.csproj | 1 - 5 files changed, 148 insertions(+), 35 deletions(-) create mode 100644 src/Analyzers.UnitTests/EditorConfigParser_Tests.cs delete mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs diff --git a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs new file mode 100644 index 00000000000..ef3215863cb --- /dev/null +++ b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +#nullable disable + +namespace Microsoft.Build.Analyzers.UnitTests +{ + public class EditorConfigParser_Tests + { + [Fact] + public void NoSectionConfigured_ResultsEmptyResultConfig() + { + var configs = new List(){ + EditorConfigFile.Parse("""" + property1=value1 +""""), + EditorConfigFile.Parse("""" + property1=value2 + """"), + EditorConfigFile.Parse("""" + property1=value3 + """"), + }; + + var parser = new EditorConfigParser(); + var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file"); + mergedResult.Keys.Count.ShouldBe(0); + } + + [Fact] + public void ProperOrderOfconfiguration_ClosestToTheFileShouldBeApplied() + { + var configs = new List(){ + EditorConfigFile.Parse("""" + [*] + property1=value1 +""""), + EditorConfigFile.Parse("""" + [*] + property1=value2 + """"), + EditorConfigFile.Parse("""" + [*] + property1=value3 + """"), + }; + + var parser = new EditorConfigParser(); + var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file.proj"); + mergedResult.Keys.Count.ShouldBe(1); + mergedResult["property1"].ShouldBe("value1"); + } + + [Fact] + public void EditorconfigFileDiscovery_RootTrue() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); + + TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", + """ + root=true + + [*.csproj] + test_key=test_value_updated + """); + + + TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + [*.csproj] + test_key=should_not_be_respected_and_parsed + """); + + var parser = new EditorConfigParser(); + var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj") ).ToList(); + // should be one because root=true so we do not need to go further + listOfEditorConfigFile.Count.ShouldBe(1); + listOfEditorConfigFile[0].IsRoot.ShouldBeTrue(); + listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); + listOfEditorConfigFile[0].NamedSections[0].Properties["test_key"].ShouldBe("test_value_updated"); + } + + [Fact] + public void EditorconfigFileDiscovery_RootFalse() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); + + TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", + """ + [*.csproj] + test_key=test_value_updated + """); + + TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + [*.csproj] + test_key=will_be_there + """); + + var parser = new EditorConfigParser(); + var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj")).ToList(); + + listOfEditorConfigFile.Count.ShouldBe(2); + listOfEditorConfigFile[0].IsRoot.ShouldBeFalse(); + listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); + } + } +} diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 24ed7476d52..64b27ef4951 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -19,7 +19,7 @@ namespace Microsoft.Build.BuildCop.Infrastructure; // TODO: https://github.com/dotnet/msbuild/issues/9628 internal class ConfigurationProvider { - private IEditorConfigParser s_editorConfigParser = new EditorConfigParser(); + private EditorConfigParser s_editorConfigParser = new EditorConfigParser(); // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) private readonly Dictionary _editorConfig = new Dictionary(); @@ -29,6 +29,7 @@ internal class ConfigurationProvider nameof(BuildAnalyzerConfiguration.IsEnabled).ToLower(), nameof(BuildAnalyzerConfiguration.Severity).ToLower() }; + /// /// Gets the user specified unrecognized configuration for the given analyzer rule. /// @@ -61,7 +62,7 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st } /// - /// + /// Verifies if previously fetched custom configurations are equal to current one. /// /// /// @@ -69,7 +70,9 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st /// public void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { - // TBD + // Note: requires another cache layer for custom configuration. + // var prevData = GetCustomConfiguration(projectFullPath, ruleId); + // if prevData in cache => raise BuildCopConfigurationException; } public BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( @@ -126,6 +129,7 @@ internal Dictionary GetConfiguration(string projectFullPath, str { // Note: catch any exception, we do not want to break because of the failed operation with parsing the editorconfig. Debug.WriteLine(ex); + throw new BuildCopConfigurationException($"Fetchin editorConfig data failed: {ex.Message}"); } var keyTosearch = $"msbuild_analyzer.{ruleId}."; diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index f2fff4b9e32..aa19a50acb6 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -15,7 +15,7 @@ namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig { - internal class EditorConfigParser : IEditorConfigParser + internal class EditorConfigParser { private const string EditorconfigFile = ".editorconfig"; private Dictionary editorConfigFileCache; @@ -25,32 +25,32 @@ internal EditorConfigParser() editorConfigFileCache = new Dictionary(); } - public Dictionary Parse(string filePath) + internal Dictionary Parse(string filePath) { var editorConfigs = EditorConfigFileDiscovery(filePath); return MergeEditorConfigFiles(editorConfigs, filePath); } - public IList EditorConfigFileDiscovery(string filePath) + internal IEnumerable EditorConfigFileDiscovery(string filePath) { var editorConfigDataFromFilesList = new List(); var directoryOfTheProject = Path.GetDirectoryName(filePath); - var editorConfigFile = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); + var editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); - while (editorConfigFile != string.Empty) + while (editorConfigFilePath != string.Empty) { EditorConfigFile editorConfig; - if (editorConfigFileCache.ContainsKey(editorConfigFile)) + if (editorConfigFileCache.ContainsKey(editorConfigFilePath)) { - editorConfig = editorConfigFileCache[editorConfigFile]; + editorConfig = editorConfigFileCache[editorConfigFilePath]; } else { - var editorConfigfileContent = File.ReadAllText(editorConfigFile); + var editorConfigfileContent = File.ReadAllText(editorConfigFilePath); editorConfig = EditorConfigFile.Parse(editorConfigfileContent); - editorConfigFileCache[editorConfigFile] = editorConfig; + editorConfigFileCache[editorConfigFilePath] = editorConfig; } editorConfigDataFromFilesList.Add(editorConfig); @@ -61,22 +61,20 @@ public IList EditorConfigFileDiscovery(string filePath) } else { - editorConfigFile = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFile))); + editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFilePath))); } } return editorConfigDataFromFilesList; } - public Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) + internal Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) { var resultingDictionary = new Dictionary(); if (editorConfigFiles.Any()) { - editorConfigFiles.Reverse(); - - foreach (var configData in editorConfigFiles) + foreach (var configData in editorConfigFiles.Reverse()) { foreach (var section in configData.NamedSections) { @@ -98,6 +96,6 @@ public Dictionary MergeEditorConfigFiles(IEnumerable Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); + internal static string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); } } diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs deleted file mode 100644 index c40685f5524..00000000000 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/IEditorConfigParser.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig -{ - internal interface IEditorConfigParser - { - public Dictionary Parse(string filePath); - } -} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index d1ee0799fae..fcdc6bd4851 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -184,7 +184,6 @@ - From c88ec21af583418f5b8b83344ee9777eae4a3937 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 11 Mar 2024 10:59:26 +0100 Subject: [PATCH 13/52] Make get custom config public only. --- .../Infrastructure/ConfigurationProvider.cs | 16 ++++++++-------- .../EditorConfig/EditorConfigParser.cs | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 64b27ef4951..7917f4ba32f 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -68,19 +68,19 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st /// /// If CustomConfigurationData differs in a build for a same ruleId /// - public void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) + internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { // Note: requires another cache layer for custom configuration. // var prevData = GetCustomConfiguration(projectFullPath, ruleId); // if prevData in cache => raise BuildCopConfigurationException; } - public BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( + internal BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( string projectFullPath, BuildAnalyzer analyzer) => FillConfiguration(projectFullPath, analyzer.SupportedRules, GetMergedConfiguration); - public BuildAnalyzerConfiguration[] GetUserConfigurations( + internal BuildAnalyzerConfiguration[] GetUserConfigurations( string projectFullPath, IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetUserConfiguration); @@ -90,7 +90,7 @@ public CustomConfigurationData[] GetCustomConfigurations( IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetCustomConfiguration); - public BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( + internal BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( BuildAnalyzerConfiguration[] userConfigs, BuildAnalyzer analyzer) { @@ -156,9 +156,9 @@ internal Dictionary GetConfiguration(string projectFullPath, str /// /// /// - public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) + internal BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, string ruleId) { - var cacheKey = $"{ruleId}-projectFullPath "; + var cacheKey = $"{ruleId}-{projectFullPath}"; if (!_editorConfig.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? editorConfig)) { @@ -184,10 +184,10 @@ public BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, s /// /// /// - public BuildAnalyzerConfigurationInternal GetMergedConfiguration(string projectFullPath, BuildAnalyzerRule analyzerRule) + internal BuildAnalyzerConfigurationInternal GetMergedConfiguration(string projectFullPath, BuildAnalyzerRule analyzerRule) => GetMergedConfiguration(projectFullPath, analyzerRule.Id, analyzerRule.DefaultConfiguration); - public BuildAnalyzerConfigurationInternal MergeConfiguration( + internal BuildAnalyzerConfigurationInternal MergeConfiguration( string ruleId, BuildAnalyzerConfiguration defaultConfig, BuildAnalyzerConfiguration editorConfig) diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index aa19a50acb6..07c29c96004 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -61,6 +61,7 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath } else { + // search in upper directory editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFilePath))); } } From 08219fa4281817a3d02be0cee6449b585056b8f9 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 11 Mar 2024 13:13:53 +0100 Subject: [PATCH 14/52] Add success configuration tests and error scope for config exceptions --- .../ConfigurationProvider_Tests.cs | 132 ++++++++++++++++++ .../BuildCopConfigurationErrorScope.cs | 17 +++ .../BuildCopConfigurationException.cs | 11 +- .../Infrastructure/BuildCopManagerProvider.cs | 5 +- .../Infrastructure/ConfigurationProvider.cs | 9 +- src/Build/Microsoft.Build.csproj | 1 + 6 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs create mode 100644 src/Build/BuildCop/Infrastructure/BuildCopConfigurationErrorScope.cs diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs new file mode 100644 index 00000000000..d931cf8a691 --- /dev/null +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCop.Infrastructure; +using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCop; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +#nullable disable + +namespace Microsoft.Build.Analyzers.UnitTests +{ + public class ConfigurationProvider_Tests + { + [Fact] + public void GetRuleIdConfiguration_ReturnsEmptyConfig() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + test_key=test_value_updated + """); + + var configurationProvider = new ConfigurationProvider(); + var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // empty + configs.ShouldBe(new Dictionary()); + } + + [Fact] + public void GetRuleIdConfiguration_ReturnsConfiguration() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + msbuild_analyzer.rule_id.property1=value1 + msbuild_analyzer.rule_id.property2=value2 + """); + + var configurationProvider = new ConfigurationProvider(); + var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + configs.Keys.Count.ShouldBe(2); + + configs.ContainsKey("property1").ShouldBeTrue(); + configs.ContainsKey("property2").ShouldBeTrue(); + + configs["property2"].ShouldBe("value2"); + configs["property1"].ShouldBe("value1"); + } + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationData() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + msbuild_analyzer.rule_id.property1=value1 + msbuild_analyzer.rule_id.property2=value2 + msbuild_analyzer.rule_id.isEnabled=true + msbuild_analyzer.rule_id.isEnabled2=true + any_other_key1=any_other_value1 + any_other_key2=any_other_value2 + any_other_key3=any_other_value3 + any_other_key3=any_other_value3 + """); + + var configurationProvider = new ConfigurationProvider(); + var customConfiguration = configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + var configs = customConfiguration.ConfigurationData; + + configs.Keys.Count().ShouldBe(3); + + configs.ContainsKey("property1").ShouldBeTrue(); + configs.ContainsKey("property2").ShouldBeTrue(); + configs.ContainsKey("isenabled2").ShouldBeTrue(); + } + + [Fact] + public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + msbuild_analyzer.rule_id.isEnabled=true + msbuild_analyzer.rule_id.Severity=Error + msbuild_analyzer.rule_id.EvaluationAnalysisScope=AnalyzedProjectOnly + """); + + var configurationProvider = new ConfigurationProvider(); + var buildConfig = configurationProvider.GetUserConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + buildConfig.ShouldNotBeNull(); + + buildConfig.IsEnabled?.ShouldBeTrue(); + buildConfig.Severity?.ShouldBe(BuildAnalyzerResultSeverity.Error); + buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.AnalyzedProjectOnly); + } + } +} diff --git a/src/Build/BuildCop/Infrastructure/BuildCopConfigurationErrorScope.cs b/src/Build/BuildCop/Infrastructure/BuildCopConfigurationErrorScope.cs new file mode 100644 index 00000000000..5836a28f2f9 --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/BuildCopConfigurationErrorScope.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.BuildCop.Infrastructure +{ + internal enum BuildCopConfigurationErrorScope + { + SingleRule, + EditorConfigParser + } +} diff --git a/src/Build/BuildCop/Infrastructure/BuildCopConfigurationException.cs b/src/Build/BuildCop/Infrastructure/BuildCopConfigurationException.cs index 4580fb03b81..fe31cdb435f 100644 --- a/src/Build/BuildCop/Infrastructure/BuildCopConfigurationException.cs +++ b/src/Build/BuildCop/Infrastructure/BuildCopConfigurationException.cs @@ -15,7 +15,16 @@ internal class BuildCopConfigurationException : Exception /// Exception to communicate issues with user specified configuration - unsupported scenarios, malformations, etc. /// This exception usually leads to defuncting the particular analyzer for the rest of the build (even if issue occured with a single project). /// - public BuildCopConfigurationException(string message) : base(message) + /// + internal BuildCopConfigurationErrorScope buildCopConfigurationErrorScope; + + public BuildCopConfigurationException(string message, Exception innerException, BuildCopConfigurationErrorScope buildCopConfigurationErrorScope = BuildCopConfigurationErrorScope.SingleRule) : base(message, innerException) + { + this.buildCopConfigurationErrorScope = buildCopConfigurationErrorScope; + } + + public BuildCopConfigurationException(string message, BuildCopConfigurationErrorScope buildCopConfigurationErrorScope = BuildCopConfigurationErrorScope.SingleRule) : base(message) { + this.buildCopConfigurationErrorScope = buildCopConfigurationErrorScope; } } diff --git a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs index 349d2078504..c880c9efd77 100644 --- a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs +++ b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs @@ -179,7 +179,10 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont { // TODO: For user analyzers - it should run only on projects where referenced // on others it should work similarly as disabling them. - // Disabled analyzer should not only post-filter results - it shouldn't even see the data + // Disabled analyzer should not only post-filter results - it shouldn't even see the data + + + // TODO:catch the exception of fetching configuration BuildAnalyzerWrapper wrapper; BuildAnalyzerConfigurationInternal[] configurations; diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index 7917f4ba32f..aff50f8293e 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -125,11 +125,9 @@ internal Dictionary GetConfiguration(string projectFullPath, str { config = s_editorConfigParser.Parse(projectFullPath); } - catch (Exception ex) + catch (Exception exception) { - // Note: catch any exception, we do not want to break because of the failed operation with parsing the editorconfig. - Debug.WriteLine(ex); - throw new BuildCopConfigurationException($"Fetchin editorConfig data failed: {ex.Message}"); + throw new BuildCopConfigurationException($"Parsing editorConfig data failed", exception, BuildCopConfigurationErrorScope.EditorConfigParser); } var keyTosearch = $"msbuild_analyzer.{ruleId}."; @@ -139,7 +137,8 @@ internal Dictionary GetConfiguration(string projectFullPath, str { if (kv.Key.StartsWith(keyTosearch, StringComparison.OrdinalIgnoreCase)) { - dictionaryConfig[kv.Key.Replace(keyTosearch.ToLower(), "")] = kv.Value; + var newKey = kv.Key.Replace(keyTosearch.ToLower(), ""); + dictionaryConfig[newKey] = kv.Value; } } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index fcdc6bd4851..45c1738a494 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -159,6 +159,7 @@ + From 40af50e01f1f090868f3dcb05efb370edef85adb Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 11 Mar 2024 14:40:55 +0100 Subject: [PATCH 15/52] Add more documentation --- .../EditorConfig/EditorConfigParser.cs | 9 ++++ .../Infrastructure/EditorConfig/README.md | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/Build/BuildCop/Infrastructure/EditorConfig/README.md diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs index 07c29c96004..6f8d7e9006b 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -31,6 +31,10 @@ internal Dictionary Parse(string filePath) return MergeEditorConfigFiles(editorConfigs, filePath); } + /// + /// Fetches the list of EditorconfigFile ordered from the nearest to the filePath. + /// + /// internal IEnumerable EditorConfigFileDiscovery(string filePath) { var editorConfigDataFromFilesList = new List(); @@ -69,6 +73,11 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath return editorConfigDataFromFilesList; } + /// + /// Retrieves the config dictionary from the sections that matched the filePath. + /// + /// + /// internal Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) { var resultingDictionary = new Dictionary(); diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/README.md b/src/Build/BuildCop/Infrastructure/EditorConfig/README.md new file mode 100644 index 00000000000..cf5029c9746 --- /dev/null +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/README.md @@ -0,0 +1,49 @@ +# EditorConfigParser + +Logic of parsing and matching copied from Roslyn implementation. +To track the request on sharing the code: https://github.com/dotnet/roslyn/issues/72324 + + +In current implementation the usage of the editorconfig is internal only and exposed via ConfigurationProvider functionality. + +Configration divided into two categories: +- Infra related configuration. IsEnabled, Severity, EvaluationAnalysisScope +- Custom configuration, any other config specified by user for this particular rule + +### Example +For the file/folder structure: +``` +├── folder1/ +│ └── .editorconfig +│ └── folder2/ + ├── folder3/ + │ └── .editorconfig + │ └── test.proj + └── .editorconfig +``` + +we want to fetch configuration for the project: /full/path/folder1/folder2/folder3/test.proj + +Infra related and custom configration flows have one common logic: Fetching the configs from editorconfig + +``` +while(editorConfig is not root && parent directory exists){ + collect, parse editorconfigs +} + +list{ + folder1/folder2/folder3/.editorconfig + folder1/folder2/.editorconfig + folder1/.editorconfig +} +``` +Reverse the order and collect all matching section key-value pairs into new dictionary +Remove non-msbuild-analyzer related key-values (keys not starting with msbuild_analyzer.RULEID) + +The implementation differs depending on category: + - Infra related config: Merges the configuration retrieved from configration module with default values (respecting the specified configs in editorconfig) + - Custom configuration: Remove all infra related keys from dictionary + +Two levels of cache introduced: +- When retrieving and parsing the editor config -> Parsed results are saved into dictionary: editorconfigPath = ParsedEditorConfig +- When retrieving Infra related config: ruleId-projectPath = BuildconfigInstance From 709199f9ee57910cdf2fa36125e5f54f38a99ce7 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 18 Mar 2024 21:33:26 +0100 Subject: [PATCH 16/52] Add exception on building the config instance --- .../BuildAnalyzerConfiguration_Test.cs | 26 ++++++++++++++----- .../ConfigurationProvider_Tests.cs | 4 +-- .../EditorConfigParser_Tests.cs | 1 - .../API/BuildAnalyzerConfiguration.cs | 17 +++++++++--- .../Infrastructure/ConfigurationProvider.cs | 5 +++- src/Build/Microsoft.Build.csproj | 1 - 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs index 42aee43c5e0..490ab73ea35 100644 --- a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -4,14 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata; using System.Text; using System.Threading.Tasks; +using Microsoft.Build.BuildCop.Infrastructure; using Microsoft.Build.Experimental.BuildCop; using Shouldly; using Xunit; -#nullable disable - namespace Microsoft.Build.Analyzers.UnitTests { public class BuildAnalyzerConfiguration_Test @@ -19,7 +19,7 @@ public class BuildAnalyzerConfiguration_Test [Fact] public void CreateWithNull_ReturnsObjectWithNullValues() { - var buildConfig = BuildAnalyzerConfiguration.Create(null); + var buildConfig = BuildAnalyzerConfiguration.Create(null!); buildConfig.ShouldNotBeNull(); buildConfig.Severity.ShouldBeNull(); buildConfig.IsEnabled.ShouldBeNull(); @@ -41,7 +41,6 @@ public void CreateWithEmpty_ReturnsObjectWithNullValues() [InlineData("info", BuildAnalyzerResultSeverity.Info)] [InlineData("warning", BuildAnalyzerResultSeverity.Warning)] [InlineData("WARNING", BuildAnalyzerResultSeverity.Warning)] - [InlineData("non-existing-option", null)] public void CreateBuildAnalyzerConfiguration_Severity(string parameter, BuildAnalyzerResultSeverity? expected) { var config = new Dictionary() @@ -62,7 +61,6 @@ public void CreateBuildAnalyzerConfiguration_Severity(string parameter, BuildAna [InlineData("TRUE", true)] [InlineData("false", false)] [InlineData("FALSE", false)] - [InlineData("", null)] public void CreateBuildAnalyzerConfiguration_IsEnabled(string parameter, bool? expected) { var config = new Dictionary() @@ -85,7 +83,6 @@ public void CreateBuildAnalyzerConfiguration_IsEnabled(string parameter, bool? e [InlineData("AnalyzedProjectWithImportsWithoutSdks", EvaluationAnalysisScope.AnalyzedProjectWithImportsWithoutSdks)] [InlineData("AnalyzedProjectWithAllImports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] [InlineData("analyzedprojectwithallimports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] - [InlineData("non existing value", null)] public void CreateBuildAnalyzerConfiguration_EvaluationAnalysisScope(string parameter, EvaluationAnalysisScope? expected) { var config = new Dictionary() @@ -101,5 +98,22 @@ public void CreateBuildAnalyzerConfiguration_EvaluationAnalysisScope(string para buildConfig.IsEnabled.ShouldBeNull(); buildConfig.Severity.ShouldBeNull(); } + + [Theory] + [InlineData("evaluationanalysisscope", "incorrec-value")] + [InlineData("isenabled", "incorrec-value")] + [InlineData("severity", "incorrec-value")] + public void CreateBuildAnalyzerConfiguration_ExceptionOnInvalidInputValue(string key, string value) + { + var config = new Dictionary() + { + { key , value}, + }; + + var exception = Should.Throw(() => { + BuildAnalyzerConfiguration.Create(config); + }); + exception.Message.ShouldContain($"Incorrect value provided in config for key {key}"); + } } } diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs index d931cf8a691..323e58cfafd 100644 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -17,8 +17,6 @@ using Xunit; using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; -#nullable disable - namespace Microsoft.Build.Analyzers.UnitTests { public class ConfigurationProvider_Tests @@ -96,7 +94,7 @@ public void GetRuleIdConfiguration_CustomConfigurationData() var customConfiguration = configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); var configs = customConfiguration.ConfigurationData; - configs.Keys.Count().ShouldBe(3); + configs!.Keys.Count().ShouldBe(3); configs.ContainsKey("property1").ShouldBeTrue(); configs.ContainsKey("property2").ShouldBeTrue(); diff --git a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs index ef3215863cb..e37d912f773 100644 --- a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs +++ b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs @@ -15,7 +15,6 @@ using Xunit; using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; -#nullable disable namespace Microsoft.Build.Analyzers.UnitTests { diff --git a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs index 136d396218c..d052bda719f 100644 --- a/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCop/API/BuildAnalyzerConfiguration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System; +using Microsoft.Build.BuildCop.Infrastructure; namespace Microsoft.Build.Experimental.BuildCop; @@ -71,19 +72,29 @@ private static bool TryExtractValue(string key, Dictionary co return false; } + bool isParsed = false; + if (typeof(T) == typeof(bool)) { if (bool.TryParse(config[key], out bool boolValue)) { value = (T)(object)boolValue; - return true; + isParsed = true; } } else if(typeof(T).IsEnum) { - return Enum.TryParse(config[key], true, out value); + + isParsed = Enum.TryParse(config[key], true, out value); + } + + if (!isParsed) + { + throw new BuildCopConfigurationException( + $"Incorrect value provided in config for key {key}", + buildCopConfigurationErrorScope: BuildCopConfigurationErrorScope.EditorConfigParser); } - return false; + return isParsed; } } diff --git a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs index aff50f8293e..1139caabd36 100644 --- a/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCop/Infrastructure/ConfigurationProvider.cs @@ -20,10 +20,13 @@ namespace Microsoft.Build.BuildCop.Infrastructure; internal class ConfigurationProvider { private EditorConfigParser s_editorConfigParser = new EditorConfigParser(); + // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) private readonly Dictionary _editorConfig = new Dictionary(); + // private readonly Dictionary _customConfigurationData = new Dictionary(); + private readonly List _infrastructureConfigurationKeys = new List() { nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), nameof(BuildAnalyzerConfiguration.IsEnabled).ToLower(), @@ -71,7 +74,7 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { // Note: requires another cache layer for custom configuration. - // var prevData = GetCustomConfiguration(projectFullPath, ruleId); + // var customConfiguration = GetCustomConfiguration(projectFullPath, ruleId); // if prevData in cache => raise BuildCopConfigurationException; } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index e57604823d0..9dfecf06339 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -185,7 +185,6 @@ - From dce803947e4678c324030c6b00fce46a91028b65 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 10:19:53 +0100 Subject: [PATCH 17/52] Update exceptions --- .../BuildCop/Infrastructure/BuildCopManagerProvider.cs | 3 --- .../EditorConfig/EditorConfigGlobsMatcher.cs | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs index 4e0c5b9d3ac..82052c4118b 100644 --- a/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs +++ b/src/Build/BuildCop/Infrastructure/BuildCopManagerProvider.cs @@ -181,9 +181,6 @@ private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryCont // on others it should work similarly as disabling them. // Disabled analyzer should not only post-filter results - it shouldn't even see the data - - // TODO:catch the exception of fetching configuration - BuildAnalyzerWrapper wrapper; BuildAnalyzerConfigurationInternal[] configurations; if (analyzerFactoryContext.MaterializedAnalyzer == null) diff --git a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index 801496b1965..9714a2e9b03 100644 --- a/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCop/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -6,7 +6,7 @@ // with some changes to make it quicker to integrate into the MSBuild. // Changes: // 1. ArrayBuilder was replaced with List. -// 2. Exceptions. TODO: Wrap in try/catch blocks for proper reporting +// 2. Exceptions. Wrap in try/catch blocks for proper reporting using System; @@ -146,7 +146,7 @@ internal static string UnescapeSectionName(string sectionName) { // We only call this on strings that were already passed through IsAbsoluteEditorConfigPath, so // we shouldn't have any other token kinds here. - throw new Exception("my new exception"); + throw new BuildCopConfigurationException($"UnexpectedToken: {tokenKind}", BuildCopConfigurationErrorScope.EditorConfigParser); } } return sb.ToString(); @@ -310,7 +310,7 @@ private static bool TryCompilePathList( } break; default: - throw new Exception("Exception from Matcher"); + throw new BuildCopConfigurationException($"UnexpectedToken: {tokenKind}", BuildCopConfigurationErrorScope.EditorConfigParser); } } // If we're parsing a choice we should not exit without a closing '}' @@ -408,7 +408,7 @@ private static bool TryCompileChoice( } else { - throw new Exception("Exception another one"); + throw new BuildCopConfigurationException($"UnexpectedValue: {lastChar}", BuildCopConfigurationErrorScope.EditorConfigParser); } } From e27f899ed854a8c7ea7e2c284055d7908832728c Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 14:59:37 +0100 Subject: [PATCH 18/52] Fix merge conflicts --- .../BuildAnalyzerConfiguration_Test.cs | 6 +++--- src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs | 2 +- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 8 ++++++-- .../Infrastructure/BuildCheckCentralContext.cs | 2 +- ...orScope.cs => BuildCheckConfigurationErrorScope.cs} | 4 ++-- .../Infrastructure/BuildCheckConfigurationException.cs | 10 +++++----- .../Infrastructure/BuildCheckManagerProvider.cs | 2 +- .../BuildCheck/Infrastructure/ConfigurationProvider.cs | 4 ++-- .../Infrastructure/EditorConfig/EditorConfigFile.cs | 2 +- .../EditorConfig/EditorConfigGlobsMatcher.cs | 8 ++++---- .../Infrastructure/EditorConfig/EditorConfigParser.cs | 4 ++-- src/Build/Microsoft.Build.csproj | 4 ++++ 12 files changed, 32 insertions(+), 24 deletions(-) rename src/Build/BuildCheck/Infrastructure/{BuildCopConfigurationErrorScope.cs => BuildCheckConfigurationErrorScope.cs} (75%) diff --git a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs index 490ab73ea35..1709fa7d5d7 100644 --- a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -7,8 +7,8 @@ using System.Reflection.Metadata; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCop.Infrastructure; -using Microsoft.Build.Experimental.BuildCop; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck; using Shouldly; using Xunit; @@ -110,7 +110,7 @@ public void CreateBuildAnalyzerConfiguration_ExceptionOnInvalidInputValue(string { key , value}, }; - var exception = Should.Throw(() => { + var exception = Should.Throw(() => { BuildAnalyzerConfiguration.Create(config); }); exception.Message.ShouldContain($"Incorrect value provided in config for key {key}"); diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs index 323e58cfafd..a1af515a394 100644 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.Build.BuildCop.Infrastructure; using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; -using Microsoft.Build.Experimental.BuildCop; +using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index 28ec3c01017..a5bd5f0fe5d 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using Microsoft.Build.BuildCheck.Infrastructure; + namespace Microsoft.Build.Experimental.BuildCheck; /// @@ -86,9 +90,9 @@ private static bool TryExtractValue(string key, Dictionary co if (!isParsed) { - throw new BuildCopConfigurationException( + throw new BuildCheckConfigurationException( $"Incorrect value provided in config for key {key}", - buildCopConfigurationErrorScope: BuildCopConfigurationErrorScope.EditorConfigParser); + buildCopConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); } return isParsed; diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs index b656ee1216f..de862ad51ba 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs @@ -16,7 +16,7 @@ namespace Microsoft.Build.BuildCheck.Infrastructure; internal sealed class BuildCheckCentralContext { private readonly ConfigurationProvider _configurationProvider; - internal BuildCopCentralContext(ConfigurationProvider configurationProvider) + internal BuildCheckCentralContext(ConfigurationProvider configurationProvider) { _configurationProvider = configurationProvider; } diff --git a/src/Build/BuildCheck/Infrastructure/BuildCopConfigurationErrorScope.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs similarity index 75% rename from src/Build/BuildCheck/Infrastructure/BuildCopConfigurationErrorScope.cs rename to src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs index 5836a28f2f9..beb3382152d 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCopConfigurationErrorScope.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs @@ -7,9 +7,9 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCop.Infrastructure +namespace Microsoft.Build.BuildCheck.Infrastructure { - internal enum BuildCopConfigurationErrorScope + internal enum BuildCheckConfigurationErrorScope { SingleRule, EditorConfigParser diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs index babedcf5a42..c599e436315 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs @@ -16,15 +16,15 @@ internal class BuildCheckConfigurationException : Exception /// This exception usually leads to defuncting the particular analyzer for the rest of the build (even if issue occured with a single project). /// /// - internal BuildCopConfigurationErrorScope buildCopConfigurationErrorScope; + internal BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope; - public BuildCheckConfigurationException(string message, Exception innerException, BuildCopConfigurationErrorScope buildCopConfigurationErrorScope = BuildCopConfigurationErrorScope.SingleRule) : base(message, innerException) + public BuildCheckConfigurationException(string message, Exception innerException, BuildCheckConfigurationErrorScope buildCopConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message, innerException) { - this.buildCopConfigurationErrorScope = buildCopConfigurationErrorScope; + this.buildCheckConfigurationErrorScope = buildCopConfigurationErrorScope; } - public BuildCopConfigurationException(string message, BuildCopConfigurationErrorScope buildCopConfigurationErrorScope = BuildCopConfigurationErrorScope.SingleRule) : base(message) + public BuildCheckConfigurationException(string message, BuildCheckConfigurationErrorScope buildCopConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message) { - this.buildCopConfigurationErrorScope = buildCopConfigurationErrorScope; + this.buildCheckConfigurationErrorScope = buildCopConfigurationErrorScope; } } diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs index f1d8d0e2dd2..96103663f2e 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs @@ -72,7 +72,7 @@ private sealed class BuildCheckManager : IBuildCheckManager { private readonly TracingReporter _tracingReporter = new TracingReporter(); private readonly ConfigurationProvider _configurationProvider = new ConfigurationProvider(); - private readonly BuildCopCentralContext _buildCopCentralContext; + private readonly BuildCheckCentralContext _buildCheckCentralContext; private readonly ILoggingService _loggingService; private readonly List _analyzersRegistry =[]; private readonly bool[] _enabledDataSources = new bool[(int)BuildCheckDataSource.ValuesCount]; diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 3b0e59faedb..b6b8ec160d4 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -11,7 +11,7 @@ using System.Text.Json; using Microsoft.Build.Experimental.BuildCheck; using System.Configuration; -using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; namespace Microsoft.Build.BuildCheck.Infrastructure; @@ -130,7 +130,7 @@ internal Dictionary GetConfiguration(string projectFullPath, str } catch (Exception exception) { - throw new BuildCopConfigurationException($"Parsing editorConfig data failed", exception, BuildCopConfigurationErrorScope.EditorConfigParser); + throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); } var keyTosearch = $"msbuild_analyzer.{ruleId}."; diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs index 6ef8cc957a6..faefc2499d9 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -18,7 +18,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig { internal partial class EditorConfigFile { diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index 9714a2e9b03..ffeeac4bb68 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -19,7 +19,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig { internal class EditorConfigGlobsMatcher { @@ -146,7 +146,7 @@ internal static string UnescapeSectionName(string sectionName) { // We only call this on strings that were already passed through IsAbsoluteEditorConfigPath, so // we shouldn't have any other token kinds here. - throw new BuildCopConfigurationException($"UnexpectedToken: {tokenKind}", BuildCopConfigurationErrorScope.EditorConfigParser); + throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); } } return sb.ToString(); @@ -310,7 +310,7 @@ private static bool TryCompilePathList( } break; default: - throw new BuildCopConfigurationException($"UnexpectedToken: {tokenKind}", BuildCopConfigurationErrorScope.EditorConfigParser); + throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); } } // If we're parsing a choice we should not exit without a closing '}' @@ -408,7 +408,7 @@ private static bool TryCompileChoice( } else { - throw new BuildCopConfigurationException($"UnexpectedValue: {lastChar}", BuildCopConfigurationErrorScope.EditorConfigParser); + throw new BuildCheckConfigurationException($"UnexpectedValue: {lastChar}", BuildCheckConfigurationErrorScope.EditorConfigParser); } } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 6f8d7e9006b..430b90b9fd5 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -11,9 +11,9 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Build.Shared; -using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; -namespace Microsoft.Build.BuildCop.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig { internal class EditorConfigParser { diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 2daff2fe714..ffdb46b168d 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -165,6 +165,10 @@ + + + + From d273404e1285b31eadc9b4a29fd555c88d048a92 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 15:14:57 +0100 Subject: [PATCH 19/52] Fix the naming --- src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs | 6 +++--- src/Analyzers.UnitTests/EditorConfigParser_Tests.cs | 4 ++-- src/Analyzers.UnitTests/EditorConfig_Tests.cs | 4 ++-- .../BuildCheck/Infrastructure/BuildCheckManagerProvider.cs | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs index a1af515a394..bd923db1ed2 100644 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -9,13 +9,13 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCop.Infrastructure; -using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.Analyzers.UnitTests { diff --git a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs index e37d912f773..e39dfb6681f 100644 --- a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs +++ b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs @@ -9,11 +9,11 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.Analyzers.UnitTests diff --git a/src/Analyzers.UnitTests/EditorConfig_Tests.cs b/src/Analyzers.UnitTests/EditorConfig_Tests.cs index 64f4e9d2832..71b367cc7a0 100644 --- a/src/Analyzers.UnitTests/EditorConfig_Tests.cs +++ b/src/Analyzers.UnitTests/EditorConfig_Tests.cs @@ -8,10 +8,10 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCop.Infrastructure.EditorConfig; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Xunit; -using static Microsoft.Build.BuildCop.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; #nullable disable diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs index 96103663f2e..a371064d4d2 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs @@ -122,6 +122,7 @@ public void ProcessAnalyzerAcquisition(AnalyzerAcquisitionData acquisitionData) internal BuildCheckManager(ILoggingService loggingService) { _loggingService = loggingService; + _buildCheckCentralContext = new(_configurationProvider); _buildEventsProcessor = new(_buildCheckCentralContext); } From 933aee181c43362efda8eb4f6f237235e679d063 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 16:55:49 +0100 Subject: [PATCH 20/52] rename to build_check --- .../ConfigurationProvider_Tests.cs | 60 ++++++++++++++++--- src/Analyzers.UnitTests/EndToEndTests.cs | 14 ++--- .../BuildCheckConfigurationException.cs | 8 +-- .../Infrastructure/ConfigurationProvider.cs | 17 ++++-- .../Infrastructure/CustomConfigurationData.cs | 6 +- 5 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs index bd923db1ed2..9d8fb580b75 100644 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -53,8 +53,8 @@ public void GetRuleIdConfiguration_ReturnsConfiguration() root=true [*.csproj] - msbuild_analyzer.rule_id.property1=value1 - msbuild_analyzer.rule_id.property2=value2 + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 """); var configurationProvider = new ConfigurationProvider(); @@ -80,10 +80,10 @@ public void GetRuleIdConfiguration_CustomConfigurationData() root=true [*.csproj] - msbuild_analyzer.rule_id.property1=value1 - msbuild_analyzer.rule_id.property2=value2 - msbuild_analyzer.rule_id.isEnabled=true - msbuild_analyzer.rule_id.isEnabled2=true + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true any_other_key1=any_other_value1 any_other_key2=any_other_value2 any_other_key3=any_other_value3 @@ -112,9 +112,9 @@ public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() root=true [*.csproj] - msbuild_analyzer.rule_id.isEnabled=true - msbuild_analyzer.rule_id.Severity=Error - msbuild_analyzer.rule_id.EvaluationAnalysisScope=AnalyzedProjectOnly + build_check.rule_id.isEnabled=true + build_check.rule_id.Severity=Error + build_check.rule_id.EvaluationAnalysisScope=AnalyzedProjectOnly """); var configurationProvider = new ConfigurationProvider(); @@ -126,5 +126,47 @@ public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() buildConfig.Severity?.ShouldBe(BuildAnalyzerResultSeverity.Error); buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.AnalyzedProjectOnly); } + + /* + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + any_other_key1=any_other_value1 + any_other_key2=any_other_value2 + any_other_key3=any_other_value3 + any_other_key3=any_other_value3 + + [test123.csproj] + build_check.rule_id.property1=value2 + build_check.rule_id.property2=value3 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=tru1 + any_other_key1=any_other_value1 + any_other_key2=any_other_value2 + any_other_key3=any_other_value3 + any_other_key3=any_other_value3 + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should fail, because the configs are the different + Should.Throw(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + }*/ } } diff --git a/src/Analyzers.UnitTests/EndToEndTests.cs b/src/Analyzers.UnitTests/EndToEndTests.cs index 82e01a169fd..91b9d6c3742 100644 --- a/src/Analyzers.UnitTests/EndToEndTests.cs +++ b/src/Analyzers.UnitTests/EndToEndTests.cs @@ -90,15 +90,15 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode) root=true [*.csproj] - msbuild_analyzer.BC0101.IsEnabled=true - msbuild_analyzer.BC0101.Severity=warning + build_check.BC0101.IsEnabled=true + build_check.BC0101.Severity=warning - msbuild_analyzer.COND0543.IsEnabled=false - msbuild_analyzer.COND0543.Severity=Error - msbuild_analyzer.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly - msbuild_analyzer.COND0543.CustomSwitch=QWERTY + build_check.COND0543.IsEnabled=false + build_check.COND0543.Severity=Error + build_check.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly + build_check.COND0543.CustomSwitch=QWERTY - msbuild_analyzer.BLA.IsEnabled=false + build_check.BLA.IsEnabled=false """); // OSX links /var into /private, which makes Path.GetTempPath() return "/var..." but Directory.GetCurrentDirectory return "/private/var...". diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs index c599e436315..ba927e1573d 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs @@ -18,13 +18,13 @@ internal class BuildCheckConfigurationException : Exception /// internal BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope; - public BuildCheckConfigurationException(string message, Exception innerException, BuildCheckConfigurationErrorScope buildCopConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message, innerException) + public BuildCheckConfigurationException(string message, Exception innerException, BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message, innerException) { - this.buildCheckConfigurationErrorScope = buildCopConfigurationErrorScope; + this.buildCheckConfigurationErrorScope = buildCheckConfigurationErrorScope; } - public BuildCheckConfigurationException(string message, BuildCheckConfigurationErrorScope buildCopConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message) + public BuildCheckConfigurationException(string message, BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message) { - this.buildCheckConfigurationErrorScope = buildCopConfigurationErrorScope; + this.buildCheckConfigurationErrorScope = buildCheckConfigurationErrorScope; } } diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index b6b8ec160d4..f868f0a816b 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -25,7 +25,7 @@ internal class ConfigurationProvider // (disabled rules and analyzers that need to run in different node) private readonly Dictionary _editorConfig = new Dictionary(); - // private readonly Dictionary _customConfigurationData = new Dictionary(); + private readonly Dictionary _customConfigurationData = new Dictionary(); private readonly List _infrastructureConfigurationKeys = new List() { nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), @@ -61,7 +61,14 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st } } - return new CustomConfigurationData(ruleId, configuration); + var data = new CustomConfigurationData(ruleId, configuration); + + if (!_customConfigurationData.ContainsKey(ruleId)) + { + _customConfigurationData[ruleId] = data; + } + + return data; } /// @@ -73,9 +80,7 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st /// internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { - // Note: requires another cache layer for custom configuration. - // var customConfiguration = GetCustomConfiguration(projectFullPath, ruleId); - // if prevData in cache => raise BuildCopConfigurationException; + // TODO: repair the comparer of the objects, to compare actual data } internal BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( @@ -133,7 +138,7 @@ internal Dictionary GetConfiguration(string projectFullPath, str throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); } - var keyTosearch = $"msbuild_analyzer.{ruleId}."; + var keyTosearch = $"build_check.{ruleId}."; var dictionaryConfig = new Dictionary(); foreach (var kv in config) diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 0da7f32387e..f2e42649c95 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -68,7 +68,11 @@ public override bool Equals(object? obj) return Equals((CustomConfigurationData)obj); } - protected bool Equals(CustomConfigurationData other) => Equals(ConfigurationData, other.ConfigurationData); + protected bool Equals(CustomConfigurationData other) { + // TODO: update the comparison. For different instances with the same data it returns false, we will need to compare the exact match + + return Equals(ConfigurationData, other.ConfigurationData); + } public override int GetHashCode() => (ConfigurationData != null ? ConfigurationData.GetHashCode() : 0); } From e0dfb8d1ce5fc1de5153e65ea04c66a6dcac6279 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 17:03:27 +0100 Subject: [PATCH 21/52] Leave todos for the next iteration or version --- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 2 +- src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs | 2 +- src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index a5bd5f0fe5d..d9e2ef2ca3c 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -92,7 +92,7 @@ private static bool TryExtractValue(string key, Dictionary co { throw new BuildCheckConfigurationException( $"Incorrect value provided in config for key {key}", - buildCopConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); + buildCheckConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); } return isParsed; diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index f868f0a816b..62e80417f91 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -80,7 +80,7 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st /// internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { - // TODO: repair the comparer of the objects, to compare actual data + // TODO: repair the comparer of the objects } internal BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index f2e42649c95..d671b9b9c22 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -69,7 +69,7 @@ public override bool Equals(object? obj) } protected bool Equals(CustomConfigurationData other) { - // TODO: update the comparison. For different instances with the same data it returns false, we will need to compare the exact match + // TODO: update the comparison: Compare ruleID, and exact match of the configuration data return Equals(ConfigurationData, other.ConfigurationData); } From ca3d5b78b5d3261ccdc50e1102c52a54cd6965b9 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 19 Mar 2024 17:14:33 +0100 Subject: [PATCH 22/52] Add more info on usage of the introduced changes. --- .../BuildCheck/Infrastructure/EditorConfig/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md index cf5029c9746..a9ba94b53a2 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md @@ -47,3 +47,14 @@ The implementation differs depending on category: Two levels of cache introduced: - When retrieving and parsing the editor config -> Parsed results are saved into dictionary: editorconfigPath = ParsedEditorConfig - When retrieving Infra related config: ruleId-projectPath = BuildconfigInstance + +Usage examples (API) + +``` +var editorConfigParser = new EditorConfigParser(); +editorConfigParser.Parse("path/to/the/file") +``` + +The snippet above will return all applied key-value Dictionary pairs collected from .editorconfig files + +Currently EditorConfigParser is used by [ConfigurationProvider](https://github.com/dotnet/msbuild/blob/e0dfb8d1ce5fc1de5153e65ea04c66a6dcac6279/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs#L129). From df1b64c365b7e7e3ab8f80d92c768390dd48fb22 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 25 Mar 2024 14:42:49 +0100 Subject: [PATCH 23/52] address PR comments + compare custom configuration data --- .../ConfigurationProvider_Tests.cs | 82 ++++++++++++++++--- .../Infrastructure/ConfigurationProvider.cs | 12 ++- .../Infrastructure/CustomConfigurationData.cs | 38 +++++++-- .../EditorConfig/EditorConfigGlobsMatcher.cs | 24 +++--- 4 files changed, 125 insertions(+), 31 deletions(-) diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs index 9d8fb580b75..826c040488e 100644 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs @@ -127,9 +127,9 @@ public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.AnalyzedProjectOnly); } - /* + [Fact] - public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() + public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentValues() { using TestEnvironment testEnvironment = TestEnvironment.Create(); @@ -143,30 +143,88 @@ public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() build_check.rule_id.property2=value2 build_check.rule_id.isEnabled=true build_check.rule_id.isEnabled2=true - any_other_key1=any_other_value1 - any_other_key2=any_other_value2 - any_other_key3=any_other_value3 - any_other_key3=any_other_value3 [test123.csproj] build_check.rule_id.property1=value2 build_check.rule_id.property2=value3 build_check.rule_id.isEnabled=true build_check.rule_id.isEnabled2=tru1 - any_other_key1=any_other_value1 - any_other_key2=any_other_value2 - any_other_key3=any_other_value3 - any_other_key3=any_other_value3 """); var configurationProvider = new ConfigurationProvider(); configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - // should fail, because the configs are the different + // should not fail => configurations are the same Should.Throw(() => { configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); }); - }*/ + } + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentKeys() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled2=true + + [test123.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled2=true + build_check.rule_id.isEnabled3=true + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should not fail => configurations are the same + Should.Throw(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + } + + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + + [test123.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should fail, because the configs are the different + Should.NotThrow(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + } } } diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 62e80417f91..a98d4e674ab 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -80,7 +80,17 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st /// internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { - // TODO: repair the comparer of the objects + var configuration = GetCustomConfiguration(projectFullPath, ruleId); + + if (_customConfigurationData.ContainsKey(ruleId)) + { + var storedConfiguration = _customConfigurationData[ruleId]; + + if (!storedConfiguration.Equals(configuration)) + { + throw new BuildCheckConfigurationException("Custom configuration should be equal between projects"); + } + } } internal BuildAnalyzerConfigurationInternal[] GetMergedConfigurations( diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index d671b9b9c22..9470dc251e3 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -65,13 +65,39 @@ public override bool Equals(object? obj) return false; } - return Equals((CustomConfigurationData)obj); - } + var customConfigObj = (CustomConfigurationData) obj; + + if(customConfigObj.RuleId != RuleId) + { + return false; + } + + // validate keys and values + if (customConfigObj.ConfigurationData != null && ConfigurationData != null) + { + if (!customConfigObj.ConfigurationData.Keys.SequenceEqual(ConfigurationData.Keys)) + { + return false; + } + + var keys = customConfigObj.ConfigurationData.Keys; + foreach (var key in keys) + { + if (customConfigObj.ConfigurationData[key] != ConfigurationData[key]) + { + return false; + } + } + }else if (customConfigObj.ConfigurationData == null && ConfigurationData == null) + { + return true; + } + else + { + return false; + } - protected bool Equals(CustomConfigurationData other) { - // TODO: update the comparison: Compare ruleID, and exact match of the configuration data - - return Equals(ConfigurationData, other.ConfigurationData); + return true; } public override int GetHashCode() => (ConfigurationData != null ? ConfigurationData.GetHashCode() : 0); diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index ffeeac4bb68..092859a5113 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -26,10 +26,10 @@ internal class EditorConfigGlobsMatcher internal readonly struct SectionNameMatcher { private readonly ImmutableArray<(int minValue, int maxValue)> _numberRangePairs; - // public for testing - public Regex Regex { get; } - public SectionNameMatcher( + internal Regex Regex { get; } + + internal SectionNameMatcher( Regex regex, ImmutableArray<(int minValue, int maxValue)> numberRangePairs) { @@ -466,17 +466,17 @@ private struct SectionNameLexer { private readonly string _sectionName; - public int Position { get; set; } + internal int Position { get; set; } - public SectionNameLexer(string sectionName) + internal SectionNameLexer(string sectionName) { _sectionName = sectionName; Position = 0; } - public bool IsDone => Position >= _sectionName.Length; + internal bool IsDone => Position >= _sectionName.Length; - public TokenKind Lex() + internal TokenKind Lex() { int lexemeStart = Position; switch (_sectionName[Position]) @@ -535,18 +535,18 @@ public TokenKind Lex() } } - public char CurrentCharacter => _sectionName[Position]; + internal char CurrentCharacter => _sectionName[Position]; /// /// Call after getting from /// - public char EatCurrentCharacter() => _sectionName[Position++]; + internal char EatCurrentCharacter() => _sectionName[Position++]; /// /// Returns false if there are no more characters in the lex stream. /// Otherwise, produces the next character in the stream and returns true. /// - public bool TryEatCurrentCharacter(out char nextChar) + internal bool TryEatCurrentCharacter(out char nextChar) { if (IsDone) { @@ -560,13 +560,13 @@ public bool TryEatCurrentCharacter(out char nextChar) } } - public char this[int position] => _sectionName[position]; + internal char this[int position] => _sectionName[position]; /// /// Returns the string representation of a decimal integer, or null if /// the current lexeme is not an integer. /// - public string? TryLexNumber() + internal string? TryLexNumber() { bool start = true; var sb = new StringBuilder(); From 3dd6ee4ebea71c730aab1d9f4e4b6d455b280d29 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 25 Mar 2024 15:14:58 +0100 Subject: [PATCH 24/52] Add equals unit test --- .../CustomConfigurationData_Tests.cs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs diff --git a/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs b/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs new file mode 100644 index 00000000000..2c1672e6adc --- /dev/null +++ b/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +namespace Microsoft.Build.Analyzers.UnitTests +{ + public class CustomConfigurationData_Tests + { + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_NullInstance() + { + var customConfigurationData1 = CustomConfigurationData.Null; + var customConfigurationData2 = CustomConfigurationData.Null; + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_SameInstance() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = customConfigurationData1; + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_DifferentObjectType() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = new object(); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_DifferentInstanceSameValues() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = new CustomConfigurationData("testRuleId"); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataSame() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key1", "val1" } + }; + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferent() + { + var config = new Dictionary() + { + { "key1", "val1" } + }; + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config); + var customConfigurationData2 = new CustomConfigurationData("testRuleId"); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentKeys() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key2", "val2" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentValues() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key1", "val2" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + } +} From d659cad5e2d6bfd9fb25067dd311d95b8e6e08fa Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 2 Apr 2024 12:02:56 +0200 Subject: [PATCH 25/52] Address formatting pr review comments --- .../Components/Caching/IConfigCache.cs | 2 +- .../API/BuildAnalyzerConfiguration.cs | 7 +++-- .../BuildCheckCentralContext.cs | 1 + .../Infrastructure/ConfigurationProvider.cs | 26 +++++++------------ .../Infrastructure/CustomConfigurationData.cs | 5 ++-- .../Infrastructure/EditorConfig/README.md | 8 +++--- 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/Build/BackEnd/Components/Caching/IConfigCache.cs b/src/Build/BackEnd/Components/Caching/IConfigCache.cs index 83b13f615fe..599a86d4c1d 100644 --- a/src/Build/BackEnd/Components/Caching/IConfigCache.cs +++ b/src/Build/BackEnd/Components/Caching/IConfigCache.cs @@ -54,7 +54,7 @@ BuildRequestConfiguration this[int configId] BuildRequestConfiguration GetMatchingConfiguration(ConfigurationMetadata configMetadata); /// - /// Gets a matching configuration. If no such configration exists, one is created and optionally loaded. + /// Gets a matching configuration. If no such configuration exists, one is created and optionally loaded. /// /// The configuration metadata to match. /// Callback to be invoked if the configuration does not exist. diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index d9e2ef2ca3c..b37595cc580 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -67,7 +67,7 @@ private static bool TryExtractValue(string key, Dictionary co { value = default; - if (config == null || !config.ContainsKey(key)) + if (config == null || !config.TryGetValue(key, out string stringValue)) { return false; } @@ -76,7 +76,7 @@ private static bool TryExtractValue(string key, Dictionary co if (typeof(T) == typeof(bool)) { - if (bool.TryParse(config[key], out bool boolValue)) + if (bool.TryParse(stringValue, out bool boolValue)) { value = (T)(object)boolValue; isParsed = true; @@ -84,8 +84,7 @@ private static bool TryExtractValue(string key, Dictionary co } else if(typeof(T).IsEnum) { - - isParsed = Enum.TryParse(config[key], true, out value); + isParsed = Enum.TryParse(stringValue, true, out value); } if (!isParsed) diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs index de862ad51ba..173a4cd2928 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs @@ -16,6 +16,7 @@ namespace Microsoft.Build.BuildCheck.Infrastructure; internal sealed class BuildCheckCentralContext { private readonly ConfigurationProvider _configurationProvider; + internal BuildCheckCentralContext(ConfigurationProvider configurationProvider) { _configurationProvider = configurationProvider; diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index a98d4e674ab..27cf2cc5a12 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -15,11 +15,10 @@ namespace Microsoft.Build.BuildCheck.Infrastructure; - // TODO: https://github.com/dotnet/msbuild/issues/9628 -internal class ConfigurationProvider +internal sealed class ConfigurationProvider { - private EditorConfigParser s_editorConfigParser = new EditorConfigParser(); + private readonly EditorConfigParser s_editorConfigParser = new EditorConfigParser(); // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) @@ -27,7 +26,7 @@ internal class ConfigurationProvider private readonly Dictionary _customConfigurationData = new Dictionary(); - private readonly List _infrastructureConfigurationKeys = new List() { + private readonly string[] _infrastructureConfigurationKeys = new string[] { nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), nameof(BuildAnalyzerConfiguration.IsEnabled).ToLower(), nameof(BuildAnalyzerConfiguration.Severity).ToLower() @@ -53,12 +52,9 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st } // remove the infrastructure owned key names - foreach(var infraConfigurationKey in _infrastructureConfigurationKeys) + foreach (var infraConfigurationKey in _infrastructureConfigurationKeys) { - if (configuration.ContainsKey(infraConfigurationKey)) - { - configuration.Remove(infraConfigurationKey); - } + configuration.Remove(infraConfigurationKey); } var data = new CustomConfigurationData(ruleId, configuration); @@ -82,10 +78,8 @@ internal void CheckCustomConfigurationDataValidity(string projectFullPath, strin { var configuration = GetCustomConfiguration(projectFullPath, ruleId); - if (_customConfigurationData.ContainsKey(ruleId)) + if (_customConfigurationData.TryGetValue(ruleId, out var storedConfiguration)) { - var storedConfiguration = _customConfigurationData[ruleId]; - if (!storedConfiguration.Equals(configuration)) { throw new BuildCheckConfigurationException("Custom configuration should be equal between projects"); @@ -138,7 +132,7 @@ private TConfig[] FillConfiguration(string projectFullPath, IRea internal Dictionary GetConfiguration(string projectFullPath, string ruleId) { - var config = new Dictionary(); + Dictionary config; try { config = s_editorConfigParser.Parse(projectFullPath); @@ -148,14 +142,14 @@ internal Dictionary GetConfiguration(string projectFullPath, str throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); } - var keyTosearch = $"build_check.{ruleId}."; + var keyToSearch = $"build_check.{ruleId}."; var dictionaryConfig = new Dictionary(); foreach (var kv in config) { - if (kv.Key.StartsWith(keyTosearch, StringComparison.OrdinalIgnoreCase)) + if (kv.Key.StartsWith(keyToSearch, StringComparison.OrdinalIgnoreCase)) { - var newKey = kv.Key.Replace(keyTosearch.ToLower(), ""); + var newKey = kv.Key.Substring(keyToSearch.Length); dictionaryConfig[newKey] = kv.Value; } } diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 9470dc251e3..468aa459547 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -67,7 +67,7 @@ public override bool Equals(object? obj) var customConfigObj = (CustomConfigurationData) obj; - if(customConfigObj.RuleId != RuleId) + if (customConfigObj.RuleId != RuleId) { return false; } @@ -88,7 +88,8 @@ public override bool Equals(object? obj) return false; } } - }else if (customConfigObj.ConfigurationData == null && ConfigurationData == null) + } + else if (customConfigObj.ConfigurationData == null && ConfigurationData == null) { return true; } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md index a9ba94b53a2..14d1e75be59 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md @@ -6,7 +6,7 @@ To track the request on sharing the code: https://github.com/dotnet/roslyn/issue In current implementation the usage of the editorconfig is internal only and exposed via ConfigurationProvider functionality. -Configration divided into two categories: +Configuration divided into two categories: - Infra related configuration. IsEnabled, Severity, EvaluationAnalysisScope - Custom configuration, any other config specified by user for this particular rule @@ -24,7 +24,7 @@ For the file/folder structure: we want to fetch configuration for the project: /full/path/folder1/folder2/folder3/test.proj -Infra related and custom configration flows have one common logic: Fetching the configs from editorconfig +Infra related and custom configuration flows have one common logic: Fetching the configs from editorconfig ``` while(editorConfig is not root && parent directory exists){ @@ -41,12 +41,12 @@ Reverse the order and collect all matching section key-value pairs into new dict Remove non-msbuild-analyzer related key-values (keys not starting with msbuild_analyzer.RULEID) The implementation differs depending on category: - - Infra related config: Merges the configuration retrieved from configration module with default values (respecting the specified configs in editorconfig) + - Infra related config: Merges the configuration retrieved from configuration module with default values (respecting the specified configs in editorconfig) - Custom configuration: Remove all infra related keys from dictionary Two levels of cache introduced: - When retrieving and parsing the editor config -> Parsed results are saved into dictionary: editorconfigPath = ParsedEditorConfig -- When retrieving Infra related config: ruleId-projectPath = BuildconfigInstance +- When retrieving Infra related config: ruleId-projectPath = BuildConfigInstance Usage examples (API) From ee9eaafa28833226b5688da651de7d5da6ca90f9 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 3 Apr 2024 11:05:59 +0200 Subject: [PATCH 26/52] Update the comparison logic --- .../CustomConfigurationData_Tests.cs | 21 +++++++++ .../API/BuildAnalyzerConfiguration.cs | 43 +++++++++++++------ .../Infrastructure/CustomConfigurationData.cs | 31 ++++++++----- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs b/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs index 2c1672e6adc..f3b52c4645c 100644 --- a/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs +++ b/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs @@ -127,5 +127,26 @@ public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDif customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataKeysOrderDiffers() + { + var config1 = new Dictionary() + { + { "key1", "val1" }, + { "key2", "val2" } + }; + + var config2 = new Dictionary() + { + { "key2", "val2" }, + { "key1", "val1" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } } } diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index b37595cc580..bfe82e52166 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -63,7 +63,7 @@ public static BuildAnalyzerConfiguration Create(Dictionary confi }; } - private static bool TryExtractValue(string key, Dictionary config, out T value) where T : struct + private static bool TryExtractValue(string key, Dictionary config, out T value) where T : struct, Enum { value = default; @@ -72,28 +72,45 @@ private static bool TryExtractValue(string key, Dictionary co return false; } - bool isParsed = false; + var isParsed = Enum.TryParse(stringValue, true, out value); - if (typeof(T) == typeof(bool)) + if (!isParsed) { - if (bool.TryParse(stringValue, out bool boolValue)) - { - value = (T)(object)boolValue; - isParsed = true; - } + ThrowIncorectValueEception(key, stringValue); } - else if(typeof(T).IsEnum) + + return isParsed; + } + + private static bool TryExtractValue(string key, Dictionary config, out bool value) + { + value = default; + + if (config == null || !config.TryGetValue(key, out string stringValue)) { - isParsed = Enum.TryParse(stringValue, true, out value); + return false; } + bool isParsed = false; + + if (bool.TryParse(stringValue, out bool boolValue)) + { + value = boolValue; + isParsed = true; + } + if (!isParsed) { - throw new BuildCheckConfigurationException( - $"Incorrect value provided in config for key {key}", - buildCheckConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); + ThrowIncorectValueEception(key, stringValue); } return isParsed; } + + private static void ThrowIncorectValueEception(string key, string value) + { + throw new BuildCheckConfigurationException( + $"Incorrect value provided in config for key {key}: '{value}'", + buildCheckConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); + } } diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 468aa459547..ba23accbf17 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -73,17 +73,11 @@ public override bool Equals(object? obj) } // validate keys and values - if (customConfigObj.ConfigurationData != null && ConfigurationData != null) + if (customConfigObj.ConfigurationData != null && ConfigurationData != null && ConfigurationData.Count == customConfigObj.ConfigurationData.Count) { - if (!customConfigObj.ConfigurationData.Keys.SequenceEqual(ConfigurationData.Keys)) + foreach (var keyVal in customConfigObj.ConfigurationData) { - return false; - } - - var keys = customConfigObj.ConfigurationData.Keys; - foreach (var key in keys) - { - if (customConfigObj.ConfigurationData[key] != ConfigurationData[key]) + if(!ConfigurationData.TryGetValue(keyVal.Key, out string value) || value != keyVal.Value) { return false; } @@ -101,5 +95,22 @@ public override bool Equals(object? obj) return true; } - public override int GetHashCode() => (ConfigurationData != null ? ConfigurationData.GetHashCode() : 0); + public override int GetHashCode() + { + if (!NotNull(this)) + { + return 0; + } + + var hashCode = RuleId.GetHashCode(); + if (ConfigurationData != null) + { + foreach (var keyVal in ConfigurationData) + { + hashCode = hashCode + keyVal.Key.GetHashCode() + keyVal.Value.GetHashCode(); + } + } + + return hashCode; + } } From 5cb1c30c24081ff0c265accdc3d656a6ca712708 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Wed, 3 Apr 2024 12:28:42 +0200 Subject: [PATCH 27/52] Address style issue/ null warnings --- .../BuildAnalyzerConfiguration_Test.cs | 2 +- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 10 +++++----- .../Infrastructure/CustomConfigurationData.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs index 1709fa7d5d7..6863e02cbf7 100644 --- a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -19,7 +19,7 @@ public class BuildAnalyzerConfiguration_Test [Fact] public void CreateWithNull_ReturnsObjectWithNullValues() { - var buildConfig = BuildAnalyzerConfiguration.Create(null!); + var buildConfig = BuildAnalyzerConfiguration.Create(null); buildConfig.ShouldNotBeNull(); buildConfig.Severity.ShouldBeNull(); buildConfig.IsEnabled.ShouldBeNull(); diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index bfe82e52166..279019ee7fd 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -53,7 +53,7 @@ public class BuildAnalyzerConfiguration /// /// The configuration dictionary containing the settings for the build analyzer. /// A new instance of with the specified settings. - public static BuildAnalyzerConfiguration Create(Dictionary configDictionary) + public static BuildAnalyzerConfiguration Create(Dictionary? configDictionary) { return new() { @@ -63,11 +63,11 @@ public static BuildAnalyzerConfiguration Create(Dictionary confi }; } - private static bool TryExtractValue(string key, Dictionary config, out T value) where T : struct, Enum + private static bool TryExtractValue(string key, Dictionary? config, out T value) where T : struct, Enum { value = default; - if (config == null || !config.TryGetValue(key, out string stringValue)) + if (config == null || !config.TryGetValue(key, out var stringValue) || stringValue is null) { return false; } @@ -82,11 +82,11 @@ private static bool TryExtractValue(string key, Dictionary co return isParsed; } - private static bool TryExtractValue(string key, Dictionary config, out bool value) + private static bool TryExtractValue(string key, Dictionary? config, out bool value) { value = default; - if (config == null || !config.TryGetValue(key, out string stringValue)) + if (config == null || !config.TryGetValue(key, out var stringValue) || stringValue is null) { return false; } diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index ba23accbf17..af925f47b6c 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -77,7 +77,7 @@ public override bool Equals(object? obj) { foreach (var keyVal in customConfigObj.ConfigurationData) { - if(!ConfigurationData.TryGetValue(keyVal.Key, out string value) || value != keyVal.Value) + if(!ConfigurationData.TryGetValue(keyVal.Key, out var value) || value != keyVal.Value) { return false; } From 1bc3c68018bf05d905a4c721d9b814192d58f549 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 09:01:41 +0200 Subject: [PATCH 28/52] fixing merge conflicts --- eng/BootStrapMSBuild.props | 21 ------------------- eng/BootStrapMsBuild.props | 21 ------------------- .../RequestBuilder/RequestBuilder.cs | 5 ----- src/Build/BuildCheck/API/BuildAnalyzerRule.cs | 6 ------ .../BuildCheckCentralContext.cs | 2 +- .../BuildCheckConfigurationException.cs | 2 -- .../Infrastructure/BuildEventsProcessor.cs | 2 -- src/Build/Microsoft.Build.csproj | 4 ++++ .../MSBuild.Bootstrap.csproj | 4 +--- .../Microsoft.Build.UnitTests.Shared.csproj | 18 +--------------- 10 files changed, 7 insertions(+), 78 deletions(-) delete mode 100644 eng/BootStrapMSBuild.props delete mode 100644 eng/BootStrapMsBuild.props diff --git a/eng/BootStrapMSBuild.props b/eng/BootStrapMSBuild.props deleted file mode 100644 index e70bcb3489d..00000000000 --- a/eng/BootStrapMSBuild.props +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - $(ArtifactsBinDir)bootstrap\ - $(BootstrapDestination)$(Platform)\ - $(BootstrapDestination)$(TargetFramework.ToLowerInvariant())\MSBuild\ - - - - $(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin - - - - $(BootstrapDestination) - - diff --git a/eng/BootStrapMsBuild.props b/eng/BootStrapMsBuild.props deleted file mode 100644 index 858cf76ac54..00000000000 --- a/eng/BootStrapMsBuild.props +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - $(ArtifactsBinDir)bootstrap\ - $(BootstrapDestination)$(Platform)\ - $(BootstrapDestination)$(TargetFramework.ToLowerInvariant())\MSBuild\ - - - - $(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin - - - - $(BootstrapDestination) - - diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index 4bf1a93f151..1a4693ba685 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -1102,11 +1102,6 @@ private void SetProjectCurrentDirectory() /// private async Task BuildProject() { - // We consider this the entrypoint for the project build for purposes of BuildCheck processing - - var buildCheckManager = (_componentHost.GetComponent(BuildComponentType.BuildCheck) as IBuildCheckManagerProvider)!.Instance; - buildCheckManager.SetDataSource(BuildCheckDataSource.BuildExecution); - ErrorUtilities.VerifyThrow(_targetBuilder != null, "Target builder is null"); // We consider this the entrypoint for the project build for purposes of BuildCheck processing diff --git a/src/Build/BuildCheck/API/BuildAnalyzerRule.cs b/src/Build/BuildCheck/API/BuildAnalyzerRule.cs index c06aa3f1c60..8b43dad4999 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerRule.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerRule.cs @@ -16,7 +16,6 @@ public BuildAnalyzerRule(string id, string title, string description, string mes Id = id; Title = title; Description = description; - Category = category; MessageFormat = messageFormat; DefaultConfiguration = defaultConfiguration; } @@ -43,11 +42,6 @@ public BuildAnalyzerRule(string id, string title, string description, string mes /// public string Description { get; } - /// - /// TODO: We might turn this into enum, or just remove this. - /// - public string Category { get; } - /// /// Message format that will be used by the actual reports () - those will just supply the actual arguments. /// diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs index 17d87ec75e4..fee086d088c 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckCentralContext.cs @@ -119,7 +119,7 @@ private void RunRegisteredActions( else { configPerRule = - ConfigurationProvider.GetMergedConfigurations(projectFullPath, + _configurationProvider.GetMergedConfigurations(projectFullPath, analyzerCallback.Item1.BuildAnalyzer); if (configPerRule.All(c => !c.IsEnabled)) { diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs index 0469969c813..4e88b492a3b 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationException.cs @@ -15,8 +15,6 @@ internal sealed class BuildCheckConfigurationException : Exception /// Exception to communicate issues with user specified configuration - unsupported scenarios, malformations, etc. /// This exception usually leads to defuncting the particular analyzer for the rest of the build (even if issue occured with a single project). /// - public BuildCheckConfigurationException(string message) : base(message) - /// internal BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope; public BuildCheckConfigurationException(string message, Exception innerException, BuildCheckConfigurationErrorScope buildCheckConfigurationErrorScope = BuildCheckConfigurationErrorScope.SingleRule) : base(message, innerException) diff --git a/src/Build/BuildCheck/Infrastructure/BuildEventsProcessor.cs b/src/Build/BuildCheck/Infrastructure/BuildEventsProcessor.cs index c3a3d1c68e8..9514f0a7ca0 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildEventsProcessor.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildEventsProcessor.cs @@ -33,8 +33,6 @@ internal void ProcessEvaluationFinishedEventArgs( AnalyzerLoggingContext buildAnalysisContext, ProjectEvaluationFinishedEventArgs evaluationFinishedEventArgs) { - LoggingContext loggingContext = buildAnalysisContext.ToLoggingContext(); - Dictionary propertiesLookup = new Dictionary(); Internal.Utilities.EnumerateProperties(evaluationFinishedEventArgs.Properties, propertiesLookup, static (dict, kvp) => dict.Add(kvp.Key, kvp.Value)); diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index dc2d2ed13ca..bceb0f4266b 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -165,6 +165,10 @@ + + + + diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj index d1a614d9805..1d116d117d2 100644 --- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj +++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj @@ -2,8 +2,6 @@ - - $(RuntimeOutputTargetFrameworks) @@ -52,4 +50,4 @@ - + \ No newline at end of file diff --git a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj index fee3abf670f..0bade6a09d5 100644 --- a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj +++ b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj @@ -1,6 +1,6 @@ - $(RuntimeOutputTargetFrameworks) + $(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild) Microsoft.Build.UnitTests.Shared true false @@ -18,21 +18,5 @@ - - false - false - - - - - - - - - - - - <_Parameter1>$(BootstrapBinaryDestination) - From 3259876178e72ef05ceb1fc9cbd11cd5997963a0 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 09:49:43 +0200 Subject: [PATCH 29/52] Fix merged conflicts. Update tests --- eng/BootStrapMsBuild.props | 21 + src/Analyzers.UnitTests/AssemblyInfo.cs | 4 - .../BuildAnalyzerConfiguration_Test.cs | 119 -- .../ConfigurationProvider_Tests.cs | 230 ---- .../CustomConfigurationData_Tests.cs | 152 --- .../EditorConfigParser_Tests.cs | 127 -- src/Analyzers.UnitTests/EditorConfig_Tests.cs | 1082 ----------------- src/Analyzers.UnitTests/EndToEndTests.cs | 118 -- ...Microsoft.Build.Analyzers.UnitTests.csproj | 44 - .../BuildAnalyzerConfiguration_Test.cs | 118 ++ .../ConfigurationProvider_Tests.cs | 229 ++++ .../CustomConfigurationData_Tests.cs | 151 +++ .../EditorConfigParser_Tests.cs | 125 ++ .../EditorConfig_Tests.cs | 1081 ++++++++++++++++ src/BuildCheck.UnitTests/EndToEndTests.cs | 33 +- .../Microsoft.Build.UnitTests.Shared.csproj | 20 +- 16 files changed, 1755 insertions(+), 1899 deletions(-) create mode 100644 eng/BootStrapMsBuild.props delete mode 100644 src/Analyzers.UnitTests/AssemblyInfo.cs delete mode 100644 src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs delete mode 100644 src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs delete mode 100644 src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs delete mode 100644 src/Analyzers.UnitTests/EditorConfigParser_Tests.cs delete mode 100644 src/Analyzers.UnitTests/EditorConfig_Tests.cs delete mode 100644 src/Analyzers.UnitTests/EndToEndTests.cs delete mode 100644 src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj create mode 100644 src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs create mode 100644 src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs create mode 100644 src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs create mode 100644 src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs create mode 100644 src/BuildCheck.UnitTests/EditorConfig_Tests.cs diff --git a/eng/BootStrapMsBuild.props b/eng/BootStrapMsBuild.props new file mode 100644 index 00000000000..c84fa2f5ca4 --- /dev/null +++ b/eng/BootStrapMsBuild.props @@ -0,0 +1,21 @@ + + + + + + $(ArtifactsBinDir)bootstrap\ + $(BootstrapDestination)$(Platform)\ + $(BootstrapDestination)$(TargetFramework.ToLowerInvariant())\MSBuild\ + + + + $(BootstrapDestination)$(TargetMSBuildToolsVersion)\Bin + + + + $(BootstrapDestination) + + \ No newline at end of file diff --git a/src/Analyzers.UnitTests/AssemblyInfo.cs b/src/Analyzers.UnitTests/AssemblyInfo.cs deleted file mode 100644 index 3b5d7bbb185..00000000000 --- a/src/Analyzers.UnitTests/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -global using NativeMethodsShared = Microsoft.Build.Framework.NativeMethods; diff --git a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs deleted file mode 100644 index 6863e02cbf7..00000000000 --- a/src/Analyzers.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Metadata; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; -using Microsoft.Build.Experimental.BuildCheck; -using Shouldly; -using Xunit; - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class BuildAnalyzerConfiguration_Test - { - [Fact] - public void CreateWithNull_ReturnsObjectWithNullValues() - { - var buildConfig = BuildAnalyzerConfiguration.Create(null); - buildConfig.ShouldNotBeNull(); - buildConfig.Severity.ShouldBeNull(); - buildConfig.IsEnabled.ShouldBeNull(); - buildConfig.EvaluationAnalysisScope.ShouldBeNull(); - } - - [Fact] - public void CreateWithEmpty_ReturnsObjectWithNullValues() - { - var buildConfig = BuildAnalyzerConfiguration.Create(new Dictionary()); - buildConfig.ShouldNotBeNull(); - buildConfig.Severity.ShouldBeNull(); - buildConfig.IsEnabled.ShouldBeNull(); - buildConfig.EvaluationAnalysisScope.ShouldBeNull(); - } - - [Theory] - [InlineData("error", BuildAnalyzerResultSeverity.Error)] - [InlineData("info", BuildAnalyzerResultSeverity.Info)] - [InlineData("warning", BuildAnalyzerResultSeverity.Warning)] - [InlineData("WARNING", BuildAnalyzerResultSeverity.Warning)] - public void CreateBuildAnalyzerConfiguration_Severity(string parameter, BuildAnalyzerResultSeverity? expected) - { - var config = new Dictionary() - { - { "severity" , parameter }, - }; - var buildConfig = BuildAnalyzerConfiguration.Create(config); - - buildConfig.ShouldNotBeNull(); - buildConfig.Severity.ShouldBe(expected); - - buildConfig.IsEnabled.ShouldBeNull(); - buildConfig.EvaluationAnalysisScope.ShouldBeNull(); - } - - [Theory] - [InlineData("true", true)] - [InlineData("TRUE", true)] - [InlineData("false", false)] - [InlineData("FALSE", false)] - public void CreateBuildAnalyzerConfiguration_IsEnabled(string parameter, bool? expected) - { - var config = new Dictionary() - { - { "isenabled" , parameter }, - }; - - var buildConfig = BuildAnalyzerConfiguration.Create(config); - - buildConfig.ShouldNotBeNull(); - buildConfig.IsEnabled.ShouldBe(expected); - - buildConfig.Severity.ShouldBeNull(); - buildConfig.EvaluationAnalysisScope.ShouldBeNull(); - } - - [Theory] - [InlineData("AnalyzedProjectOnly", EvaluationAnalysisScope.AnalyzedProjectOnly)] - [InlineData("AnalyzedProjectWithImportsFromCurrentWorkTree", EvaluationAnalysisScope.AnalyzedProjectWithImportsFromCurrentWorkTree)] - [InlineData("AnalyzedProjectWithImportsWithoutSdks", EvaluationAnalysisScope.AnalyzedProjectWithImportsWithoutSdks)] - [InlineData("AnalyzedProjectWithAllImports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] - [InlineData("analyzedprojectwithallimports", EvaluationAnalysisScope.AnalyzedProjectWithAllImports)] - public void CreateBuildAnalyzerConfiguration_EvaluationAnalysisScope(string parameter, EvaluationAnalysisScope? expected) - { - var config = new Dictionary() - { - { "evaluationanalysisscope" , parameter }, - }; - - var buildConfig = BuildAnalyzerConfiguration.Create(config); - - buildConfig.ShouldNotBeNull(); - buildConfig.EvaluationAnalysisScope.ShouldBe(expected); - - buildConfig.IsEnabled.ShouldBeNull(); - buildConfig.Severity.ShouldBeNull(); - } - - [Theory] - [InlineData("evaluationanalysisscope", "incorrec-value")] - [InlineData("isenabled", "incorrec-value")] - [InlineData("severity", "incorrec-value")] - public void CreateBuildAnalyzerConfiguration_ExceptionOnInvalidInputValue(string key, string value) - { - var config = new Dictionary() - { - { key , value}, - }; - - var exception = Should.Throw(() => { - BuildAnalyzerConfiguration.Create(config); - }); - exception.Message.ShouldContain($"Incorrect value provided in config for key {key}"); - } - } -} diff --git a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs b/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs deleted file mode 100644 index 826c040488e..00000000000 --- a/src/Analyzers.UnitTests/ConfigurationProvider_Tests.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; -using Microsoft.Build.Experimental.BuildCheck; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class ConfigurationProvider_Tests - { - [Fact] - public void GetRuleIdConfiguration_ReturnsEmptyConfig() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - test_key=test_value_updated - """); - - var configurationProvider = new ConfigurationProvider(); - var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - // empty - configs.ShouldBe(new Dictionary()); - } - - [Fact] - public void GetRuleIdConfiguration_ReturnsConfiguration() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - """); - - var configurationProvider = new ConfigurationProvider(); - var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - configs.Keys.Count.ShouldBe(2); - - configs.ContainsKey("property1").ShouldBeTrue(); - configs.ContainsKey("property2").ShouldBeTrue(); - - configs["property2"].ShouldBe("value2"); - configs["property1"].ShouldBe("value1"); - } - - [Fact] - public void GetRuleIdConfiguration_CustomConfigurationData() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled=true - build_check.rule_id.isEnabled2=true - any_other_key1=any_other_value1 - any_other_key2=any_other_value2 - any_other_key3=any_other_value3 - any_other_key3=any_other_value3 - """); - - var configurationProvider = new ConfigurationProvider(); - var customConfiguration = configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - var configs = customConfiguration.ConfigurationData; - - configs!.Keys.Count().ShouldBe(3); - - configs.ContainsKey("property1").ShouldBeTrue(); - configs.ContainsKey("property2").ShouldBeTrue(); - configs.ContainsKey("isenabled2").ShouldBeTrue(); - } - - [Fact] - public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.isEnabled=true - build_check.rule_id.Severity=Error - build_check.rule_id.EvaluationAnalysisScope=AnalyzedProjectOnly - """); - - var configurationProvider = new ConfigurationProvider(); - var buildConfig = configurationProvider.GetUserConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - buildConfig.ShouldNotBeNull(); - - buildConfig.IsEnabled?.ShouldBeTrue(); - buildConfig.Severity?.ShouldBe(BuildAnalyzerResultSeverity.Error); - buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.AnalyzedProjectOnly); - } - - - [Fact] - public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentValues() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled=true - build_check.rule_id.isEnabled2=true - - [test123.csproj] - build_check.rule_id.property1=value2 - build_check.rule_id.property2=value3 - build_check.rule_id.isEnabled=true - build_check.rule_id.isEnabled2=tru1 - """); - - var configurationProvider = new ConfigurationProvider(); - configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - // should not fail => configurations are the same - Should.Throw(() => - { - configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); - }); - } - - [Fact] - public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentKeys() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled2=true - - [test123.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled2=true - build_check.rule_id.isEnabled3=true - """); - - var configurationProvider = new ConfigurationProvider(); - configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - // should not fail => configurations are the same - Should.Throw(() => - { - configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); - }); - } - - - [Fact] - public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled=true - build_check.rule_id.isEnabled2=true - - [test123.csproj] - build_check.rule_id.property1=value1 - build_check.rule_id.property2=value2 - build_check.rule_id.isEnabled=true - build_check.rule_id.isEnabled2=true - """); - - var configurationProvider = new ConfigurationProvider(); - configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); - - // should fail, because the configs are the different - Should.NotThrow(() => - { - configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); - }); - } - } -} diff --git a/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs b/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs deleted file mode 100644 index f3b52c4645c..00000000000 --- a/src/Analyzers.UnitTests/CustomConfigurationData_Tests.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; -using Microsoft.Build.Experimental.BuildCheck; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class CustomConfigurationData_Tests - { - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeTrue_NullInstance() - { - var customConfigurationData1 = CustomConfigurationData.Null; - var customConfigurationData2 = CustomConfigurationData.Null; - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeTrue_SameInstance() - { - var customConfigurationData1 = new CustomConfigurationData("testRuleId"); - var customConfigurationData2 = customConfigurationData1; - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeFalse_DifferentObjectType() - { - var customConfigurationData1 = new CustomConfigurationData("testRuleId"); - var customConfigurationData2 = new object(); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeTrue_DifferentInstanceSameValues() - { - var customConfigurationData1 = new CustomConfigurationData("testRuleId"); - var customConfigurationData2 = new CustomConfigurationData("testRuleId"); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); - } - - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataSame() - { - var config1 = new Dictionary() - { - { "key1", "val1" } - }; - - var config2 = new Dictionary() - { - { "key1", "val1" } - }; - var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); - var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); - } - - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferent() - { - var config = new Dictionary() - { - { "key1", "val1" } - }; - var customConfigurationData1 = new CustomConfigurationData("testRuleId", config); - var customConfigurationData2 = new CustomConfigurationData("testRuleId"); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentKeys() - { - var config1 = new Dictionary() - { - { "key1", "val1" } - }; - - var config2 = new Dictionary() - { - { "key2", "val2" } - }; - - var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); - var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentValues() - { - var config1 = new Dictionary() - { - { "key1", "val1" } - }; - - var config2 = new Dictionary() - { - { "key1", "val2" } - }; - - var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); - var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); - } - - [Fact] - public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataKeysOrderDiffers() - { - var config1 = new Dictionary() - { - { "key1", "val1" }, - { "key2", "val2" } - }; - - var config2 = new Dictionary() - { - { "key2", "val2" }, - { "key1", "val1" } - }; - - var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); - var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); - - customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); - } - } -} diff --git a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs b/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs deleted file mode 100644 index e39dfb6681f..00000000000 --- a/src/Analyzers.UnitTests/EditorConfigParser_Tests.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; -using Microsoft.Build.UnitTests; -using Shouldly; -using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; - - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class EditorConfigParser_Tests - { - [Fact] - public void NoSectionConfigured_ResultsEmptyResultConfig() - { - var configs = new List(){ - EditorConfigFile.Parse("""" - property1=value1 -""""), - EditorConfigFile.Parse("""" - property1=value2 - """"), - EditorConfigFile.Parse("""" - property1=value3 - """"), - }; - - var parser = new EditorConfigParser(); - var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file"); - mergedResult.Keys.Count.ShouldBe(0); - } - - [Fact] - public void ProperOrderOfconfiguration_ClosestToTheFileShouldBeApplied() - { - var configs = new List(){ - EditorConfigFile.Parse("""" - [*] - property1=value1 -""""), - EditorConfigFile.Parse("""" - [*] - property1=value2 - """"), - EditorConfigFile.Parse("""" - [*] - property1=value3 - """"), - }; - - var parser = new EditorConfigParser(); - var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file.proj"); - mergedResult.Keys.Count.ShouldBe(1); - mergedResult["property1"].ShouldBe("value1"); - } - - [Fact] - public void EditorconfigFileDiscovery_RootTrue() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); - - TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", - """ - root=true - - [*.csproj] - test_key=test_value_updated - """); - - - TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - [*.csproj] - test_key=should_not_be_respected_and_parsed - """); - - var parser = new EditorConfigParser(); - var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj") ).ToList(); - // should be one because root=true so we do not need to go further - listOfEditorConfigFile.Count.ShouldBe(1); - listOfEditorConfigFile[0].IsRoot.ShouldBeTrue(); - listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); - listOfEditorConfigFile[0].NamedSections[0].Properties["test_key"].ShouldBe("test_value_updated"); - } - - [Fact] - public void EditorconfigFileDiscovery_RootFalse() - { - using TestEnvironment testEnvironment = TestEnvironment.Create(); - - TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); - TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); - - TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", - """ - [*.csproj] - test_key=test_value_updated - """); - - TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", - """ - [*.csproj] - test_key=will_be_there - """); - - var parser = new EditorConfigParser(); - var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj")).ToList(); - - listOfEditorConfigFile.Count.ShouldBe(2); - listOfEditorConfigFile[0].IsRoot.ShouldBeFalse(); - listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); - } - } -} diff --git a/src/Analyzers.UnitTests/EditorConfig_Tests.cs b/src/Analyzers.UnitTests/EditorConfig_Tests.cs deleted file mode 100644 index 71b367cc7a0..00000000000 --- a/src/Analyzers.UnitTests/EditorConfig_Tests.cs +++ /dev/null @@ -1,1082 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; -using Microsoft.Build.UnitTests; -using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; - -#nullable disable - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class EditorConfig_Tests - { - - #region AssertEqualityComparer - private sealed class AssertEqualityComparer : IEqualityComparer - { - public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); - - private static bool CanBeNull() - { - var type = typeof(T); - return !type.GetTypeInfo().IsValueType || - (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); - } - - public static bool IsNull(T @object) - { - if (!CanBeNull()) - { - return false; - } - - return object.Equals(@object, default(T)); - } - - public static bool Equals(T left, T right) - { - return Instance.Equals(left, right); - } - - bool IEqualityComparer.Equals(T x, T y) - { - if (CanBeNull()) - { - if (object.Equals(x, default(T))) - { - return object.Equals(y, default(T)); - } - - if (object.Equals(y, default(T))) - { - return false; - } - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - if (x is IEquatable equatable) - { - return equatable.Equals(y); - } - - if (x is IComparable comparableT) - { - return comparableT.CompareTo(y) == 0; - } - - if (x is IComparable comparable) - { - return comparable.CompareTo(y) == 0; - } - - var enumerableX = x as IEnumerable; - var enumerableY = y as IEnumerable; - - if (enumerableX != null && enumerableY != null) - { - var enumeratorX = enumerableX.GetEnumerator(); - var enumeratorY = enumerableY.GetEnumerator(); - - while (true) - { - bool hasNextX = enumeratorX.MoveNext(); - bool hasNextY = enumeratorY.MoveNext(); - - if (!hasNextX || !hasNextY) - { - return hasNextX == hasNextY; - } - - if (!Equals(enumeratorX.Current, enumeratorY.Current)) - { - return false; - } - } - } - - return object.Equals(x, y); - } - - int IEqualityComparer.GetHashCode(T obj) - { - throw new NotImplementedException(); - } - } - - #endregion - - // Section Matchin Test cases: https://github.com/dotnet/roslyn/blob/ba163e712b01358a217065eec8a4a82f94a7efd5/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs#L337 - #region Section Matching Tests - [Fact] - public void SimpleNameMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc").Value; - Assert.Equal("^.*/abc$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc")); - Assert.False(matcher.IsMatch("/aabc")); - Assert.False(matcher.IsMatch("/ abc")); - Assert.False(matcher.IsMatch("/cabc")); - } - - [Fact] - public void StarOnlyMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("*").Value; - Assert.Equal("^.*/[^/]*$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc")); - Assert.True(matcher.IsMatch("/123")); - Assert.True(matcher.IsMatch("/abc/123")); - } - - [Fact] - public void StarNameMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.cs").Value; - Assert.Equal("^.*/[^/]*\\.cs$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc.cs")); - Assert.True(matcher.IsMatch("/123.cs")); - Assert.True(matcher.IsMatch("/dir/subpath.cs")); - // Only '/' is defined as a directory separator, so the caller - // is responsible for converting any other machine directory - // separators to '/' before matching - Assert.True(matcher.IsMatch("/dir\\subpath.cs")); - - Assert.False(matcher.IsMatch("/abc.vb")); - } - - [Fact] - public void StarStarNameMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("**.cs").Value; - Assert.Equal("^.*/.*\\.cs$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc.cs")); - Assert.True(matcher.IsMatch("/dir/subpath.cs")); - } - - [Fact] - public void EscapeDot() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("...").Value; - Assert.Equal("^.*/\\.\\.\\.$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/...")); - Assert.True(matcher.IsMatch("/subdir/...")); - Assert.False(matcher.IsMatch("/aaa")); - Assert.False(matcher.IsMatch("/???")); - Assert.False(matcher.IsMatch("/abc")); - } - - [Fact] - public void EndBackslashMatch() - { - SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc\\"); - Assert.Null(matcher); - } - - [Fact] - public void QuestionMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab?def").Value; - Assert.Equal("^.*/ab.def$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abcdef")); - Assert.True(matcher.IsMatch("/ab?def")); - Assert.True(matcher.IsMatch("/abzdef")); - Assert.True(matcher.IsMatch("/ab/def")); - Assert.True(matcher.IsMatch("/ab\\def")); - } - - [Fact] - public void LiteralBackslash() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab\\\\c").Value; - Assert.Equal("^.*/ab\\\\c$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/ab\\c")); - Assert.False(matcher.IsMatch("/ab/c")); - Assert.False(matcher.IsMatch("/ab\\\\c")); - } - - [Fact] - public void LiteralStars() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\***\\*\\**").Value; - Assert.Equal("^.*/\\*.*\\*\\*[^/]*$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/*ab/cd**efg*")); - Assert.False(matcher.IsMatch("/ab/cd**efg*")); - Assert.False(matcher.IsMatch("/*ab/cd*efg*")); - Assert.False(matcher.IsMatch("/*ab/cd**ef/gh")); - } - - [Fact] - public void LiteralQuestions() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\??\\?*\\??").Value; - Assert.Equal("^.*/\\?.\\?[^/]*\\?.$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/?a?cde?f")); - Assert.True(matcher.IsMatch("/???????f")); - Assert.False(matcher.IsMatch("/aaaaaaaa")); - Assert.False(matcher.IsMatch("/aa?cde?f")); - Assert.False(matcher.IsMatch("/?a?cdexf")); - Assert.False(matcher.IsMatch("/?axcde?f")); - } - - [Fact] - public void LiteralBraces() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\{\\}def").Value; - Assert.Equal(@"^.*/abc\{}def$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc{}def")); - Assert.True(matcher.IsMatch("/subdir/abc{}def")); - Assert.False(matcher.IsMatch("/abcdef")); - Assert.False(matcher.IsMatch("/abc}{def")); - } - - [Fact] - public void LiteralComma() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\,def").Value; - Assert.Equal("^.*/abc,def$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc,def")); - Assert.True(matcher.IsMatch("/subdir/abc,def")); - Assert.False(matcher.IsMatch("/abcdef")); - Assert.False(matcher.IsMatch("/abc\\,def")); - Assert.False(matcher.IsMatch("/abc`def")); - } - - [Fact] - public void SimpleChoice() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.{cs,vb,fs}").Value; - Assert.Equal("^.*/[^/]*\\.(?:cs|vb|fs)$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc.cs")); - Assert.True(matcher.IsMatch("/abc.vb")); - Assert.True(matcher.IsMatch("/abc.fs")); - Assert.True(matcher.IsMatch("/subdir/abc.cs")); - Assert.True(matcher.IsMatch("/subdir/abc.vb")); - Assert.True(matcher.IsMatch("/subdir/abc.fs")); - - Assert.False(matcher.IsMatch("/abcxcs")); - Assert.False(matcher.IsMatch("/abcxvb")); - Assert.False(matcher.IsMatch("/abcxfs")); - Assert.False(matcher.IsMatch("/subdir/abcxcs")); - Assert.False(matcher.IsMatch("/subdir/abcxcb")); - Assert.False(matcher.IsMatch("/subdir/abcxcs")); - } - - [Fact] - public void OneChoiceHasSlashes() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs,subdir/test.vb}").Value; - // This is an interesting case that may be counterintuitive. A reasonable understanding - // of the section matching could interpret the choice as generating multiple identical - // sections, so [{a, b, c}] would be equivalent to [a] ... [b] ... [c] with all of the - // same properties in each section. This is somewhat true, but the rules of how the matching - // prefixes are constructed violate this assumption because they are defined as whether or - // not a section contains a slash, not whether any of the choices contain a slash. So while - // [*.cs] usually translates into '**/*.cs' because it contains no slashes, the slashes in - // the second choice make this into '/*.cs', effectively matching only files in the root - // directory of the match, instead of all subdirectories. - Assert.Equal("^/(?:[^/]*\\.cs|subdir/test\\.vb)$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/test.cs")); - Assert.True(matcher.IsMatch("/subdir/test.vb")); - - Assert.False(matcher.IsMatch("/subdir/test.cs")); - Assert.False(matcher.IsMatch("/subdir/subdir/test.vb")); - Assert.False(matcher.IsMatch("/test.vb")); - } - - [Fact] - public void EmptyChoice() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("{}").Value; - Assert.Equal("^.*/(?:)$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/")); - Assert.True(matcher.IsMatch("/subdir/")); - Assert.False(matcher.IsMatch("/.")); - Assert.False(matcher.IsMatch("/anything")); - } - - [Fact] - public void SingleChoice() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs}").Value; - Assert.Equal("^.*/(?:[^/]*\\.cs)$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/test.cs")); - Assert.True(matcher.IsMatch("/subdir/test.cs")); - Assert.False(matcher.IsMatch("test.vb")); - Assert.False(matcher.IsMatch("testxcs")); - } - - [Fact] - public void UnmatchedBraces() - { - SectionNameMatcher? matcher = TryCreateSectionNameMatcher("{{{{}}"); - Assert.Null(matcher); - } - - [Fact] - public void CommaOutsideBraces() - { - SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc,def"); - Assert.Null(matcher); - } - - [Fact] - public void RecursiveChoice() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("{test{.cs,.vb},other.{a{bb,cc}}}").Value; - Assert.Equal("^.*/(?:test(?:\\.cs|\\.vb)|other\\.(?:a(?:bb|cc)))$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/test.cs")); - Assert.True(matcher.IsMatch("/test.vb")); - Assert.True(matcher.IsMatch("/subdir/test.cs")); - Assert.True(matcher.IsMatch("/subdir/test.vb")); - Assert.True(matcher.IsMatch("/other.abb")); - Assert.True(matcher.IsMatch("/other.acc")); - - Assert.False(matcher.IsMatch("/test.fs")); - Assert.False(matcher.IsMatch("/other.bbb")); - Assert.False(matcher.IsMatch("/other.ccc")); - Assert.False(matcher.IsMatch("/subdir/other.bbb")); - Assert.False(matcher.IsMatch("/subdir/other.ccc")); - } - - [Fact] - public void DashChoice() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{-}cd{-,}ef").Value; - Assert.Equal("^.*/ab(?:-)cd(?:-|)ef$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/ab-cd-ef")); - Assert.True(matcher.IsMatch("/ab-cdef")); - - Assert.False(matcher.IsMatch("/abcdef")); - Assert.False(matcher.IsMatch("/ab--cd-ef")); - Assert.False(matcher.IsMatch("/ab--cd--ef")); - } - - [Fact] - public void MiddleMatch() - { - SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{cs,vb,fs}cd").Value; - Assert.Equal("^.*/ab(?:cs|vb|fs)cd$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abcscd")); - Assert.True(matcher.IsMatch("/abvbcd")); - Assert.True(matcher.IsMatch("/abfscd")); - - Assert.False(matcher.IsMatch("/abcs")); - Assert.False(matcher.IsMatch("/abcd")); - Assert.False(matcher.IsMatch("/vbcd")); - } - - private static IEnumerable<(string, string)> RangeAndInverse(string s1, string s2) - { - yield return (s1, s2); - yield return (s2, s1); - } - - [Fact] - public void NumberMatch() - { - foreach (var (i1, i2) in RangeAndInverse("0", "10")) - { - var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; - - Assert.True(matcher.IsMatch("/0")); - Assert.True(matcher.IsMatch("/10")); - Assert.True(matcher.IsMatch("/5")); - Assert.True(matcher.IsMatch("/000005")); - Assert.False(matcher.IsMatch("/-1")); - Assert.False(matcher.IsMatch("/-00000001")); - Assert.False(matcher.IsMatch("/11")); - } - } - - [Fact] - public void NumberMatchNegativeRange() - { - foreach (var (i1, i2) in RangeAndInverse("-10", "0")) - { - var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; - - Assert.True(matcher.IsMatch("/0")); - Assert.True(matcher.IsMatch("/-10")); - Assert.True(matcher.IsMatch("/-5")); - Assert.False(matcher.IsMatch("/1")); - Assert.False(matcher.IsMatch("/-11")); - Assert.False(matcher.IsMatch("/--0")); - } - } - - [Fact] - public void NumberMatchNegToPos() - { - foreach (var (i1, i2) in RangeAndInverse("-10", "10")) - { - var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; - - Assert.True(matcher.IsMatch("/0")); - Assert.True(matcher.IsMatch("/-5")); - Assert.True(matcher.IsMatch("/5")); - Assert.True(matcher.IsMatch("/-10")); - Assert.True(matcher.IsMatch("/10")); - Assert.False(matcher.IsMatch("/-11")); - Assert.False(matcher.IsMatch("/11")); - Assert.False(matcher.IsMatch("/--0")); - } - } - - [Fact] - public void MultipleNumberRanges() - { - foreach (var matchString in new[] { "a{-10..0}b{0..10}", "a{0..-10}b{10..0}" }) - { - var matcher = TryCreateSectionNameMatcher(matchString).Value; - - Assert.True(matcher.IsMatch("/a0b0")); - Assert.True(matcher.IsMatch("/a-5b0")); - Assert.True(matcher.IsMatch("/a-5b5")); - Assert.True(matcher.IsMatch("/a-5b10")); - Assert.True(matcher.IsMatch("/a-10b10")); - Assert.True(matcher.IsMatch("/a-10b0")); - Assert.True(matcher.IsMatch("/a-0b0")); - Assert.True(matcher.IsMatch("/a-0b-0")); - - Assert.False(matcher.IsMatch("/a-11b10")); - Assert.False(matcher.IsMatch("/a-11b10")); - Assert.False(matcher.IsMatch("/a-10b11")); - } - } - - [Fact] - public void BadNumberRanges() - { - var matcherOpt = TryCreateSectionNameMatcher("{0.."); - - Assert.Null(matcherOpt); - - var matcher = TryCreateSectionNameMatcher("{0..}").Value; - - Assert.True(matcher.IsMatch("/0..")); - Assert.False(matcher.IsMatch("/0")); - Assert.False(matcher.IsMatch("/0.")); - Assert.False(matcher.IsMatch("/0abc")); - - matcher = TryCreateSectionNameMatcher("{0..A}").Value; - Assert.True(matcher.IsMatch("/0..A")); - Assert.False(matcher.IsMatch("/0")); - Assert.False(matcher.IsMatch("/0abc")); - - // The reference implementation uses atoi here so we can presume - // numbers out of range of Int32 are not well supported - matcherOpt = TryCreateSectionNameMatcher($"{{0..{UInt32.MaxValue}}}"); - - Assert.Null(matcherOpt); - } - - [Fact] - public void CharacterClassSimple() - { - var matcher = TryCreateSectionNameMatcher("*.[cf]s").Value; - Assert.Equal(@"^.*/[^/]*\.[cf]s$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc.cs")); - Assert.True(matcher.IsMatch("/abc.fs")); - Assert.False(matcher.IsMatch("/abc.vs")); - } - - [Fact] - public void CharacterClassNegative() - { - var matcher = TryCreateSectionNameMatcher("*.[!cf]s").Value; - Assert.Equal(@"^.*/[^/]*\.[^cf]s$", matcher.Regex.ToString()); - - Assert.False(matcher.IsMatch("/abc.cs")); - Assert.False(matcher.IsMatch("/abc.fs")); - Assert.True(matcher.IsMatch("/abc.vs")); - Assert.True(matcher.IsMatch("/abc.xs")); - Assert.False(matcher.IsMatch("/abc.vxs")); - } - - [Fact] - public void CharacterClassCaret() - { - var matcher = TryCreateSectionNameMatcher("*.[^cf]s").Value; - Assert.Equal(@"^.*/[^/]*\.[\^cf]s$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/abc.cs")); - Assert.True(matcher.IsMatch("/abc.fs")); - Assert.True(matcher.IsMatch("/abc.^s")); - Assert.False(matcher.IsMatch("/abc.vs")); - Assert.False(matcher.IsMatch("/abc.xs")); - Assert.False(matcher.IsMatch("/abc.vxs")); - } - - [Fact] - public void CharacterClassRange() - { - var matcher = TryCreateSectionNameMatcher("[0-9]x").Value; - Assert.Equal("^.*/[0-9]x$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/0x")); - Assert.True(matcher.IsMatch("/1x")); - Assert.True(matcher.IsMatch("/9x")); - Assert.False(matcher.IsMatch("/yx")); - Assert.False(matcher.IsMatch("/00x")); - } - - [Fact] - public void CharacterClassNegativeRange() - { - var matcher = TryCreateSectionNameMatcher("[!0-9]x").Value; - Assert.Equal("^.*/[^0-9]x$", matcher.Regex.ToString()); - - Assert.False(matcher.IsMatch("/0x")); - Assert.False(matcher.IsMatch("/1x")); - Assert.False(matcher.IsMatch("/9x")); - Assert.True(matcher.IsMatch("/yx")); - Assert.False(matcher.IsMatch("/00x")); - } - - [Fact] - public void CharacterClassRangeAndChoice() - { - var matcher = TryCreateSectionNameMatcher("[ab0-9]x").Value; - Assert.Equal("^.*/[ab0-9]x$", matcher.Regex.ToString()); - - Assert.True(matcher.IsMatch("/ax")); - Assert.True(matcher.IsMatch("/bx")); - Assert.True(matcher.IsMatch("/0x")); - Assert.True(matcher.IsMatch("/1x")); - Assert.True(matcher.IsMatch("/9x")); - Assert.False(matcher.IsMatch("/yx")); - Assert.False(matcher.IsMatch("/0ax")); - } - - [Fact] - public void CharacterClassOpenEnded() - { - var matcher = TryCreateSectionNameMatcher("["); - Assert.Null(matcher); - } - - [Fact] - public void CharacterClassEscapedOpenEnded() - { - var matcher = TryCreateSectionNameMatcher(@"[\]"); - Assert.Null(matcher); - } - - [Fact] - public void CharacterClassEscapeAtEnd() - { - var matcher = TryCreateSectionNameMatcher(@"[\"); - Assert.Null(matcher); - } - - [Fact] - public void CharacterClassOpenBracketInside() - { - var matcher = TryCreateSectionNameMatcher(@"[[a]bc").Value; - - Assert.True(matcher.IsMatch("/abc")); - Assert.True(matcher.IsMatch("/[bc")); - Assert.False(matcher.IsMatch("/ab")); - Assert.False(matcher.IsMatch("/[b")); - Assert.False(matcher.IsMatch("/bc")); - Assert.False(matcher.IsMatch("/ac")); - Assert.False(matcher.IsMatch("/[c")); - - Assert.Equal(@"^.*/[\[a]bc$", matcher.Regex.ToString()); - } - - [Fact] - public void CharacterClassStartingDash() - { - var matcher = TryCreateSectionNameMatcher(@"[-ac]bd").Value; - - Assert.True(matcher.IsMatch("/abd")); - Assert.True(matcher.IsMatch("/cbd")); - Assert.True(matcher.IsMatch("/-bd")); - Assert.False(matcher.IsMatch("/bbd")); - Assert.False(matcher.IsMatch("/-cd")); - Assert.False(matcher.IsMatch("/bcd")); - - Assert.Equal(@"^.*/[-ac]bd$", matcher.Regex.ToString()); - } - - [Fact] - public void CharacterClassEndingDash() - { - var matcher = TryCreateSectionNameMatcher(@"[ac-]bd").Value; - - Assert.True(matcher.IsMatch("/abd")); - Assert.True(matcher.IsMatch("/cbd")); - Assert.True(matcher.IsMatch("/-bd")); - Assert.False(matcher.IsMatch("/bbd")); - Assert.False(matcher.IsMatch("/-cd")); - Assert.False(matcher.IsMatch("/bcd")); - - Assert.Equal(@"^.*/[ac-]bd$", matcher.Regex.ToString()); - } - - [Fact] - public void CharacterClassEndBracketAfter() - { - var matcher = TryCreateSectionNameMatcher(@"[ab]]cd").Value; - - Assert.True(matcher.IsMatch("/a]cd")); - Assert.True(matcher.IsMatch("/b]cd")); - Assert.False(matcher.IsMatch("/acd")); - Assert.False(matcher.IsMatch("/bcd")); - Assert.False(matcher.IsMatch("/acd")); - - Assert.Equal(@"^.*/[ab]]cd$", matcher.Regex.ToString()); - } - - [Fact] - public void CharacterClassEscapeBackslash() - { - var matcher = TryCreateSectionNameMatcher(@"[ab\\]cd").Value; - - Assert.True(matcher.IsMatch("/acd")); - Assert.True(matcher.IsMatch("/bcd")); - Assert.True(matcher.IsMatch("/\\cd")); - Assert.False(matcher.IsMatch("/dcd")); - Assert.False(matcher.IsMatch("/\\\\cd")); - Assert.False(matcher.IsMatch("/cd")); - - Assert.Equal(@"^.*/[ab\\]cd$", matcher.Regex.ToString()); - } - - [Fact] - public void EscapeOpenBracket() - { - var matcher = TryCreateSectionNameMatcher(@"ab\[cd").Value; - - Assert.True(matcher.IsMatch("/ab[cd")); - Assert.False(matcher.IsMatch("/ab[[cd")); - Assert.False(matcher.IsMatch("/abc")); - Assert.False(matcher.IsMatch("/abd")); - - Assert.Equal(@"^.*/ab\[cd$", matcher.Regex.ToString()); - } - #endregion - - #region Parsing Tests - - private static void SetEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, string message = null) - { - var expectedSet = new HashSet(expected, comparer); - var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual); - Assert.True(result, message); - } - - private static void Equal( - IEnumerable expected, - IEnumerable actual, - IEqualityComparer comparer = null, - string message = null) - { - if (expected == null) - { - Assert.Null(actual); - } - else - { - Assert.NotNull(actual); - } - - if (SequenceEqual(expected, actual, comparer)) - { - return; - } - - Assert.True(false, message); - } - - private static bool SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null) - { - if (ReferenceEquals(expected, actual)) - { - return true; - } - - var enumerator1 = expected.GetEnumerator(); - var enumerator2 = actual.GetEnumerator(); - - while (true) - { - var hasNext1 = enumerator1.MoveNext(); - var hasNext2 = enumerator2.MoveNext(); - - if (hasNext1 != hasNext2) - { - return false; - } - - if (!hasNext1) - { - break; - } - - var value1 = enumerator1.Current; - var value2 = enumerator2.Current; - - if (!(comparer != null ? comparer.Equals(value1, value2) : AssertEqualityComparer.Equals(value1, value2))) - { - return false; - } - } - - return true; - } - - public static KeyValuePair Create(K key, V value) - { - return new KeyValuePair(key, value); - } - - [Fact] - public void SimpleCase() - { - var config = EditorConfigFile.Parse(""" -root = true - -# Comment1 -# Comment2 -################################## - -my_global_prop = my_global_val - -[*.cs] -my_prop = my_val -"""); - Assert.Equal("", config.GlobalSection.Name); - var properties = config.GlobalSection.Properties; - - SetEqual( - new[] { Create("my_global_prop", "my_global_val") , - Create("root", "true") }, - properties); - - var namedSections = config.NamedSections; - Assert.Equal("*.cs", namedSections[0].Name); - SetEqual( - new[] { Create("my_prop", "my_val") }, - namedSections[0].Properties); - - Assert.True(config.IsRoot); - } - - - [Fact] - // [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] - public void ConfigWithEscapedValues() - { - var config = EditorConfigFile.Parse(@"is_global = true - -[c:/\{f\*i\?le1\}.cs] -build_metadata.Compile.ToRetrieve = abc123 - -[c:/f\,ile\#2.cs] -build_metadata.Compile.ToRetrieve = def456 - -[c:/f\;i\!le\[3\].cs] -build_metadata.Compile.ToRetrieve = ghi789 -"); - - var namedSections = config.NamedSections; - Assert.Equal("c:/\\{f\\*i\\?le1\\}.cs", namedSections[0].Name); - Equal( - new[] { Create("build_metadata.compile.toretrieve", "abc123") }, - namedSections[0].Properties); - - Assert.Equal("c:/f\\,ile\\#2.cs", namedSections[1].Name); - Equal( - new[] { Create("build_metadata.compile.toretrieve", "def456") }, - namedSections[1].Properties); - - Assert.Equal("c:/f\\;i\\!le\\[3\\].cs", namedSections[2].Name); - Equal( - new[] { Create("build_metadata.compile.toretrieve", "ghi789") }, - namedSections[2].Properties); - } - - /* - [Fact] - [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] - public void CanGetSectionsWithSpecialCharacters() - { - var config = ParseConfigFile(@"is_global = true - -[/home/foo/src/\{releaseid\}.cs] -build_metadata.Compile.ToRetrieve = abc123 - -[/home/foo/src/Pages/\#foo/HomePage.cs] -build_metadata.Compile.ToRetrieve = def456 -"); - - var set = AnalyzerConfigSet.Create(ImmutableArray.Create(config)); - - var sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/{releaseid}.cs"); - Assert.Equal("abc123", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); - - sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/Pages/#foo/HomePage.cs"); - Assert.Equal("def456", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); - }*/ - - [Fact] - public void MissingClosingBracket() - { - var config = EditorConfigFile.Parse(@" -[*.cs -my_prop = my_val"); - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_prop", "my_val") }, - properties); - - Assert.Equal(0, config.NamedSections.Length); - } - - - [Fact] - public void EmptySection() - { - var config = EditorConfigFile.Parse(@" -[] -my_prop = my_val"); - - var properties = config.GlobalSection.Properties; - Assert.Equal(new[] { Create("my_prop", "my_val") }, properties); - Assert.Equal(0, config.NamedSections.Length); - } - - - [Fact] - public void CaseInsensitivePropKey() - { - var config = EditorConfigFile.Parse(@" -my_PROP = my_VAL"); - var properties = config.GlobalSection.Properties; - - Assert.True(properties.TryGetValue("my_PrOp", out var val)); - Assert.Equal("my_VAL", val); - Assert.Equal("my_prop", properties.Keys.Single()); - } - - // there is no reversed keys support for msbuild - /*[Fact] - public void NonReservedKeyPreservedCaseVal() - { - var config = ParseConfigFile(string.Join(Environment.NewLine, - AnalyzerConfig.ReservedKeys.Select(k => "MY_" + k + " = MY_VAL"))); - AssertEx.SetEqual( - AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create("my_" + k, "MY_VAL")).ToList(), - config.GlobalSection.Properties); - }*/ - - - [Fact] - public void DuplicateKeys() - { - var config = EditorConfigFile.Parse(@" -my_prop = my_val -my_prop = my_other_val"); - - var properties = config.GlobalSection.Properties; - Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); - } - - - [Fact] - public void DuplicateKeysCasing() - { - var config = EditorConfigFile.Parse(@" -my_prop = my_val -my_PROP = my_other_val"); - - var properties = config.GlobalSection.Properties; - Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); - } - - - [Fact] - public void MissingKey() - { - var config = EditorConfigFile.Parse(@" -= my_val1 -my_prop = my_val2"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_prop", "my_val2") }, - properties); - } - - - - [Fact] - public void MissingVal() - { - var config = EditorConfigFile.Parse(@" -my_prop1 = -my_prop2 = my_val"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_prop1", ""), - Create("my_prop2", "my_val") }, - properties); - } - - - [Fact] - public void SpacesInProperties() - { - var config = EditorConfigFile.Parse(@" -my prop1 = my_val1 -my_prop2 = my val2"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_prop2", "my val2") }, - properties); - } - - - [Fact] - public void EndOfLineComments() - { - var config = EditorConfigFile.Parse(@" -my_prop2 = my val2 # Comment"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_prop2", "my val2") }, - properties); - } - - [Fact] - public void SymbolsStartKeys() - { - var config = EditorConfigFile.Parse(@" -@!$abc = my_val1 -@!$\# = my_val2"); - - var properties = config.GlobalSection.Properties; - Assert.Equal(0, properties.Count); - } - - - [Fact] - public void EqualsAndColon() - { - var config = EditorConfigFile.Parse(@" -my:key1 = my_val -my_key2 = my:val"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my", "key1 = my_val"), - Create("my_key2", "my:val")}, - properties); - } - - [Fact] - public void SymbolsInProperties() - { - var config = EditorConfigFile.Parse(@" -my@key1 = my_val -my_key2 = my@val"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("my_key2", "my@val") }, - properties); - } - - [Fact] - public void LongLines() - { - // This example is described in the Python ConfigParser as allowing - // line continuation via the RFC 822 specification, section 3.1.1 - // LONG HEADER FIELDS. The VS parser does not accept this as a - // valid parse for an editorconfig file. We follow similarly. - var config = EditorConfigFile.Parse(@" -long: this value continues - in the next line"); - - var properties = config.GlobalSection.Properties; - SetEqual( - new[] { Create("long", "this value continues") }, - properties); - } - - - [Fact] - public void CaseInsensitiveRoot() - { - var config = EditorConfigFile.Parse(@" -RoOt = TruE"); - Assert.True(config.IsRoot); - } - - - /* - Reserved values are not supported at the moment - [Fact] - public void ReservedValues() - { - int index = 0; - var config = ParseConfigFile(string.Join(Environment.NewLine, - AnalyzerConfig.ReservedValues.Select(v => "MY_KEY" + (index++) + " = " + v.ToUpperInvariant()))); - index = 0; - AssertEx.SetEqual( - AnalyzerConfig.ReservedValues.Select(v => KeyValuePair.Create("my_key" + (index++), v)).ToList(), - config.GlobalSection.Properties); - } - */ - - /* - [Fact] - public void ReservedKeys() - { - var config = ParseConfigFile(string.Join(Environment.NewLine, - AnalyzerConfig.ReservedKeys.Select(k => k + " = MY_VAL"))); - AssertEx.SetEqual( - AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create(k, "my_val")).ToList(), - config.GlobalSection.Properties); - } - */ - #endregion - } -} diff --git a/src/Analyzers.UnitTests/EndToEndTests.cs b/src/Analyzers.UnitTests/EndToEndTests.cs deleted file mode 100644 index 91b9d6c3742..00000000000 --- a/src/Analyzers.UnitTests/EndToEndTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.UnitTests; -using Microsoft.Build.UnitTests.Shared; -using Shouldly; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Build.Analyzers.UnitTests -{ - public class EndToEndTests : IDisposable - { - private readonly TestEnvironment _env; - public EndToEndTests(ITestOutputHelper output) - { - _env = TestEnvironment.Create(output); - - // this is needed to ensure the binary logger does not pollute the environment - _env.WithEnvironmentInvariant(); - } - - public void Dispose() => _env.Dispose(); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode) - { - string contents = $""" - - - - Exe - net8.0 - enable - enable - - - - Test - - - - - - - - - - - - """; - - string contents2 = $""" - - - - Exe - net8.0 - enable - enable - - - - Test - - - - - - - - - - - """; - TransientTestFolder workFolder = _env.CreateFolder(createFolder: true); - TransientTestFile projectFile = _env.CreateFile(workFolder, "FooBar.csproj", contents); - TransientTestFile projectFile2 = _env.CreateFile(workFolder, "FooBar-Copy.csproj", contents2); - TransientTestFile config = _env.CreateFile(workFolder, ".editorconfig", - """ - root=true - - [*.csproj] - build_check.BC0101.IsEnabled=true - build_check.BC0101.Severity=warning - - build_check.COND0543.IsEnabled=false - build_check.COND0543.Severity=Error - build_check.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly - build_check.COND0543.CustomSwitch=QWERTY - - build_check.BLA.IsEnabled=false - """); - - // OSX links /var into /private, which makes Path.GetTempPath() return "/var..." but Directory.GetCurrentDirectory return "/private/var...". - // This discrepancy breaks path equality checks in analyzers if we pass to MSBuild full path to the initial project. - // TODO: See if there is a way of fixing it in the engine. - _env.SetCurrentDirectory(Path.GetDirectoryName(projectFile.Path)); - - _env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", buildInOutOfProcessNode ? "1" : "0"); - _env.SetEnvironmentVariable("MSBUILDLOGPROPERTIESANDITEMSAFTEREVALUATION", "1"); - string output = RunnerUtilities.ExecBootstrapedMSBuild($"{Path.GetFileName(projectFile.Path)} /m:1 -nr:False -restore -analyze", out bool success); - _env.Output.WriteLine(output); - success.ShouldBeTrue(); - // The conflicting outputs warning appears - output.ShouldContain("warning : BC0101"); - } - } -} diff --git a/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj b/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj deleted file mode 100644 index 5890b8fce94..00000000000 --- a/src/Analyzers.UnitTests/Microsoft.Build.Analyzers.UnitTests.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - $(LatestDotNetCoreForMSBuild) - $(FullFrameworkTFM);$(TargetFrameworks) - - $(RuntimeOutputPlatformTarget) - false - True - - - - - - - - - - - - - - - - - - - - - SharedUtilities\AssemblyResources.cs - - - - - - App.config - Designer - - - PreserveNewest - - - diff --git a/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs new file mode 100644 index 00000000000..4b76786f3b4 --- /dev/null +++ b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.BuildCheck.UnitTests; + +public class BuildAnalyzerConfiguration_Test +{ + [Fact] + public void CreateWithNull_ReturnsObjectWithNullValues() + { + var buildConfig = BuildAnalyzerConfiguration.Create(null); + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBeNull(); + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Fact] + public void CreateWithEmpty_ReturnsObjectWithNullValues() + { + var buildConfig = BuildAnalyzerConfiguration.Create(new Dictionary()); + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBeNull(); + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("error", BuildAnalyzerResultSeverity.Error)] + [InlineData("info", BuildAnalyzerResultSeverity.Info)] + [InlineData("warning", BuildAnalyzerResultSeverity.Warning)] + [InlineData("WARNING", BuildAnalyzerResultSeverity.Warning)] + public void CreateBuildAnalyzerConfiguration_Severity(string parameter, BuildAnalyzerResultSeverity? expected) + { + var config = new Dictionary() + { + { "severity" , parameter }, + }; + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.Severity.ShouldBe(expected); + + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("false", false)] + [InlineData("FALSE", false)] + public void CreateBuildAnalyzerConfiguration_IsEnabled(string parameter, bool? expected) + { + var config = new Dictionary() + { + { "isenabled" , parameter }, + }; + + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.IsEnabled.ShouldBe(expected); + + buildConfig.Severity.ShouldBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBeNull(); + } + + [Theory] + [InlineData("ProjectOnly", EvaluationAnalysisScope.ProjectOnly)] + [InlineData("ProjectWithImportsFromCurrentWorkTree", EvaluationAnalysisScope.ProjectWithImportsFromCurrentWorkTree)] + [InlineData("ProjectWithImportsWithoutSdks", EvaluationAnalysisScope.ProjectWithImportsWithoutSdks)] + [InlineData("ProjectWithAllImports", EvaluationAnalysisScope.ProjectWithAllImports)] + [InlineData("projectwithallimports", EvaluationAnalysisScope.ProjectWithAllImports)] + public void CreateBuildAnalyzerConfiguration_EvaluationAnalysisScope(string parameter, EvaluationAnalysisScope? expected) + { + var config = new Dictionary() + { + { "evaluationanalysisscope" , parameter }, + }; + + var buildConfig = BuildAnalyzerConfiguration.Create(config); + + buildConfig.ShouldNotBeNull(); + buildConfig.EvaluationAnalysisScope.ShouldBe(expected); + + buildConfig.IsEnabled.ShouldBeNull(); + buildConfig.Severity.ShouldBeNull(); + } + + [Theory] + [InlineData("evaluationanalysisscope", "incorrec-value")] + [InlineData("isenabled", "incorrec-value")] + [InlineData("severity", "incorrec-value")] + public void CreateBuildAnalyzerConfiguration_ExceptionOnInvalidInputValue(string key, string value) + { + var config = new Dictionary() + { + { key , value }, + }; + + var exception = Should.Throw(() => { + BuildAnalyzerConfiguration.Create(config); + }); + exception.Message.ShouldContain($"Incorrect value provided in config for key {key}"); + } +} diff --git a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs new file mode 100644 index 00000000000..1d5fec680b0 --- /dev/null +++ b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +namespace Microsoft.Build.BuildCheck.UnitTests; + +public class ConfigurationProvider_Tests +{ + [Fact] + public void GetRuleIdConfiguration_ReturnsEmptyConfig() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + test_key=test_value_updated + """); + + var configurationProvider = new ConfigurationProvider(); + var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // empty + configs.ShouldBe(new Dictionary()); + } + + [Fact] + public void GetRuleIdConfiguration_ReturnsConfiguration() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + """); + + var configurationProvider = new ConfigurationProvider(); + var configs = configurationProvider.GetConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + configs.Keys.Count.ShouldBe(2); + + configs.ContainsKey("property1").ShouldBeTrue(); + configs.ContainsKey("property2").ShouldBeTrue(); + + configs["property2"].ShouldBe("value2"); + configs["property1"].ShouldBe("value1"); + } + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationData() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + any_other_key1=any_other_value1 + any_other_key2=any_other_value2 + any_other_key3=any_other_value3 + any_other_key3=any_other_value3 + """); + + var configurationProvider = new ConfigurationProvider(); + var customConfiguration = configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + var configs = customConfiguration.ConfigurationData; + + configs!.Keys.Count().ShouldBe(3); + + configs.ContainsKey("property1").ShouldBeTrue(); + configs.ContainsKey("property2").ShouldBeTrue(); + configs.ContainsKey("isenabled2").ShouldBeTrue(); + } + + [Fact] + public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.isEnabled=true + build_check.rule_id.Severity=Error + build_check.rule_id.EvaluationAnalysisScope=ProjectOnly + """); + + var configurationProvider = new ConfigurationProvider(); + var buildConfig = configurationProvider.GetUserConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + buildConfig.ShouldNotBeNull(); + + buildConfig.IsEnabled?.ShouldBeTrue(); + buildConfig.Severity?.ShouldBe(BuildAnalyzerResultSeverity.Error); + buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.ProjectOnly); + } + + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentValues() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + + [test123.csproj] + build_check.rule_id.property1=value2 + build_check.rule_id.property2=value3 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=tru1 + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should not fail => configurations are the same + Should.Throw(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + } + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentKeys() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled2=true + + [test123.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled2=true + build_check.rule_id.isEnabled3=true + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should not fail => configurations are the same + Should.Throw(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + } + + + [Fact] + public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFile config1 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + + [test123.csproj] + build_check.rule_id.property1=value1 + build_check.rule_id.property2=value2 + build_check.rule_id.isEnabled=true + build_check.rule_id.isEnabled2=true + """); + + var configurationProvider = new ConfigurationProvider(); + configurationProvider.GetCustomConfiguration(Path.Combine(workFolder1.Path, "test.csproj"), "rule_id"); + + // should fail, because the configs are the different + Should.NotThrow(() => + { + configurationProvider.CheckCustomConfigurationDataValidity(Path.Combine(workFolder1.Path, "test123.csproj"), "rule_id"); + }); + } +} diff --git a/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs new file mode 100644 index 00000000000..0de7e02e1c4 --- /dev/null +++ b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +namespace Microsoft.Build.BuildCheck.UnitTests; + +public class CustomConfigurationData_Tests +{ + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_NullInstance() + { + var customConfigurationData1 = CustomConfigurationData.Null; + var customConfigurationData2 = CustomConfigurationData.Null; + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_SameInstance() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = customConfigurationData1; + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_DifferentObjectType() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = new object(); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_DifferentInstanceSameValues() + { + var customConfigurationData1 = new CustomConfigurationData("testRuleId"); + var customConfigurationData2 = new CustomConfigurationData("testRuleId"); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataSame() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key1", "val1" } + }; + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } + + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferent() + { + var config = new Dictionary() + { + { "key1", "val1" } + }; + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config); + var customConfigurationData2 = new CustomConfigurationData("testRuleId"); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentKeys() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key2", "val2" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeFalse_CustomConfigDataDifferentValues() + { + var config1 = new Dictionary() + { + { "key1", "val1" } + }; + + var config2 = new Dictionary() + { + { "key1", "val2" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeFalse(); + } + + [Fact] + public void TestCustomConfigurationData_Equals_ShouldBeTrue_CustomConfigDataKeysOrderDiffers() + { + var config1 = new Dictionary() + { + { "key1", "val1" }, + { "key2", "val2" } + }; + + var config2 = new Dictionary() + { + { "key2", "val2" }, + { "key1", "val1" } + }; + + var customConfigurationData1 = new CustomConfigurationData("testRuleId", config1); + var customConfigurationData2 = new CustomConfigurationData("testRuleId", config2); + + customConfigurationData1.Equals(customConfigurationData2).ShouldBeTrue(); + } +} diff --git a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs new file mode 100644 index 00000000000..2c1a65018a2 --- /dev/null +++ b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +namespace Microsoft.Build.BuildCheck.UnitTests; + +public class EditorConfigParser_Tests +{ + [Fact] + public void NoSectionConfigured_ResultsEmptyResultConfig() + { + var configs = new List(){ + EditorConfigFile.Parse("""" + property1=value1 +""""), + EditorConfigFile.Parse("""" + property1=value2 + """"), + EditorConfigFile.Parse("""" + property1=value3 + """"), + }; + + var parser = new EditorConfigParser(); + var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file"); + mergedResult.Keys.Count.ShouldBe(0); + } + + [Fact] + public void ProperOrderOfconfiguration_ClosestToTheFileShouldBeApplied() + { + var configs = new List(){ + EditorConfigFile.Parse("""" + [*] + property1=value1 +""""), + EditorConfigFile.Parse("""" + [*] + property1=value2 + """"), + EditorConfigFile.Parse("""" + [*] + property1=value3 + """"), + }; + + var parser = new EditorConfigParser(); + var mergedResult = parser.MergeEditorConfigFiles(configs, "/some/path/to/file.proj"); + mergedResult.Keys.Count.ShouldBe(1); + mergedResult["property1"].ShouldBe("value1"); + } + + [Fact] + public void EditorconfigFileDiscovery_RootTrue() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); + + TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", + """ + root=true + + [*.csproj] + test_key=test_value_updated + """); + + + TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + [*.csproj] + test_key=should_not_be_respected_and_parsed + """); + + var parser = new EditorConfigParser(); + var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj") ).ToList(); + // should be one because root=true so we do not need to go further + listOfEditorConfigFile.Count.ShouldBe(1); + listOfEditorConfigFile[0].IsRoot.ShouldBeTrue(); + listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); + listOfEditorConfigFile[0].NamedSections[0].Properties["test_key"].ShouldBe("test_value_updated"); + } + + [Fact] + public void EditorconfigFileDiscovery_RootFalse() + { + using TestEnvironment testEnvironment = TestEnvironment.Create(); + + TransientTestFolder workFolder1 = testEnvironment.CreateFolder(createFolder: true); + TransientTestFolder workFolder2 = testEnvironment.CreateFolder(Path.Combine(workFolder1.Path, "subfolder"), createFolder: true); + + TransientTestFile config1 = testEnvironment.CreateFile(workFolder2, ".editorconfig", + """ + [*.csproj] + test_key=test_value_updated + """); + + TransientTestFile config2 = testEnvironment.CreateFile(workFolder1, ".editorconfig", + """ + [*.csproj] + test_key=will_be_there + """); + + var parser = new EditorConfigParser(); + var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj")).ToList(); + + listOfEditorConfigFile.Count.ShouldBe(2); + listOfEditorConfigFile[0].IsRoot.ShouldBeFalse(); + listOfEditorConfigFile[0].NamedSections[0].Name.ShouldBe("*.csproj"); + } +} diff --git a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs new file mode 100644 index 00000000000..2bf7856c43e --- /dev/null +++ b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs @@ -0,0 +1,1081 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.UnitTests; +using Xunit; +using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; + +#nullable disable + +namespace Microsoft.Build.BuildCheck.UnitTests; + +public class EditorConfig_Tests +{ + + #region AssertEqualityComparer + private sealed class AssertEqualityComparer : IEqualityComparer + { + public static readonly IEqualityComparer Instance = new AssertEqualityComparer(); + + private static bool CanBeNull() + { + var type = typeof(T); + return !type.GetTypeInfo().IsValueType || + (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); + } + + public static bool IsNull(T @object) + { + if (!CanBeNull()) + { + return false; + } + + return object.Equals(@object, default(T)); + } + + public static bool Equals(T left, T right) + { + return Instance.Equals(left, right); + } + + bool IEqualityComparer.Equals(T x, T y) + { + if (CanBeNull()) + { + if (object.Equals(x, default(T))) + { + return object.Equals(y, default(T)); + } + + if (object.Equals(y, default(T))) + { + return false; + } + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + if (x is IEquatable equatable) + { + return equatable.Equals(y); + } + + if (x is IComparable comparableT) + { + return comparableT.CompareTo(y) == 0; + } + + if (x is IComparable comparable) + { + return comparable.CompareTo(y) == 0; + } + + var enumerableX = x as IEnumerable; + var enumerableY = y as IEnumerable; + + if (enumerableX != null && enumerableY != null) + { + var enumeratorX = enumerableX.GetEnumerator(); + var enumeratorY = enumerableY.GetEnumerator(); + + while (true) + { + bool hasNextX = enumeratorX.MoveNext(); + bool hasNextY = enumeratorY.MoveNext(); + + if (!hasNextX || !hasNextY) + { + return hasNextX == hasNextY; + } + + if (!Equals(enumeratorX.Current, enumeratorY.Current)) + { + return false; + } + } + } + + return object.Equals(x, y); + } + + int IEqualityComparer.GetHashCode(T obj) + { + throw new NotImplementedException(); + } + } + + #endregion + + // Section Matchin Test cases: https://github.com/dotnet/roslyn/blob/ba163e712b01358a217065eec8a4a82f94a7efd5/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs#L337 + #region Section Matching Tests + [Fact] + public void SimpleNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc").Value; + Assert.Equal("^.*/abc$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc")); + Assert.False(matcher.IsMatch("/aabc")); + Assert.False(matcher.IsMatch("/ abc")); + Assert.False(matcher.IsMatch("/cabc")); + } + + [Fact] + public void StarOnlyMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*").Value; + Assert.Equal("^.*/[^/]*$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc")); + Assert.True(matcher.IsMatch("/123")); + Assert.True(matcher.IsMatch("/abc/123")); + } + + [Fact] + public void StarNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.cs").Value; + Assert.Equal("^.*/[^/]*\\.cs$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/123.cs")); + Assert.True(matcher.IsMatch("/dir/subpath.cs")); + // Only '/' is defined as a directory separator, so the caller + // is responsible for converting any other machine directory + // separators to '/' before matching + Assert.True(matcher.IsMatch("/dir\\subpath.cs")); + + Assert.False(matcher.IsMatch("/abc.vb")); + } + + [Fact] + public void StarStarNameMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("**.cs").Value; + Assert.Equal("^.*/.*\\.cs$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/dir/subpath.cs")); + } + + [Fact] + public void EscapeDot() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("...").Value; + Assert.Equal("^.*/\\.\\.\\.$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/...")); + Assert.True(matcher.IsMatch("/subdir/...")); + Assert.False(matcher.IsMatch("/aaa")); + Assert.False(matcher.IsMatch("/???")); + Assert.False(matcher.IsMatch("/abc")); + } + + [Fact] + public void EndBackslashMatch() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc\\"); + Assert.Null(matcher); + } + + [Fact] + public void QuestionMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab?def").Value; + Assert.Equal("^.*/ab.def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abcdef")); + Assert.True(matcher.IsMatch("/ab?def")); + Assert.True(matcher.IsMatch("/abzdef")); + Assert.True(matcher.IsMatch("/ab/def")); + Assert.True(matcher.IsMatch("/ab\\def")); + } + + [Fact] + public void LiteralBackslash() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab\\\\c").Value; + Assert.Equal("^.*/ab\\\\c$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ab\\c")); + Assert.False(matcher.IsMatch("/ab/c")); + Assert.False(matcher.IsMatch("/ab\\\\c")); + } + + [Fact] + public void LiteralStars() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\***\\*\\**").Value; + Assert.Equal("^.*/\\*.*\\*\\*[^/]*$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/*ab/cd**efg*")); + Assert.False(matcher.IsMatch("/ab/cd**efg*")); + Assert.False(matcher.IsMatch("/*ab/cd*efg*")); + Assert.False(matcher.IsMatch("/*ab/cd**ef/gh")); + } + + [Fact] + public void LiteralQuestions() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("\\??\\?*\\??").Value; + Assert.Equal("^.*/\\?.\\?[^/]*\\?.$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/?a?cde?f")); + Assert.True(matcher.IsMatch("/???????f")); + Assert.False(matcher.IsMatch("/aaaaaaaa")); + Assert.False(matcher.IsMatch("/aa?cde?f")); + Assert.False(matcher.IsMatch("/?a?cdexf")); + Assert.False(matcher.IsMatch("/?axcde?f")); + } + + [Fact] + public void LiteralBraces() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\{\\}def").Value; + Assert.Equal(@"^.*/abc\{}def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc{}def")); + Assert.True(matcher.IsMatch("/subdir/abc{}def")); + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/abc}{def")); + } + + [Fact] + public void LiteralComma() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("abc\\,def").Value; + Assert.Equal("^.*/abc,def$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc,def")); + Assert.True(matcher.IsMatch("/subdir/abc,def")); + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/abc\\,def")); + Assert.False(matcher.IsMatch("/abc`def")); + } + + [Fact] + public void SimpleChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("*.{cs,vb,fs}").Value; + Assert.Equal("^.*/[^/]*\\.(?:cs|vb|fs)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.vb")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/subdir/abc.cs")); + Assert.True(matcher.IsMatch("/subdir/abc.vb")); + Assert.True(matcher.IsMatch("/subdir/abc.fs")); + + Assert.False(matcher.IsMatch("/abcxcs")); + Assert.False(matcher.IsMatch("/abcxvb")); + Assert.False(matcher.IsMatch("/abcxfs")); + Assert.False(matcher.IsMatch("/subdir/abcxcs")); + Assert.False(matcher.IsMatch("/subdir/abcxcb")); + Assert.False(matcher.IsMatch("/subdir/abcxcs")); + } + + [Fact] + public void OneChoiceHasSlashes() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs,subdir/test.vb}").Value; + // This is an interesting case that may be counterintuitive. A reasonable understanding + // of the section matching could interpret the choice as generating multiple identical + // sections, so [{a, b, c}] would be equivalent to [a] ... [b] ... [c] with all of the + // same properties in each section. This is somewhat true, but the rules of how the matching + // prefixes are constructed violate this assumption because they are defined as whether or + // not a section contains a slash, not whether any of the choices contain a slash. So while + // [*.cs] usually translates into '**/*.cs' because it contains no slashes, the slashes in + // the second choice make this into '/*.cs', effectively matching only files in the root + // directory of the match, instead of all subdirectories. + Assert.Equal("^/(?:[^/]*\\.cs|subdir/test\\.vb)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.vb")); + + Assert.False(matcher.IsMatch("/subdir/test.cs")); + Assert.False(matcher.IsMatch("/subdir/subdir/test.vb")); + Assert.False(matcher.IsMatch("/test.vb")); + } + + [Fact] + public void EmptyChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{}").Value; + Assert.Equal("^.*/(?:)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/")); + Assert.True(matcher.IsMatch("/subdir/")); + Assert.False(matcher.IsMatch("/.")); + Assert.False(matcher.IsMatch("/anything")); + } + + [Fact] + public void SingleChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{*.cs}").Value; + Assert.Equal("^.*/(?:[^/]*\\.cs)$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.cs")); + Assert.False(matcher.IsMatch("test.vb")); + Assert.False(matcher.IsMatch("testxcs")); + } + + [Fact] + public void UnmatchedBraces() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("{{{{}}"); + Assert.Null(matcher); + } + + [Fact] + public void CommaOutsideBraces() + { + SectionNameMatcher? matcher = TryCreateSectionNameMatcher("abc,def"); + Assert.Null(matcher); + } + + [Fact] + public void RecursiveChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("{test{.cs,.vb},other.{a{bb,cc}}}").Value; + Assert.Equal("^.*/(?:test(?:\\.cs|\\.vb)|other\\.(?:a(?:bb|cc)))$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/test.cs")); + Assert.True(matcher.IsMatch("/test.vb")); + Assert.True(matcher.IsMatch("/subdir/test.cs")); + Assert.True(matcher.IsMatch("/subdir/test.vb")); + Assert.True(matcher.IsMatch("/other.abb")); + Assert.True(matcher.IsMatch("/other.acc")); + + Assert.False(matcher.IsMatch("/test.fs")); + Assert.False(matcher.IsMatch("/other.bbb")); + Assert.False(matcher.IsMatch("/other.ccc")); + Assert.False(matcher.IsMatch("/subdir/other.bbb")); + Assert.False(matcher.IsMatch("/subdir/other.ccc")); + } + + [Fact] + public void DashChoice() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{-}cd{-,}ef").Value; + Assert.Equal("^.*/ab(?:-)cd(?:-|)ef$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ab-cd-ef")); + Assert.True(matcher.IsMatch("/ab-cdef")); + + Assert.False(matcher.IsMatch("/abcdef")); + Assert.False(matcher.IsMatch("/ab--cd-ef")); + Assert.False(matcher.IsMatch("/ab--cd--ef")); + } + + [Fact] + public void MiddleMatch() + { + SectionNameMatcher matcher = TryCreateSectionNameMatcher("ab{cs,vb,fs}cd").Value; + Assert.Equal("^.*/ab(?:cs|vb|fs)cd$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abcscd")); + Assert.True(matcher.IsMatch("/abvbcd")); + Assert.True(matcher.IsMatch("/abfscd")); + + Assert.False(matcher.IsMatch("/abcs")); + Assert.False(matcher.IsMatch("/abcd")); + Assert.False(matcher.IsMatch("/vbcd")); + } + + private static IEnumerable<(string, string)> RangeAndInverse(string s1, string s2) + { + yield return (s1, s2); + yield return (s2, s1); + } + + [Fact] + public void NumberMatch() + { + foreach (var (i1, i2) in RangeAndInverse("0", "10")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/10")); + Assert.True(matcher.IsMatch("/5")); + Assert.True(matcher.IsMatch("/000005")); + Assert.False(matcher.IsMatch("/-1")); + Assert.False(matcher.IsMatch("/-00000001")); + Assert.False(matcher.IsMatch("/11")); + } + } + + [Fact] + public void NumberMatchNegativeRange() + { + foreach (var (i1, i2) in RangeAndInverse("-10", "0")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/-10")); + Assert.True(matcher.IsMatch("/-5")); + Assert.False(matcher.IsMatch("/1")); + Assert.False(matcher.IsMatch("/-11")); + Assert.False(matcher.IsMatch("/--0")); + } + } + + [Fact] + public void NumberMatchNegToPos() + { + foreach (var (i1, i2) in RangeAndInverse("-10", "10")) + { + var matcher = TryCreateSectionNameMatcher($"{{{i1}..{i2}}}").Value; + + Assert.True(matcher.IsMatch("/0")); + Assert.True(matcher.IsMatch("/-5")); + Assert.True(matcher.IsMatch("/5")); + Assert.True(matcher.IsMatch("/-10")); + Assert.True(matcher.IsMatch("/10")); + Assert.False(matcher.IsMatch("/-11")); + Assert.False(matcher.IsMatch("/11")); + Assert.False(matcher.IsMatch("/--0")); + } + } + + [Fact] + public void MultipleNumberRanges() + { + foreach (var matchString in new[] { "a{-10..0}b{0..10}", "a{0..-10}b{10..0}" }) + { + var matcher = TryCreateSectionNameMatcher(matchString).Value; + + Assert.True(matcher.IsMatch("/a0b0")); + Assert.True(matcher.IsMatch("/a-5b0")); + Assert.True(matcher.IsMatch("/a-5b5")); + Assert.True(matcher.IsMatch("/a-5b10")); + Assert.True(matcher.IsMatch("/a-10b10")); + Assert.True(matcher.IsMatch("/a-10b0")); + Assert.True(matcher.IsMatch("/a-0b0")); + Assert.True(matcher.IsMatch("/a-0b-0")); + + Assert.False(matcher.IsMatch("/a-11b10")); + Assert.False(matcher.IsMatch("/a-11b10")); + Assert.False(matcher.IsMatch("/a-10b11")); + } + } + + [Fact] + public void BadNumberRanges() + { + var matcherOpt = TryCreateSectionNameMatcher("{0.."); + + Assert.Null(matcherOpt); + + var matcher = TryCreateSectionNameMatcher("{0..}").Value; + + Assert.True(matcher.IsMatch("/0..")); + Assert.False(matcher.IsMatch("/0")); + Assert.False(matcher.IsMatch("/0.")); + Assert.False(matcher.IsMatch("/0abc")); + + matcher = TryCreateSectionNameMatcher("{0..A}").Value; + Assert.True(matcher.IsMatch("/0..A")); + Assert.False(matcher.IsMatch("/0")); + Assert.False(matcher.IsMatch("/0abc")); + + // The reference implementation uses atoi here so we can presume + // numbers out of range of Int32 are not well supported + matcherOpt = TryCreateSectionNameMatcher($"{{0..{UInt32.MaxValue}}}"); + + Assert.Null(matcherOpt); + } + + [Fact] + public void CharacterClassSimple() + { + var matcher = TryCreateSectionNameMatcher("*.[cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[cf]s$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.False(matcher.IsMatch("/abc.vs")); + } + + [Fact] + public void CharacterClassNegative() + { + var matcher = TryCreateSectionNameMatcher("*.[!cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[^cf]s$", matcher.Regex.ToString()); + + Assert.False(matcher.IsMatch("/abc.cs")); + Assert.False(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/abc.vs")); + Assert.True(matcher.IsMatch("/abc.xs")); + Assert.False(matcher.IsMatch("/abc.vxs")); + } + + [Fact] + public void CharacterClassCaret() + { + var matcher = TryCreateSectionNameMatcher("*.[^cf]s").Value; + Assert.Equal(@"^.*/[^/]*\.[\^cf]s$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/abc.cs")); + Assert.True(matcher.IsMatch("/abc.fs")); + Assert.True(matcher.IsMatch("/abc.^s")); + Assert.False(matcher.IsMatch("/abc.vs")); + Assert.False(matcher.IsMatch("/abc.xs")); + Assert.False(matcher.IsMatch("/abc.vxs")); + } + + [Fact] + public void CharacterClassRange() + { + var matcher = TryCreateSectionNameMatcher("[0-9]x").Value; + Assert.Equal("^.*/[0-9]x$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/0x")); + Assert.True(matcher.IsMatch("/1x")); + Assert.True(matcher.IsMatch("/9x")); + Assert.False(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/00x")); + } + + [Fact] + public void CharacterClassNegativeRange() + { + var matcher = TryCreateSectionNameMatcher("[!0-9]x").Value; + Assert.Equal("^.*/[^0-9]x$", matcher.Regex.ToString()); + + Assert.False(matcher.IsMatch("/0x")); + Assert.False(matcher.IsMatch("/1x")); + Assert.False(matcher.IsMatch("/9x")); + Assert.True(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/00x")); + } + + [Fact] + public void CharacterClassRangeAndChoice() + { + var matcher = TryCreateSectionNameMatcher("[ab0-9]x").Value; + Assert.Equal("^.*/[ab0-9]x$", matcher.Regex.ToString()); + + Assert.True(matcher.IsMatch("/ax")); + Assert.True(matcher.IsMatch("/bx")); + Assert.True(matcher.IsMatch("/0x")); + Assert.True(matcher.IsMatch("/1x")); + Assert.True(matcher.IsMatch("/9x")); + Assert.False(matcher.IsMatch("/yx")); + Assert.False(matcher.IsMatch("/0ax")); + } + + [Fact] + public void CharacterClassOpenEnded() + { + var matcher = TryCreateSectionNameMatcher("["); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassEscapedOpenEnded() + { + var matcher = TryCreateSectionNameMatcher(@"[\]"); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassEscapeAtEnd() + { + var matcher = TryCreateSectionNameMatcher(@"[\"); + Assert.Null(matcher); + } + + [Fact] + public void CharacterClassOpenBracketInside() + { + var matcher = TryCreateSectionNameMatcher(@"[[a]bc").Value; + + Assert.True(matcher.IsMatch("/abc")); + Assert.True(matcher.IsMatch("/[bc")); + Assert.False(matcher.IsMatch("/ab")); + Assert.False(matcher.IsMatch("/[b")); + Assert.False(matcher.IsMatch("/bc")); + Assert.False(matcher.IsMatch("/ac")); + Assert.False(matcher.IsMatch("/[c")); + + Assert.Equal(@"^.*/[\[a]bc$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassStartingDash() + { + var matcher = TryCreateSectionNameMatcher(@"[-ac]bd").Value; + + Assert.True(matcher.IsMatch("/abd")); + Assert.True(matcher.IsMatch("/cbd")); + Assert.True(matcher.IsMatch("/-bd")); + Assert.False(matcher.IsMatch("/bbd")); + Assert.False(matcher.IsMatch("/-cd")); + Assert.False(matcher.IsMatch("/bcd")); + + Assert.Equal(@"^.*/[-ac]bd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEndingDash() + { + var matcher = TryCreateSectionNameMatcher(@"[ac-]bd").Value; + + Assert.True(matcher.IsMatch("/abd")); + Assert.True(matcher.IsMatch("/cbd")); + Assert.True(matcher.IsMatch("/-bd")); + Assert.False(matcher.IsMatch("/bbd")); + Assert.False(matcher.IsMatch("/-cd")); + Assert.False(matcher.IsMatch("/bcd")); + + Assert.Equal(@"^.*/[ac-]bd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEndBracketAfter() + { + var matcher = TryCreateSectionNameMatcher(@"[ab]]cd").Value; + + Assert.True(matcher.IsMatch("/a]cd")); + Assert.True(matcher.IsMatch("/b]cd")); + Assert.False(matcher.IsMatch("/acd")); + Assert.False(matcher.IsMatch("/bcd")); + Assert.False(matcher.IsMatch("/acd")); + + Assert.Equal(@"^.*/[ab]]cd$", matcher.Regex.ToString()); + } + + [Fact] + public void CharacterClassEscapeBackslash() + { + var matcher = TryCreateSectionNameMatcher(@"[ab\\]cd").Value; + + Assert.True(matcher.IsMatch("/acd")); + Assert.True(matcher.IsMatch("/bcd")); + Assert.True(matcher.IsMatch("/\\cd")); + Assert.False(matcher.IsMatch("/dcd")); + Assert.False(matcher.IsMatch("/\\\\cd")); + Assert.False(matcher.IsMatch("/cd")); + + Assert.Equal(@"^.*/[ab\\]cd$", matcher.Regex.ToString()); + } + + [Fact] + public void EscapeOpenBracket() + { + var matcher = TryCreateSectionNameMatcher(@"ab\[cd").Value; + + Assert.True(matcher.IsMatch("/ab[cd")); + Assert.False(matcher.IsMatch("/ab[[cd")); + Assert.False(matcher.IsMatch("/abc")); + Assert.False(matcher.IsMatch("/abd")); + + Assert.Equal(@"^.*/ab\[cd$", matcher.Regex.ToString()); + } + #endregion + + #region Parsing Tests + + private static void SetEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null, string message = null) + { + var expectedSet = new HashSet(expected, comparer); + var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual); + Assert.True(result, message); + } + + private static void Equal( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer = null, + string message = null) + { + if (expected == null) + { + Assert.Null(actual); + } + else + { + Assert.NotNull(actual); + } + + if (SequenceEqual(expected, actual, comparer)) + { + return; + } + + Assert.True(false, message); + } + + private static bool SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer = null) + { + if (ReferenceEquals(expected, actual)) + { + return true; + } + + var enumerator1 = expected.GetEnumerator(); + var enumerator2 = actual.GetEnumerator(); + + while (true) + { + var hasNext1 = enumerator1.MoveNext(); + var hasNext2 = enumerator2.MoveNext(); + + if (hasNext1 != hasNext2) + { + return false; + } + + if (!hasNext1) + { + break; + } + + var value1 = enumerator1.Current; + var value2 = enumerator2.Current; + + if (!(comparer != null ? comparer.Equals(value1, value2) : AssertEqualityComparer.Equals(value1, value2))) + { + return false; + } + } + + return true; + } + + public static KeyValuePair Create(K key, V value) + { + return new KeyValuePair(key, value); + } + + [Fact] + public void SimpleCase() + { + var config = EditorConfigFile.Parse(""" +root = true + +# Comment1 +# Comment2 +################################## + +my_global_prop = my_global_val + +[*.cs] +my_prop = my_val +"""); + Assert.Equal("", config.GlobalSection.Name); + var properties = config.GlobalSection.Properties; + + SetEqual( + new[] { Create("my_global_prop", "my_global_val") , + Create("root", "true") }, + properties); + + var namedSections = config.NamedSections; + Assert.Equal("*.cs", namedSections[0].Name); + SetEqual( + new[] { Create("my_prop", "my_val") }, + namedSections[0].Properties); + + Assert.True(config.IsRoot); + } + + + [Fact] + // [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] + public void ConfigWithEscapedValues() + { + var config = EditorConfigFile.Parse(@"is_global = true + +[c:/\{f\*i\?le1\}.cs] +build_metadata.Compile.ToRetrieve = abc123 + +[c:/f\,ile\#2.cs] +build_metadata.Compile.ToRetrieve = def456 + +[c:/f\;i\!le\[3\].cs] +build_metadata.Compile.ToRetrieve = ghi789 +"); + + var namedSections = config.NamedSections; + Assert.Equal("c:/\\{f\\*i\\?le1\\}.cs", namedSections[0].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "abc123") }, + namedSections[0].Properties); + + Assert.Equal("c:/f\\,ile\\#2.cs", namedSections[1].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "def456") }, + namedSections[1].Properties); + + Assert.Equal("c:/f\\;i\\!le\\[3\\].cs", namedSections[2].Name); + Equal( + new[] { Create("build_metadata.compile.toretrieve", "ghi789") }, + namedSections[2].Properties); + } + + /* + [Fact] + [WorkItem(52469, "https://github.com/dotnet/roslyn/issues/52469")] + public void CanGetSectionsWithSpecialCharacters() + { + var config = ParseConfigFile(@"is_global = true + +[/home/foo/src/\{releaseid\}.cs] +build_metadata.Compile.ToRetrieve = abc123 + +[/home/foo/src/Pages/\#foo/HomePage.cs] +build_metadata.Compile.ToRetrieve = def456 +"); + + var set = AnalyzerConfigSet.Create(ImmutableArray.Create(config)); + + var sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/{releaseid}.cs"); + Assert.Equal("abc123", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); + + sectionOptions = set.GetOptionsForSourcePath("/home/foo/src/Pages/#foo/HomePage.cs"); + Assert.Equal("def456", sectionOptions.AnalyzerOptions["build_metadata.compile.toretrieve"]); + }*/ + + [Fact] + public void MissingClosingBracket() + { + var config = EditorConfigFile.Parse(@" +[*.cs +my_prop = my_val"); + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop", "my_val") }, + properties); + + Assert.Equal(0, config.NamedSections.Length); + } + + + [Fact] + public void EmptySection() + { + var config = EditorConfigFile.Parse(@" +[] +my_prop = my_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_val") }, properties); + Assert.Equal(0, config.NamedSections.Length); + } + + + [Fact] + public void CaseInsensitivePropKey() + { + var config = EditorConfigFile.Parse(@" +my_PROP = my_VAL"); + var properties = config.GlobalSection.Properties; + + Assert.True(properties.TryGetValue("my_PrOp", out var val)); + Assert.Equal("my_VAL", val); + Assert.Equal("my_prop", properties.Keys.Single()); + } + + // there is no reversed keys support for msbuild + /*[Fact] + public void NonReservedKeyPreservedCaseVal() + { + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedKeys.Select(k => "MY_" + k + " = MY_VAL"))); + AssertEx.SetEqual( + AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create("my_" + k, "MY_VAL")).ToList(), + config.GlobalSection.Properties); + }*/ + + + [Fact] + public void DuplicateKeys() + { + var config = EditorConfigFile.Parse(@" +my_prop = my_val +my_prop = my_other_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); + } + + + [Fact] + public void DuplicateKeysCasing() + { + var config = EditorConfigFile.Parse(@" +my_prop = my_val +my_PROP = my_other_val"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(new[] { Create("my_prop", "my_other_val") }, properties); + } + + + [Fact] + public void MissingKey() + { + var config = EditorConfigFile.Parse(@" += my_val1 +my_prop = my_val2"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop", "my_val2") }, + properties); + } + + + + [Fact] + public void MissingVal() + { + var config = EditorConfigFile.Parse(@" +my_prop1 = +my_prop2 = my_val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop1", ""), + Create("my_prop2", "my_val") }, + properties); + } + + + [Fact] + public void SpacesInProperties() + { + var config = EditorConfigFile.Parse(@" +my prop1 = my_val1 +my_prop2 = my val2"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop2", "my val2") }, + properties); + } + + + [Fact] + public void EndOfLineComments() + { + var config = EditorConfigFile.Parse(@" +my_prop2 = my val2 # Comment"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_prop2", "my val2") }, + properties); + } + + [Fact] + public void SymbolsStartKeys() + { + var config = EditorConfigFile.Parse(@" +@!$abc = my_val1 +@!$\# = my_val2"); + + var properties = config.GlobalSection.Properties; + Assert.Equal(0, properties.Count); + } + + + [Fact] + public void EqualsAndColon() + { + var config = EditorConfigFile.Parse(@" +my:key1 = my_val +my_key2 = my:val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my", "key1 = my_val"), + Create("my_key2", "my:val")}, + properties); + } + + [Fact] + public void SymbolsInProperties() + { + var config = EditorConfigFile.Parse(@" +my@key1 = my_val +my_key2 = my@val"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("my_key2", "my@val") }, + properties); + } + + [Fact] + public void LongLines() + { + // This example is described in the Python ConfigParser as allowing + // line continuation via the RFC 822 specification, section 3.1.1 + // LONG HEADER FIELDS. The VS parser does not accept this as a + // valid parse for an editorconfig file. We follow similarly. + var config = EditorConfigFile.Parse(@" +long: this value continues + in the next line"); + + var properties = config.GlobalSection.Properties; + SetEqual( + new[] { Create("long", "this value continues") }, + properties); + } + + + [Fact] + public void CaseInsensitiveRoot() + { + var config = EditorConfigFile.Parse(@" +RoOt = TruE"); + Assert.True(config.IsRoot); + } + + + /* + Reserved values are not supported at the moment + [Fact] + public void ReservedValues() + { + int index = 0; + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedValues.Select(v => "MY_KEY" + (index++) + " = " + v.ToUpperInvariant()))); + index = 0; + AssertEx.SetEqual( + AnalyzerConfig.ReservedValues.Select(v => KeyValuePair.Create("my_key" + (index++), v)).ToList(), + config.GlobalSection.Properties); + } + */ + + /* + [Fact] + public void ReservedKeys() + { + var config = ParseConfigFile(string.Join(Environment.NewLine, + AnalyzerConfig.ReservedKeys.Select(k => k + " = MY_VAL"))); + AssertEx.SetEqual( + AnalyzerConfig.ReservedKeys.Select(k => KeyValuePair.Create(k, "my_val")).ToList(), + config.GlobalSection.Properties); + } + */ + #endregion +} diff --git a/src/BuildCheck.UnitTests/EndToEndTests.cs b/src/BuildCheck.UnitTests/EndToEndTests.cs index a0007d2c103..6740eeacd27 100644 --- a/src/BuildCheck.UnitTests/EndToEndTests.cs +++ b/src/BuildCheck.UnitTests/EndToEndTests.cs @@ -62,7 +62,6 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode, bool ana string contents2 = $""" - Exe net8.0 @@ -88,28 +87,20 @@ public void SampleAnalyzerIntegrationTest(bool buildInOutOfProcessNode, bool ana TransientTestFile projectFile = _env.CreateFile(workFolder, "FooBar.csproj", contents); TransientTestFile projectFile2 = _env.CreateFile(workFolder, "FooBar-Copy.csproj", contents2); - // var cache = new SimpleProjectRootElementCache(); - // ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(projectFile.Path, /*unused*/null, /*unused*/null, cache, false /*Not explicitly loaded - unused*/); + TransientTestFile config = _env.CreateFile(workFolder, ".editorconfig", + """ + root=true + + [*.csproj] + build_check.BC0101.IsEnabled=true + build_check.BC0101.Severity=warning + build_check.COND0543.IsEnabled=false + build_check.COND0543.Severity=Error + build_check.COND0543.EvaluationAnalysisScope=AnalyzedProjectOnly + build_check.COND0543.CustomSwitch=QWERTY - TransientTestFile config = _env.CreateFile(workFolder, "editorconfig.json", - /*lang=json,strict*/ - """ - { - "BC0101": { - "IsEnabled": true, - "Severity": "Error" - }, - "COND0543": { - "IsEnabled": false, - "Severity": "Error", - "EvaluationAnalysisScope": "AnalyzedProjectOnly", - "CustomSwitch": "QWERTY" - }, - "BLA": { - "IsEnabled": false - } - } + build_check.BLA.IsEnabled=false """); // OSX links /var into /private, which makes Path.GetTempPath() return "/var..." but Directory.GetCurrentDirectory return "/private/var...". diff --git a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj index 0bade6a09d5..d908e0fbcdd 100644 --- a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj +++ b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj @@ -1,6 +1,6 @@ - $(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild) + $(RuntimeOutputTargetFrameworks) Microsoft.Build.UnitTests.Shared true false @@ -18,5 +18,21 @@ + + false + false + - + + + + + + + + + + <_Parameter1>$(BootstrapBinaryDestination) + + + \ No newline at end of file From 19115fbe9947b45f2ddb089aecd6b832f0a23f67 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 10:56:47 +0200 Subject: [PATCH 30/52] Fix differences --- eng/cibuild_bootstrapped_msbuild.ps1 | 3 --- eng/cibuild_bootstrapped_msbuild.sh | 3 --- src/Build/AssemblyInfo.cs | 1 - .../Infrastructure/BuildCheckConnectorLogger.cs | 9 --------- .../Infrastructure/BuildCheckManagerProvider.cs | 1 - .../BuildCheck/Infrastructure/TracingReporter.cs | 1 - .../BuildAnalysisLoggingContextExtensions.cs | 15 --------------- .../Logging/IBuildAnalysisLoggingContext.cs | 7 ------- src/BuildCheck.UnitTests/AssemblyInfo.cs | 10 ---------- 9 files changed, 50 deletions(-) delete mode 100644 src/Build/BuildCheck/Logging/BuildAnalysisLoggingContextExtensions.cs delete mode 100644 src/Build/BuildCheck/Logging/IBuildAnalysisLoggingContext.cs diff --git a/eng/cibuild_bootstrapped_msbuild.ps1 b/eng/cibuild_bootstrapped_msbuild.ps1 index a5c0b8d7f10..b6e3c089135 100644 --- a/eng/cibuild_bootstrapped_msbuild.ps1 +++ b/eng/cibuild_bootstrapped_msbuild.ps1 @@ -113,9 +113,6 @@ try { # Opt into performance logging. https://github.com/dotnet/msbuild/issues/5900 $env:DOTNET_PERFLOG_DIR=$PerfLogDir - # Expose stage 1 path so unit tests can find the bootstrapped MSBuild. - $env:MSBUILD_BOOTSTRAPPED_BINDIR=$Stage1BinDir - # When using bootstrapped MSBuild: # - Turn off node reuse (so that bootstrapped MSBuild processes don't stay running and lock files) # - Create bootstrap environment as it's required when also running tests diff --git a/eng/cibuild_bootstrapped_msbuild.sh b/eng/cibuild_bootstrapped_msbuild.sh index 4165de68eba..8edd377ec73 100755 --- a/eng/cibuild_bootstrapped_msbuild.sh +++ b/eng/cibuild_bootstrapped_msbuild.sh @@ -71,9 +71,6 @@ mv $ArtifactsDir $Stage1Dir # Ensure that debug bits fail fast, rather than hanging waiting for a debugger attach. export MSBUILDDONOTLAUNCHDEBUGGER=true -# Expose stage 1 path so unit tests can find the bootstrapped MSBuild. -export MSBUILD_BOOTSTRAPPED_BINDIR="$Stage1Dir/bin" - # Opt into performance logging. export DOTNET_PERFLOG_DIR=$PerfLogDir diff --git a/src/Build/AssemblyInfo.cs b/src/Build/AssemblyInfo.cs index 33624345665..e4292d1ef73 100644 --- a/src/Build/AssemblyInfo.cs +++ b/src/Build/AssemblyInfo.cs @@ -24,7 +24,6 @@ [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] -[assembly: InternalsVisibleTo("Microsoft.Build.Analyzers.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] // DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs index 177d5ef2e5b..8f345954e12 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs @@ -33,18 +33,9 @@ private void EventSource_AnyEventRaised(object sender, BuildEventArgs e) return; } - try - { buildCheckManager.ProcessEvaluationFinishedEventArgs( loggingContextFactory.CreateLoggingContext(e.BuildEventContext!), projectEvaluationFinishedEventArgs); - } - catch (Exception exception) - { - Debugger.Launch(); - Console.WriteLine(exception); - throw; - } buildCheckManager.EndProjectEvaluation(BuildCheckDataSource.EventArgs, e.BuildEventContext!); } diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs index 5f749932f76..0eaa631348b 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs @@ -275,7 +275,6 @@ private void SetupAnalyzersForNewProject(string projectFullPath, BuildEventConte _loggingService.LogErrorFromText(buildEventContext, null, null, null, new BuildEventFileInfo(projectFullPath), e.Message); - _loggingService.LogCommentFromText(buildEventContext, MessageImportance.High, $"Dismounting analyzer '{analyzerFactoryContext.FriendlyName}'"); analyzersToRemove.Add(analyzerFactoryContext); } } diff --git a/src/Build/BuildCheck/Infrastructure/TracingReporter.cs b/src/Build/BuildCheck/Infrastructure/TracingReporter.cs index 614a1711a77..2d6d850737b 100644 --- a/src/Build/BuildCheck/Infrastructure/TracingReporter.cs +++ b/src/Build/BuildCheck/Infrastructure/TracingReporter.cs @@ -11,7 +11,6 @@ namespace Microsoft.Build.BuildCheck.Infrastructure; internal class TracingReporter { - internal const string INFRA_STAT_NAME = "Infrastructure"; internal Dictionary TracingStats { get; } = new(); public void AddStats(string name, TimeSpan subtotal) diff --git a/src/Build/BuildCheck/Logging/BuildAnalysisLoggingContextExtensions.cs b/src/Build/BuildCheck/Logging/BuildAnalysisLoggingContextExtensions.cs deleted file mode 100644 index 4951fd7e3c6..00000000000 --- a/src/Build/BuildCheck/Logging/BuildAnalysisLoggingContextExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Build.BackEnd.Logging; -using Microsoft.Build.Experimental.BuildCheck; - -namespace Microsoft.Build.BuildCheck.Logging; - -internal static class BuildAnalysisLoggingContextExtensions -{ - public static LoggingContext ToLoggingContext(this IBuildAnalysisLoggingContext loggingContext) => - loggingContext as AnalyzerLoggingContext ?? - throw new InvalidOperationException("The logging context is not an AnalyzerLoggingContext"); -} diff --git a/src/Build/BuildCheck/Logging/IBuildAnalysisLoggingContext.cs b/src/Build/BuildCheck/Logging/IBuildAnalysisLoggingContext.cs deleted file mode 100644 index c7433a14eb9..00000000000 --- a/src/Build/BuildCheck/Logging/IBuildAnalysisLoggingContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Build.Experimental.BuildCheck; - -public interface IBuildAnalysisLoggingContext -{ } diff --git a/src/BuildCheck.UnitTests/AssemblyInfo.cs b/src/BuildCheck.UnitTests/AssemblyInfo.cs index 5b383e24105..3b5d7bbb185 100644 --- a/src/BuildCheck.UnitTests/AssemblyInfo.cs +++ b/src/BuildCheck.UnitTests/AssemblyInfo.cs @@ -2,13 +2,3 @@ // The .NET Foundation licenses this file to you under the MIT license. global using NativeMethodsShared = Microsoft.Build.Framework.NativeMethods; - -namespace Microsoft.Build.UnitTests.Shared; - -[System.AttributeUsage(System.AttributeTargets.Assembly)] -internal sealed class BootstrapLocationAttribute(string bootstrapRoot, string bootstrapMsbuildBinaryLocation) - : System.Attribute -{ - public string BootstrapRoot { get; } = bootstrapRoot; - public string BootstrapMsbuildBinaryLocation { get; } = bootstrapMsbuildBinaryLocation; -} From 478087120843a18c2df865ce81fc4f209c6522a6 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 11:25:07 +0200 Subject: [PATCH 31/52] small fixes --- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 6 +++--- src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs | 2 -- .../Infrastructure/BuildCheckConfigurationErrorScope.cs | 6 ------ .../BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index 2be67ba43bb..78fe9a56240 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -76,7 +76,7 @@ private static bool TryExtractValue(string key, Dictionary? c if (!isParsed) { - ThrowIncorectValueEception(key, stringValue); + ThrowIncorectValueException(key, stringValue); } return isParsed; @@ -101,13 +101,13 @@ private static bool TryExtractValue(string key, Dictionary? conf if (!isParsed) { - ThrowIncorectValueEception(key, stringValue); + ThrowIncorectValueException(key, stringValue); } return isParsed; } - private static void ThrowIncorectValueEception(string key, string value) + private static void ThrowIncorectValueException(string key, string value) { throw new BuildCheckConfigurationException( $"Incorrect value provided in config for key {key}: '{value}'", diff --git a/src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs b/src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs index 9ebf529abba..174fb305b83 100644 --- a/src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs +++ b/src/Build/BuildCheck/Analyzers/SharedOutputPathAnalyzer.cs @@ -12,8 +12,6 @@ namespace Microsoft.Build.BuildCheck.Analyzers; - - internal sealed class SharedOutputPathAnalyzer : BuildAnalyzer { public static BuildAnalyzerRule SupportedRule = new BuildAnalyzerRule("BC0101", "ConflictingOutputPath", diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs index beb3382152d..79ed49edb46 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs @@ -1,12 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.Build.BuildCheck.Infrastructure { internal enum BuildCheckConfigurationErrorScope diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs index 8f345954e12..9be71d2a288 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs @@ -78,7 +78,6 @@ private void EventSource_BuildFinished(object sender, BuildFinishedEventArgs e) _stats.Merge(buildCheckManager.CreateTracingStats(), (span1, span2) => span1 + span2); string msg = string.Join(Environment.NewLine, _stats.Select(a => a.Key + ": " + a.Value)); - BuildEventContext buildEventContext = e.BuildEventContext ?? new BuildEventContext( BuildEventContext.InvalidNodeId, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId); From c593e385a1d3cf5095cb0c4fd3001deee632c4bd Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 11:40:34 +0200 Subject: [PATCH 32/52] empty lines (not-needed changes removed) --- eng/BootStrapMsBuild.props | 2 +- src/Build/AssemblyInfo.cs | 1 - src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eng/BootStrapMsBuild.props b/eng/BootStrapMsBuild.props index c84fa2f5ca4..858cf76ac54 100644 --- a/eng/BootStrapMsBuild.props +++ b/eng/BootStrapMsBuild.props @@ -18,4 +18,4 @@ $(BootstrapDestination) - \ No newline at end of file + diff --git a/src/Build/AssemblyInfo.cs b/src/Build/AssemblyInfo.cs index e4292d1ef73..f07bea4f265 100644 --- a/src/Build/AssemblyInfo.cs +++ b/src/Build/AssemblyInfo.cs @@ -24,7 +24,6 @@ [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] [assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] - // DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, diff --git a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj index d908e0fbcdd..fee3abf670f 100644 --- a/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj +++ b/src/UnitTests.Shared/Microsoft.Build.UnitTests.Shared.csproj @@ -35,4 +35,4 @@ <_Parameter1>$(BootstrapBinaryDestination) - \ No newline at end of file + From 68c8667b8863acd53590cf60bd21dfa3120d223a Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 11:43:17 +0200 Subject: [PATCH 33/52] Empty line --- src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj index 1d116d117d2..8a2a558e452 100644 --- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj +++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj @@ -50,4 +50,4 @@ - \ No newline at end of file + From 12f51677b772f55456330dafe12667cb18d2ac45 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Thu, 18 Apr 2024 15:35:14 +0200 Subject: [PATCH 34/52] Address PR comments add more docs to the fields --- .../Infrastructure/ConfigurationProvider.cs | 101 ++++++++++++++---- .../ConfigurationProvider_Tests.cs | 2 - 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 5bf3236a05d..f58c66ec182 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -15,15 +15,26 @@ namespace Microsoft.Build.BuildCheck.Infrastructure; -// TODO: https://github.com/dotnet/msbuild/issues/9628 internal sealed class ConfigurationProvider { private readonly EditorConfigParser s_editorConfigParser = new EditorConfigParser(); // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) - private readonly Dictionary _editorConfig = new Dictionary(); + /// + /// The dictionary used for storing the BuildAnalyzerConfiguration per projectfile and rule id. The key is equal to {projectFullPath}-{ruleId} + /// + private readonly Dictionary _buildAnalyzerConfiguration = new Dictionary(); + + /// + /// The dictionary used for storing the key-value pairs retrieved from the .editorconfigs for specific projectfile. The key is equal to projectFullPath + /// + private readonly Dictionary> _editorConfigData = new Dictionary>(); + + /// + /// The dictionary used for storing the CustomConfigurationData per ruleId. The key is equal to ruleId. + /// private readonly Dictionary _customConfigurationData = new Dictionary(); private readonly string[] _infrastructureConfigurationKeys = new string[] { @@ -47,9 +58,9 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st var configuration = GetConfiguration(projectFullPath, ruleId); if (configuration is null || !configuration.Any()) - { - return CustomConfigurationData.Null; - } + { + return CustomConfigurationData.Null; + } // remove the infrastructure owned key names foreach (var infraConfigurationKey in _infrastructureConfigurationKeys) @@ -83,7 +94,7 @@ internal void CheckCustomConfigurationDataValidity(string projectFullPath, strin if (!storedConfiguration.Equals(configuration)) { throw new BuildCheckConfigurationException("Custom configuration should be equal between projects"); - } + } } } @@ -97,6 +108,12 @@ internal BuildAnalyzerConfiguration[] GetUserConfigurations( IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetUserConfiguration); + /// + /// Retrieve array of CustomConfigurationData for a given projectPath and ruleIds + /// + /// + /// + /// public CustomConfigurationData[] GetCustomConfigurations( string projectFullPath, IReadOnlyList ruleIds) @@ -130,8 +147,51 @@ private TConfig[] FillConfiguration(string projectFullPath, IRea return configurations; } - internal Dictionary GetConfiguration(string projectFullPath, string ruleId) + + /// + /// Generates a new dictionary that contains the key-value pairs from the original dictionary if the key starts with 'keyFilter'. + /// If updateKey is set to 'true', the keys of the new dictionary will not include keyFilter. + /// + /// + /// + /// + /// + private Dictionary FilterDictionaryByKeys(string keyFilter, Dictionary originalConfiguration, bool updateKey = false) { + var filteredConfig = new Dictionary(); + + foreach (var kv in originalConfiguration) + { + if (kv.Key.StartsWith(keyFilter, StringComparison.OrdinalIgnoreCase)) + { + var newKey = kv.Key; + if (updateKey) + { + newKey = kv.Key.Substring(keyFilter.Length); + } + + filteredConfig[newKey] = kv.Value; + } + } + + return filteredConfig; + } + + /// + /// Fetches the .editorconfig data in form of Key-Value pair. + /// Resulted dictionary will contain only BuildCheck related rules. + /// + /// + /// + /// + private Dictionary FetchEditorConfigRules(string projectFullPath) + { + // check if we have the data already + if (_editorConfigData.TryGetValue(projectFullPath, out var cachedConfig)) + { + return cachedConfig; + } + Dictionary config; try { @@ -142,19 +202,17 @@ internal Dictionary GetConfiguration(string projectFullPath, str throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); } - var keyToSearch = $"build_check.{ruleId}."; - var dictionaryConfig = new Dictionary(); + // clear the dictionary from the key-value pairs not BuildCheck related and + // store the data so there is no need to parse the .editorconfigs all over again + _editorConfigData[projectFullPath] = FilterDictionaryByKeys("build_check.", config); - foreach (var kv in config) - { - if (kv.Key.StartsWith(keyToSearch, StringComparison.OrdinalIgnoreCase)) - { - var newKey = kv.Key.Substring(keyToSearch.Length); - dictionaryConfig[newKey] = kv.Value; - } - } + return _editorConfigData[projectFullPath]; + } - return dictionaryConfig; + internal Dictionary GetConfiguration(string projectFullPath, string ruleId) + { + var config = FetchEditorConfigRules(projectFullPath); + return FilterDictionaryByKeys($"build_check.{ruleId}.", config, updateKey: true); } /// @@ -171,11 +229,12 @@ internal BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, { var cacheKey = $"{ruleId}-{projectFullPath}"; - if (!_editorConfig.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? editorConfig)) + if (_buildAnalyzerConfiguration.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? cachedEditorConfig)) { - editorConfig = BuildAnalyzerConfiguration.Null; + return cachedEditorConfig; } + BuildAnalyzerConfiguration? editorConfig = BuildAnalyzerConfiguration.Null; var config = GetConfiguration(projectFullPath, ruleId); if (config.Any()) @@ -183,7 +242,7 @@ internal BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, editorConfig = BuildAnalyzerConfiguration.Create(config); } - _editorConfig[cacheKey] = editorConfig; + _buildAnalyzerConfiguration[cacheKey] = editorConfig; return editorConfig; } diff --git a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs index 1d5fec680b0..715653df17d 100644 --- a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs @@ -127,7 +127,6 @@ public void GetRuleIdConfiguration_ReturnsBuildRuleConfiguration() buildConfig.EvaluationAnalysisScope?.ShouldBe(EvaluationAnalysisScope.ProjectOnly); } - [Fact] public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_DifferentValues() { @@ -193,7 +192,6 @@ public void GetRuleIdConfiguration_CustomConfigurationValidity_NotValid_Differen }); } - [Fact] public void GetRuleIdConfiguration_CustomConfigurationValidity_Valid() { From 4b80888ceb0f3384a889873eb0f0ae7ffbb8cda0 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 09:17:00 +0200 Subject: [PATCH 35/52] Add comments --- .../BuildCheck/API/BuildAnalyzerConfiguration.cs | 3 ++- .../Infrastructure/ConfigurationProvider.cs | 12 +++++++++--- .../Infrastructure/CustomConfigurationData.cs | 4 ++-- .../EditorConfig/EditorConfigParser.cs | 7 +------ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index 78fe9a56240..e8d6a3610dc 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -49,7 +49,8 @@ public class BuildAnalyzerConfiguration /// /// Creates a object based on the provided configuration dictionary. - /// If key, equals to the name of the property in lowercase, exists in the dictionary => the value is parsed and assigned to the instance property value. + /// If the BuildAnalyzerConfiguration's property name presented in the dictionary, the value of this key-value pair is parsed and assigned to the instance's field. + /// If parsing failed the value will be equal to null. /// /// The configuration dictionary containing the settings for the build analyzer. /// A new instance of with the specified settings. diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index f58c66ec182..7b012cd3b22 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -19,6 +19,8 @@ internal sealed class ConfigurationProvider { private readonly EditorConfigParser s_editorConfigParser = new EditorConfigParser(); + private const string BuildCheck_ConfigurationKey = "build_check"; + // TODO: This module should have a mechanism for removing unneeded configurations // (disabled rules and analyzers that need to run in different node) @@ -88,10 +90,14 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st internal void CheckCustomConfigurationDataValidity(string projectFullPath, string ruleId) { var configuration = GetCustomConfiguration(projectFullPath, ruleId); + VerifyCustomConfigurationEquality(ruleId, configuration); + } + internal void VerifyCustomConfigurationEquality(string ruleId, CustomConfigurationData configurationData) + { if (_customConfigurationData.TryGetValue(ruleId, out var storedConfiguration)) { - if (!storedConfiguration.Equals(configuration)) + if (!storedConfiguration.Equals(configurationData)) { throw new BuildCheckConfigurationException("Custom configuration should be equal between projects"); } @@ -204,7 +210,7 @@ private Dictionary FetchEditorConfigRules(string projectFullPath // clear the dictionary from the key-value pairs not BuildCheck related and // store the data so there is no need to parse the .editorconfigs all over again - _editorConfigData[projectFullPath] = FilterDictionaryByKeys("build_check.", config); + _editorConfigData[projectFullPath] = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); return _editorConfigData[projectFullPath]; } @@ -212,7 +218,7 @@ private Dictionary FetchEditorConfigRules(string projectFullPath internal Dictionary GetConfiguration(string projectFullPath, string ruleId) { var config = FetchEditorConfigRules(projectFullPath); - return FilterDictionaryByKeys($"build_check.{ruleId}.", config, updateKey: true); + return FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.{ruleId}.", config, updateKey: true); } /// diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 5bafbfefeab..3f2068fb8b1 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -77,7 +77,7 @@ public override bool Equals(object? obj) { foreach (var keyVal in customConfigObj.ConfigurationData) { - if(!ConfigurationData.TryGetValue(keyVal.Key, out var value) || value != keyVal.Value) + if (!ConfigurationData.TryGetValue(keyVal.Key, out var value) || value != keyVal.Value) { return false; } @@ -100,7 +100,7 @@ public override int GetHashCode() if (!NotNull(this)) { return 0; - } + } var hashCode = RuleId.GetHashCode(); if (ConfigurationData != null) diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 430b90b9fd5..4146ba92ad4 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -18,12 +18,7 @@ namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig internal class EditorConfigParser { private const string EditorconfigFile = ".editorconfig"; - private Dictionary editorConfigFileCache; - - internal EditorConfigParser() - { - editorConfigFileCache = new Dictionary(); - } + private Dictionary editorConfigFileCache = new Dictionary(); internal Dictionary Parse(string filePath) { From 5a77f963cabe02438ac60eb23ff97431ebcdf3b7 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 09:34:42 +0200 Subject: [PATCH 36/52] Comparer specified --- .../Infrastructure/ConfigurationProvider.cs | 6 +++--- .../EditorConfig/EditorConfigParser.cs | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 7b012cd3b22..3ed01264544 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -27,17 +27,17 @@ internal sealed class ConfigurationProvider /// /// The dictionary used for storing the BuildAnalyzerConfiguration per projectfile and rule id. The key is equal to {projectFullPath}-{ruleId} /// - private readonly Dictionary _buildAnalyzerConfiguration = new Dictionary(); + private readonly Dictionary _buildAnalyzerConfiguration = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// /// The dictionary used for storing the key-value pairs retrieved from the .editorconfigs for specific projectfile. The key is equal to projectFullPath /// - private readonly Dictionary> _editorConfigData = new Dictionary>(); + private readonly Dictionary> _editorConfigData = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); /// /// The dictionary used for storing the CustomConfigurationData per ruleId. The key is equal to ruleId. /// - private readonly Dictionary _customConfigurationData = new Dictionary(); + private readonly Dictionary _customConfigurationData = new Dictionary(StringComparer.InvariantCultureIgnoreCase); private readonly string[] _infrastructureConfigurationKeys = new string[] { nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 4146ba92ad4..029918bf122 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -18,7 +18,11 @@ namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig internal class EditorConfigParser { private const string EditorconfigFile = ".editorconfig"; - private Dictionary editorConfigFileCache = new Dictionary(); + + /// + /// Cache layer of the parsed editor configs the key is the path to the .editorconfig file. + /// + private Dictionary _editorConfigFileCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); internal Dictionary Parse(string filePath) { @@ -41,15 +45,15 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath { EditorConfigFile editorConfig; - if (editorConfigFileCache.ContainsKey(editorConfigFilePath)) + if (_editorConfigFileCache.ContainsKey(editorConfigFilePath)) { - editorConfig = editorConfigFileCache[editorConfigFilePath]; + editorConfig = _editorConfigFileCache[editorConfigFilePath]; } else { var editorConfigfileContent = File.ReadAllText(editorConfigFilePath); editorConfig = EditorConfigFile.Parse(editorConfigfileContent); - editorConfigFileCache[editorConfigFilePath] = editorConfig; + _editorConfigFileCache[editorConfigFilePath] = editorConfig; } editorConfigDataFromFilesList.Add(editorConfig); @@ -75,7 +79,7 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath /// internal Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) { - var resultingDictionary = new Dictionary(); + var resultingDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); if (editorConfigFiles.Any()) { From 68f9eeba12e5002ceb31a4c7892f03d5d08b2dea Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 12:24:53 +0200 Subject: [PATCH 37/52] Cover the case when the project config is not presented --- .../BuildCheck/Infrastructure/ConfigurationProvider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 3ed01264544..b71b9f73d6a 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -59,7 +59,7 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st { var configuration = GetConfiguration(projectFullPath, ruleId); - if (configuration is null || !configuration.Any()) + if (configuration is null) { return CustomConfigurationData.Null; } @@ -70,6 +70,11 @@ public CustomConfigurationData GetCustomConfiguration(string projectFullPath, st configuration.Remove(infraConfigurationKey); } + if (!configuration.Any()) + { + return CustomConfigurationData.Null; + } + var data = new CustomConfigurationData(ruleId, configuration); if (!_customConfigurationData.ContainsKey(ruleId)) @@ -210,7 +215,7 @@ private Dictionary FetchEditorConfigRules(string projectFullPath // clear the dictionary from the key-value pairs not BuildCheck related and // store the data so there is no need to parse the .editorconfigs all over again - _editorConfigData[projectFullPath] = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); + _editorConfigData[projectFullPath] = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); return _editorConfigData[projectFullPath]; } From 76fa557a6a025c6473fd65e95dda8c66d7d58625 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 14:11:28 +0200 Subject: [PATCH 38/52] Address PR comments --- .../API/BuildAnalyzerConfiguration.cs | 6 +-- .../Infrastructure/ConfigurationProvider.cs | 8 ++-- .../EditorConfig/EditorConfigParser.cs | 48 +++++++++---------- .../EditorConfigParser_Tests.cs | 4 +- 4 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index e8d6a3610dc..99aadd558eb 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -77,7 +77,7 @@ private static bool TryExtractValue(string key, Dictionary? c if (!isParsed) { - ThrowIncorectValueException(key, stringValue); + ThrowIncorrectValueException(key, stringValue); } return isParsed; @@ -102,13 +102,13 @@ private static bool TryExtractValue(string key, Dictionary? conf if (!isParsed) { - ThrowIncorectValueException(key, stringValue); + ThrowIncorrectValueException(key, stringValue); } return isParsed; } - private static void ThrowIncorectValueException(string key, string value) + private static void ThrowIncorrectValueException(string key, string value) { throw new BuildCheckConfigurationException( $"Incorrect value provided in config for key {key}: '{value}'", diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index b71b9f73d6a..d56f0c7834e 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -125,7 +125,7 @@ internal BuildAnalyzerConfiguration[] GetUserConfigurations( /// /// /// - public CustomConfigurationData[] GetCustomConfigurations( + public CustomConfigurationData[] GetCustomConfigurations( string projectFullPath, IReadOnlyList ruleIds) => FillConfiguration(projectFullPath, ruleIds, GetCustomConfiguration); @@ -215,9 +215,9 @@ private Dictionary FetchEditorConfigRules(string projectFullPath // clear the dictionary from the key-value pairs not BuildCheck related and // store the data so there is no need to parse the .editorconfigs all over again - _editorConfigData[projectFullPath] = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); - - return _editorConfigData[projectFullPath]; + Dictionary result = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); + _editorConfigData[projectFullPath] = result; + return result; } internal Dictionary GetConfiguration(string projectFullPath, string ruleId) diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 029918bf122..a154d52033b 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -15,18 +15,18 @@ namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig { - internal class EditorConfigParser + internal sealed class EditorConfigParser { private const string EditorconfigFile = ".editorconfig"; /// /// Cache layer of the parsed editor configs the key is the path to the .editorconfig file. /// - private Dictionary _editorConfigFileCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly Dictionary _editorConfigFileCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); internal Dictionary Parse(string filePath) { - var editorConfigs = EditorConfigFileDiscovery(filePath); + var editorConfigs = DiscoverEditorConfigFiles(filePath); return MergeEditorConfigFiles(editorConfigs, filePath); } @@ -34,7 +34,7 @@ internal Dictionary Parse(string filePath) /// Fetches the list of EditorconfigFile ordered from the nearest to the filePath. /// /// - internal IEnumerable EditorConfigFileDiscovery(string filePath) + internal List DiscoverEditorConfigFiles(string filePath) { var editorConfigDataFromFilesList = new List(); @@ -43,17 +43,15 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath while (editorConfigFilePath != string.Empty) { - EditorConfigFile editorConfig; - - if (_editorConfigFileCache.ContainsKey(editorConfigFilePath)) - { - editorConfig = _editorConfigFileCache[editorConfigFilePath]; - } - else + if (!_editorConfigFileCache.TryGetValue(editorConfigFilePath, out var editorConfig)) { - var editorConfigfileContent = File.ReadAllText(editorConfigFilePath); - editorConfig = EditorConfigFile.Parse(editorConfigfileContent); - _editorConfigFileCache[editorConfigFilePath] = editorConfig; + using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using StreamReader sr = new StreamReader(editorConfigFilePath); + var editorConfigfileContent = sr.ReadToEnd(); + editorConfig = EditorConfigFile.Parse(editorConfigfileContent); + _editorConfigFileCache[editorConfigFilePath] = editorConfig; + } } editorConfigDataFromFilesList.Add(editorConfig); @@ -77,31 +75,29 @@ internal IEnumerable EditorConfigFileDiscovery(string filePath /// /// /// - internal Dictionary MergeEditorConfigFiles(IEnumerable editorConfigFiles, string filePath) + internal Dictionary MergeEditorConfigFiles(List editorConfigFiles, string filePath) { var resultingDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + editorConfigFiles.Reverse(); - if (editorConfigFiles.Any()) + foreach (var configData in editorConfigFiles) { - foreach (var configData in editorConfigFiles.Reverse()) + foreach (var section in configData.NamedSections) { - foreach (var section in configData.NamedSections) + SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); + if (sectionNameMatcher != null) { - SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); - if (sectionNameMatcher != null) + if (sectionNameMatcher.Value.IsMatch(NormalizeWithForwardSlash(filePath))) { - if (sectionNameMatcher.Value.IsMatch(NormalizeWithForwardSlash(filePath))) + foreach (var property in section.Properties) { - foreach (var property in section.Properties) - { - resultingDictionary[property.Key] = property.Value; - } + resultingDictionary[property.Key] = property.Value; } } } } } - + return resultingDictionary; } diff --git a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs index 2c1a65018a2..968ca624408 100644 --- a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs @@ -87,7 +87,7 @@ public void EditorconfigFileDiscovery_RootTrue() """); var parser = new EditorConfigParser(); - var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj") ).ToList(); + var listOfEditorConfigFile = parser.DiscoverEditorConfigFiles(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj") ).ToList(); // should be one because root=true so we do not need to go further listOfEditorConfigFile.Count.ShouldBe(1); listOfEditorConfigFile[0].IsRoot.ShouldBeTrue(); @@ -116,7 +116,7 @@ public void EditorconfigFileDiscovery_RootFalse() """); var parser = new EditorConfigParser(); - var listOfEditorConfigFile = parser.EditorConfigFileDiscovery(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj")).ToList(); + var listOfEditorConfigFile = parser.DiscoverEditorConfigFiles(Path.Combine(workFolder1.Path, "subfolder", "projectfile.proj")).ToList(); listOfEditorConfigFile.Count.ShouldBe(2); listOfEditorConfigFile[0].IsRoot.ShouldBeFalse(); From 5ccdf6aba749214fa6b6e1c0e2e3f9afd02c002d Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 15:40:49 +0200 Subject: [PATCH 39/52] Address more comments --- .../BuildCheck/Infrastructure/CustomConfigurationData.cs | 5 ++++- .../Infrastructure/EditorConfig/EditorConfigParser.cs | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 3f2068fb8b1..f4535327ce2 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -107,7 +107,10 @@ public override int GetHashCode() { foreach (var keyVal in ConfigurationData) { - hashCode = hashCode + keyVal.Key.GetHashCode() + keyVal.Value.GetHashCode(); + unchecked + { + hashCode = hashCode + keyVal.Key.GetHashCode() + keyVal.Value.GetHashCode(); + } } } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index a154d52033b..8a1a57f38a5 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -78,11 +78,10 @@ internal List DiscoverEditorConfigFiles(string filePath) internal Dictionary MergeEditorConfigFiles(List editorConfigFiles, string filePath) { var resultingDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - editorConfigFiles.Reverse(); - foreach (var configData in editorConfigFiles) + for (int i = editorConfigFiles.Count - 1; i >= 0; i--) { - foreach (var section in configData.NamedSections) + foreach (var section in editorConfigFiles[i].NamedSections) { SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); if (sectionNameMatcher != null) From 92c5099ced556ac68a56fe9085cefc1d9b294f53 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 16:05:37 +0200 Subject: [PATCH 40/52] Add comment TODO --- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index 99aadd558eb..d45a5797b5d 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -110,6 +110,7 @@ private static bool TryExtractValue(string key, Dictionary? conf private static void ThrowIncorrectValueException(string key, string value) { + // TODO: It will be nice to have the filename where the incorrect configuration was placed. throw new BuildCheckConfigurationException( $"Incorrect value provided in config for key {key}: '{value}'", buildCheckConfigurationErrorScope: BuildCheckConfigurationErrorScope.EditorConfigParser); From c0fd21606ba9d0bef6df544fed96f83e36a9a647 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 16:23:55 +0200 Subject: [PATCH 41/52] Add comment to the configuration parameter --- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index d45a5797b5d..68e8ddd0607 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -52,7 +52,7 @@ public class BuildAnalyzerConfiguration /// If the BuildAnalyzerConfiguration's property name presented in the dictionary, the value of this key-value pair is parsed and assigned to the instance's field. /// If parsing failed the value will be equal to null. ///
- /// The configuration dictionary containing the settings for the build analyzer. + /// The configuration dictionary containing the settings for the build analyzer. The configuration's keys are expected to be in lower case or the EqualityComparer to ignore case. /// A new instance of with the specified settings. public static BuildAnalyzerConfiguration Create(Dictionary? configDictionary) { From 414854511b91ede9cfeccade11a3d1af6623a324 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 23 Apr 2024 19:47:06 +0200 Subject: [PATCH 42/52] Add Concurrent Dictionary for thread safety of EditorConfigParser --- .../Infrastructure/EditorConfig/EditorConfigParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 8a1a57f38a5..c399c83f2f4 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -3,11 +3,11 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing.Design; using System.IO; using System.Linq; -using System.Reflection.Metadata.Ecma335; using System.Text; using System.Threading.Tasks; using Microsoft.Build.Shared; @@ -22,7 +22,7 @@ internal sealed class EditorConfigParser /// /// Cache layer of the parsed editor configs the key is the path to the .editorconfig file. /// - private readonly Dictionary _editorConfigFileCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly ConcurrentDictionary _editorConfigFileCache = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); internal Dictionary Parse(string filePath) { From a91c971c2621ab09dd69a522ce6b6dea6794b72f Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Sun, 28 Apr 2024 10:13:48 +0200 Subject: [PATCH 43/52] Fix merge conflicts --- .../BuildCheck/Infrastructure/ConfigurationProvider.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 187525a3ff4..d56f0c7834e 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -22,13 +22,6 @@ internal sealed class ConfigurationProvider private const string BuildCheck_ConfigurationKey = "build_check"; // TODO: This module should have a mechanism for removing unneeded configurations -========= -// Let's flip form statics to instance, with exposed interface (so that we can easily swap implementations) -// Tracked via: https://github.com/dotnet/msbuild/issues/9828 -internal static class ConfigurationProvider -{ - // We might want to have a mechanism for removing unneeded configurations ->>>>>>>>> Temporary merge branch 2 // (disabled rules and analyzers that need to run in different node) /// From 9c6eae873ae4b3120fd0fdf21ed58df94d8ae06c Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Sun, 28 Apr 2024 11:15:12 +0200 Subject: [PATCH 44/52] Address PR review --- .../API/BuildAnalyzerConfiguration.cs | 12 ++++++------ .../BuildCheckConfigurationErrorScope.cs | 18 ++++++++++++------ .../Infrastructure/ConfigurationProvider.cs | 17 ++++------------- .../EditorConfig/EditorConfigParser.cs | 1 + .../Infrastructure/EditorConfig/README.md | 3 ++- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index 68e8ddd0607..b895a36c9be 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -54,13 +54,13 @@ public class BuildAnalyzerConfiguration /// /// The configuration dictionary containing the settings for the build analyzer. The configuration's keys are expected to be in lower case or the EqualityComparer to ignore case. /// A new instance of with the specified settings. - public static BuildAnalyzerConfiguration Create(Dictionary? configDictionary) + internal static BuildAnalyzerConfiguration Create(Dictionary? configDictionary) { return new() { - EvaluationAnalysisScope = TryExtractValue(nameof(EvaluationAnalysisScope).ToLower(), configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, - Severity = TryExtractValue(nameof(Severity).ToLower(), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, - IsEnabled = TryExtractValue(nameof(IsEnabled).ToLower(), configDictionary, out bool isEnabled) ? isEnabled : null, + EvaluationAnalysisScope = TryExtractValue(nameof(EvaluationAnalysisScope), configDictionary, out EvaluationAnalysisScope evaluationAnalysisScope) ? evaluationAnalysisScope : null, + Severity = TryExtractValue(nameof(Severity), configDictionary, out BuildAnalyzerResultSeverity severity) ? severity : null, + IsEnabled = TryExtractValue(nameof(IsEnabled), configDictionary, out bool isEnabled) ? isEnabled : null, }; } @@ -68,7 +68,7 @@ private static bool TryExtractValue(string key, Dictionary? c { value = default; - if (config == null || !config.TryGetValue(key, out var stringValue) || stringValue is null) + if (config == null || !config.TryGetValue(key.ToLower(), out var stringValue) || stringValue is null) { return false; } @@ -87,7 +87,7 @@ private static bool TryExtractValue(string key, Dictionary? conf { value = default; - if (config == null || !config.TryGetValue(key, out var stringValue) || stringValue is null) + if (config == null || !config.TryGetValue(key.ToLower(), out var stringValue) || stringValue is null) { return false; } diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs index 79ed49edb46..720ae3c9570 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs @@ -1,11 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Build.BuildCheck.Infrastructure +namespace Microsoft.Build.BuildCheck.Infrastructure; + +internal enum BuildCheckConfigurationErrorScope { - internal enum BuildCheckConfigurationErrorScope - { - SingleRule, - EditorConfigParser - } + /// + /// Error related to the single rule. + /// + SingleRule, + + /// + /// Error related to the parsing of .editorconfig file. + /// + EditorConfigParser } diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index d56f0c7834e..a0e4fde6e5f 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -3,15 +3,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Text.Json; -using Microsoft.Build.Experimental.BuildCheck; -using System.Configuration; using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck; namespace Microsoft.Build.BuildCheck.Infrastructure; @@ -21,16 +15,13 @@ internal sealed class ConfigurationProvider private const string BuildCheck_ConfigurationKey = "build_check"; - // TODO: This module should have a mechanism for removing unneeded configurations - // (disabled rules and analyzers that need to run in different node) - /// - /// The dictionary used for storing the BuildAnalyzerConfiguration per projectfile and rule id. The key is equal to {projectFullPath}-{ruleId} + /// The dictionary used for storing the BuildAnalyzerConfiguration per projectfile and rule id. The key is equal to {projectFullPath}-{ruleId}. /// private readonly Dictionary _buildAnalyzerConfiguration = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// - /// The dictionary used for storing the key-value pairs retrieved from the .editorconfigs for specific projectfile. The key is equal to projectFullPath + /// The dictionary used for storing the key-value pairs retrieved from the .editorconfigs for specific projectfile. The key is equal to projectFullPath. /// private readonly Dictionary> _editorConfigData = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); @@ -180,7 +171,7 @@ private Dictionary FilterDictionaryByKeys(string keyFilter, Dict { newKey = kv.Key.Substring(keyFilter.Length); } - + filteredConfig[newKey] = kv.Value; } } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index c399c83f2f4..da1ede9ec39 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -39,6 +39,7 @@ internal List DiscoverEditorConfigFiles(string filePath) var editorConfigDataFromFilesList = new List(); var directoryOfTheProject = Path.GetDirectoryName(filePath); + // The method will look for the file in parent directory if not found in current until found or the directory is root. var editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); while (editorConfigFilePath != string.Empty) diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md index 14d1e75be59..a725757b1ec 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md @@ -44,8 +44,9 @@ The implementation differs depending on category: - Infra related config: Merges the configuration retrieved from configuration module with default values (respecting the specified configs in editorconfig) - Custom configuration: Remove all infra related keys from dictionary -Two levels of cache introduced: +Three levels of cache introduced: - When retrieving and parsing the editor config -> Parsed results are saved into dictionary: editorconfigPath = ParsedEditorConfig +- When retrieving and merging the editor config data for project -> Parsed and merged results are saved into dictionary: projectFilePath = MargedData of ParsedEditorConfig - When retrieving Infra related config: ruleId-projectPath = BuildConfigInstance Usage examples (API) From 6d465b98a7f97953e3144148900db5ede855c6ad Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Sun, 28 Apr 2024 13:25:22 +0200 Subject: [PATCH 45/52] Add editorconfig with filescoped namespace warning enabled --- src/Build/BuildCheck/.editorconfig | 2 + .../EditorConfig/EditorConfigFile.cs | 249 +++-- .../EditorConfig/EditorConfigGlobsMatcher.cs | 955 +++++++++--------- .../EditorConfig/EditorConfigParser.cs | 129 ++- 4 files changed, 667 insertions(+), 668 deletions(-) create mode 100644 src/Build/BuildCheck/.editorconfig diff --git a/src/Build/BuildCheck/.editorconfig b/src/Build/BuildCheck/.editorconfig new file mode 100644 index 00000000000..731ad1c1785 --- /dev/null +++ b/src/Build/BuildCheck/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +csharp_style_namespace_declarations = file_scoped:warning diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs index faefc2499d9..4472f92dddb 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -18,168 +18,167 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; + +internal partial class EditorConfigFile { - internal partial class EditorConfigFile - { - // Matches EditorConfig section header such as "[*.{js,py}]", see https://editorconfig.org for details - private const string s_sectionMatcherPattern = @"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"; + // Matches EditorConfig section header such as "[*.{js,py}]", see https://editorconfig.org for details + private const string s_sectionMatcherPattern = @"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"; - // Matches EditorConfig property such as "indent_style = space", see https://editorconfig.org for details - private const string s_propertyMatcherPattern = @"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"; + // Matches EditorConfig property such as "indent_style = space", see https://editorconfig.org for details + private const string s_propertyMatcherPattern = @"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"; #if NETCOREAPP - [GeneratedRegex(s_sectionMatcherPattern)] - private static partial Regex GetSectionMatcherRegex(); +[GeneratedRegex(s_sectionMatcherPattern)] +private static partial Regex GetSectionMatcherRegex(); - [GeneratedRegex(s_propertyMatcherPattern)] - private static partial Regex GetPropertyMatcherRegex(); +[GeneratedRegex(s_propertyMatcherPattern)] +private static partial Regex GetPropertyMatcherRegex(); #else - private static readonly Regex s_sectionMatcher = new Regex(s_sectionMatcherPattern, RegexOptions.Compiled); + private static readonly Regex s_sectionMatcher = new Regex(s_sectionMatcherPattern, RegexOptions.Compiled); - private static readonly Regex s_propertyMatcher = new Regex(s_propertyMatcherPattern, RegexOptions.Compiled); + private static readonly Regex s_propertyMatcher = new Regex(s_propertyMatcherPattern, RegexOptions.Compiled); - private static Regex GetSectionMatcherRegex() => s_sectionMatcher; + private static Regex GetSectionMatcherRegex() => s_sectionMatcher; - private static Regex GetPropertyMatcherRegex() => s_propertyMatcher; + private static Regex GetPropertyMatcherRegex() => s_propertyMatcher; #endif - internal Section GlobalSection { get; } + internal Section GlobalSection { get; } - internal ImmutableArray
NamedSections { get; } + internal ImmutableArray
NamedSections { get; } - /// - /// Gets whether this editorconfig is a topmost editorconfig. - /// - internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val?.ToLower() == "true"; + /// + /// Gets whether this editorconfig is a topmost editorconfig. + /// + internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val?.ToLower() == "true"; - private EditorConfigFile( - Section globalSection, - ImmutableArray
namedSections) - { - GlobalSection = globalSection; - NamedSections = namedSections; - } + private EditorConfigFile( + Section globalSection, + ImmutableArray
namedSections) + { + GlobalSection = globalSection; + NamedSections = namedSections; + } - /// - /// Parses an editor config file text located at the given path. No parsing - /// errors are reported. If any line contains a parse error, it is dropped. - /// - internal static EditorConfigFile Parse(string text) + /// + /// Parses an editor config file text located at the given path. No parsing + /// errors are reported. If any line contains a parse error, it is dropped. + /// + internal static EditorConfigFile Parse(string text) + { + Section? globalSection = null; + var namedSectionBuilder = ImmutableArray.CreateBuilder
(); + + // N.B. The editorconfig documentation is quite loose on property interpretation. + // Specifically, it says: + // Currently all properties and values are case-insensitive. + // They are lowercased when parsed. + // To accommodate this, we use a lower case Unicode mapping when adding to the + // dictionary, but we also use a case-insensitive key comparer when doing lookups + var activeSectionProperties = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); + string activeSectionName = ""; + var lines = string.IsNullOrEmpty(text) ? Array.Empty() : text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + + foreach(var line in lines) { - Section? globalSection = null; - var namedSectionBuilder = ImmutableArray.CreateBuilder
(); - - // N.B. The editorconfig documentation is quite loose on property interpretation. - // Specifically, it says: - // Currently all properties and values are case-insensitive. - // They are lowercased when parsed. - // To accommodate this, we use a lower case Unicode mapping when adding to the - // dictionary, but we also use a case-insensitive key comparer when doing lookups - var activeSectionProperties = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase); - string activeSectionName = ""; - var lines = string.IsNullOrEmpty(text) ? Array.Empty() : text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); - - foreach(var line in lines) + if (string.IsNullOrWhiteSpace(line)) { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - if (IsComment(line)) - { - continue; - } - - var sectionMatches = GetSectionMatcherRegex().Matches(line); - if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) - { - addNewSection(); - - var sectionName = sectionMatches[0].Groups[1].Value; - Debug.Assert(!string.IsNullOrEmpty(sectionName)); - - activeSectionName = sectionName; - activeSectionProperties = ImmutableDictionary.CreateBuilder(); - continue; - } - - var propMatches = GetPropertyMatcherRegex().Matches(line); - if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) - { - var key = propMatches[0].Groups[1].Value.ToLower(); - var value = propMatches[0].Groups[2].Value; - - Debug.Assert(!string.IsNullOrEmpty(key)); - Debug.Assert(key == key.Trim()); - Debug.Assert(value == value?.Trim()); - - activeSectionProperties[key] = value ?? ""; - continue; - } + continue; } - // Add the last section - addNewSection(); + if (IsComment(line)) + { + continue; + } + + var sectionMatches = GetSectionMatcherRegex().Matches(line); + if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) + { + addNewSection(); + + var sectionName = sectionMatches[0].Groups[1].Value; + Debug.Assert(!string.IsNullOrEmpty(sectionName)); - return new EditorConfigFile(globalSection!, namedSectionBuilder.ToImmutable()); + activeSectionName = sectionName; + activeSectionProperties = ImmutableDictionary.CreateBuilder(); + continue; + } - void addNewSection() + var propMatches = GetPropertyMatcherRegex().Matches(line); + if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) { - // Close out the previous section - var previousSection = new Section(activeSectionName, activeSectionProperties.ToImmutable()); - if (activeSectionName == "") - { - // This is the global section - globalSection = previousSection; - } - else - { - namedSectionBuilder.Add(previousSection); - } + var key = propMatches[0].Groups[1].Value.ToLower(); + var value = propMatches[0].Groups[2].Value; + + Debug.Assert(!string.IsNullOrEmpty(key)); + Debug.Assert(key == key.Trim()); + Debug.Assert(value == value?.Trim()); + + activeSectionProperties[key] = value ?? ""; + continue; } } - private static bool IsComment(string line) + // Add the last section + addNewSection(); + + return new EditorConfigFile(globalSection!, namedSectionBuilder.ToImmutable()); + + void addNewSection() { - foreach (char c in line) + // Close out the previous section + var previousSection = new Section(activeSectionName, activeSectionProperties.ToImmutable()); + if (activeSectionName == "") { - if (!char.IsWhiteSpace(c)) - { - return c == '#' || c == ';'; - } + // This is the global section + globalSection = previousSection; + } + else + { + namedSectionBuilder.Add(previousSection); } - - return false; } + } - /// - /// Represents a named section of the editorconfig file, which consists of a name followed by a set - /// of key-value pairs. - /// - internal sealed class Section + private static bool IsComment(string line) + { + foreach (char c in line) { - public Section(string name, ImmutableDictionary properties) + if (!char.IsWhiteSpace(c)) { - Name = name; - Properties = properties; + return c == '#' || c == ';'; } + } - /// - /// For regular files, the name as present directly in the section specification of the editorconfig file. For sections in - /// global configs, this is the unescaped full file path. - /// - public string Name { get; } - - /// - /// Keys and values for this section. All keys are lower-cased according to the - /// EditorConfig specification and keys are compared case-insensitively. - /// - public ImmutableDictionary Properties { get; } + return false; + } + + /// + /// Represents a named section of the editorconfig file, which consists of a name followed by a set + /// of key-value pairs. + /// + internal sealed class Section + { + public Section(string name, ImmutableDictionary properties) + { + Name = name; + Properties = properties; } + + /// + /// For regular files, the name as present directly in the section specification of the editorconfig file. For sections in + /// global configs, this is the unescaped full file path. + /// + public string Name { get; } + + /// + /// Keys and values for this section. All keys are lower-cased according to the + /// EditorConfig specification and keys are compared case-insensitively. + /// + public ImmutableDictionary Properties { get; } } } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index 092859a5113..516190c2ab3 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -19,597 +19,596 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; + +internal class EditorConfigGlobsMatcher { - internal class EditorConfigGlobsMatcher + internal readonly struct SectionNameMatcher { - internal readonly struct SectionNameMatcher - { - private readonly ImmutableArray<(int minValue, int maxValue)> _numberRangePairs; + private readonly ImmutableArray<(int minValue, int maxValue)> _numberRangePairs; - internal Regex Regex { get; } + internal Regex Regex { get; } + + internal SectionNameMatcher( + Regex regex, + ImmutableArray<(int minValue, int maxValue)> numberRangePairs) + { + Debug.Assert(regex.GetGroupNumbers().Length - 1 == numberRangePairs.Length); + Regex = regex; + _numberRangePairs = numberRangePairs; + } - internal SectionNameMatcher( - Regex regex, - ImmutableArray<(int minValue, int maxValue)> numberRangePairs) + internal bool IsMatch(string s) + { + if (_numberRangePairs.IsEmpty) { - Debug.Assert(regex.GetGroupNumbers().Length - 1 == numberRangePairs.Length); - Regex = regex; - _numberRangePairs = numberRangePairs; + return Regex.IsMatch(s); } - internal bool IsMatch(string s) + var match = Regex.Match(s); + if (!match.Success) { - if (_numberRangePairs.IsEmpty) - { - return Regex.IsMatch(s); - } + return false; + } - var match = Regex.Match(s); - if (!match.Success) + Debug.Assert(match.Groups.Count - 1 == _numberRangePairs.Length); + for (int i = 0; i < _numberRangePairs.Length; i++) + { + var (minValue, maxValue) = _numberRangePairs[i]; + // Index 0 is the whole regex + if (!int.TryParse(match.Groups[i + 1].Value, out int matchedNum) || + matchedNum < minValue || + matchedNum > maxValue) { return false; } - - Debug.Assert(match.Groups.Count - 1 == _numberRangePairs.Length); - for (int i = 0; i < _numberRangePairs.Length; i++) - { - var (minValue, maxValue) = _numberRangePairs[i]; - // Index 0 is the whole regex - if (!int.TryParse(match.Groups[i + 1].Value, out int matchedNum) || - matchedNum < minValue || - matchedNum > maxValue) - { - return false; - } - } - return true; } + return true; } + } - /// - /// Takes a and creates a matcher that - /// matches the given language. Returns null if the section name is - /// invalid. - /// - internal static SectionNameMatcher? TryCreateSectionNameMatcher(string sectionName) + /// + /// Takes a and creates a matcher that + /// matches the given language. Returns null if the section name is + /// invalid. + /// + internal static SectionNameMatcher? TryCreateSectionNameMatcher(string sectionName) + { + // An editorconfig section name is a language for recognizing file paths + // defined by the following grammar: + // + // ::= + // ::= | + // ::= "*" | "**" | "?" | | | + // ::= any unicode character + // ::= "{" "}" + // ::= | "," + // ::= "{" ".." "}" + // ::= "-" | + // ::= | + // ::= 0-9 + + var sb = new StringBuilder(); + sb.Append('^'); + + // EditorConfig matching depends on the whether or not there are + // directory separators and where they are located in the section + // name. Specifically, the editorconfig core parser says: + // https://github.com/editorconfig/editorconfig-core-c/blob/5d3996811e962a717a7d7fdd0a941192382241a7/src/lib/editorconfig.c#L231 + // + // Pattern would be: + // /dir/of/editorconfig/file[double_star]/[section] if section does not contain '/', + // /dir/of/editorconfig/file[section] if section starts with a '/', or + // /dir/of/editorconfig/file/[section] if section contains '/' but does not start with '/'. + + if (!sectionName.Contains("/")) + { + sb.Append(".*/"); + } + else if (sectionName[0] != '/') { - // An editorconfig section name is a language for recognizing file paths - // defined by the following grammar: - // - // ::= - // ::= | - // ::= "*" | "**" | "?" | | | - // ::= any unicode character - // ::= "{" "}" - // ::= | "," - // ::= "{" ".." "}" - // ::= "-" | - // ::= | - // ::= 0-9 + sb.Append('/'); + } - var sb = new StringBuilder(); - sb.Append('^'); + var lexer = new SectionNameLexer(sectionName); + var numberRangePairs = new List<(int minValue, int maxValue)>(); + if (!TryCompilePathList(ref lexer, sb, parsingChoice: false, numberRangePairs)) + { + numberRangePairs.Clear(); + return null; + } + sb.Append('$'); - // EditorConfig matching depends on the whether or not there are - // directory separators and where they are located in the section - // name. Specifically, the editorconfig core parser says: - // https://github.com/editorconfig/editorconfig-core-c/blob/5d3996811e962a717a7d7fdd0a941192382241a7/src/lib/editorconfig.c#L231 - // - // Pattern would be: - // /dir/of/editorconfig/file[double_star]/[section] if section does not contain '/', - // /dir/of/editorconfig/file[section] if section starts with a '/', or - // /dir/of/editorconfig/file/[section] if section contains '/' but does not start with '/'. - - if (!sectionName.Contains("/")) - { - sb.Append(".*/"); - } - else if (sectionName[0] != '/') - { - sb.Append('/'); - } - var lexer = new SectionNameLexer(sectionName); - var numberRangePairs = new List<(int minValue, int maxValue)>(); - if (!TryCompilePathList(ref lexer, sb, parsingChoice: false, numberRangePairs)) - { - numberRangePairs.Clear(); - return null; - } - sb.Append('$'); + var imArray = ImmutableArray.CreateBuilder<(int, int)>(numberRangePairs is null ? 0 : numberRangePairs.Count); + if (numberRangePairs?.Count > 0) + { + imArray.AddRange(numberRangePairs); + } + return new SectionNameMatcher( + new Regex(sb.ToString(), RegexOptions.Compiled), + imArray.ToImmutableArray()); + } - var imArray = ImmutableArray.CreateBuilder<(int, int)>(numberRangePairs is null ? 0 : numberRangePairs.Count); - if (numberRangePairs?.Count > 0) + internal static string UnescapeSectionName(string sectionName) + { + var sb = new StringBuilder(); + SectionNameLexer lexer = new SectionNameLexer(sectionName); + while (!lexer.IsDone) + { + var tokenKind = lexer.Lex(); + if (tokenKind == TokenKind.SimpleCharacter) { - imArray.AddRange(numberRangePairs); + sb.Append(lexer.EatCurrentCharacter()); } - - return new SectionNameMatcher( - new Regex(sb.ToString(), RegexOptions.Compiled), - imArray.ToImmutableArray()); - } - - internal static string UnescapeSectionName(string sectionName) - { - var sb = new StringBuilder(); - SectionNameLexer lexer = new SectionNameLexer(sectionName); - while (!lexer.IsDone) + else { - var tokenKind = lexer.Lex(); - if (tokenKind == TokenKind.SimpleCharacter) - { - sb.Append(lexer.EatCurrentCharacter()); - } - else - { - // We only call this on strings that were already passed through IsAbsoluteEditorConfigPath, so - // we shouldn't have any other token kinds here. - throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); - } + // We only call this on strings that were already passed through IsAbsoluteEditorConfigPath, so + // we shouldn't have any other token kinds here. + throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); } - return sb.ToString(); } + return sb.ToString(); + } - internal static bool IsAbsoluteEditorConfigPath(string sectionName) - { - // NOTE: editorconfig paths must use '/' as a directory separator character on all OS. + internal static bool IsAbsoluteEditorConfigPath(string sectionName) + { + // NOTE: editorconfig paths must use '/' as a directory separator character on all OS. - // on all unix systems this is thus a simple test: does the path start with '/' - // and contain no special chars? + // on all unix systems this is thus a simple test: does the path start with '/' + // and contain no special chars? - // on windows, a path can be either drive rooted or not (e.g. start with 'c:' or just '') - // in addition to being absolute or relative. - // for example c:myfile.cs is a relative path, but rooted on drive c: - // /myfile2.cs is an absolute path but rooted to the current drive. + // on windows, a path can be either drive rooted or not (e.g. start with 'c:' or just '') + // in addition to being absolute or relative. + // for example c:myfile.cs is a relative path, but rooted on drive c: + // /myfile2.cs is an absolute path but rooted to the current drive. - // in addition there are UNC paths and volume guids (see https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats) - // but these start with \\ (and thus '/' in editor config terminology) + // in addition there are UNC paths and volume guids (see https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats) + // but these start with \\ (and thus '/' in editor config terminology) - // in this implementation we choose to ignore the drive root for the purposes of - // determining validity. On windows c:/file.cs and /file.cs are both assumed to be - // valid absolute paths, even though the second one is technically relative to - // the current drive of the compiler working directory. + // in this implementation we choose to ignore the drive root for the purposes of + // determining validity. On windows c:/file.cs and /file.cs are both assumed to be + // valid absolute paths, even though the second one is technically relative to + // the current drive of the compiler working directory. - // Note that this check has no impact on config correctness. Files on windows - // will still be compared using their full path (including drive root) so it's - // not possible to target the wrong file. It's just possible that the user won't - // receive a warning that this section is ignored on windows in this edge case. + // Note that this check has no impact on config correctness. Files on windows + // will still be compared using their full path (including drive root) so it's + // not possible to target the wrong file. It's just possible that the user won't + // receive a warning that this section is ignored on windows in this edge case. - SectionNameLexer nameLexer = new SectionNameLexer(sectionName); - bool sawStartChar = false; - int logicalIndex = 0; - while (!nameLexer.IsDone) + SectionNameLexer nameLexer = new SectionNameLexer(sectionName); + bool sawStartChar = false; + int logicalIndex = 0; + while (!nameLexer.IsDone) + { + if (nameLexer.Lex() != TokenKind.SimpleCharacter) { - if (nameLexer.Lex() != TokenKind.SimpleCharacter) + return false; + } + var simpleChar = nameLexer.EatCurrentCharacter(); + + // check the path starts with '/' + if (logicalIndex == 0) + { + if (simpleChar == '/') + { + sawStartChar = true; + } + else if (Path.DirectorySeparatorChar == '/') { return false; } - var simpleChar = nameLexer.EatCurrentCharacter(); - - // check the path starts with '/' - if (logicalIndex == 0) + } + // on windows we get a second chance to find the start char + else if (!sawStartChar && Path.DirectorySeparatorChar == '\\') + { + if (logicalIndex == 1 && simpleChar != ':') { - if (simpleChar == '/') - { - sawStartChar = true; - } - else if (Path.DirectorySeparatorChar == '/') - { - return false; - } + return false; } - // on windows we get a second chance to find the start char - else if (!sawStartChar && Path.DirectorySeparatorChar == '\\') + else if (logicalIndex == 2) { - if (logicalIndex == 1 && simpleChar != ':') + if (simpleChar != '/') { return false; } - else if (logicalIndex == 2) + else { - if (simpleChar != '/') - { - return false; - } - else - { - sawStartChar = true; - } + sawStartChar = true; } } - logicalIndex++; } - return sawStartChar; + logicalIndex++; } + return sawStartChar; + } - /// - /// ::= | - /// ::= "*" | "**" | "?" | | | - /// ::= any unicode character - /// ::= "{" "}" - /// ::= | "," - /// ]]> - /// - private static bool TryCompilePathList( - ref SectionNameLexer lexer, - StringBuilder sb, - bool parsingChoice, - List<(int minValue, int maxValue)> numberRangePairs) + /// + /// ::= | + /// ::= "*" | "**" | "?" | | | + /// ::= any unicode character + /// ::= "{" "}" + /// ::= | "," + /// ]]> + /// + private static bool TryCompilePathList( + ref SectionNameLexer lexer, + StringBuilder sb, + bool parsingChoice, + List<(int minValue, int maxValue)> numberRangePairs) + { + while (!lexer.IsDone) { - while (!lexer.IsDone) + var tokenKind = lexer.Lex(); + switch (tokenKind) { - var tokenKind = lexer.Lex(); - switch (tokenKind) - { - case TokenKind.BadToken: - // Parsing failure - return false; - case TokenKind.SimpleCharacter: - // Matches just this character - sb.Append(Regex.Escape(lexer.EatCurrentCharacter().ToString())); - break; - case TokenKind.Question: - // '?' matches any single character - sb.Append('.'); - break; - case TokenKind.Star: - // Matches any string of characters except directory separator - // Directory separator is defined in editorconfig spec as '/' - sb.Append("[^/]*"); - break; - case TokenKind.StarStar: - // Matches any string of characters - sb.Append(".*"); - break; - case TokenKind.OpenCurly: - // Back up token stream. The following helpers all expect a '{' - lexer.Position--; - // This is ambiguous between {num..num} and {item1,item2} - // We need to look ahead to disambiguate. Looking for {num..num} - // is easier because it can't be recursive. - (string numStart, string numEnd)? rangeOpt = TryParseNumberRange(ref lexer); - if (rangeOpt is null) - { - // Not a number range. Try a choice expression - if (!TryCompileChoice(ref lexer, sb, numberRangePairs)) - { - return false; - } - // Keep looping. There may be more after the '}'. - break; - } - else + case TokenKind.BadToken: + // Parsing failure + return false; + case TokenKind.SimpleCharacter: + // Matches just this character + sb.Append(Regex.Escape(lexer.EatCurrentCharacter().ToString())); + break; + case TokenKind.Question: + // '?' matches any single character + sb.Append('.'); + break; + case TokenKind.Star: + // Matches any string of characters except directory separator + // Directory separator is defined in editorconfig spec as '/' + sb.Append("[^/]*"); + break; + case TokenKind.StarStar: + // Matches any string of characters + sb.Append(".*"); + break; + case TokenKind.OpenCurly: + // Back up token stream. The following helpers all expect a '{' + lexer.Position--; + // This is ambiguous between {num..num} and {item1,item2} + // We need to look ahead to disambiguate. Looking for {num..num} + // is easier because it can't be recursive. + (string numStart, string numEnd)? rangeOpt = TryParseNumberRange(ref lexer); + if (rangeOpt is null) + { + // Not a number range. Try a choice expression + if (!TryCompileChoice(ref lexer, sb, numberRangePairs)) { - (string numStart, string numEnd) = rangeOpt.GetValueOrDefault(); - if (int.TryParse(numStart, out var intStart) && int.TryParse(numEnd, out var intEnd)) - { - var pair = intStart < intEnd ? (intStart, intEnd) : (intEnd, intStart); - numberRangePairs.Add(pair); - // Group allowing any digit sequence. The validity will be checked outside of the regex - sb.Append("(-?[0-9]+)"); - // Keep looping - break; - } return false; } - case TokenKind.CloseCurly: - // Either the end of a choice, or a failed parse - return parsingChoice; - case TokenKind.Comma: - // The end of a choice section, or a failed parse - return parsingChoice; - case TokenKind.OpenBracket: - sb.Append('['); - if (!TryCompileCharacterClass(ref lexer, sb)) + // Keep looping. There may be more after the '}'. + break; + } + else + { + (string numStart, string numEnd) = rangeOpt.GetValueOrDefault(); + if (int.TryParse(numStart, out var intStart) && int.TryParse(numEnd, out var intEnd)) { - return false; + var pair = intStart < intEnd ? (intStart, intEnd) : (intEnd, intStart); + numberRangePairs.Add(pair); + // Group allowing any digit sequence. The validity will be checked outside of the regex + sb.Append("(-?[0-9]+)"); + // Keep looping + break; } - break; - default: - throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); - } + return false; + } + case TokenKind.CloseCurly: + // Either the end of a choice, or a failed parse + return parsingChoice; + case TokenKind.Comma: + // The end of a choice section, or a failed parse + return parsingChoice; + case TokenKind.OpenBracket: + sb.Append('['); + if (!TryCompileCharacterClass(ref lexer, sb)) + { + return false; + } + break; + default: + throw new BuildCheckConfigurationException($"UnexpectedToken: {tokenKind}", BuildCheckConfigurationErrorScope.EditorConfigParser); } - // If we're parsing a choice we should not exit without a closing '}' - return !parsingChoice; } + // If we're parsing a choice we should not exit without a closing '}' + return !parsingChoice; + } - /// - /// Compile a globbing character class of the form [...]. Returns true if - /// the character class was successfully compiled. False if there was a syntax - /// error. The starting character is expected to be directly after the '['. - /// - private static bool TryCompileCharacterClass(ref SectionNameLexer lexer, StringBuilder sb) + /// + /// Compile a globbing character class of the form [...]. Returns true if + /// the character class was successfully compiled. False if there was a syntax + /// error. The starting character is expected to be directly after the '['. + /// + private static bool TryCompileCharacterClass(ref SectionNameLexer lexer, StringBuilder sb) + { + // [...] should match any of the characters in the brackets, with special + // behavior for four characters: '!' immediately after the opening bracket + // implies the negation of the character class, '-' implies matching + // between the locale-dependent range of the previous and next characters, + // '\' escapes the following character, and ']' ends the range + if (!lexer.IsDone && lexer.CurrentCharacter == '!') { - // [...] should match any of the characters in the brackets, with special - // behavior for four characters: '!' immediately after the opening bracket - // implies the negation of the character class, '-' implies matching - // between the locale-dependent range of the previous and next characters, - // '\' escapes the following character, and ']' ends the range - if (!lexer.IsDone && lexer.CurrentCharacter == '!') - { - sb.Append('^'); - lexer.Position++; - } - while (!lexer.IsDone) - { - var currentChar = lexer.EatCurrentCharacter(); - switch (currentChar) - { - case '-': - // '-' means the same thing in regex as it does in the glob, so - // put it in verbatim - sb.Append(currentChar); - break; - - case '\\': - // Escape the next char - if (lexer.IsDone) - { - return false; - } - sb.Append('\\'); - sb.Append(lexer.EatCurrentCharacter()); - break; - - case ']': - sb.Append(currentChar); - return true; - - default: - sb.Append(Regex.Escape(currentChar.ToString())); - break; - } - } - // Stream ended without a closing bracket - return false; + sb.Append('^'); + lexer.Position++; } - - /// - /// Parses choice defined by the following grammar: - /// ::= "{" "}" - /// ::= | "," - /// ]]> - /// - private static bool TryCompileChoice( - ref SectionNameLexer lexer, - StringBuilder sb, - List<(int, int)> numberRangePairs) + while (!lexer.IsDone) { - if (lexer.Lex() != TokenKind.OpenCurly) + var currentChar = lexer.EatCurrentCharacter(); + switch (currentChar) { - return false; - } - - // Start a non-capturing group for the choice - sb.Append("(?:"); + case '-': + // '-' means the same thing in regex as it does in the glob, so + // put it in verbatim + sb.Append(currentChar); + break; + + case '\\': + // Escape the next char + if (lexer.IsDone) + { + return false; + } + sb.Append('\\'); + sb.Append(lexer.EatCurrentCharacter()); + break; - // We start immediately after a '{' - // Try to compile the nested - while (TryCompilePathList(ref lexer, sb, parsingChoice: true, numberRangePairs)) - { - // If we've successfully compiled a the last token should - // have been a ',' or a '}' - char lastChar = lexer[lexer.Position - 1]; - if (lastChar == ',') - { - // Another option - sb.Append('|'); - } - else if (lastChar == '}') - { - // Close out the capture group - sb.Append(')'); + case ']': + sb.Append(currentChar); return true; - } - else - { - throw new BuildCheckConfigurationException($"UnexpectedValue: {lastChar}", BuildCheckConfigurationErrorScope.EditorConfigParser); - } + + default: + sb.Append(Regex.Escape(currentChar.ToString())); + break; } + } + // Stream ended without a closing bracket + return false; + } - // Propagate failure + /// + /// Parses choice defined by the following grammar: + /// ::= "{" "}" + /// ::= | "," + /// ]]> + /// + private static bool TryCompileChoice( + ref SectionNameLexer lexer, + StringBuilder sb, + List<(int, int)> numberRangePairs) + { + if (lexer.Lex() != TokenKind.OpenCurly) + { return false; } - /// - /// Parses range defined by the following grammar. - /// ::= "{" ".." "}" - /// ::= "-" | - /// ::= | - /// ::= 0-9 - /// ]]> - /// - private static (string numStart, string numEnd)? TryParseNumberRange(ref SectionNameLexer lexer) + // Start a non-capturing group for the choice + sb.Append("(?:"); + + // We start immediately after a '{' + // Try to compile the nested + while (TryCompilePathList(ref lexer, sb, parsingChoice: true, numberRangePairs)) { - var saved = lexer.Position; - if (lexer.Lex() != TokenKind.OpenCurly) + // If we've successfully compiled a the last token should + // have been a ',' or a '}' + char lastChar = lexer[lexer.Position - 1]; + if (lastChar == ',') { - lexer.Position = saved; - return null; + // Another option + sb.Append('|'); } - - var numStart = lexer.TryLexNumber(); - if (numStart is null) + else if (lastChar == '}') { - // Not a number - lexer.Position = saved; - return null; + // Close out the capture group + sb.Append(')'); + return true; } - - // The next two characters must be ".." - if (!lexer.TryEatCurrentCharacter(out char c) || c != '.' || - !lexer.TryEatCurrentCharacter(out c) || c != '.') + else { - lexer.Position = saved; - return null; + throw new BuildCheckConfigurationException($"UnexpectedValue: {lastChar}", BuildCheckConfigurationErrorScope.EditorConfigParser); } + } - // Now another number - var numEnd = lexer.TryLexNumber(); - if (numEnd is null || lexer.IsDone || lexer.Lex() != TokenKind.CloseCurly) - { - // Not a number or no '}' - lexer.Position = saved; - return null; - } + // Propagate failure + return false; + } - return (numStart, numEnd); + /// + /// Parses range defined by the following grammar. + /// ::= "{" ".." "}" + /// ::= "-" | + /// ::= | + /// ::= 0-9 + /// ]]> + /// + private static (string numStart, string numEnd)? TryParseNumberRange(ref SectionNameLexer lexer) + { + var saved = lexer.Position; + if (lexer.Lex() != TokenKind.OpenCurly) + { + lexer.Position = saved; + return null; } - private struct SectionNameLexer + var numStart = lexer.TryLexNumber(); + if (numStart is null) { - private readonly string _sectionName; + // Not a number + lexer.Position = saved; + return null; + } - internal int Position { get; set; } + // The next two characters must be ".." + if (!lexer.TryEatCurrentCharacter(out char c) || c != '.' || + !lexer.TryEatCurrentCharacter(out c) || c != '.') + { + lexer.Position = saved; + return null; + } - internal SectionNameLexer(string sectionName) - { - _sectionName = sectionName; - Position = 0; - } + // Now another number + var numEnd = lexer.TryLexNumber(); + if (numEnd is null || lexer.IsDone || lexer.Lex() != TokenKind.CloseCurly) + { + // Not a number or no '}' + lexer.Position = saved; + return null; + } + + return (numStart, numEnd); + } + + private struct SectionNameLexer + { + private readonly string _sectionName; - internal bool IsDone => Position >= _sectionName.Length; + internal int Position { get; set; } - internal TokenKind Lex() + internal SectionNameLexer(string sectionName) + { + _sectionName = sectionName; + Position = 0; + } + + internal bool IsDone => Position >= _sectionName.Length; + + internal TokenKind Lex() + { + int lexemeStart = Position; + switch (_sectionName[Position]) { - int lexemeStart = Position; - switch (_sectionName[Position]) - { - case '*': + case '*': + { + int nextPos = Position + 1; + if (nextPos < _sectionName.Length && + _sectionName[nextPos] == '*') { - int nextPos = Position + 1; - if (nextPos < _sectionName.Length && - _sectionName[nextPos] == '*') - { - Position += 2; - return TokenKind.StarStar; - } - else - { - Position++; - return TokenKind.Star; - } + Position += 2; + return TokenKind.StarStar; } + else + { + Position++; + return TokenKind.Star; + } + } - case '?': - Position++; - return TokenKind.Question; + case '?': + Position++; + return TokenKind.Question; - case '{': - Position++; - return TokenKind.OpenCurly; + case '{': + Position++; + return TokenKind.OpenCurly; - case ',': - Position++; - return TokenKind.Comma; + case ',': + Position++; + return TokenKind.Comma; - case '}': - Position++; - return TokenKind.CloseCurly; + case '}': + Position++; + return TokenKind.CloseCurly; - case '[': - Position++; - return TokenKind.OpenBracket; + case '[': + Position++; + return TokenKind.OpenBracket; - case '\\': + case '\\': + { + // Backslash escapes the next character + Position++; + if (IsDone) { - // Backslash escapes the next character - Position++; - if (IsDone) - { - return TokenKind.BadToken; - } - - return TokenKind.SimpleCharacter; + return TokenKind.BadToken; } - default: - // Don't increment position, since caller needs to fetch the character return TokenKind.SimpleCharacter; - } + } + + default: + // Don't increment position, since caller needs to fetch the character + return TokenKind.SimpleCharacter; } + } - internal char CurrentCharacter => _sectionName[Position]; + internal char CurrentCharacter => _sectionName[Position]; - /// - /// Call after getting from - /// - internal char EatCurrentCharacter() => _sectionName[Position++]; + /// + /// Call after getting from + /// + internal char EatCurrentCharacter() => _sectionName[Position++]; - /// - /// Returns false if there are no more characters in the lex stream. - /// Otherwise, produces the next character in the stream and returns true. - /// - internal bool TryEatCurrentCharacter(out char nextChar) + /// + /// Returns false if there are no more characters in the lex stream. + /// Otherwise, produces the next character in the stream and returns true. + /// + internal bool TryEatCurrentCharacter(out char nextChar) + { + if (IsDone) { - if (IsDone) - { - nextChar = default; - return false; - } - else - { - nextChar = EatCurrentCharacter(); - return true; - } + nextChar = default; + return false; } + else + { + nextChar = EatCurrentCharacter(); + return true; + } + } - internal char this[int position] => _sectionName[position]; + internal char this[int position] => _sectionName[position]; - /// - /// Returns the string representation of a decimal integer, or null if - /// the current lexeme is not an integer. - /// - internal string? TryLexNumber() - { - bool start = true; - var sb = new StringBuilder(); + /// + /// Returns the string representation of a decimal integer, or null if + /// the current lexeme is not an integer. + /// + internal string? TryLexNumber() + { + bool start = true; + var sb = new StringBuilder(); - while (!IsDone) + while (!IsDone) + { + char currentChar = CurrentCharacter; + if (start && currentChar == '-') { - char currentChar = CurrentCharacter; - if (start && currentChar == '-') - { - Position++; - sb.Append('-'); - } - else if (char.IsDigit(currentChar)) - { - Position++; - sb.Append(currentChar); - } - else - { - break; - } - start = false; + Position++; + sb.Append('-'); } - - var str = sb.ToString(); - return str.Length == 0 || str == "-" - ? null - : str; + else if (char.IsDigit(currentChar)) + { + Position++; + sb.Append(currentChar); + } + else + { + break; + } + start = false; } - } - private enum TokenKind - { - BadToken, - SimpleCharacter, - Star, - StarStar, - Question, - OpenCurly, - CloseCurly, - Comma, - DoubleDot, - OpenBracket, + var str = sb.ToString(); + return str.Length == 0 || str == "-" + ? null + : str; } } + + private enum TokenKind + { + BadToken, + SimpleCharacter, + Star, + StarStar, + Question, + OpenCurly, + CloseCurly, + Comma, + DoubleDot, + OpenBracket, + } } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index da1ede9ec39..e7b895c495b 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -13,94 +13,93 @@ using Microsoft.Build.Shared; using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig +namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; + +internal sealed class EditorConfigParser { - internal sealed class EditorConfigParser - { - private const string EditorconfigFile = ".editorconfig"; + private const string EditorconfigFile = ".editorconfig"; - /// - /// Cache layer of the parsed editor configs the key is the path to the .editorconfig file. - /// - private readonly ConcurrentDictionary _editorConfigFileCache = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + /// + /// Cache layer of the parsed editor configs the key is the path to the .editorconfig file. + /// + private readonly ConcurrentDictionary _editorConfigFileCache = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); - internal Dictionary Parse(string filePath) - { - var editorConfigs = DiscoverEditorConfigFiles(filePath); - return MergeEditorConfigFiles(editorConfigs, filePath); - } + internal Dictionary Parse(string filePath) + { + var editorConfigs = DiscoverEditorConfigFiles(filePath); + return MergeEditorConfigFiles(editorConfigs, filePath); + } - /// - /// Fetches the list of EditorconfigFile ordered from the nearest to the filePath. - /// - /// - internal List DiscoverEditorConfigFiles(string filePath) - { - var editorConfigDataFromFilesList = new List(); + /// + /// Fetches the list of EditorconfigFile ordered from the nearest to the filePath. + /// + /// + internal List DiscoverEditorConfigFiles(string filePath) + { + var editorConfigDataFromFilesList = new List(); - var directoryOfTheProject = Path.GetDirectoryName(filePath); - // The method will look for the file in parent directory if not found in current until found or the directory is root. - var editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); + var directoryOfTheProject = Path.GetDirectoryName(filePath); + // The method will look for the file in parent directory if not found in current until found or the directory is root. + var editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, directoryOfTheProject); - while (editorConfigFilePath != string.Empty) + while (editorConfigFilePath != string.Empty) + { + if (!_editorConfigFileCache.TryGetValue(editorConfigFilePath, out var editorConfig)) { - if (!_editorConfigFileCache.TryGetValue(editorConfigFilePath, out var editorConfig)) + using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using StreamReader sr = new StreamReader(editorConfigFilePath); - var editorConfigfileContent = sr.ReadToEnd(); - editorConfig = EditorConfigFile.Parse(editorConfigfileContent); - _editorConfigFileCache[editorConfigFilePath] = editorConfig; - } + using StreamReader sr = new StreamReader(editorConfigFilePath); + var editorConfigfileContent = sr.ReadToEnd(); + editorConfig = EditorConfigFile.Parse(editorConfigfileContent); + _editorConfigFileCache[editorConfigFilePath] = editorConfig; } + } - editorConfigDataFromFilesList.Add(editorConfig); + editorConfigDataFromFilesList.Add(editorConfig); - if (editorConfig.IsRoot) - { - break; - } - else - { - // search in upper directory - editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFilePath))); - } + if (editorConfig.IsRoot) + { + break; + } + else + { + // search in upper directory + editorConfigFilePath = FileUtilities.GetPathOfFileAbove(EditorconfigFile, Path.GetDirectoryName(Path.GetDirectoryName(editorConfigFilePath))); } - - return editorConfigDataFromFilesList; } - /// - /// Retrieves the config dictionary from the sections that matched the filePath. - /// - /// - /// - internal Dictionary MergeEditorConfigFiles(List editorConfigFiles, string filePath) - { - var resultingDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + return editorConfigDataFromFilesList; + } + + /// + /// Retrieves the config dictionary from the sections that matched the filePath. + /// + /// + /// + internal Dictionary MergeEditorConfigFiles(List editorConfigFiles, string filePath) + { + var resultingDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - for (int i = editorConfigFiles.Count - 1; i >= 0; i--) + for (int i = editorConfigFiles.Count - 1; i >= 0; i--) + { + foreach (var section in editorConfigFiles[i].NamedSections) { - foreach (var section in editorConfigFiles[i].NamedSections) + SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); + if (sectionNameMatcher != null) { - SectionNameMatcher? sectionNameMatcher = TryCreateSectionNameMatcher(section.Name); - if (sectionNameMatcher != null) + if (sectionNameMatcher.Value.IsMatch(NormalizeWithForwardSlash(filePath))) { - if (sectionNameMatcher.Value.IsMatch(NormalizeWithForwardSlash(filePath))) + foreach (var property in section.Properties) { - foreach (var property in section.Properties) - { - resultingDictionary[property.Key] = property.Value; - } + resultingDictionary[property.Key] = property.Value; } } } } - - return resultingDictionary; } - - internal static string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); + + return resultingDictionary; } + + internal static string NormalizeWithForwardSlash(string p) => Path.DirectorySeparatorChar == '/' ? p : p.Replace(Path.DirectorySeparatorChar, '/'); } From a15089ba487b399313a6fab67f21d7e01d39e251 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 30 Apr 2024 07:52:57 +0200 Subject: [PATCH 46/52] remove implementation of the GetHashCode of CustomConfigdata --- .../Infrastructure/CustomConfigurationData.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index f4535327ce2..4857d9f522d 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -97,23 +97,6 @@ public override bool Equals(object? obj) public override int GetHashCode() { - if (!NotNull(this)) - { - return 0; - } - - var hashCode = RuleId.GetHashCode(); - if (ConfigurationData != null) - { - foreach (var keyVal in ConfigurationData) - { - unchecked - { - hashCode = hashCode + keyVal.Key.GetHashCode() + keyVal.Value.GetHashCode(); - } - } - } - - return hashCode; + throw new NotImplementedException("CustomConfigurationData does not implement GetHashCode method"); } } From bb94e1954dfb15edd88afd36c07ee4fe7b5f2f3d Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 7 May 2024 19:17:15 +0200 Subject: [PATCH 47/52] Fix warning --- src/Build/BuildCheck/Utilities/Constants.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Build/BuildCheck/Utilities/Constants.cs b/src/Build/BuildCheck/Utilities/Constants.cs index 9ba6b58a1e6..11082116a70 100644 --- a/src/Build/BuildCheck/Utilities/Constants.cs +++ b/src/Build/BuildCheck/Utilities/Constants.cs @@ -7,13 +7,12 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCheck.Utilities +namespace Microsoft.Build.BuildCheck.Utilities; + +/// +/// Constants to be shared within BuildCheck infrastructure +/// +internal static class BuildCheckConstants { - /// - /// Constants to be shared within BuildCheck infrastructure - /// - internal static class BuildCheckConstants - { - internal const string infraStatPrefix = "infrastructureStat_"; - } + internal const string infraStatPrefix = "infrastructureStat_"; } From 11fc46d5e015eba2ff3517c256c9a5ec7c7e3bc1 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 14 May 2024 09:05:57 +0200 Subject: [PATCH 48/52] fix filescope namespace --- src/Build/BuildCheck/Utilities/Constants.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Build/BuildCheck/Utilities/Constants.cs b/src/Build/BuildCheck/Utilities/Constants.cs index 8ba5eaf65f6..50a3d1bc63c 100644 --- a/src/Build/BuildCheck/Utilities/Constants.cs +++ b/src/Build/BuildCheck/Utilities/Constants.cs @@ -7,13 +7,12 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.Build.Experimental.BuildCheck.Utilities +namespace Microsoft.Build.Experimental.BuildCheck.Utilities; + +/// +/// Constants to be shared within BuildCheck infrastructure +/// +internal static class BuildCheckConstants { - /// - /// Constants to be shared within BuildCheck infrastructure - /// - internal static class BuildCheckConstants - { - internal const string infraStatPrefix = "infrastructureStat_"; - } + internal const string infraStatPrefix = "infrastructureStat_"; } From 11dc94596b9ea3fae22bffee5f9225456d597f1b Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 14 May 2024 11:25:31 +0200 Subject: [PATCH 49/52] Move to experimental --- src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs | 2 +- .../Infrastructure/BuildCheckConfigurationErrorScope.cs | 2 +- .../BuildCheck/Infrastructure/ConfigurationProvider.cs | 6 +++--- .../Infrastructure/EditorConfig/EditorConfigFile.cs | 2 +- .../Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs | 2 +- .../Infrastructure/EditorConfig/EditorConfigParser.cs | 6 +++--- src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs | 2 +- src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs | 6 +++--- src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs | 6 +++--- src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs | 4 ++-- src/BuildCheck.UnitTests/EditorConfig_Tests.cs | 4 ++-- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs index b895a36c9be..ab817077725 100644 --- a/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs +++ b/src/Build/BuildCheck/API/BuildAnalyzerConfiguration.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure; namespace Microsoft.Build.Experimental.BuildCheck; diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs index 720ae3c9570..9c4c06511be 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckConfigurationErrorScope.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Build.BuildCheck.Infrastructure; +namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure; internal enum BuildCheckConfigurationErrorScope { diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 4c4a72a0c1c..337ffec9580 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -4,14 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure; internal sealed class ConfigurationProvider { - private readonly EditorConfigParser s_editorConfigParser = new EditorConfigParser(); + private readonly EditorConfigParser _editorConfigParser = new EditorConfigParser(); private const string BuildCheck_ConfigurationKey = "build_check"; @@ -197,7 +197,7 @@ private Dictionary FetchEditorConfigRules(string projectFullPath Dictionary config; try { - config = s_editorConfigParser.Parse(projectFullPath); + config = _editorConfigParser.Parse(projectFullPath); } catch (Exception exception) { diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs index 4472f92dddb..d2f93664369 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigFile.cs @@ -18,7 +18,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; internal partial class EditorConfigFile { diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs index 516190c2ab3..60df42cb36d 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigGlobsMatcher.cs @@ -19,7 +19,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; internal class EditorConfigGlobsMatcher { diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index e7b895c495b..28350023d9e 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -11,9 +11,9 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Build.Shared; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; -namespace Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; internal sealed class EditorConfigParser { @@ -46,7 +46,7 @@ internal List DiscoverEditorConfigFiles(string filePath) { if (!_editorConfigFileCache.TryGetValue(editorConfigFilePath, out var editorConfig)) { - using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) { using StreamReader sr = new StreamReader(editorConfigFilePath); var editorConfigfileContent = sr.ReadToEnd(); diff --git a/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs index 4b76786f3b4..95c0f6f611f 100644 --- a/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -7,7 +7,7 @@ using System.Reflection.Metadata; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.Experimental.BuildCheck; using Shouldly; using Xunit; diff --git a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs index 715653df17d..c9a2595a8c0 100644 --- a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs @@ -9,13 +9,13 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs index 0de7e02e1c4..909a843f405 100644 --- a/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs +++ b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs @@ -9,13 +9,13 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs index 968ca624408..476951b7945 100644 --- a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs @@ -9,11 +9,11 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs index 2bf7856c43e..5bc77ec13b5 100644 --- a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs @@ -8,10 +8,10 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.BuildCheck.Infrastructure.EditorConfig; +using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Xunit; -using static Microsoft.Build.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; +using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; #nullable disable From 02f5ca35f319833569c9cd2e06b0f335e01b2e61 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Fri, 17 May 2024 10:42:40 +0200 Subject: [PATCH 50/52] Update the documentation, update the configurationContext --- .../BuildCheck/API/ConfigurationContext.cs | 12 +----- .../BuildCheckManagerProvider.cs | 1 - .../Infrastructure/EditorConfig/README.md | 43 +++++++++++++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/Build/BuildCheck/API/ConfigurationContext.cs b/src/Build/BuildCheck/API/ConfigurationContext.cs index 81576a42fb5..3ecdd7c6527 100644 --- a/src/Build/BuildCheck/API/ConfigurationContext.cs +++ b/src/Build/BuildCheck/API/ConfigurationContext.cs @@ -20,19 +20,9 @@ private ConfigurationContext(CustomConfigurationData[] customConfigurationData) internal static ConfigurationContext FromDataEnumeration(CustomConfigurationData[] customConfigurationData) { - if (!customConfigurationData.Any(BuildCheck.CustomConfigurationData.NotNull)) - { - return Null; - } - - return new ConfigurationContext( - customConfigurationData - .Where(BuildCheck.CustomConfigurationData.NotNull) - .ToArray()); + return new ConfigurationContext(customConfigurationData); } - internal static ConfigurationContext Null { get; } = new(Array.Empty()); - /// /// Custom configuration data - per each rule that has some specified. /// diff --git a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs index a3e27a2996c..f76b6434356 100644 --- a/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/BuildCheckManagerProvider.cs @@ -74,7 +74,6 @@ internal sealed class BuildCheckManager : IBuildCheckManager internal BuildCheckManager(ILoggingService loggingService) { - _analyzersRegistry = new List(); _acquisitionModule = new BuildCheckAcquisitionModule(loggingService); _loggingService = loggingService; diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md index a725757b1ec..0b3374af791 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md @@ -10,7 +10,7 @@ Configuration divided into two categories: - Infra related configuration. IsEnabled, Severity, EvaluationAnalysisScope - Custom configuration, any other config specified by user for this particular rule -### Example +### Example For the file/folder structure: ``` ├── folder1/ @@ -44,10 +44,11 @@ The implementation differs depending on category: - Infra related config: Merges the configuration retrieved from configuration module with default values (respecting the specified configs in editorconfig) - Custom configuration: Remove all infra related keys from dictionary -Three levels of cache introduced: +Four levels of cache introduced: - When retrieving and parsing the editor config -> Parsed results are saved into dictionary: editorconfigPath = ParsedEditorConfig - When retrieving and merging the editor config data for project -> Parsed and merged results are saved into dictionary: projectFilePath = MargedData of ParsedEditorConfig - When retrieving Infra related config: ruleId-projectPath = BuildConfigInstance +- CustomConfigurationData: In order to verify that the config data is the same between projects Usage examples (API) @@ -58,4 +59,40 @@ editorConfigParser.Parse("path/to/the/file") The snippet above will return all applied key-value Dictionary pairs collected from .editorconfig files -Currently EditorConfigParser is used by [ConfigurationProvider](https://github.com/dotnet/msbuild/blob/e0dfb8d1ce5fc1de5153e65ea04c66a6dcac6279/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs#L129). +Currently EditorConfigParser is used by [ConfigurationProvider](https://github.com/dotnet/msbuild/blob/e0dfb8d1ce5fc1de5153e65ea04c66a6dcac6279/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs#L129). + +#### Cache lifetime +The lifetime of the cached configuration is defined by the usage of the instance of ConfigurationProvider. The instance of the ConfigurationProvider is created per BuildCheckManager. +Lifecycle of BuildCheckManager could be found [here](https://github.com/dotnet/msbuild/blob/main/documentation/specs/proposed/BuildCheck-Architecture.md#handling-the-distributed-model) + + +#### Custom configuration data +CustomConfigurationData is propogated to the BuildCheck Analyzer instance by passing the instance of [ConfigurationContext](https://github.com/dotnet/msbuild/blob/393c2fea652873416c8a2028810932a4fa94403f/src/Build/BuildCheck/API/ConfigurationContext.cs#L14) +during the initialization of the [BuildAnalyzer](https://github.com/dotnet/msbuild/blob/393c2fea652873416c8a2028810932a4fa94403f/src/Build/BuildCheck/API/BuildAnalyzer.cs#L36). + + +#### Example of consuming the CustomConfigurationData +The `Initialize` method of BuildCheck Analyzer: +``` +public override void Initialize(ConfigurationContext configurationContext) +{ + Console.WriteLine(configurationContext.CustomConfigurationData.Count); + for (int i = 0; i < configurationContext.CustomConfigurationData.Count; i++) + { + var customConfigPerRule = configurationContext.CustomConfigurationData[i]; + Console.WriteLine(customConfigPerRule.RuleId); + + if (customConfigPerRule.ConfigurationData is not null) // null when the configuration was not provided from editorconfig + { + foreach (var kv in customConfigPerRule.ConfigurationData) + { + Console.WriteLine($"{kv.Key}------{kv.Value}"); + } + } + else + { + Console.WriteLine($"The data is null for index: {i}"); + } + } +} +``` From d6381ce7b3b0ffcfc4c9ea4a0ffdd94d15e8ee5b Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 20 May 2024 09:28:24 +0200 Subject: [PATCH 51/52] Address PR comments first iteration --- .../Infrastructure/CustomConfigurationData.cs | 4 ++-- .../BuildCheck/Infrastructure/EditorConfig/README.md | 2 +- .../BuildAnalyzerConfigurationInternalTests.cs | 5 ----- .../BuildAnalyzerConfiguration_Test.cs | 5 ----- .../ConfigurationProvider_Tests.cs | 6 ------ .../CustomConfigurationData_Tests.cs | 11 ----------- src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs | 6 ------ src/BuildCheck.UnitTests/EditorConfig_Tests.cs | 3 --- .../ParsedItemsAnalysisDataTests.cs | 4 ---- 9 files changed, 3 insertions(+), 43 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs index 4857d9f522d..afd3645cf2e 100644 --- a/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs +++ b/src/Build/BuildCheck/Infrastructure/CustomConfigurationData.cs @@ -15,7 +15,7 @@ namespace Microsoft.Build.Experimental.BuildCheck; /// that were attribute to a particular rule, but were not recognized by the infrastructure. /// The configuration data that is recognized by the infrastructure is passed as . /// -public class CustomConfigurationData +public sealed class CustomConfigurationData { public static CustomConfigurationData Null { get; } = new(string.Empty); @@ -60,7 +60,7 @@ public override bool Equals(object? obj) return true; } - if (obj.GetType() != this.GetType()) + if (obj is not CustomConfigurationData) { return false; } diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md index 0b3374af791..e19b61c2c40 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/README.md @@ -73,7 +73,7 @@ during the initialization of the [BuildAnalyzer](https://github.com/dotnet/msbui #### Example of consuming the CustomConfigurationData The `Initialize` method of BuildCheck Analyzer: -``` +```C# public override void Initialize(ConfigurationContext configurationContext) { Console.WriteLine(configurationContext.CustomConfigurationData.Count); diff --git a/src/BuildCheck.UnitTests/BuildAnalyzerConfigurationInternalTests.cs b/src/BuildCheck.UnitTests/BuildAnalyzerConfigurationInternalTests.cs index a3f8b019439..7bd57f8014b 100644 --- a/src/BuildCheck.UnitTests/BuildAnalyzerConfigurationInternalTests.cs +++ b/src/BuildCheck.UnitTests/BuildAnalyzerConfigurationInternalTests.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.Experimental.BuildCheck; diff --git a/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs index 95c0f6f611f..edfdfaf4589 100644 --- a/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs +++ b/src/BuildCheck.UnitTests/BuildAnalyzerConfiguration_Test.cs @@ -1,12 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection.Metadata; -using System.Text; -using System.Threading.Tasks; using Microsoft.Build.Experimental.BuildCheck.Infrastructure; using Microsoft.Build.Experimental.BuildCheck; using Shouldly; diff --git a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs index c9a2595a8c0..d559e1724b1 100644 --- a/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs +++ b/src/BuildCheck.UnitTests/ConfigurationProvider_Tests.cs @@ -2,20 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Microsoft.Build.Experimental.BuildCheck.Infrastructure; -using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs index 909a843f405..e8ff337e1a8 100644 --- a/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs +++ b/src/BuildCheck.UnitTests/CustomConfigurationData_Tests.cs @@ -1,21 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Build.Experimental.BuildCheck.Infrastructure; -using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; -using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs index 476951b7945..17bd60abbd1 100644 --- a/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfigParser_Tests.cs @@ -1,19 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; -using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; namespace Microsoft.Build.BuildCheck.UnitTests; diff --git a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs index 5bc77ec13b5..1b1b0c5aaa4 100644 --- a/src/BuildCheck.UnitTests/EditorConfig_Tests.cs +++ b/src/BuildCheck.UnitTests/EditorConfig_Tests.cs @@ -6,10 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; -using Microsoft.Build.UnitTests; using Xunit; using static Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig.EditorConfigGlobsMatcher; diff --git a/src/BuildCheck.UnitTests/ParsedItemsAnalysisDataTests.cs b/src/BuildCheck.UnitTests/ParsedItemsAnalysisDataTests.cs index 05d1266d2ac..7734c19f311 100644 --- a/src/BuildCheck.UnitTests/ParsedItemsAnalysisDataTests.cs +++ b/src/BuildCheck.UnitTests/ParsedItemsAnalysisDataTests.cs @@ -1,12 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; using Microsoft.Build.Construction; using Microsoft.Build.Experimental.BuildCheck; using Microsoft.Build.UnitTests; From ed6c50d723c1d3fbd668511128b516f7bfc569c1 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 20 May 2024 12:37:48 +0200 Subject: [PATCH 52/52] Address PR comments --- .../Infrastructure/ConfigurationProvider.cs | 65 +++++++++---------- .../EditorConfig/EditorConfigParser.cs | 7 +- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs index 337ffec9580..ab2e298879b 100644 --- a/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs +++ b/src/Build/BuildCheck/Infrastructure/ConfigurationProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; using Microsoft.Build.Experimental.BuildCheck; +using System.Collections.Concurrent; namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure; @@ -18,17 +19,17 @@ internal sealed class ConfigurationProvider /// /// The dictionary used for storing the BuildAnalyzerConfiguration per projectfile and rule id. The key is equal to {projectFullPath}-{ruleId}. /// - private readonly Dictionary _buildAnalyzerConfiguration = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly ConcurrentDictionary _buildAnalyzerConfiguration = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); /// /// The dictionary used for storing the key-value pairs retrieved from the .editorconfigs for specific projectfile. The key is equal to projectFullPath. /// - private readonly Dictionary> _editorConfigData = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + private readonly ConcurrentDictionary> _editorConfigData = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); /// /// The dictionary used for storing the CustomConfigurationData per ruleId. The key is equal to ruleId. /// - private readonly Dictionary _customConfigurationData = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly ConcurrentDictionary _customConfigurationData = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); private readonly string[] _infrastructureConfigurationKeys = new string[] { nameof(BuildAnalyzerConfiguration.EvaluationAnalysisScope).ToLower(), @@ -188,27 +189,25 @@ private Dictionary FilterDictionaryByKeys(string keyFilter, Dict /// private Dictionary FetchEditorConfigRules(string projectFullPath) { - // check if we have the data already - if (_editorConfigData.TryGetValue(projectFullPath, out var cachedConfig)) + var editorConfigRules = _editorConfigData.GetOrAdd(projectFullPath, (key) => { - return cachedConfig; - } - - Dictionary config; - try - { - config = _editorConfigParser.Parse(projectFullPath); - } - catch (Exception exception) - { - throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); - } + Dictionary config; + try + { + config = _editorConfigParser.Parse(projectFullPath); + } + catch (Exception exception) + { + throw new BuildCheckConfigurationException($"Parsing editorConfig data failed", exception, BuildCheckConfigurationErrorScope.EditorConfigParser); + } - // clear the dictionary from the key-value pairs not BuildCheck related and - // store the data so there is no need to parse the .editorconfigs all over again - Dictionary result = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); - _editorConfigData[projectFullPath] = result; - return result; + // clear the dictionary from the key-value pairs not BuildCheck related and + // store the data so there is no need to parse the .editorconfigs all over again + Dictionary filteredData = FilterDictionaryByKeys($"{BuildCheck_ConfigurationKey}.", config); + return filteredData; + }); + + return editorConfigRules; } internal Dictionary GetConfiguration(string projectFullPath, string ruleId) @@ -231,22 +230,20 @@ internal BuildAnalyzerConfiguration GetUserConfiguration(string projectFullPath, { var cacheKey = $"{ruleId}-{projectFullPath}"; - if (_buildAnalyzerConfiguration.TryGetValue(cacheKey, out BuildAnalyzerConfiguration? cachedEditorConfig)) + var editorConfigValue = _buildAnalyzerConfiguration.GetOrAdd(cacheKey, (key) => { - return cachedEditorConfig; - } - - BuildAnalyzerConfiguration? editorConfig = BuildAnalyzerConfiguration.Null; - var config = GetConfiguration(projectFullPath, ruleId); + BuildAnalyzerConfiguration? editorConfig = BuildAnalyzerConfiguration.Null; + var config = GetConfiguration(projectFullPath, ruleId); - if (config.Any()) - { - editorConfig = BuildAnalyzerConfiguration.Create(config); - } + if (config.Any()) + { + editorConfig = BuildAnalyzerConfiguration.Create(config); + } - _buildAnalyzerConfiguration[cacheKey] = editorConfig; + return editorConfig; + }); - return editorConfig; + return editorConfigValue; } /// diff --git a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs index 28350023d9e..76baa1f1e66 100644 --- a/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs +++ b/src/Build/BuildCheck/Infrastructure/EditorConfig/EditorConfigParser.cs @@ -44,16 +44,15 @@ internal List DiscoverEditorConfigFiles(string filePath) while (editorConfigFilePath != string.Empty) { - if (!_editorConfigFileCache.TryGetValue(editorConfigFilePath, out var editorConfig)) + var editorConfig = _editorConfigFileCache.GetOrAdd(editorConfigFilePath, (key) => { using (FileStream stream = new FileStream(editorConfigFilePath, FileMode.Open, System.IO.FileAccess.Read, FileShare.Read)) { using StreamReader sr = new StreamReader(editorConfigFilePath); var editorConfigfileContent = sr.ReadToEnd(); - editorConfig = EditorConfigFile.Parse(editorConfigfileContent); - _editorConfigFileCache[editorConfigFilePath] = editorConfig; + return EditorConfigFile.Parse(editorConfigfileContent); } - } + }); editorConfigDataFromFilesList.Add(editorConfig);