diff --git a/analyzers/its/SonarAnalyzer.Testing.ImportBefore.targets b/analyzers/its/SonarAnalyzer.Testing.ImportBefore.targets index f3fda742c51..ea85c04a441 100644 --- a/analyzers/its/SonarAnalyzer.Testing.ImportBefore.targets +++ b/analyzers/its/SonarAnalyzer.Testing.ImportBefore.targets @@ -11,6 +11,8 @@ Product Test + + Unknown diff --git a/analyzers/its/config/SonarLintExclusions/SonarLint.xml b/analyzers/its/config/SonarLintExclusions/SonarLint.xml new file mode 100644 index 00000000000..be8726ef80a --- /dev/null +++ b/analyzers/its/config/SonarLintExclusions/SonarLint.xml @@ -0,0 +1,40 @@ + + + + + + + sonar.cs.ignoreHeaderComments + true + + + sonar.vbnet.ignoreHeaderComments + true + + + sonar.inclusions + **/Included/*.cs + + + sonar.exclusions + **/ExcludedByExclusion.cs,**/ExcludedByExclusion2.cs + + + sonar.global.exclusions + **/ExcludedByGlobalExclusion.cs + + + + sonar.test.inclusions + **/IncludedTest/*.cs + + + sonar.test.exclusions + **/ExcludedByExclusionTest.cs + + + sonar.global.test.exclusions + **/ExcludedByGlobalExclusionTest.cs + + + \ No newline at end of file diff --git a/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S1451.json b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S1451.json new file mode 100644 index 00000000000..d4028681cc7 --- /dev/null +++ b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S1451.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S1451", +"message": "Add or update the header of this file.", +"location": { +"uri": "sources\SonarLintExclusions\SonarLintExclusions\Included\Included.cs", +"region": { +"startLine": 1, +"startColumn": 1, +"endLine": 1, +"endColumn": 1 +} +} +} +] +} diff --git a/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S2094.json b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S2094.json new file mode 100644 index 00000000000..e86aa12d401 --- /dev/null +++ b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S2094.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S2094", +"message": "Remove this empty class, write its code or make it an "interface".", +"location": { +"uri": "sources\SonarLintExclusions\SonarLintExclusions\Included\Included.cs", +"region": { +"startLine": 3, +"startColumn": 18, +"endLine": 3, +"endColumn": 26 +} +} +} +] +} diff --git a/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3990.json b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3990.json new file mode 100644 index 00000000000..17786f2ed3f --- /dev/null +++ b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3990.json @@ -0,0 +1,9 @@ +{ +"issues": [ +{ +"id": "S3990", +"message": "Provide a 'CLSCompliant' attribute for assembly 'SonarLintExclusions'.", +"location": null +} +] +} diff --git a/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3992.json b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3992.json new file mode 100644 index 00000000000..dfd144e2a2e --- /dev/null +++ b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusions--net7.0-S3992.json @@ -0,0 +1,9 @@ +{ +"issues": [ +{ +"id": "S3992", +"message": "Provide a 'ComVisible' attribute for assembly 'SonarLintExclusions'.", +"location": null +} +] +} diff --git a/analyzers/its/expected/SonarLintExclusions/SonarLintExclusionsTest--net7.0-S2699.json b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusionsTest--net7.0-S2699.json new file mode 100644 index 00000000000..9a66c44306c --- /dev/null +++ b/analyzers/its/expected/SonarLintExclusions/SonarLintExclusionsTest--net7.0-S2699.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S2699", +"message": "Add at least one assertion to this test case.", +"location": { +"uri": "sources\SonarLintExclusions\SonarLintExclusionsTest\IncludedTest\IncludedTest.cs", +"region": { +"startLine": 8, +"startColumn": 21, +"endLine": 8, +"endColumn": 32 +} +} +} +] +} diff --git a/analyzers/its/regression-test.ps1 b/analyzers/its/regression-test.ps1 index ad7cf9d8057..513681eedbc 100644 --- a/analyzers/its/regression-test.ps1 +++ b/analyzers/its/regression-test.ps1 @@ -12,7 +12,7 @@ param $ruleId, [Parameter(HelpMessage = "The name of single project to build. If ommited, all projects will be build.")] - [ValidateSet("AnalyzeGenerated.CS", "AnalyzeGenerated.VB", "akka.net", "Automapper", "Ember-MM", "Nancy", "NetCore31", "Net5", "Net6", "Net7", "NetCore31WithConfigurableRules" , "ManuallyAddedNoncompliantIssues.CS", "ManuallyAddedNoncompliantIssues.VB", "Roslyn.1.3.1", "SkipGenerated.CS", "SkipGenerated.VB", "WebConfig")] + [ValidateSet("AnalyzeGenerated.CS", "AnalyzeGenerated.VB", "akka.net", "Automapper", "Ember-MM", "Nancy", "NetCore31", "Net5", "Net6", "Net7", "NetCore31WithConfigurableRules" , "ManuallyAddedNoncompliantIssues.CS", "ManuallyAddedNoncompliantIssues.VB", "Roslyn.1.3.1", "SkipGenerated.CS", "SkipGenerated.VB", "SonarLintExclusions", "WebConfig")] [string] $project ) @@ -499,6 +499,7 @@ try { Build-Project-DotnetTool "NetCore31WithConfigurableRules" "NetCore31WithConfigurableRules.sln" Build-Project-DotnetTool "akka.net" "src\Akka.sln" Build-Project-DotnetTool "Automapper" "Automapper.sln" + Build-Project-DotnetTool "SonarLintExclusions" "SonarLintExclusions.sln" Write-Header "Processing analyzer results" diff --git a/analyzers/its/sources/AnalyzeGenerated.CS/SonarLint.xml b/analyzers/its/sources/AnalyzeGenerated.CS/SonarLint.xml index 15dbb76706c..637fabc2996 100644 --- a/analyzers/its/sources/AnalyzeGenerated.CS/SonarLint.xml +++ b/analyzers/its/sources/AnalyzeGenerated.CS/SonarLint.xml @@ -1,4 +1,4 @@ - + @@ -27,4 +27,4 @@ - + \ No newline at end of file diff --git a/analyzers/its/sources/SkipGenerated.CS/SonarLint.xml b/analyzers/its/sources/SkipGenerated.CS/SonarLint.xml index b99df9c60f6..b11b39f3e3c 100644 --- a/analyzers/its/sources/SkipGenerated.CS/SonarLint.xml +++ b/analyzers/its/sources/SkipGenerated.CS/SonarLint.xml @@ -1,4 +1,4 @@ - + @@ -27,4 +27,4 @@ - + \ No newline at end of file diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions.sln b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions.sln new file mode 100644 index 00000000000..3e5bcbbe34b --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33417.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SonarLintExclusions", "SonarLintExclusions\SonarLintExclusions.csproj", "{F5D0F2AC-2BED-42AA-B219-674F970E8400}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SonarLintExclusionsTest", "SonarLintExclusionsTest\SonarLintExclusionsTest.csproj", "{0F14329D-7A71-489B-BB85-2B257A866429}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1CF8B86B-6A00-4E05-931E-F278FCFDF1CF}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F5D0F2AC-2BED-42AA-B219-674F970E8400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5D0F2AC-2BED-42AA-B219-674F970E8400}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5D0F2AC-2BED-42AA-B219-674F970E8400}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5D0F2AC-2BED-42AA-B219-674F970E8400}.Release|Any CPU.Build.0 = Release|Any CPU + {0F14329D-7A71-489B-BB85-2B257A866429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F14329D-7A71-489B-BB85-2B257A866429}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F14329D-7A71-489B-BB85-2B257A866429}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F14329D-7A71-489B-BB85-2B257A866429}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9FA0AE0B-19EB-4F4F-A69E-5AB50EE8240E} + EndGlobalSection +EndGlobal diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion.cs new file mode 100644 index 00000000000..c33727affc7 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion.cs @@ -0,0 +1,4 @@ +namespace SonarLintExclusions +{ + public class ExcludedByExclusion { } // S2094 +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion2.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion2.cs new file mode 100644 index 00000000000..8366890afef --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByExclusion2.cs @@ -0,0 +1,4 @@ +namespace SonarLintExclusions +{ + public class ExcludedByExclusion2 { } // S2094 +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByGlobalExclusion.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByGlobalExclusion.cs new file mode 100644 index 00000000000..8eafecd8f7f --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/ExcludedByGlobalExclusion.cs @@ -0,0 +1,4 @@ +namespace SonarLintExclusions +{ + public class ExcludedByGlobalExclusion { } // S2094 +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/Included.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/Included.cs new file mode 100644 index 00000000000..6fba906c697 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/Included/Included.cs @@ -0,0 +1,4 @@ +namespace SonarLintExclusions +{ + public class Included { } // S2094 +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/NotIncluded.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/NotIncluded.cs new file mode 100644 index 00000000000..95a9911ce76 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/NotIncluded.cs @@ -0,0 +1,4 @@ +namespace SonarLintExclusions +{ + public class NotIncluded { } // S2094 +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/SonarLintExclusions.csproj b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/SonarLintExclusions.csproj new file mode 100644 index 00000000000..15297210401 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusions/SonarLintExclusions.csproj @@ -0,0 +1,8 @@ + + + + net7.0 + true + + + diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByExclusionTest.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByExclusionTest.cs new file mode 100644 index 00000000000..d3fcf14795b --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByExclusionTest.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace SonarLintExclusionsTest +{ + [TestClass] + public class ExcludedByExclusionTest + { + [TestMethod] + public void TestMethod1() { } + } +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByGlobalExclusionTest.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByGlobalExclusionTest.cs new file mode 100644 index 00000000000..b1aadee0880 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/ExcludedByGlobalExclusionTest.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace SonarLintExclusionsTest +{ + [TestClass] + public class ExcludedByGlobalExclusionTest + { + [TestMethod] + public void TestMethod1() { } + } +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/IncludedTest.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/IncludedTest.cs new file mode 100644 index 00000000000..cc364afdccc --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/IncludedTest/IncludedTest.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace SonarLintExclusionsTest +{ + [TestClass] + public class IncludedTest + { + [TestMethod] + public void TestMethod1() { } + } +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/NotIncludedTest.cs b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/NotIncludedTest.cs new file mode 100644 index 00000000000..8b81a00464c --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/NotIncludedTest.cs @@ -0,0 +1,10 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace SonarLintExclusionsTest +{ + [TestClass] + public class NotIncludedTest + { + [TestMethod] + public void TestMethod1() { } + } +} diff --git a/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/SonarLintExclusionsTest.csproj b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/SonarLintExclusionsTest.csproj new file mode 100644 index 00000000000..0d99fb1aff7 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/SonarLintExclusionsTest/SonarLintExclusionsTest.csproj @@ -0,0 +1,15 @@ + + + + net7.0 + false + true + + + + + + + + + diff --git a/analyzers/its/sources/SonarLintExclusions/global.json b/analyzers/its/sources/SonarLintExclusions/global.json new file mode 100644 index 00000000000..4037c7755e7 --- /dev/null +++ b/analyzers/its/sources/SonarLintExclusions/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.100", + "rollForward": "latestMinor" + } +} \ No newline at end of file diff --git a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs index e18e7365ae7..cc9dedd3618 100644 --- a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.Collections.Concurrent; using System.IO; using System.Runtime.CompilerServices; using static SonarAnalyzer.Helpers.DiagnosticDescriptorFactory; @@ -26,18 +27,12 @@ namespace SonarAnalyzer.AnalysisContext; public class SonarAnalysisContextBase { + protected static readonly ConditionalWeakTable> FileInclusionCache = new(); protected static readonly ConditionalWeakTable> UnchangedFilesCache = new(); protected static readonly SourceTextValueProvider ProjectConfigProvider = new(x => new ProjectConfigReader(x)); - private static readonly Lazy> ShouldAnalyzeGeneratedCS = new(() => CreateAnalyzeGeneratedProvider(LanguageNames.CSharp)); - private static readonly Lazy> ShouldAnalyzeGeneratedVB = new(() => CreateAnalyzeGeneratedProvider(LanguageNames.VisualBasic)); + protected static readonly SourceTextValueProvider SonarLintXmlProvider = new(x => new SonarLintXmlReader(x)); protected SonarAnalysisContextBase() { } - - protected static SourceTextValueProvider ShouldAnalyzeGeneratedProvider(string language) => - language == LanguageNames.CSharp ? ShouldAnalyzeGeneratedCS.Value : ShouldAnalyzeGeneratedVB.Value; - - private static SourceTextValueProvider CreateAnalyzeGeneratedProvider(string language) => - new(x => PropertiesHelper.ReadAnalyzeGeneratedCodeProperty(PropertiesHelper.ParseXmlSettings(x), language)); } public abstract class SonarAnalysisContextBase : SonarAnalysisContextBase @@ -58,8 +53,9 @@ protected SonarAnalysisContextBase(SonarAnalysisContext analysisContext, TContex /// Tree to decide on. Can be null for Symbol-based and Compilation-based scenarios. And we want to analyze those too. /// When set, generated trees are analyzed only when language-specific 'analyzeGeneratedCode' configuration property is also set. public bool ShouldAnalyzeTree(SyntaxTree tree, GeneratedCodeRecognizer generatedCodeRecognizer) => - (generatedCodeRecognizer is null || ShouldAnalyzeGenerated() || !tree.IsGenerated(generatedCodeRecognizer, Compilation)) - && (tree is null || !IsUnchanged(tree)); + SonarLintXml() is var sonarLintXml + && (generatedCodeRecognizer is null || sonarLintXml.AnalyzeGeneratedCode(Compilation.Language) || !tree.IsGenerated(generatedCodeRecognizer, Compilation)) + && (tree is null || (!IsUnchanged(tree) && !IsExcluded(sonarLintXml, tree.FilePath))); /// /// Reads configuration from SonarProjectConfig.xml file and caches the result for scope of this analysis. @@ -80,6 +76,24 @@ public ProjectConfigReader ProjectConfiguration() } } + /// + /// Reads the properties from the SonarLint.xml file and caches the result for the scope of this analysis. + /// + public SonarLintXmlReader SonarLintXml() + { + if (Options.SonarLintXml() is { } sonarLintXml) + { + return sonarLintXml.GetText() is { } sourceText + && AnalysisContext.TryGetValue(sourceText, SonarLintXmlProvider, out var sonarLintXmlReader) + ? sonarLintXmlReader + : throw new InvalidOperationException($"File '{Path.GetFileName(sonarLintXml.Path)}' has been added as an AdditionalFile but could not be read and parsed."); + } + else + { + return Helpers.SonarLintXmlReader.Empty; + } + } + public bool IsTestProject() { var projectType = ProjectConfiguration().ProjectType; @@ -107,11 +121,29 @@ public bool HasMatchingScope(DiagnosticDescriptor descriptor) descriptor.CustomTags.Contains(tag); } + private bool IsExcluded(SonarLintXmlReader sonarLintXml, string filePath) => + // If ProjectType is not 'Unknown' it means we are in S4NET context and all files are analyzed. + // If ProjectType is 'Unknown' then we are in SonarLint or NuGet context and we need to check if the file has been excluded from analysis through SonarLint.xml. + ProjectConfiguration().ProjectType == ProjectType.Unknown + && FileInclusionCache.GetValue(Compilation, _ => new()) is var cache + && !cache.GetOrAdd(filePath, _ => IsFileIncluded(sonarLintXml, filePath)); + private ImmutableHashSet CreateUnchangedFilesHashSet() => ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, ProjectConfiguration().AnalysisConfig?.UnchangedFiles() ?? Array.Empty()); - private bool ShouldAnalyzeGenerated() => - Options.SonarLintXml() is { } sonarLintXml - && AnalysisContext.TryGetValue(sonarLintXml.GetText(), ShouldAnalyzeGeneratedProvider(Compilation.Language), out var shouldAnalyzeGenerated) - && shouldAnalyzeGenerated; + private bool IsFileIncluded(SonarLintXmlReader sonarLintXml, string filePath) => + IsTestProject() + ? IsFileIncluded(sonarLintXml.TestInclusions, sonarLintXml.TestExclusions, sonarLintXml.GlobalTestExclusions, filePath) + : IsFileIncluded(sonarLintXml.Inclusions, sonarLintXml.Exclusions, sonarLintXml.GlobalExclusions, filePath); + + private static bool IsFileIncluded(string[] inclusions, string[] exclusions, string[] globalExclusions, string filePath) => + IsIncluded(inclusions, filePath) + && !IsExcluded(exclusions, filePath) + && !IsExcluded(globalExclusions, filePath); + + private static bool IsIncluded(string[] inclusions, string filePath) => + inclusions is { Length: 0 } || inclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath, true)); + + private static bool IsExcluded(string[] exclusions, string filePath) => + exclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath, false)); } diff --git a/analyzers/src/SonarAnalyzer.Common/Common/UnexpectedLanguageException.cs b/analyzers/src/SonarAnalyzer.Common/Common/UnexpectedLanguageException.cs index c71a99fca52..5585b04b6ad 100644 --- a/analyzers/src/SonarAnalyzer.Common/Common/UnexpectedLanguageException.cs +++ b/analyzers/src/SonarAnalyzer.Common/Common/UnexpectedLanguageException.cs @@ -22,6 +22,8 @@ namespace SonarAnalyzer.Common { public sealed class UnexpectedLanguageException : Exception { - public UnexpectedLanguageException(AnalyzerLanguage language) : base($"Unexpected language: {language}") { } + public UnexpectedLanguageException(AnalyzerLanguage language) : this(language.LanguageName) { } + + public UnexpectedLanguageException(string language) : base($"Unexpected language: {language}") { } } } diff --git a/analyzers/src/SonarAnalyzer.Common/DiagnosticAnalyzer/ParametrizedDiagnosticAnalyzer.cs b/analyzers/src/SonarAnalyzer.Common/DiagnosticAnalyzer/ParametrizedDiagnosticAnalyzer.cs index c1ae6279f85..46f7ea5533a 100644 --- a/analyzers/src/SonarAnalyzer.Common/DiagnosticAnalyzer/ParametrizedDiagnosticAnalyzer.cs +++ b/analyzers/src/SonarAnalyzer.Common/DiagnosticAnalyzer/ParametrizedDiagnosticAnalyzer.cs @@ -32,7 +32,7 @@ protected sealed override void Initialize(SonarAnalysisContext context) context.RegisterCompilationStartAction( c => { - ParameterLoader.SetParameterValues(this, c.Options); + ParameterLoader.SetParameterValues(this, c.SonarLintXml()); parameterContext.ExecutePostponedActions(c); }); } diff --git a/analyzers/src/SonarAnalyzer.Common/Extensions/AnalyzerOptionsExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Extensions/AnalyzerOptionsExtensions.cs index 6cea60b71a5..d3ed1f510bd 100644 --- a/analyzers/src/SonarAnalyzer.Common/Extensions/AnalyzerOptionsExtensions.cs +++ b/analyzers/src/SonarAnalyzer.Common/Extensions/AnalyzerOptionsExtensions.cs @@ -19,7 +19,6 @@ */ using System.IO; -using System.Xml.Linq; namespace SonarAnalyzer.Extensions; @@ -34,9 +33,6 @@ public static class AnalyzerOptionsExtensions public static AdditionalText ProjectOutFolderPath(this AnalyzerOptions options) => options.AdditionalFile("ProjectOutFolderPath.txt"); - public static XElement[] ParseSonarLintXmlSettings(this AnalyzerOptions options) => - options.SonarLintXml() is { } sonarLintXml ? PropertiesHelper.ParseXmlSettings(sonarLintXml.GetText()) : Array.Empty(); - private static AdditionalText AdditionalFile(this AnalyzerOptions options, string fileName) => options.AdditionalFiles.FirstOrDefault(x => x.Path is not null && Path.GetFileName(x.Path).Equals(fileName, StringComparison.OrdinalIgnoreCase)); } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/ParameterLoader.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/ParameterLoader.cs index b472cd3e53e..98772ed382c 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/ParameterLoader.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ParameterLoader.cs @@ -19,10 +19,7 @@ */ using System.Globalization; -using System.IO; using System.Reflection; -using System.Xml; -using System.Xml.Linq; namespace SonarAnalyzer.Helpers { @@ -39,84 +36,30 @@ internal static class ParameterLoader * - diffing the contents of the configuration file * - associating the file with a unique identifier for the build project */ - internal static void SetParameterValues(ParametrizedDiagnosticAnalyzer parameteredAnalyzer, - AnalyzerOptions options) + internal static void SetParameterValues(ParametrizedDiagnosticAnalyzer parameteredAnalyzer, SonarLintXmlReader sonarLintXml) { - var sonarLintXml = options.SonarLintXml(); - if (sonarLintXml == null) - { - return; - } - - var parameters = ParseParameters(sonarLintXml); - if (parameters.IsEmpty) + if (!sonarLintXml.ParametrizedRules.Any()) { return; } var propertyParameterPairs = parameteredAnalyzer.GetType() .GetRuntimeProperties() - .Select(p => new { Property = p, Descriptor = p.GetCustomAttributes().SingleOrDefault() }) - .Where(p => p.Descriptor != null); + .Select(x => new { Property = x, Descriptor = x.GetCustomAttributes().SingleOrDefault() }) + .Where(x => x.Descriptor is not null); var ids = new HashSet(parameteredAnalyzer.SupportedDiagnostics.Select(diagnostic => diagnostic.Id)); foreach (var propertyParameterPair in propertyParameterPairs) { - var parameter = parameters - .FirstOrDefault(p => ids.Contains(p.RuleId)); - - var parameterValue = parameter?.ParameterValues - .FirstOrDefault(pv => pv.ParameterKey == propertyParameterPair.Descriptor.Key); - - if (TryConvertToParameterType(parameterValue?.ParameterValue, propertyParameterPair.Descriptor.Type, out var value)) + var parameter = sonarLintXml.ParametrizedRules.FirstOrDefault(x => ids.Contains(x.Key)); + var parameterValue = parameter?.Parameters.FirstOrDefault(x => x.Key == propertyParameterPair.Descriptor.Key); + if (TryConvertToParameterType(parameterValue?.Value, propertyParameterPair.Descriptor.Type, out var value)) { propertyParameterPair.Property.SetValue(parameteredAnalyzer, value); } } } - private static ImmutableList ParseParameters(AdditionalText sonarLintXml) - { - try - { - var xml = XDocument.Parse(sonarLintXml.GetText().ToString()); - return ParseParameters(xml); - } - catch (Exception ex) when (ex is IOException || ex is XmlException) - { - // cannot log exception - return ImmutableList.Create(); - } - } - - private static ImmutableList ParseParameters(XContainer xml) - { - var builder = ImmutableList.CreateBuilder(); - foreach (var rule in xml.Descendants("Rule").Where(e => e.Elements("Parameters").Any())) - { - var analyzerId = rule.Elements("Key").Single().Value; - - var parameterValues = rule - .Elements("Parameters").Single() - .Elements("Parameter") - .Select(e => new RuleParameterValue - { - ParameterKey = e.Elements("Key").Single().Value, - ParameterValue = e.Elements("Value").Single().Value - }); - - var pvs = new RuleParameterValues - { - RuleId = analyzerId - }; - pvs.ParameterValues.AddRange(parameterValues); - - builder.Add(pvs); - } - - return builder.ToImmutable(); - } - private static bool TryConvertToParameterType(string parameter, PropertyType type, out object result) { if (parameter == null) @@ -144,19 +87,5 @@ private static bool TryConvertToParameterType(string parameter, PropertyType typ return false; } } - - private sealed class RuleParameterValues - { - public string RuleId { get; set; } - - public List ParameterValues { get; } = new List(); - } - - private sealed class RuleParameterValue - { - public string ParameterKey { get; set; } - - public string ParameterValue { get; set; } - } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/PropertiesHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/PropertiesHelper.cs deleted file mode 100644 index 55844a6ba58..00000000000 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/PropertiesHelper.cs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA - * mailto: contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.Xml.Linq; -using Microsoft.CodeAnalysis.Text; - -namespace SonarAnalyzer.Helpers -{ - internal static class PropertiesHelper - { - public static XElement[] ParseXmlSettings(SourceText sourceText) - { - try - { - return XDocument.Parse(sourceText.ToString()).Descendants("Setting").ToArray(); - } - catch - { - return Array.Empty(); // Can not log the exception, so ignore it - } - } - - public static bool ReadAnalyzeGeneratedCodeProperty(IEnumerable settings, string language) => - ReadBooleanProperty(settings, language, "analyzeGeneratedCode"); - - public static bool ReadIgnoreHeaderCommentsProperty(IEnumerable settings, string language) => - ReadBooleanProperty(settings, language, "ignoreHeaderComments"); - - private static bool ReadBooleanProperty(IEnumerable settings, string language, string propertySuffix, bool defaultValue = false) - { - var propertyLanguage = language == LanguageNames.CSharp ? "cs" : "vbnet"; - var propertyName = $"sonar.{propertyLanguage}.{propertySuffix}"; - return settings.Any() - && GetPropertyStringValue(propertyName) is { } propertyStringValue - && bool.TryParse(propertyStringValue, out var propertyValue) - ? propertyValue - : defaultValue; - - string GetPropertyStringValue(string propName) => - settings.FirstOrDefault(s => s.Element("Key")?.Value == propName)?.Element("Value").Value; - } - } -} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXml.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXml.cs new file mode 100644 index 00000000000..d699d960638 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXml.cs @@ -0,0 +1,59 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Xml.Serialization; + +namespace SonarAnalyzer.Helpers; + +/// +/// DTO to represent the SonarLint.xml for our analyzers. +/// +/// +/// This class should not be used in this codebase. To get SonarLint.xml properties, use . +/// +[XmlRoot(ElementName = "AnalysisInput")] +public class SonarLintXml +{ + public static readonly SonarLintXml Empty = new(); + + [XmlArray("Settings")] + [XmlArrayItem("Setting")] + public List Settings { get; set; } + + [XmlArray("Rules")] + [XmlArrayItem("Rule")] + public List Rules { get; set; } +} + +public class SonarLintXmlRule +{ + [XmlElement("Key")] + public string Key { get; set; } + + [XmlArray("Parameters")] + [XmlArrayItem("Parameter")] + public List Parameters { get; set; } +} + +public class SonarLintXmlKeyValuePair +{ + public string Key { get; set; } + public string Value { get; set; } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXmlReader.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXmlReader.cs new file mode 100644 index 00000000000..ec69d9c96a3 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarLintXmlReader.cs @@ -0,0 +1,108 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis.Text; + +namespace SonarAnalyzer.Helpers; + +public class SonarLintXmlReader +{ + public static readonly SonarLintXmlReader Empty = new(null); + + private readonly SonarLintXml sonarLintXml; + + private bool? ignoreHeaderCommentsCS; + private bool? ignoreHeaderCommentsVB; + public bool IgnoreHeaderComments(string language) => + language switch + { + LanguageNames.CSharp => ignoreHeaderCommentsCS ??= ReadBoolean(ReadSettingsProperty("sonar.cs.ignoreHeaderComments")), + LanguageNames.VisualBasic => ignoreHeaderCommentsVB ??= ReadBoolean(ReadSettingsProperty("sonar.vbnet.ignoreHeaderComments")), + _ => throw new UnexpectedLanguageException(language) + }; + + private bool? analyzeGeneratedCodeCS; + private bool? analyzeGeneratedCodeVB; + public bool AnalyzeGeneratedCode(string language) => + language switch + { + LanguageNames.CSharp => analyzeGeneratedCodeCS ??= ReadBoolean(ReadSettingsProperty("sonar.cs.analyzeGeneratedCode")), + LanguageNames.VisualBasic => analyzeGeneratedCodeVB ??= ReadBoolean(ReadSettingsProperty("sonar.vbnet.analyzeGeneratedCode")), + _ => throw new UnexpectedLanguageException(language) + }; + + private string[] exclusions; + public string[] Exclusions => exclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.exclusions")); + + private string[] inclusions; + public string[] Inclusions => inclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.inclusions")); + + private string[] globalExclusions; + public string[] GlobalExclusions => globalExclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.global.exclusions")); + + private string[] testExclusions; + public string[] TestExclusions => testExclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.test.exclusions")); + + private string[] testInclusions; + public string[] TestInclusions => testInclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.test.inclusions")); + + private string[] globalTestExclusions; + public string[] GlobalTestExclusions => globalTestExclusions ??= ReadCommaSeparatedArray(ReadSettingsProperty("sonar.global.test.exclusions")); + + private List parametrizedRules; + public List ParametrizedRules => parametrizedRules ??= ReadRuleParameters(); + + public SonarLintXmlReader(SourceText sonarLintXml) => + this.sonarLintXml = sonarLintXml == null ? SonarLintXml.Empty : ParseContent(sonarLintXml); + + private static SonarLintXml ParseContent(SourceText sonarLintXml) + { + try + { + var serializer = new XmlSerializer(typeof(SonarLintXml)); + using var sr = new StringReader(sonarLintXml.ToString()); + return (SonarLintXml)serializer.Deserialize(sr); + } + catch + { + return SonarLintXml.Empty; + } + } + + private List ReadRuleParameters() => + sonarLintXml is { Rules: { } rules } + ? rules.Where(x => x.Parameters.Any()).ToList() + : new(); + + private string ReadSettingsProperty(string property) => + sonarLintXml is { Settings: { } settings } + ? settings.Where(x => x.Key.Equals(property)).Select(x => x.Value).FirstOrDefault() + : null; + + private static string[] ReadCommaSeparatedArray(string str) => + string.IsNullOrEmpty(str) ? Array.Empty() : str.Split(','); + + private static bool ReadBoolean(string str, bool defaultValue = false) => + bool.TryParse(str, out var propertyValue) ? propertyValue : defaultValue; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs new file mode 100644 index 00000000000..1054d7d867b --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs @@ -0,0 +1,102 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.Collections.Concurrent; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace SonarAnalyzer.Helpers; + +internal static class WildcardPatternMatcher +{ + private static readonly ConcurrentDictionary Cache = new(); + + public static bool IsMatch(string pattern, string input, bool timeoutFallbackResult) => + !(string.IsNullOrWhiteSpace(pattern) || string.IsNullOrWhiteSpace(input)) + && Cache.GetOrAdd(pattern, _ => new Regex(ToRegex(pattern), RegexOptions.None, RegexConstants.DefaultTimeout)) is var regex + && IsMatch(regex, input, timeoutFallbackResult); + + private static bool IsMatch(Regex regex, string value, bool timeoutFallbackResult) + { + try + { + return regex.IsMatch(value.Trim('/')); + } + catch (RegexMatchTimeoutException) + { + return timeoutFallbackResult; + } + } + + /// + /// Copied from https://github.com/SonarSource/sonar-plugin-api/blob/a9bd7ff48f0f77811ed909070030678c443c975a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WildcardPattern.java. + /// + private static string ToRegex(string wildcardPattern) + { + var escapedDirectorySeparator = Regex.Escape(Path.DirectorySeparatorChar.ToString()); + var sb = new StringBuilder("^", wildcardPattern.Length); + var i = IsSlash(wildcardPattern[0]) ? 1 : 0; + while (i < wildcardPattern.Length) + { + var ch = wildcardPattern[i]; + if (ch == '*') + { + if (i + 1 < wildcardPattern.Length && wildcardPattern[i + 1] == '*') + { + // Double asterisk - Zero or more directories + if (i + 2 < wildcardPattern.Length && IsSlash(wildcardPattern[i + 2])) + { + sb.Append($"(.*{escapedDirectorySeparator}|)"); + i += 2; + } + else + { + sb.Append(".*"); + i += 1; + } + } + else + { + // Single asterisk - Zero or more characters excluding directory separator + sb.Append($"[^{escapedDirectorySeparator}]*?"); + } + } + else if (ch == '?') + { + // Any single character excluding directory separator + sb.Append($"[^{escapedDirectorySeparator}]"); + } + else if (IsSlash(ch)) + { + sb.Append(escapedDirectorySeparator); + } + else + { + sb.Append(Regex.Escape(ch.ToString())); + } + i++; + } + return sb.Append('$').ToString(); + } + + private static bool IsSlash(char ch) => + ch == '/' || ch == '\\'; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 9ccc48ce48f..c2508a645c2 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -52,19 +52,18 @@ public abstract class UtilityAnalyzerBase : SonarDiagnosticAnalyzer protected void ReadParameters(SonarCompilationStartAnalysisContext context) { - var settings = context.Options.ParseSonarLintXmlSettings(); var outPath = context.ProjectConfiguration().OutPath; // For backward compatibility with S4MSB <= 5.0 if (outPath == null && context.Options.ProjectOutFolderPath() is { } projectOutFolderAdditionalFile) { outPath = projectOutFolderAdditionalFile.GetText().ToString().TrimEnd(); } - if (settings.Any() && !string.IsNullOrEmpty(outPath)) + if (context.Options.SonarLintXml() != null && !string.IsNullOrEmpty(outPath)) { - var language = context.Compilation.Language; - IgnoreHeaderComments = PropertiesHelper.ReadIgnoreHeaderCommentsProperty(settings, language); - AnalyzeGeneratedCode = PropertiesHelper.ReadAnalyzeGeneratedCodeProperty(settings, language); - OutPath = Path.Combine(outPath, language == LanguageNames.CSharp ? "output-cs" : "output-vbnet"); + var sonarLintXml = context.SonarLintXml(); + IgnoreHeaderComments = sonarLintXml.IgnoreHeaderComments(context.Compilation.Language); + AnalyzeGeneratedCode = sonarLintXml.AnalyzeGeneratedCode(context.Compilation.Language); + OutPath = Path.Combine(outPath, context.Compilation.Language == LanguageNames.CSharp ? "output-cs" : "output-vbnet"); IsAnalyzerEnabled = true; IsTestProject = context.IsTestProject(); } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.ShouldAnalyzeTree.cs b/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.ShouldAnalyzeTree.cs index 0d1d18583c9..c667b8898e2 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.ShouldAnalyzeTree.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.ShouldAnalyzeTree.cs @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -using System.IO; using System.Text; using Microsoft.CodeAnalysis.Text; using Moq; @@ -78,7 +77,7 @@ public void ShouldAnalyzeTree_Scanner_UnchangedFiles_ContainsOtherFile() [DataRow(OtherFileName, true)] public void ShouldAnalyzeTree_GeneratedFile_NoSonarLintXml(string fileName, bool expected) { - var sonarLintXml = CreateSonarLintXml(true); + var sonarLintXml = new DummySourceText(AnalysisScaffolding.GenerateSonarLintXmlContent(analyzeGeneratedCode: true)); var (compilation, tree) = CreateDummyCompilation(AnalyzerLanguage.CSharp, fileName); var sut = CreateSut(compilation, CreateOptions(sonarLintXml, @"ResourceTests\Foo.xml")); @@ -89,7 +88,7 @@ public void ShouldAnalyzeTree_GeneratedFile_NoSonarLintXml(string fileName, bool [TestMethod] public void ShouldAnalyzeTree_GeneratedFile_ShouldAnalyzeGeneratedProvider_IsCached() { - var sonarLintXml = CreateSonarLintXml(true); + var sonarLintXml = new DummySourceText(AnalysisScaffolding.GenerateSonarLintXmlContent(analyzeGeneratedCode: true)); var additionalText = new Mock(); additionalText.Setup(x => x.Path).Returns("SonarLint.xml"); additionalText.Setup(x => x.GetText(default)).Returns(sonarLintXml); @@ -129,7 +128,7 @@ public void ShouldAnalyzeTree_GeneratedFile_InvalidSonarLintXml(string fileName, [DataRow(OtherFileName)] public void ShouldAnalyzeTree_GeneratedFile_AnalyzeGenerated_AnalyzeAllFiles(string fileName) { - var sonarLintXml = CreateSonarLintXml(true); + var sonarLintXml = new DummySourceText(AnalysisScaffolding.GenerateSonarLintXmlContent(analyzeGeneratedCode: true)); var (compilation, tree) = CreateDummyCompilation(AnalyzerLanguage.CSharp, fileName); var sut = CreateSut(compilation, CreateOptions(sonarLintXml)); @@ -137,25 +136,24 @@ public void ShouldAnalyzeTree_GeneratedFile_AnalyzeGenerated_AnalyzeAllFiles(str } [DataTestMethod] - [DataRow(GeneratedFileName, false)] - [DataRow(OtherFileName, true)] - public void ShouldAnalyzeTree_CorrectSettingUsed(string fileName, bool expectedCSharp) + [DataRow(GeneratedFileName, LanguageNames.CSharp, false)] + [DataRow(OtherFileName, LanguageNames.CSharp, true)] + [DataRow(GeneratedFileName, LanguageNames.VisualBasic, false)] + [DataRow(OtherFileName, LanguageNames.VisualBasic, true)] + public void ShouldAnalyzeTree_CorrectSettingUsed(string fileName, string language, bool expected) { - var sonarLintXml = CreateSonarLintXml(false); - var (compilationCS, treeCS) = CreateDummyCompilation(AnalyzerLanguage.CSharp, fileName); - var (compilationVB, treeVB) = CreateDummyCompilation(AnalyzerLanguage.VisualBasic, fileName); - var sutCS = CreateSut(compilationCS, CreateOptions(sonarLintXml)); - var sutVB = CreateSut(compilationVB, CreateOptions(sonarLintXml)); - - sutCS.ShouldAnalyzeTree(treeCS, CSharpGeneratedCodeRecognizer.Instance).Should().Be(expectedCSharp); - sutVB.ShouldAnalyzeTree(treeVB, VisualBasicGeneratedCodeRecognizer.Instance).Should().BeTrue(); + var sonarLintXml = new DummySourceText(AnalysisScaffolding.GenerateSonarLintXmlContent(language: language, analyzeGeneratedCode: false)); + var analyzerLanguage = language == LanguageNames.CSharp ? AnalyzerLanguage.CSharp : AnalyzerLanguage.VisualBasic; + var (compilation, tree) = CreateDummyCompilation(analyzerLanguage, fileName); + var sut = CreateSut(compilation, CreateOptions(sonarLintXml)); + GeneratedCodeRecognizer generatedCodeRecognizer = language == LanguageNames.CSharp ? CSharpGeneratedCodeRecognizer.Instance : VisualBasicGeneratedCodeRecognizer.Instance; - sonarLintXml.ToStringCallCount.Should().Be(2, "file should be read once per language"); + sut.ShouldAnalyzeTree(tree, generatedCodeRecognizer).Should().Be(expected); + sonarLintXml.ToStringCallCount.Should().Be(1, "file should be read once per language"); // Read again to check caching - sutVB.ShouldAnalyzeTree(treeVB, VisualBasicGeneratedCodeRecognizer.Instance).Should().BeTrue(); - - sonarLintXml.ToStringCallCount.Should().Be(2, "file should not have been read again"); + sut.ShouldAnalyzeTree(tree, generatedCodeRecognizer).Should().Be(expected); + sonarLintXml.ToStringCallCount.Should().Be(1, "file should not have been read again"); } // Until https://github.com/SonarSource/sonar-dotnet/issues/2228, we were considering a file as generated if the word "generated" was contained inside a region. @@ -329,26 +327,106 @@ public class GenericAttribute : Attribute { } VerifyEmpty("test.cs", sourceCs, new CS.EmptyStatement()); } - private static DummySourceText CreateSonarLintXml(bool analyzeGeneratedCSharp) => - new($""" - - - - - dummy - false - - - sonar.cs.analyzeGeneratedCode - {analyzeGeneratedCSharp.ToString().ToLower()} - - - sonar.vbnet.analyzeGeneratedCode - true - - - - """); + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, false)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, true)] + public void ShouldAnalyzeTree_Exclusions_ReturnExpected(string filePath, string[] exclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, exclusions: exclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, false)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, true)] + public void ShouldAnalyzeTree_GlobalExclusions_ReturnExpected(string filePath, string[] globalExclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, globalExclusions: globalExclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, false)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, true)] + public void ShouldAnalyzeTree_TestExclusions_ReturnExpected(string filePath, string[] testExclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, testExclusions: testExclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, false)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, true)] + public void ShouldAnalyzeTree_GlobalTestExclusions_ReturnExpected(string filePath, string[] globalTestExclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, globalTestExclusions: globalTestExclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, false)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, true)] + public void ShouldAnalyzeTree_Inclusions_ReturnExpected(string filePath, string[] inclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, inclusions: inclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Product, true)] + [DataRow("Foo", new string[] { "Foo" }, ProjectType.Test, true)] + [DataRow("Foo", new string[] { "NotFoo" }, ProjectType.Test, false)] + public void ShouldAnalyzeTree_TestInclusions_ReturnExpected(string filePath, string[] testInclusions, ProjectType projectType, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, projectType, expectedResult, testInclusions: testInclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, new string[] { "Foo" }, false)] + [DataRow("Foo", new string[] { "NotFoo" }, new string[] { "Foo" }, false)] + [DataRow("Foo", new string[] { "Foo" }, new string[] { "NotFoo" }, true)] + [DataRow("Foo", new string[] { "NotFoo" }, new string[] { "NotFoo" }, false)] + public void ShouldAnalyzeTree_MixedInput_ProductProject_ReturnExpected(string filePath, string[] inclusions, string[] exclusions, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, ProjectType.Product, expectedResult, inclusions: inclusions, exclusions: exclusions); + + [DataTestMethod] + [DataRow("Foo", new string[] { "Foo" }, new string[] { "Foo" }, false)] + [DataRow("Foo", new string[] { "NotFoo" }, new string[] { "Foo" }, false)] + [DataRow("Foo", new string[] { "Foo" }, new string[] { "NotFoo" }, true)] + [DataRow("Foo", new string[] { "NotFoo" }, new string[] { "NotFoo" }, false)] + public void ShouldAnalyzeTree_MixedInput_TestProject_ReturnExpected(string filePath, string[] testInclusions, string[] testExclusions, bool expectedResult) => + ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly(filePath, ProjectType.Test, expectedResult, testInclusions: testInclusions, testExclusions: testExclusions); + + private void ShouldAnalyzeTree_WithExclusionInclusionParametersSet_ReturnsTrueForIncludedFilesOnly( + string fileName, + ProjectType projectType, + bool shouldAnalyze, + string language = LanguageNames.CSharp, + string[] exclusions = null, + string[] inclusions = null, + string[] globalExclusions = null, + string[] testExclusions = null, + string[] testInclusions = null, + string[] globalTestExclusions = null) + { + var analyzerLanguage = language == LanguageNames.CSharp ? AnalyzerLanguage.CSharp : AnalyzerLanguage.VisualBasic; + var sonarLintXml = AnalysisScaffolding.CreateSonarLintXml( + TestContext, + language: language, + exclusions: exclusions, + inclusions: inclusions, + globalExclusions: globalExclusions, + testExclusions: testExclusions, + testInclusions: testInclusions, + globalTestExclusions: globalTestExclusions); + var options = AnalysisScaffolding.CreateOptions(sonarLintXml); + + var compilation = SolutionBuilder + .Create() + .AddProject(analyzerLanguage, createExtraEmptyFile: false) + .AddReferences(TestHelper.ProjectTypeReference(projectType)) + .AddSnippet(string.Empty, fileName) + .GetCompilation(); + var tree = compilation.SyntaxTrees.Single(x => x.FilePath.Contains(fileName)); + var sut = CreateSut(compilation, options); + + GeneratedCodeRecognizer codeRecognizer = language == LanguageNames.CSharp ? CSharpGeneratedCodeRecognizer.Instance : VisualBasicGeneratedCodeRecognizer.Instance; + sut.ShouldAnalyzeTree(tree, codeRecognizer).Should().Be(shouldAnalyze); + } private AnalyzerOptions CreateOptions(string[] unchangedFiles) => AnalysisScaffolding.CreateOptions(AnalysisScaffolding.CreateSonarProjectConfigWithUnchangedFiles(TestContext, unchangedFiles)); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.cs index d402e7b82a4..bc44a11477e 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/AnalysisContext/SonarAnalysisContextBaseTest.cs @@ -19,6 +19,7 @@ */ using SonarAnalyzer.AnalysisContext; +using SonarAnalyzer.Common; namespace SonarAnalyzer.UnitTest.AnalysisContext; @@ -131,7 +132,7 @@ public void ProjectConfiguration_WhenFileChanges_RebuildsCache() secondConfig.Should().NotBeSameAs(firstConfig); } - [TestMethod] + [DataTestMethod] [DataRow(null)] [DataRow("/foo/bar/does-not-exit")] [DataRow("/foo/bar/x.xml")] @@ -170,6 +171,98 @@ public void ProjectConfiguration_WhenInvalidXml_ThrowException() .WithMessage("File 'SonarProjectConfig.xml' has been added as an AdditionalFile but could not be read and parsed."); } + [DataTestMethod] + [DataRow("cs")] + [DataRow("vbnet")] + public void SonarLintFile_LoadsExpectedValues(string language) + { + var analyzerLanguage = language == "cs" ? AnalyzerLanguage.CSharp : AnalyzerLanguage.VisualBasic; + var (compilation, _) = CreateDummyCompilation(analyzerLanguage, "ExtraEmptyFile"); + var options = AnalysisScaffolding.CreateOptions($"ResourceTests\\SonarLintXml\\All_properties_{language}\\SonarLint.xml"); + var sut = CreateSut(compilation, options).SonarLintXml(); + + sut.IgnoreHeaderComments(analyzerLanguage.LanguageName).Should().BeTrue(); + sut.AnalyzeGeneratedCode(analyzerLanguage.LanguageName).Should().BeFalse(); + AssertArrayContent(sut.Exclusions, nameof(sut.Exclusions)); + AssertArrayContent(sut.Inclusions, nameof(sut.Inclusions)); + AssertArrayContent(sut.GlobalExclusions, nameof(sut.GlobalExclusions)); + AssertArrayContent(sut.TestExclusions, nameof(sut.TestExclusions)); + AssertArrayContent(sut.TestInclusions, nameof(sut.TestInclusions)); + AssertArrayContent(sut.GlobalTestExclusions, nameof(sut.GlobalTestExclusions)); + + static void AssertArrayContent(string[] array, string folder) + { + array.Should().HaveCount(2); + array[0].Should().BeEquivalentTo($"Fake/{folder}/**/*"); + array[1].Should().BeEquivalentTo($"Fake/{folder}/Second*/**/*"); + } + } + + [TestMethod] + public void SonarLintFile_UsesCachedValue() + { + var options = AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"); + var firstSut = CreateSut(options); + var secondSut = CreateSut(options); + var firstFile = firstSut.SonarLintXml(); + var secondFile = secondSut.SonarLintXml(); + + secondFile.Should().BeSameAs(firstFile); + } + + [TestMethod] + public void SonarLintFile_WhenFileChanges_RebuildsCache() + { + var firstOptions = AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"); + var secondOptions = AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLintXml\\All_properties_vbnet\\SonarLint.xml"); + var firstFile = CreateSut(firstOptions).SonarLintXml(); + var secondFile = CreateSut(secondOptions).SonarLintXml(); + + secondFile.Should().NotBeSameAs(firstFile); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("\\foo\\bar\\does-not-exit")] + [DataRow("\\foo\\bar\\x.xml")] + [DataRow("path//aSonarLint.xml")] // different name + [DataRow("path//SonarLint.xmla")] // different extension + public void SonarLintFile_WhenAdditionalFileNotPresent_ReturnsDefaultValues(string folder) + { + var sut = CreateSut(AnalysisScaffolding.CreateOptions(folder)).SonarLintXml(); + CheckSonarLintXmlDefaultValues(sut); + } + + [TestMethod] + public void SonarLintFile_WhenInvalidXml_ReturnsDefaultValues() + { + var sut = CreateSut(AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLintXml\\Invalid_Xml\\SonarLint.xml")).SonarLintXml(); + CheckSonarLintXmlDefaultValues(sut); + } + + [TestMethod] + public void SonarLintFile_WhenFileIsMissing_ThrowException() + { + var sut = CreateSut(AnalysisScaffolding.CreateOptions("ThisPathDoesNotExist\\SonarLint.xml")); + + sut.Invoking(x => x.SonarLintXml()) + .Should() + .Throw() + .WithMessage("File 'SonarLint.xml' has been added as an AdditionalFile but could not be read and parsed."); + } + + private static void CheckSonarLintXmlDefaultValues(SonarLintXmlReader sut) + { + sut.AnalyzeGeneratedCode(LanguageNames.CSharp).Should().BeFalse(); + sut.IgnoreHeaderComments(LanguageNames.CSharp).Should().BeFalse(); + sut.Exclusions.Should().NotBeNull().And.HaveCount(0); + sut.Inclusions.Should().NotBeNull().And.HaveCount(0); + sut.GlobalExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestInclusions.Should().NotBeNull().And.HaveCount(0); + sut.GlobalTestExclusions.Should().NotBeNull().And.HaveCount(0); + } + private SonarCompilationReportingContext CreateSut(ProjectType projectType, bool isScannerRun) => CreateSut(AnalysisScaffolding.CreateOptions(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, projectType, isScannerRun))); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Common/UnexpectedLanguageExceptionTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Common/UnexpectedLanguageExceptionTest.cs index 99c548a1acf..166d08a855c 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Common/UnexpectedLanguageExceptionTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Common/UnexpectedLanguageExceptionTest.cs @@ -25,6 +25,10 @@ namespace SonarAnalyzer.UnitTest.Common [TestClass] public class UnexpectedLanguageExceptionTest { + [TestMethod] + public void Message_String_Ctor() => + new UnexpectedLanguageException("F#").Message.Should().Be("Unexpected language: F#"); + [TestMethod] public void Message_CS() => new UnexpectedLanguageException(AnalyzerLanguage.CSharp).Message.Should().Be("Unexpected language: C#"); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/ParameterLoaderTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/ParameterLoaderTest.cs index 646ceb2fb34..5d599cd21c9 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/ParameterLoaderTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/ParameterLoaderTest.cs @@ -20,6 +20,8 @@ using System.IO; using Microsoft.CodeAnalysis.Text; +using SonarAnalyzer.AnalysisContext; +using SonarAnalyzer.Common; using SonarAnalyzer.Rules.CSharp; namespace SonarAnalyzer.UnitTest.Helpers @@ -27,17 +29,19 @@ namespace SonarAnalyzer.UnitTest.Helpers [TestClass] public class ParameterLoaderTest { + public TestContext TestContext { get; set; } + [TestMethod] [DataRow("path//aSonarLint.xml")] // different name [DataRow("path//SonarLint.xmla")] // different extension - public void SetParameterValues_WhenNoSonarLintIsGiven_DoesNotPopulateParameters(string filePath) + public void SetParameterValues_WithInvalidSonarLintPath_DoesNotPopulateParameters(string filePath) { // Arrange - var options = AnalysisScaffolding.CreateOptions(filePath, SourceText.From(File.ReadAllText("ResourceTests\\SonarLint.xml"))); + var compilation = CreateCompilationWithOption(filePath, SourceText.From(File.ReadAllText("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"))); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(3); // Default value @@ -46,144 +50,117 @@ public void SetParameterValues_WhenNoSonarLintIsGiven_DoesNotPopulateParameters( [TestMethod] [DataRow("a/SonarLint.xml")] // unix path [DataRow("a\\SonarLint.xml")] - public void SetParameterValues_WhenGivenValidSonarLintFilePath_PopulatesProperties(string filePath) + public void SetParameterValues_WithValidSonarLintPath_PopulatesProperties(string filePath) { // Arrange - var options = AnalysisScaffolding.CreateOptions(filePath, SourceText.From(File.ReadAllText("ResourceTests\\SonarLint.xml"))); + var compilation = CreateCompilationWithOption(filePath, SourceText.From(File.ReadAllText("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"))); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(1); // Value from the xml file } [TestMethod] - public void SetParameterValues_WhenGivenSonarLintFileHasIntParameterType_PopulatesProperties() + public void SetParameterValues_SonarLintFileWithIntParameterType_PopulatesProperties() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLint.xml"); + var compilation = CreateCompilationWithOption("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(1); // Value from the xml file } [TestMethod] - public void SetParameterValues_WhenGivenSonarLintFileHasStringParameterType_OnlyOneParameter_PopulatesProperty() + public void SetParameterValues_SonarLintFileWithStringParameterType_PopulatesProperty() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\RuleWithStringParameter\\SonarLint.xml"); + var parameterValue = "1"; + var filePath = GenerateSonarLintXmlWithParametrizedRule("S2342", "flagsAttributeFormat", parameterValue); + var compilation = CreateCompilationWithOption(filePath); var analyzer = new EnumNameShouldFollowRegex(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert - analyzer.FlagsEnumNamePattern.Should().Be("1"); // value from XML file + analyzer.FlagsEnumNamePattern.Should().Be(parameterValue); // value from XML file } [TestMethod] - public void SetParameterValues_WhenGivenSonarLintFileHasBooleanParameterType_OnlyOneParameter_PopulatesProperty() + public void SetParameterValues_SonarLintFileWithBooleanParameterType_PopulatesProperty() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\RuleWithBooleanParameter\\SonarLint.xml"); + var parameterValue = true; + var filePath = GenerateSonarLintXmlWithParametrizedRule("S1451", "isRegularExpression", parameterValue.ToString()); + var compilation = CreateCompilationWithOption(filePath); var analyzer = new CheckFileLicense(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert - analyzer.IsRegularExpression.Should().BeTrue(); // value from XML file + analyzer.IsRegularExpression.Should().Be(parameterValue); // value from XML file } [TestMethod] - public void SetParameterValues_WhenGivenValidSonarLintFileAndDoesNotContainAnalyzerParameters_DoesNotPopulateProperties() + public void SetParameterValues_SonarLintFileWithoutRuleParameters_DoesNotPopulateProperties() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\SonarLint.xml"); + var compilation = CreateCompilationWithOption("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"); var analyzer = new LineLength(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(200); // Default value } - [TestMethod] - public void SetParameterValues_WithNonExistentPath_UsesInMemoryText() - { - // Arrange - const string fakeSonarLintXmlFilePath = "ThisPathDoesNotExist\\SonarLint.xml"; - const string sonarLintXmlContent = @" - - - - - S1067 - - - max - 1 - - - - - - -"; - - var options = AnalysisScaffolding.CreateOptions(fakeSonarLintXmlFilePath, SourceText.From(sonarLintXmlContent)); - var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. - - // Act - ParameterLoader.SetParameterValues(analyzer, options); - - // Assert - analyzer.Maximum.Should().Be(1); // In-memory value - } - [TestMethod] public void SetParameterValues_CalledTwiceAfterChangeInConfigFile_UpdatesProperties() { // Arrange - const string fakeSonarLintXmlFilePath = "ThisPathDoesNotExist\\SonarLint.xml"; - const string originalSonarLintXmlContent = @" - - - - - S1067 - - - max - 1 - - - - - - -"; - - var options = AnalysisScaffolding.CreateOptions(fakeSonarLintXmlFilePath, SourceText.From(originalSonarLintXmlContent)); + var maxValue = 1; + var ruleParameters = new List() + { + new SonarLintXmlRule() + { + Key = "S1067", + Parameters = new List() + { + new SonarLintXmlKeyValuePair() + { + Key = "max", + Value = maxValue.ToString() + } + } + } + }; + var sonarLintXml = AnalysisScaffolding.GenerateSonarLintXmlContent(rulesParameters: ruleParameters); + var filePath = TestHelper.WriteFile(TestContext, "SonarLint.xml", sonarLintXml); + var compilation = CreateCompilationWithOption(filePath); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); - analyzer.Maximum.Should().Be(1); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); + analyzer.Maximum.Should().Be(maxValue); // Modify the in-memory additional file - var modifiedSonarLintXmlContent = originalSonarLintXmlContent.Replace("1", "42"); - var modifiedOptions = AnalysisScaffolding.CreateOptions(fakeSonarLintXmlFilePath, SourceText.From(modifiedSonarLintXmlContent)); - - ParameterLoader.SetParameterValues(analyzer, modifiedOptions); - analyzer.Maximum.Should().Be(42); + maxValue = 42; + ruleParameters.First().Parameters.First().Value = maxValue.ToString(); + var modifiedSonarLintXml = AnalysisScaffolding.GenerateSonarLintXmlContent(rulesParameters: ruleParameters); + var modifiedFilePath = TestHelper.WriteFile(TestContext, "SonarLint.xml", modifiedSonarLintXml); + compilation = CreateCompilationWithOption(modifiedFilePath); + + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); + analyzer.Maximum.Should().Be(maxValue); } [TestMethod] @@ -193,42 +170,76 @@ public void SetParameterValues_CalledTwiceAfterChangeInConfigFile_UpdatesPropert public void SetParameterValues_WithMalformedXml_DoesNotPopulateProperties(string sonarLintXmlContent) { // Arrange - var options = AnalysisScaffolding.CreateOptions("fakePath\\SonarLint.xml", SourceText.From(sonarLintXmlContent)); + var compilation = CreateCompilationWithOption("fakePath\\SonarLint.xml", SourceText.From(sonarLintXmlContent)); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(3); // Default value } [TestMethod] - public void SetParameterValues_WithWrongPropertyType_StringInsteadOfInt_DoesNotPopulateProperties() + public void SetParameterValues_SonarLintFileWithStringInsteadOfIntParameterType_PopulatesProperty() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\StringInsteadOfInt\\SonarLint.xml"); + var parameterValue = "fooBar"; + var filePath = GenerateSonarLintXmlWithParametrizedRule("S1067", "max", parameterValue); + var compilation = CreateCompilationWithOption(filePath); var analyzer = new ExpressionComplexity(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.Maximum.Should().Be(3); // Default value } [TestMethod] - public void SetParameterValues_WithWrongPropertyType_StringInsteadOfBoolean_DoesNotPopulateProperties() + public void SetParameterValues_SonarLintFileWithStringInsteadOfBooleanParameterType_PopulatesProperty() { // Arrange - var options = AnalysisScaffolding.CreateOptions("ResourceTests\\StringInsteadOfBoolean\\SonarLint.xml"); + var parameterValue = "fooBar"; + var filePath = GenerateSonarLintXmlWithParametrizedRule("S1451", "isRegularExpression", parameterValue); + var compilation = CreateCompilationWithOption(filePath); var analyzer = new CheckFileLicense(); // Cannot use mock because we use reflection to find properties. // Act - ParameterLoader.SetParameterValues(analyzer, options); + ParameterLoader.SetParameterValues(analyzer, compilation.SonarLintXml()); // Assert analyzer.IsRegularExpression.Should().BeFalse(); // Default value } + + private static SonarCompilationReportingContext CreateCompilationWithOption(string filePath, SourceText text = null) + { + var options = text is null + ? AnalysisScaffolding.CreateOptions(filePath) + : AnalysisScaffolding.CreateOptions(filePath, text); + var compilation = SolutionBuilder.Create().AddProject(AnalyzerLanguage.CSharp).GetCompilation(); + var compilationContext = new CompilationAnalysisContext(compilation, options, _ => { }, _ => true, default); + return new(AnalysisScaffolding.CreateSonarAnalysisContext(), compilationContext); + } + + private string GenerateSonarLintXmlWithParametrizedRule(string ruleId, string key, string value) + { + var ruleParameters = new List() + { + new SonarLintXmlRule() + { + Key = ruleId, + Parameters = new List() + { + new SonarLintXmlKeyValuePair() + { + Key = key, + Value = value + } + } + } + }; + return AnalysisScaffolding.CreateSonarLintXml(TestContext, rulesParameters: ruleParameters); + } } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/PropertiesHelperTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/PropertiesHelperTest.cs deleted file mode 100644 index acb23ca9d43..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/PropertiesHelperTest.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarAnalyzer for .NET - * Copyright (C) 2015-2023 SonarSource SA - * mailto: contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -using System.IO; -using Microsoft.CodeAnalysis.Text; -using SonarAnalyzer.Extensions; - -namespace SonarAnalyzer.UnitTest.Helpers -{ - [TestClass] - public class PropertiesHelperTest - { - [TestMethod] - [DataRow("a/SonarLint.xml")] // unix path - [DataRow("a\\SonarLint.xml")] - public void ShouldAnalyzeGeneratedCode_WithTrueSetting_ReturnsTrue(string filePath) => - GetSetting(SourceText.From(File.ReadAllText("ResourceTests\\AnalyzeGeneratedTrue\\SonarLint.xml")), filePath).Should().BeTrue(); - - [TestMethod] - public void ShouldAnalyzeGeneratedCode_WithFalseSetting_ReturnsFalse() => - GetSetting(SourceText.From(File.ReadAllText("ResourceTests\\AnalyzeGeneratedFalse\\SonarLint.xml"))).Should().BeFalse(); - - [TestMethod] - public void ShouldAnalyzeGeneratedCode_WithNoSetting_ReturnsFalse() => - GetSetting(SourceText.From(File.ReadAllText("ResourceTests\\NoSettings\\SonarLint.xml"))).Should().BeFalse(); - - [TestMethod] - [DataRow("")] - [DataRow("this is not an xml")] - [DataRow(@"")] - public void ShouldAnalyzeGeneratedCode_WithMalformedXml_ReturnsFalse(string sonarLintXmlContent) => - GetSetting(SourceText.From(sonarLintXmlContent)).Should().BeFalse(); - - [TestMethod] - public void ShouldAnalyzeGeneratedCode_WithNotBooleanValue_ReturnsFalse() => - GetSetting(SourceText.From(File.ReadAllText("ResourceTests\\NotBoolean\\SonarLint.xml"))).Should().BeFalse(); - - [TestMethod] - [DataRow("path//aSonarLint.xml")] // different name - [DataRow("path//SonarLint.xmla")] // different extension - public void ShouldAnalyzeGeneratedCode_NonSonarLintXmlPath_ReturnsFalse(string filePath) => - GetSetting(SourceText.From(File.ReadAllText("ResourceTests\\AnalyzeGeneratedTrue\\SonarLint.xml")), filePath).Should().BeFalse(); - - private static bool GetSetting(SourceText text, string path = "fakePath\\SonarLint.xml") - { - var options = AnalysisScaffolding.CreateOptions(path, text); - return PropertiesHelper.ReadAnalyzeGeneratedCodeProperty(options.ParseSonarLintXmlSettings(), LanguageNames.CSharp); - } - } -} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlReaderTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlReaderTest.cs new file mode 100644 index 00000000000..5caa7a84e9e --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlReaderTest.cs @@ -0,0 +1,134 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using Microsoft.CodeAnalysis.Text; +using SonarAnalyzer.Common; + +namespace SonarAnalyzer.UnitTest.Helpers; + +[TestClass] +public class SonarLintXmlReaderTest +{ + [DataTestMethod] + [DataRow(LanguageNames.CSharp, "cs")] + [DataRow(LanguageNames.VisualBasic, "vbnet")] + public void SonarLintXmlReader_WhenAllValuesAreSet_ExpectedValues(string language, string xmlLanguageName) + { + var sut = CreateSonarLintXmlReader($"ResourceTests\\SonarLintXml\\All_Properties_{xmlLanguageName}\\SonarLint.xml"); + sut.IgnoreHeaderComments(language).Should().BeTrue(); + sut.AnalyzeGeneratedCode(language).Should().BeFalse(); + AssertArrayContent(sut.Exclusions, nameof(sut.Exclusions)); + AssertArrayContent(sut.Inclusions, nameof(sut.Inclusions)); + AssertArrayContent(sut.GlobalExclusions, nameof(sut.GlobalExclusions)); + AssertArrayContent(sut.TestExclusions, nameof(sut.TestExclusions)); + AssertArrayContent(sut.TestInclusions, nameof(sut.TestInclusions)); + AssertArrayContent(sut.GlobalTestExclusions, nameof(sut.GlobalTestExclusions)); + + sut.ParametrizedRules.Should().HaveCount(2); + var rule = sut.ParametrizedRules.First(x => x.Key.Equals("S2342")); + rule.Parameters[0].Key.Should().Be("format"); + rule.Parameters[0].Value.Should().Be("^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"); + rule.Parameters[1].Key.Should().Be("flagsAttributeFormat"); + rule.Parameters[1].Value.Should().Be("^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"); + + static void AssertArrayContent(string[] array, string folder) + { + array.Should().HaveCount(2); + array[0].Should().BeEquivalentTo($"Fake/{folder}/**/*"); + array[1].Should().BeEquivalentTo($"Fake/{folder}/Second*/**/*"); + } + } + + [TestMethod] + public void SonarLintXmlReader_PartiallyMissingProperties_ExpectedAndDefaultValues() + { + var sut = CreateSonarLintXmlReader("ResourceTests\\SonarLintXml\\Partially_missing_properties\\SonarLint.xml"); + sut.IgnoreHeaderComments(LanguageNames.CSharp).Should().BeFalse(); + sut.AnalyzeGeneratedCode(LanguageNames.CSharp).Should().BeTrue(); + AssertArrayContent(sut.Exclusions, nameof(sut.Exclusions)); + AssertArrayContent(sut.Inclusions, nameof(sut.Inclusions)); + sut.GlobalExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestInclusions.Should().NotBeNull().And.HaveCount(0); + sut.GlobalTestExclusions.Should().NotBeNull().And.HaveCount(0); + sut.ParametrizedRules.Should().NotBeNull().And.HaveCount(0); + } + + [TestMethod] + public void SonarLintXmlReader_PropertiesCSharpTrueVBNetFalse_ExpectedValues() + { + var sut = CreateSonarLintXmlReader("ResourceTests\\SonarLintXml\\PropertiesCSharpTrueVbnetFalse\\SonarLint.xml"); + sut.IgnoreHeaderComments(LanguageNames.CSharp).Should().BeTrue(); + sut.IgnoreHeaderComments(LanguageNames.VisualBasic).Should().BeFalse(); + sut.AnalyzeGeneratedCode(LanguageNames.CSharp).Should().BeTrue(); + sut.AnalyzeGeneratedCode(LanguageNames.VisualBasic).Should().BeFalse(); + } + + [DataTestMethod] + [DataRow("")] + [DataRow("this is not an xml")] + [DataRow(@"")] + public void SonarLintXmlReader_WithMalformedXml_DefaultBehaviour(string sonarLintXmlContent) => + CheckSonarLintXmlReaderDefaultValues(new SonarLintXmlReader(SourceText.From(sonarLintXmlContent))); + + [TestMethod] + public void SonarLintXmlReader_MissingProperties_DefaultBehaviour() => + CheckSonarLintXmlReaderDefaultValues(CreateSonarLintXmlReader("ResourceTests\\SonarLintXml\\Missing_properties\\SonarLint.xml")); + + [TestMethod] + public void SonarLintXmlReader_WithIncorrectValueType_DefaultBehaviour() => + CheckSonarLintXmlReaderDefaultValues(CreateSonarLintXmlReader("ResourceTests\\SonarLintXml\\Incorrect_value_type\\SonarLint.xml")); + + [TestMethod] + public void SonarLintXmlReader_CheckEmpty_DefaultBehaviour() => + CheckSonarLintXmlReaderDefaultValues(SonarLintXmlReader.Empty); + + [TestMethod] + public void SonarLintXmlReader_LanguageDoesNotExist_Throws() + { + var sut = CreateSonarLintXmlReader($"ResourceTests\\SonarLintXml\\All_Properties_cs\\SonarLint.xml"); + sut.Invoking(x => x.IgnoreHeaderComments(LanguageNames.FSharp)).Should().Throw().WithMessage("Unexpected language: F#"); + sut.Invoking(x => x.AnalyzeGeneratedCode(LanguageNames.FSharp)).Should().Throw().WithMessage("Unexpected language: F#"); + } + + private static void CheckSonarLintXmlReaderDefaultValues(SonarLintXmlReader sut) + { + sut.AnalyzeGeneratedCode(LanguageNames.CSharp).Should().BeFalse(); + sut.IgnoreHeaderComments(LanguageNames.CSharp).Should().BeFalse(); + sut.Exclusions.Should().NotBeNull().And.HaveCount(0); + sut.Inclusions.Should().NotBeNull().And.HaveCount(0); + sut.GlobalExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestExclusions.Should().NotBeNull().And.HaveCount(0); + sut.TestInclusions.Should().NotBeNull().And.HaveCount(0); + sut.GlobalTestExclusions.Should().NotBeNull().And.HaveCount(0); + sut.ParametrizedRules.Should().NotBeNull().And.HaveCount(0); + } + + private static void AssertArrayContent(string[] array, string folder) + { + array.Should().HaveCount(2); + array[0].Should().BeEquivalentTo($"Fake/{folder}/**/*"); + array[1].Should().BeEquivalentTo($"Fake/{folder}/Second*/**/*"); + } + + private static SonarLintXmlReader CreateSonarLintXmlReader(string relativePath) => + new(SourceText.From(File.ReadAllText(relativePath))); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlTest.cs new file mode 100644 index 00000000000..47c834dbe53 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SonarLintXmlTest.cs @@ -0,0 +1,82 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using System.Xml.Serialization; + +namespace SonarAnalyzer.UnitTest.Helpers; + +[TestClass] +public class SonarLintXmlTest +{ + [TestMethod] + public void SonarLintXml_DeserializeFile_ExpectedValues() + { + var deserializer = new XmlSerializer(typeof(SonarLintXml)); + using TextReader textReader = new StreamReader("ResourceTests\\SonarLintXml\\All_properties_cs\\SonarLint.xml"); + var sonarLintXml = (SonarLintXml)deserializer.Deserialize(textReader); + + AssertSettings(sonarLintXml.Settings); + AssertRules(sonarLintXml.Rules); + } + + private static void AssertSettings(List settings) + { + settings.Should().HaveCount(10); + + AssertKeyValuePair(settings[0], "sonar.cs.ignoreHeaderComments", "true"); + AssertKeyValuePair(settings[1], "sonar.cs.analyzeGeneratedCode", "false"); + AssertKeyValuePair(settings[2], "sonar.cs.file.suffixes", ".cs"); + AssertKeyValuePair(settings[3], "sonar.cs.roslyn.ignoreIssues", "false"); + AssertKeyValuePair(settings[4], "sonar.exclusions", "Fake/Exclusions/**/*,Fake/Exclusions/Second*/**/*"); + AssertKeyValuePair(settings[5], "sonar.inclusions", "Fake/Inclusions/**/*,Fake/Inclusions/Second*/**/*"); + AssertKeyValuePair(settings[6], "sonar.global.exclusions", "Fake/GlobalExclusions/**/*,Fake/GlobalExclusions/Second*/**/*"); + AssertKeyValuePair(settings[7], "sonar.test.exclusions", "Fake/TestExclusions/**/*,Fake/TestExclusions/Second*/**/*"); + AssertKeyValuePair(settings[8], "sonar.test.inclusions", "Fake/TestInclusions/**/*,Fake/TestInclusions/Second*/**/*"); + AssertKeyValuePair(settings[9], "sonar.global.test.exclusions", "Fake/GlobalTestExclusions/**/*,Fake/GlobalTestExclusions/Second*/**/*"); + } + + private static void AssertRules(List rules) + { + rules.Should().HaveCount(4); + rules.Where(x => x.Parameters.Any()).Should().HaveCount(2); + + rules[0].Key.Should().BeEquivalentTo("S2225"); + rules[0].Parameters.Should().BeEmpty(); + + rules[1].Key.Should().BeEquivalentTo("S2342"); + rules[1].Parameters.Should().HaveCount(2); + AssertKeyValuePair(rules[1].Parameters[0], "format", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$"); + AssertKeyValuePair(rules[1].Parameters[1], "flagsAttributeFormat", "^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$"); + + rules[2].Key.Should().BeEquivalentTo("S2346"); + rules[2].Parameters.Should().BeEmpty(); + + rules[3].Key.Should().BeEquivalentTo("S1067"); + rules[3].Parameters.Should().HaveCount(1); + AssertKeyValuePair(rules[3].Parameters[0], "max", "1"); + } + + private static void AssertKeyValuePair(SonarLintXmlKeyValuePair pair, string expectedKey, string expectedValue) + { + pair.Key.Should().BeEquivalentTo(expectedKey); + pair.Value.Should().BeEquivalentTo(expectedValue); + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs new file mode 100644 index 00000000000..79273d566d2 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs @@ -0,0 +1,108 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; + +namespace SonarAnalyzer.UnitTest.Helpers +{ + [TestClass] + public class WildcardPatternMatcherTest + { + /// + /// Based on https://github.com/SonarSource/sonar-plugin-api/blob/master/plugin-api/src/test/java/org/sonar/api/utils/WildcardPatternTest.java. + /// + [DataTestMethod] + [DataRow("Foo", "Foo", true)] + [DataRow("foo", "FOO", false)] + [DataRow("Foo", "Foot", false)] + [DataRow("Foo", "Bar", false)] + [DataRow("org/T?st.cs", "org/Test.cs", true)] + [DataRow("org/T?st.cs", "org/Tost.cs", true)] + [DataRow("org/T?st.cs", "org/Teeest.cs", false)] + [DataRow("org/*.cs", "org/Foo.cs", true)] + [DataRow("org/*.cs", "org/Bar.cs", true)] + [DataRow("org/**", "org/Foo.cs", true)] + [DataRow("org/**", "org/foo/bar.jsp", true)] + [DataRow("org/**/Test.cs", "org/Test.cs", true)] + [DataRow("org/**/Test.cs", "org/foo/Test.cs", true)] + [DataRow("org/**/Test.cs", "org/foo/bar/Test.cs", true)] + [DataRow("org/**/*.cs", "org/Foo.cs", true)] + [DataRow("org/**/*.cs", "org/foo/Bar.cs", true)] + [DataRow("org/**/*.cs", "org/foo/bar/Baz.cs", true)] + [DataRow("o?/**/*.cs", "org/test.cs", false)] + [DataRow("o?/**/*.cs", "o/test.cs", false)] + [DataRow("o?/**/*.cs", "og/test.cs", true)] + [DataRow("o?/**/*.cs", "og/foo/bar/test.cs", true)] + [DataRow("o?/**/*.cs", "og/foo/bar/test.c", false)] + [DataRow("org/sonar/**", "org/sonar/commons/Foo", true)] + [DataRow("org/sonar/**", "org/sonar/Foo.cs", true)] + [DataRow("xxx/org/sonar/**", "org/sonar/Foo", false)] + [DataRow("org/sonar/**/**", "org/sonar/commons/Foo", true)] + [DataRow("org/sonar/**/**", "org/sonar/commons/sub/Foo.cs", true)] + [DataRow("org/sonar/**/Foo", "org/sonar/commons/sub/Foo", true)] + [DataRow("org/sonar/**/Foo", "org/sonar/Foo", true)] + [DataRow("*/foo/*", "org/foo/Bar", true)] + [DataRow("*/foo/*", "foo/Bar", false)] + [DataRow("*/foo/*", "foo", false)] + [DataRow("*/foo/*", "org/foo/bar/Hello", false)] + [DataRow("hell?", "hell", false)] + [DataRow("hell?", "hello", true)] + [DataRow("hell?", "helloworld", false)] + [DataRow("**/Reader", "java/io/Reader", true)] + [DataRow("**/Reader", "org/sonar/channel/CodeReader", false)] + [DataRow("**", "java/io/Reader", true)] + [DataRow("**/app/**", "com/app/Utils", true)] + [DataRow("**/app/**", "com/application/MyService", false)] + [DataRow("**/*$*", "foo/bar", false)] + [DataRow("**/*$*", "foo/bar$baz", true)] + [DataRow("a+", "aa", false)] + [DataRow("a+", "a+", true)] + [DataRow("[ab]", "a", false)] + [DataRow("[ab]", "[ab]", true)] + [DataRow("\\n", "\n", false)] + [DataRow("foo\\bar", "foo/bar", true)] + [DataRow("/foo", "foo", true)] + [DataRow("\\foo", "foo", true)] + [DataRow("foo\\bar", "foo\\bar", true)] + [DataRow("foo/bar", "foo\\bar", true)] + [DataRow("foo\\bar/baz", "foo\\bar\\baz", true)] + public void IsMatch_MatchesPatternsAsExpected(string pattern, string input, bool expectedResult) + { + // The test cases are copied from the plugin-api and the directory separators need replacing as Roslyn will not give us the paths with '/'. + input = input.Replace("/", Path.DirectorySeparatorChar.ToString()); + + WildcardPatternMatcher.IsMatch(pattern, input, false).Should().Be(expectedResult); + } + + [DataTestMethod] + [DataRow("")] + [DataRow(" ")] + [DataRow("/")] + [DataRow("\\")] + public void IsMatch_InvalidPattern_ReturnsFalse(string pattern) => + WildcardPatternMatcher.IsMatch(pattern, "foo", false).Should().BeFalse(); + + [DataTestMethod] + [DataRow(null, "foo")] + [DataRow("foo", null)] + public void IsMatch_InputParametersArenull_DoesNotThrow(string pattern, string input) => + WildcardPatternMatcher.IsMatch(pattern, input, false).Should().BeFalse(); + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalse/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalse/SonarLint.xml deleted file mode 100644 index 06ff5bc2533..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalse/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.cs.analyzeGeneratedCode - false - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalseVbnet/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalseVbnet/SonarLint.xml deleted file mode 100644 index a3e4d99a79d..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedFalseVbnet/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.vbnet.analyzeGeneratedCode - false - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrue/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrue/SonarLint.xml deleted file mode 100644 index 058877ce0c4..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrue/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.cs.analyzeGeneratedCode - true - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrueVbnet/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrueVbnet/SonarLint.xml deleted file mode 100644 index 60d17ee38f1..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/AnalyzeGeneratedTrueVbnet/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.vbnet.analyzeGeneratedCode - true - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseCSharp/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseCSharp/SonarLint.xml deleted file mode 100644 index 4656ffb30d8..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseCSharp/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - false - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseVbnet/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseVbnet/SonarLint.xml deleted file mode 100644 index 19364407ace..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsFalseVbnet/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.vbnet.ignoreHeaderComments - false - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueCSharp/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueCSharp/SonarLint.xml deleted file mode 100644 index b9a6871be55..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueCSharp/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueVbnet/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueVbnet/SonarLint.xml deleted file mode 100644 index 89a7cfce399..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/IgnoreHeaderCommentsTrueVbnet/SonarLint.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - sonar.vbnet.ignoreHeaderComments - true - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NoSettings/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NoSettings/SonarLint.xml deleted file mode 100644 index 7a00c312796..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NoSettings/SonarLint.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - S1067 - - - max - 1 - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NotBoolean/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NotBoolean/SonarLint.xml deleted file mode 100644 index fd29e9c1f0a..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/NotBoolean/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - not-boolean-value - - - sonar.cs.analyzeGeneratedCode - not-boolean-value - - - sonar.cs.file.suffixes - .cs - - - - - S1067 - - - max - 1 - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithBooleanParameter/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithBooleanParameter/SonarLint.xml deleted file mode 100644 index 4170e60a90a..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithBooleanParameter/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S1451 - - - isRegularExpression - true - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithStringParameter/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithStringParameter/SonarLint.xml deleted file mode 100644 index d45ac7addb5..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/RuleWithStringParameter/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S2342 - - - flagsAttributeFormat - 1 - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLint.xml deleted file mode 100644 index b99df9c60f6..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S1067 - - - max - 1 - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_cs/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_cs/SonarLint.xml new file mode 100644 index 00000000000..54a1ed9e464 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_cs/SonarLint.xml @@ -0,0 +1,77 @@ + + + + + sonar.cs.ignoreHeaderComments + true + + + sonar.cs.analyzeGeneratedCode + false + + + sonar.cs.file.suffixes + .cs + + + sonar.cs.roslyn.ignoreIssues + false + + + sonar.exclusions + Fake/Exclusions/**/*,Fake/Exclusions/Second*/**/* + + + sonar.inclusions + Fake/Inclusions/**/*,Fake/Inclusions/Second*/**/* + + + sonar.global.exclusions + Fake/GlobalExclusions/**/*,Fake/GlobalExclusions/Second*/**/* + + + sonar.test.exclusions + Fake/TestExclusions/**/*,Fake/TestExclusions/Second*/**/* + + + sonar.test.inclusions + Fake/TestInclusions/**/*,Fake/TestInclusions/Second*/**/* + + + sonar.global.test.exclusions + Fake/GlobalTestExclusions/**/*,Fake/GlobalTestExclusions/Second*/**/* + + + + + S2225 + + + S2342 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + + + S2346 + + + S1067 + + + max + 1 + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_vbnet/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_vbnet/SonarLint.xml new file mode 100644 index 00000000000..270c5c95dd8 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/All_properties_vbnet/SonarLint.xml @@ -0,0 +1,81 @@ + + + + + sonar.vbnet.ignoreHeaderComments + true + + + sonar.vbnet.analyzeGeneratedCode + false + + + sonar.vbnet.file.suffixes + .vb + + + sonar.vbnet.roslyn.ignoreIssues + false + + + sonar.exclusions + Fake/Exclusions/**/*,Fake/Exclusions/Second*/**/* + + + sonar.inclusions + Fake/Inclusions/**/*,Fake/Inclusions/Second*/**/* + + + sonar.global.exclusions + Fake/GlobalExclusions/**/*,Fake/GlobalExclusions/Second*/**/* + + + sonar.test.exclusions + Fake/TestExclusions/**/*,Fake/TestExclusions/Second*/**/* + + + sonar.test.inclusions + Fake/TestInclusions/**/*,Fake/TestInclusions/Second*/**/* + + + sonar.global.test.exclusions + Fake/GlobalTestExclusions/**/*,Fake/GlobalTestExclusions/Second*/**/* + + + + + S2225 + + + S2342 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + + + S2115 + + + S3776 + + + threshold + 15 + + + propertyThreshold + 3 + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Incorrect_value_type/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Incorrect_value_type/SonarLint.xml new file mode 100644 index 00000000000..ff59ba614a5 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Incorrect_value_type/SonarLint.xml @@ -0,0 +1,21 @@ + + + + + sonar.cs.ignoreHeaderComments + abc + + + sonar.cs.analyzeGeneratedCode + null + + + sonar.cs.roslyn.ignoreIssues + + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Invalid_Xml/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Invalid_Xml/SonarLint.xml new file mode 100644 index 00000000000..9a259cbeb32 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Invalid_Xml/SonarLint.xml @@ -0,0 +1,13 @@ + + + Setting> + sonar.nothing + nothing + + + + + Files> + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Missing_properties/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Missing_properties/SonarLint.xml new file mode 100644 index 00000000000..8d8f8135bfc --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Missing_properties/SonarLint.xml @@ -0,0 +1,13 @@ + + + + + sonar.nothing + nothing + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Partially_missing_properties/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Partially_missing_properties/SonarLint.xml new file mode 100644 index 00000000000..ed671af08b6 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/Partially_missing_properties/SonarLint.xml @@ -0,0 +1,25 @@ + + + + + sonar.nothing + nothing + + + sonar.cs.analyzeGeneratedCode + true + + + sonar.exclusions + Fake/Exclusions/**/*,Fake/Exclusions/Second*/**/* + + + sonar.inclusions + Fake/Inclusions/**/*,Fake/Inclusions/Second*/**/* + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/PropertiesCSharpTrueVbnetFalse/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/PropertiesCSharpTrueVbnetFalse/SonarLint.xml new file mode 100644 index 00000000000..39ddbc9a84e --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/SonarLintXml/PropertiesCSharpTrueVbnetFalse/SonarLint.xml @@ -0,0 +1,21 @@ + + + + + sonar.cs.ignoreHeaderComments + true + + + sonar.vbnet.ignoreHeaderComments + false + + + sonar.cs.analyzeGeneratedCode + true + + + sonar.vbnet.analyzeGeneratedCode + false + + + \ No newline at end of file diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfBoolean/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfBoolean/SonarLint.xml deleted file mode 100644 index 913b8886a2d..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfBoolean/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S1451 - - - isRegularExpression - fooBar - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfInt/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfInt/SonarLint.xml deleted file mode 100644 index 4596b44be4b..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/StringInsteadOfInt/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S1067 - - - max - fooBar - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/ToChange/SonarLint.xml b/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/ToChange/SonarLint.xml deleted file mode 100644 index b99df9c60f6..00000000000 --- a/analyzers/tests/SonarAnalyzer.UnitTest/ResourceTests/ToChange/SonarLint.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - sonar.cs.ignoreHeaderComments - true - - - sonar.cs.analyzeGeneratedCode - false - - - sonar.cs.file.suffixes - .cs - - - - - S1067 - - - max - 1 - - - - - - - diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Utilities/UtilityAnalyzerBaseTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Utilities/UtilityAnalyzerBaseTest.cs index 8f03a14688f..d1e6b4e61b2 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Utilities/UtilityAnalyzerBaseTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Utilities/UtilityAnalyzerBaseTest.cs @@ -34,6 +34,7 @@ public class UtilityAnalyzerBaseTest { private const string DefaultSonarProjectConfig = @"ResourceTests\SonarProjectConfig\Path_Windows\SonarProjectConfig.xml"; private const string DefaultProjectOutFolderPath = @"ResourceTests\ProjectOutFolderPath.txt"; + public TestContext TestContext { get; set; } [DataTestMethod] [DataRow(LanguageNames.CSharp, DefaultProjectOutFolderPath, @"path\output-cs")] @@ -43,7 +44,7 @@ public class UtilityAnalyzerBaseTest public void ReadConfig_OutPath(string language, string additionalPath, string expectedOutPath) { // We do not test what is read from the SonarLint file, but we need it - var utilityAnalyzer = new TestUtilityAnalyzer(language, @"ResourceTests\SonarLint.xml", additionalPath); + var utilityAnalyzer = new TestUtilityAnalyzer(language, @"ResourceTests\SonarLintXml\All_properties_cs\SonarLint.xml", additionalPath); utilityAnalyzer.TestOutPath.Should().Be(expectedOutPath); utilityAnalyzer.TestIsAnalyzerEnabled.Should().BeTrue(); @@ -55,35 +56,37 @@ public void ReadConfig_OutPath(string language, string additionalPath, string ex public void ReadConfig_OutPath_FromSonarProjectConfig_HasPriority(string firstFile, string secondFile) { // We do not test what is read from the SonarLint file, but we need it - var utilityAnalyzer = new TestUtilityAnalyzer(LanguageNames.CSharp, @"ResourceTests\SonarLint.xml", firstFile, secondFile); + var utilityAnalyzer = new TestUtilityAnalyzer(LanguageNames.CSharp, @"ResourceTests\SonarLintXml\All_properties_cs\SonarLint.xml", firstFile, secondFile); utilityAnalyzer.TestOutPath.Should().Be(@"C:\foo\bar\.sonarqube\out\0\output-cs"); utilityAnalyzer.TestIsAnalyzerEnabled.Should().BeTrue(); } [DataTestMethod] - [DataRow(LanguageNames.CSharp, @"ResourceTests\AnalyzeGeneratedTrue\SonarLint.xml", true)] - [DataRow(LanguageNames.CSharp, @"ResourceTests\AnalyzeGeneratedFalse\SonarLint.xml", false)] - [DataRow(LanguageNames.VisualBasic, @"ResourceTests\AnalyzeGeneratedTrueVbnet\SonarLint.xml", true)] - [DataRow(LanguageNames.VisualBasic, @"ResourceTests\AnalyzeGeneratedFalseVbnet\SonarLint.xml", false)] - public void ReadsSettings_AnalyzeGenerated(string language, string sonarLintXmlPath, bool expectedAnalyzeGeneratedCodeValue) + [DataRow(LanguageNames.CSharp, true)] + [DataRow(LanguageNames.CSharp, false)] + [DataRow(LanguageNames.VisualBasic, true)] + [DataRow(LanguageNames.VisualBasic, false)] + public void ReadsSettings_AnalyzeGenerated(string language, bool analyzeGenerated) { + var sonarLintXmlPath = AnalysisScaffolding.CreateSonarLintXml(TestContext, language: language, analyzeGeneratedCode: analyzeGenerated); var utilityAnalyzer = new TestUtilityAnalyzer(language, sonarLintXmlPath, DefaultSonarProjectConfig); - utilityAnalyzer.TestAnalyzeGeneratedCode.Should().Be(expectedAnalyzeGeneratedCodeValue); + utilityAnalyzer.TestAnalyzeGeneratedCode.Should().Be(analyzeGenerated); utilityAnalyzer.TestIsAnalyzerEnabled.Should().BeTrue(); } [DataTestMethod] - [DataRow(LanguageNames.CSharp, @"ResourceTests\IgnoreHeaderCommentsTrueCSharp\SonarLint.xml", true)] - [DataRow(LanguageNames.CSharp, @"ResourceTests\IgnoreHeaderCommentsFalseCSharp\SonarLint.xml", false)] - [DataRow(LanguageNames.VisualBasic, @"ResourceTests\IgnoreHeaderCommentsTrueVbnet\SonarLint.xml", true)] - [DataRow(LanguageNames.VisualBasic, @"ResourceTests\IgnoreHeaderCommentsFalseVbnet\SonarLint.xml", false)] - public void ReadsSettings_IgnoreHeaderComments(string language, string sonarLintXmlPath, bool expectedIgnoreHeaderComments) + [DataRow(LanguageNames.CSharp, true)] + [DataRow(LanguageNames.CSharp, false)] + [DataRow(LanguageNames.VisualBasic, true)] + [DataRow(LanguageNames.VisualBasic, false)] + public void ReadsSettings_IgnoreHeaderComments(string language, bool ignoreHeaderComments) { + var sonarLintXmlPath = AnalysisScaffolding.CreateSonarLintXml(TestContext, language: language, ignoreHeaderComments: ignoreHeaderComments); var utilityAnalyzer = new TestUtilityAnalyzer(language, sonarLintXmlPath, DefaultSonarProjectConfig); - utilityAnalyzer.TestIgnoreHeaderComments.Should().Be(expectedIgnoreHeaderComments); + utilityAnalyzer.TestIgnoreHeaderComments.Should().Be(ignoreHeaderComments); utilityAnalyzer.TestIsAnalyzerEnabled.Should().BeTrue(); } @@ -96,7 +99,7 @@ public void NoSonarLintXml_AnalyzerNotEnabled() [TestMethod] public void NoOutputPath_AnalyzerNotEnabled() => - new TestUtilityAnalyzer(LanguageNames.CSharp, @"ResourceTests\AnalyzeGeneratedTrue\SonarLint.xml").TestIsAnalyzerEnabled.Should().BeFalse(); + new TestUtilityAnalyzer(LanguageNames.CSharp, AnalysisScaffolding.CreateSonarLintXml(TestContext, analyzeGeneratedCode: true)).TestIsAnalyzerEnabled.Should().BeFalse(); [TestMethod] public void GetTextRange() diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/AnalysisScaffolding.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/AnalysisScaffolding.cs index d15e6b096af..6c6b8ef5e42 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/AnalysisScaffolding.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/AnalysisScaffolding.cs @@ -19,6 +19,8 @@ */ using System.IO; +using System.Runtime.CompilerServices; +using System.Xml.Linq; using Microsoft.CodeAnalysis.Text; using Moq; using SonarAnalyzer.AnalysisContext; @@ -80,6 +82,72 @@ public static string CreateSonarProjectConfigWithFilesToAnalyze(TestContext cont public static string CreateSonarProjectConfig(TestContext context, ProjectType projectType, bool isScannerRun = true) => CreateSonarProjectConfig(context, "ProjectType", projectType.ToString(), isScannerRun); + public static string CreateSonarLintXml( + TestContext context, + string language = LanguageNames.CSharp, + bool analyzeGeneratedCode = false, + bool ignoreHeaderComments = false, + string[] exclusions = null, + string[] inclusions = null, + string[] globalExclusions = null, + string[] testExclusions = null, + string[] testInclusions = null, + string[] globalTestExclusions = null, + List rulesParameters = null) => + TestHelper.WriteFile(context, "SonarLint.xml", GenerateSonarLintXmlContent(language, analyzeGeneratedCode, ignoreHeaderComments, exclusions, inclusions, globalExclusions, testExclusions, testInclusions, globalTestExclusions, rulesParameters)); + + public static string GenerateSonarLintXmlContent( + string language = LanguageNames.CSharp, + bool analyzeGeneratedCode = false, + bool ignoreHeaderComments = false, + string[] exclusions = null, + string[] inclusions = null, + string[] globalExclusions = null, + string[] testExclusions = null, + string[] testInclusions = null, + string[] globalTestExclusions = null, + List rulesParameters = null) => + new XDocument( + new XDeclaration("1.0", "utf-8", "yes"), + new XElement("AnalysisInput", + new XElement("Settings", + CreateSetting($"sonar.{(language == LanguageNames.CSharp ? "cs" : "vbnet")}.analyzeGeneratedCode", analyzeGeneratedCode.ToString()), + CreateSetting($"sonar.{(language == LanguageNames.CSharp ? "cs" : "vbnet")}.ignoreHeaderComments", ignoreHeaderComments.ToString()), + CreateSetting("sonar.exclusions", ConcatenateStringArray(exclusions)), + CreateSetting("sonar.inclusions", ConcatenateStringArray(inclusions)), + CreateSetting("sonar.global.exclusions", ConcatenateStringArray(globalExclusions)), + CreateSetting("sonar.test.exclusions", ConcatenateStringArray(testExclusions)), + CreateSetting("sonar.test.inclusions", ConcatenateStringArray(testInclusions)), + CreateSetting("sonar.global.test.exclusions", ConcatenateStringArray(globalTestExclusions))), + new XElement("Rules", CreateRules(rulesParameters)))).ToString(); + + private static IEnumerable CreateRules(List ruleParameters) + { + foreach (var rule in ruleParameters ?? new()) + { + yield return CreateRule(rule); + } + } + + private static XElement CreateRule(SonarLintXmlRule rule) + { + List elements = new(); + foreach (var param in rule.Parameters) + { + elements.Add(CreateKeyValuePair("Parameter", param.Key, param.Value)); + } + return new("Rule", new XElement("Key", rule.Key), new XElement("Parameters", elements)); + } + + private static XElement CreateSetting(string key, string value) => + CreateKeyValuePair("Setting", key, value); + + private static XElement CreateKeyValuePair(string containerName, string key, string value) => + new(containerName, new XElement("Key", key), new XElement("Value", value)); + + private static string ConcatenateStringArray(string[] array) => + string.Join(",", array ?? Array.Empty()); + private static string CreateSonarProjectConfig(TestContext context, string element, string value, bool isScannerRun, string analysisConfigPath = null) { var sonarProjectConfigPath = TestHelper.TestPath(context, "SonarProjectConfig.xml");