/
UtilityAnalyzerBase.cs
135 lines (119 loc) · 6.11 KB
/
UtilityAnalyzerBase.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
/*
* 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 Google.Protobuf;
using SonarAnalyzer.Protobuf;
namespace SonarAnalyzer.Rules
{
public abstract class UtilityAnalyzerBase : SonarDiagnosticAnalyzer
{
protected static readonly ISet<string> FileExtensionWhitelist = new HashSet<string> { ".cs", ".csx", ".vb" };
private readonly DiagnosticDescriptor rule;
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(rule);
protected bool IsAnalyzerEnabled { get; set; }
protected bool IgnoreHeaderComments { get; set; }
protected virtual bool AnalyzeGeneratedCode { get; set; }
protected virtual bool AnalyzeTestProjects => true;
protected string OutPath { get; set; }
protected bool IsTestProject { get; set; }
protected override bool EnableConcurrentExecution => false;
protected UtilityAnalyzerBase(string diagnosticId, string title) =>
rule = DiagnosticDescriptorFactory.CreateUtility(diagnosticId, title);
internal static TextRange GetTextRange(FileLinePositionSpan lineSpan) =>
new()
{
StartLine = lineSpan.StartLinePosition.GetLineNumberToReport(),
EndLine = lineSpan.EndLinePosition.GetLineNumberToReport(),
StartOffset = lineSpan.StartLinePosition.Character,
EndOffset = lineSpan.EndLinePosition.Character
};
protected void ReadParameters(SonarCompilationStartAnalysisContext context)
{
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 (context.Options.SonarLintXml() != null && !string.IsNullOrEmpty(outPath))
{
var sonarLintXml = context.SonarLintFile();
IgnoreHeaderComments = sonarLintXml.IgnoreHeaderComments;
AnalyzeGeneratedCode = sonarLintXml.AnalyzeGeneratedCode;
OutPath = Path.Combine(outPath, context.Compilation.Language == LanguageNames.CSharp ? "output-cs" : "output-vbnet");
IsAnalyzerEnabled = true;
IsTestProject = context.IsTestProject();
}
}
}
public abstract class UtilityAnalyzerBase<TSyntaxKind, TMessage> : UtilityAnalyzerBase
where TSyntaxKind : struct
where TMessage : class, IMessage, new()
{
private static readonly object FileWriteLock = new TMessage();
protected abstract ILanguageFacade<TSyntaxKind> Language { get; }
protected abstract string FileName { get; }
protected abstract TMessage CreateMessage(SyntaxTree syntaxTree, SemanticModel semanticModel);
protected virtual bool AnalyzeUnchangedFiles => false;
protected virtual IEnumerable<TMessage> CreateAnalysisMessages(SonarCompilationReportingContext c) => Enumerable.Empty<TMessage>();
protected UtilityAnalyzerBase(string diagnosticId, string title) : base(diagnosticId, title) { }
protected sealed override void Initialize(SonarAnalysisContext context) =>
context.RegisterCompilationStartAction(startContext =>
{
ReadParameters(startContext);
if (!IsAnalyzerEnabled)
{
return;
}
var treeMessages = new List<TMessage>();
startContext.RegisterSemanticModelAction(modelContext =>
{
if (ShouldGenerateMetrics(modelContext))
{
treeMessages.Add(CreateMessage(modelContext.Tree, modelContext.SemanticModel));
}
});
startContext.RegisterCompilationEndAction(endContext =>
{
var allMessages = CreateAnalysisMessages(endContext)
.Concat(treeMessages)
.WhereNotNull()
.ToArray();
lock (FileWriteLock)
{
Directory.CreateDirectory(OutPath);
using var stream = File.Create(Path.Combine(OutPath, FileName));
foreach (var message in allMessages)
{
message.WriteDelimitedTo(stream);
}
}
});
});
protected virtual bool ShouldGenerateMetrics(SyntaxTree tree) =>
// The results of Metrics and CopyPasteToken analyzers are not needed for Test projects yet the plugin side expects the protobuf files, so we create empty ones.
(AnalyzeTestProjects || !IsTestProject)
&& FileExtensionWhitelist.Contains(Path.GetExtension(tree.FilePath))
&& (AnalyzeGeneratedCode || !Language.GeneratedCodeRecognizer.IsGenerated(tree));
private bool ShouldGenerateMetrics(SonarSematicModelReportingContext context) =>
(AnalyzeUnchangedFiles || !context.IsUnchanged(context.Tree))
&& ShouldGenerateMetrics(context.Tree);
}
}