Skip to content

Commit

Permalink
Apply exclusion inclusion logic for all rules (except utility) (#6860)
Browse files Browse the repository at this point in the history
  • Loading branch information
1 parent f4908db commit 262b3b7
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 41 deletions.
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@ namespace SonarAnalyzer.AnalysisContext;

public class SonarAnalysisContextBase
{
protected static readonly ConditionalWeakTable<Compilation, ConcurrentDictionary<string, bool>> FileInclusionCache = new();
protected static readonly ConditionalWeakTable<Compilation, ImmutableHashSet<string>> UnchangedFilesCache = new();
protected static readonly SourceTextValueProvider<ProjectConfigReader> ProjectConfigProvider = new(x => new ProjectConfigReader(x));
protected static readonly SourceTextValueProvider<SonarLintXmlReader> SonarLintXmlProviderCS = new(x => new SonarLintXmlReader(x, LanguageNames.CSharp));
Expand Down Expand Up @@ -55,8 +57,9 @@ protected SonarAnalysisContextBase(SonarAnalysisContext analysisContext, TContex
/// <param name="tree">Tree to decide on. Can be null for Symbol-based and Compilation-based scenarios. And we want to analyze those too.</param>
/// <param name="generatedCodeRecognizer">When set, generated trees are analyzed only when language-specific 'analyzeGeneratedCode' configuration property is also set.</param>
public bool ShouldAnalyzeTree(SyntaxTree tree, GeneratedCodeRecognizer generatedCodeRecognizer) =>
(generatedCodeRecognizer is null || SonarLintFile().AnalyzeGeneratedCode || !tree.IsGenerated(generatedCodeRecognizer, Compilation))
&& (tree is null || !IsUnchanged(tree));
SonarLintFile() is var sonarLintReader
&& (generatedCodeRecognizer is null || sonarLintReader.AnalyzeGeneratedCode || !tree.IsGenerated(generatedCodeRecognizer, Compilation))
&& (tree is null || (!IsUnchanged(tree) && ShouldAnalyzeFile(sonarLintReader, tree.FilePath)));

/// <summary>
/// Reads configuration from SonarProjectConfig.xml file and caches the result for scope of this analysis.
Expand Down Expand Up @@ -122,6 +125,27 @@ public bool HasMatchingScope(DiagnosticDescriptor descriptor)
descriptor.CustomTags.Contains(tag);
}

private bool ShouldAnalyzeFile(SonarLintXmlReader sonarLintXml, string filePath) =>
ProjectConfiguration().ProjectType != ProjectType.Unknown // Not SonarLint context, NuGet or Scanner <= 5.0
|| (FileInclusionCache.GetValue(Compilation, _ => new()) is var cache
&& cache.GetOrAdd(filePath, _ => IsFileIncluded(sonarLintXml, filePath)));

private ImmutableHashSet<string> CreateUnchangedFilesHashSet() =>
ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, ProjectConfiguration().AnalysisConfig?.UnchangedFiles() ?? Array.Empty<string>());

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));

private static bool IsExcluded(string[] exclusions, string filePath) =>
exclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath));
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));

Expand All @@ -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>();
additionalText.Setup(x => x.Path).Returns("SonarLint.xml");
additionalText.Setup(x => x.GetText(default)).Returns(sonarLintXml);
Expand Down Expand Up @@ -129,33 +128,32 @@ 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));

sut.ShouldAnalyzeTree(tree, CSharpGeneratedCodeRecognizer.Instance).Should().BeTrue();
}

[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.
Expand Down Expand Up @@ -329,26 +327,106 @@ public class GenericAttribute<T> : Attribute { }
VerifyEmpty("test.cs", sourceCs, new CS.EmptyStatement());
}

private static DummySourceText CreateSonarLintXml(bool analyzeGeneratedCSharp) =>
new($"""
<?xml version="1.0" encoding="UTF-8"?>
<AnalysisInput>
<Settings>
<Setting>
<Key>dummy</Key>
<Value>false</Value>
</Setting>
<Setting>
<Key>sonar.cs.analyzeGeneratedCode</Key>
<Value>{analyzeGeneratedCSharp.ToString().ToLower()}</Value>
</Setting>
<Setting>
<Key>sonar.vbnet.analyzeGeneratedCode</Key>
<Value>true</Value>
</Setting>
</Settings>
</AnalysisInput>
""");
[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));
Expand Down
Expand Up @@ -19,6 +19,7 @@
*/

using System.IO;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Text;
using Moq;
using SonarAnalyzer.AnalysisContext;
Expand Down Expand Up @@ -80,6 +81,45 @@ 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,
string[] exclusions = null,
string[] inclusions = null,
string[] globalExclusions = null,
string[] testExclusions = null,
string[] testInclusions = null,
string[] globalTestExclusions = null) =>
TestHelper.WriteFile(context, "SonarLint.xml", GenerateSonarLintXmlContent(language, analyzeGeneratedCode, exclusions, inclusions, globalExclusions, testExclusions, testInclusions, globalTestExclusions));

public static string GenerateSonarLintXmlContent(
string language = LanguageNames.CSharp,
bool analyzeGeneratedCode = false,
string[] exclusions = null,
string[] inclusions = null,
string[] globalExclusions = null,
string[] testExclusions = null,
string[] testInclusions = null,
string[] globalTestExclusions = 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.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))))).ToString();

private static XElement CreateSetting(string key, string value) =>
new("Setting", new XElement("Key", key), new XElement("Value", value));

private static string ConcatenateStringArray(string[] array) =>
string.Join(",", array ?? Array.Empty<string>());

private static string CreateSonarProjectConfig(TestContext context, string element, string value, bool isScannerRun, string analysisConfigPath = null)
{
var sonarProjectConfigPath = TestHelper.TestPath(context, "SonarProjectConfig.xml");
Expand Down

0 comments on commit 262b3b7

Please sign in to comment.