/
SonarAnalysisContextBase.cs
151 lines (129 loc) · 7.73 KB
/
SonarAnalysisContextBase.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*
* 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.Runtime.CompilerServices;
using static SonarAnalyzer.Helpers.DiagnosticDescriptorFactory;
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));
protected static readonly SourceTextValueProvider<SonarLintXmlReader> SonarLintXmlProviderVB = new(x => new SonarLintXmlReader(x, LanguageNames.VisualBasic));
protected SonarAnalysisContextBase() { }
protected static SourceTextValueProvider<SonarLintXmlReader> SonarLintXmlReader(string language) =>
language == LanguageNames.CSharp ? SonarLintXmlProviderCS : SonarLintXmlProviderVB;
}
public abstract class SonarAnalysisContextBase<TContext> : SonarAnalysisContextBase
{
public abstract Compilation Compilation { get; }
public abstract AnalyzerOptions Options { get; }
public abstract CancellationToken Cancel { get; }
public SonarAnalysisContext AnalysisContext { get; }
public TContext Context { get; }
protected SonarAnalysisContextBase(SonarAnalysisContext analysisContext, TContext context)
{
AnalysisContext = analysisContext ?? throw new ArgumentNullException(nameof(analysisContext));
Context = context;
}
/// <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) =>
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.
/// </summary>
public ProjectConfigReader ProjectConfiguration()
{
if (Options.SonarProjectConfig() is { } sonarProjectConfig)
{
return sonarProjectConfig.GetText() is { } sourceText
// TryGetValue catches all exceptions from SourceTextValueProvider and returns false when exception is thrown
&& AnalysisContext.TryGetValue(sourceText, ProjectConfigProvider, out var cachedProjectConfigReader)
? cachedProjectConfigReader
: throw new InvalidOperationException($"File '{Path.GetFileName(sonarProjectConfig.Path)}' has been added as an AdditionalFile but could not be read and parsed.");
}
else
{
return ProjectConfigReader.Empty;
}
}
/// <summary>
/// Reads the properties from the SonarLint.xml file and caches the result for the scope of this analysis.
/// </summary>
public SonarLintXmlReader SonarLintFile()
{
if (Options.SonarLintXml() is { } sonarLintXml)
{
return sonarLintXml.GetText() is { } sourceText
&& AnalysisContext.TryGetValue(sourceText, SonarLintXmlReader(Compilation.Language), 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;
return projectType == ProjectType.Unknown
? Compilation.IsTest() // SonarLint, NuGet or Scanner <= 5.0
: projectType == ProjectType.Test; // Scanner >= 5.1 does authoritative decision that we follow
}
public bool IsUnchanged(SyntaxTree tree) =>
UnchangedFilesCache.GetValue(Compilation, _ => CreateUnchangedFilesHashSet()).Contains(tree.FilePath);
public bool HasMatchingScope(IEnumerable<DiagnosticDescriptor> descriptors) =>
descriptors.Any(HasMatchingScope);
public bool HasMatchingScope(DiagnosticDescriptor descriptor)
{
// MMF-2297: Test Code as 1st Class Citizen is not ready on server side yet.
// ScannerRun: Only utility rules and rules with TEST-ONLY scope are executed for test projects for now.
// SonarLint & Standalone NuGet: Respect the scope as before.
return IsTestProject()
? ContainsTag(TestSourceScopeTag) && !(ProjectConfiguration().IsScannerRun && ContainsTag(MainSourceScopeTag) && !ContainsTag(UtilityTag))
: ContainsTag(MainSourceScopeTag);
bool ContainsTag(string tag) =>
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, true));
private static bool IsExcluded(string[] exclusions, string filePath) =>
exclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath, false));
}