Skip to content

Commit

Permalink
UtilityAnalyzer: Use RegisterCompilationStartAction (#6576)
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-strecker-sonarsource committed Feb 23, 2023
1 parent 55a4d46 commit bb07dd6
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public sealed class SonarCompilationStartAnalysisContext : SonarAnalysisContextB
public void RegisterSymbolAction(Action<SonarSymbolReportingContext> action, params SymbolKind[] symbolKinds) =>
Context.RegisterSymbolAction(x => action(new(AnalysisContext, x)), symbolKinds);

public void RegisterSemanticModelAction(Action<SonarSematicModelReportingContext> action) =>
Context.RegisterSemanticModelAction(x => action(new(AnalysisContext, x)));

public void RegisterCompilationEndAction(Action<SonarCompilationReportingContext> action) =>
Context.RegisterCompilationEndAction(x => action(new(AnalysisContext, x)));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.
*/

namespace SonarAnalyzer.AnalysisContext;

public sealed class SonarSematicModelReportingContext : SonarTreeReportingContextBase<SemanticModelAnalysisContext>
{
public override SyntaxTree Tree => SemanticModel.SyntaxTree;
public override Compilation Compilation => Context.SemanticModel.Compilation;
public override AnalyzerOptions Options => Context.Options;
public override CancellationToken Cancel => Context.CancellationToken;
public SemanticModel SemanticModel => Context.SemanticModel;

internal SonarSematicModelReportingContext(SonarAnalysisContext analysisContext, SemanticModelAnalysisContext context) : base(analysisContext, context) { }

private protected override ReportingContext CreateReportingContext(Diagnostic diagnostic) =>
new(this, diagnostic);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public ReportingContext(SonarSymbolReportingContext context, Diagnostic diagnost
public ReportingContext(SonarCodeBlockReportingContext context, Diagnostic diagnostic)
: this(diagnostic, context.Context.ReportDiagnostic, context.Compilation, context.Tree) { }

public ReportingContext(SonarSematicModelReportingContext context, Diagnostic diagnostic)
: this(diagnostic, context.Context.ReportDiagnostic, context.Compilation, context.Tree) { }

private ReportingContext(Diagnostic diagnostic,
Action<Diagnostic> roslynReportDiagnostic,
Compilation compilation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,23 @@ public abstract class UtilityAnalyzerBase : SonarDiagnosticAnalyzer
EndOffset = lineSpan.EndLinePosition.Character
};

protected void ReadParameters(SonarCompilationReportingContext c)
protected void ReadParameters(SonarCompilationStartAnalysisContext context)
{
var settings = c.Options.ParseSonarLintXmlSettings();
var outPath = c.ProjectConfiguration().OutPath;
var settings = context.Options.ParseSonarLintXmlSettings();
var outPath = context.ProjectConfiguration().OutPath;
// For backward compatibility with S4MSB <= 5.0
if (outPath == null && c.Options.ProjectOutFolderPath() is { } projectOutFolderAdditionalFile)
if (outPath == null && context.Options.ProjectOutFolderPath() is { } projectOutFolderAdditionalFile)
{
outPath = projectOutFolderAdditionalFile.GetText().ToString().TrimEnd();
}
if (settings.Any() && !string.IsNullOrEmpty(outPath))
{
IgnoreHeaderComments = PropertiesHelper.ReadIgnoreHeaderCommentsProperty(settings, c.Compilation.Language);
AnalyzeGeneratedCode = PropertiesHelper.ReadAnalyzeGeneratedCodeProperty(settings, c.Compilation.Language);
OutPath = Path.Combine(outPath, c.Compilation.Language == LanguageNames.CSharp ? "output-cs" : "output-vbnet");
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");
IsAnalyzerEnabled = true;
IsTestProject = c.IsTestProject();
IsTestProject = context.IsTestProject();
}
}
}
Expand All @@ -87,40 +88,49 @@ public abstract class UtilityAnalyzerBase<TSyntaxKind, TMessage> : UtilityAnalyz
protected UtilityAnalyzerBase(string diagnosticId, string title) : base(diagnosticId, title) { }

protected sealed override void Initialize(SonarAnalysisContext context) =>
context.RegisterCompilationAction(c =>
context.RegisterCompilationStartAction(startContext =>
{
ReadParameters(startContext);
if (!IsAnalyzerEnabled)
{
return;
}
var treeMessages = new List<TMessage>();
startContext.RegisterSemanticModelAction(modelContext =>
{
ReadParameters(c);
if (!IsAnalyzerEnabled)
if (ShouldGenerateMetrics(modelContext))
{
return;
treeMessages.Add(CreateMessage(modelContext.Tree, modelContext.SemanticModel));
}
});
var treeMessages = c.Compilation.SyntaxTrees
.Where(x => ShouldGenerateMetrics(c, x))
.Select(x => CreateMessage(x, c.Compilation.GetSemanticModel(x)));
var messages = CreateAnalysisMessages(c)
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 messages)
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(SonarCompilationReportingContext context, SyntaxTree tree) =>
(AnalyzeUnchangedFiles || !context.IsUnchanged(tree))
&& ShouldGenerateMetrics(tree);
private bool ShouldGenerateMetrics(SonarSematicModelReportingContext context) =>
(AnalyzeUnchangedFiles || !context.IsUnchanged(context.Tree))
&& ShouldGenerateMetrics(context.Tree);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

extern alias csharp;
extern alias vbnet;

using Microsoft.CodeAnalysis.CSharp;
using Moq;
using SonarAnalyzer.AnalysisContext;
Expand Down Expand Up @@ -132,6 +131,86 @@ public void RegisterCodeBlockStartAction_UnchangedFiles_SonarAnalysisContext(str
context.AssertDelegateInvoked(expected);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterCompilationEndAction()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
sut.RegisterCompilationEndAction(_ => { });

startContext.AssertExpectedInvocationCounts(expectedCompilationEndCount: 1);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterSemanticModel()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
sut.RegisterSemanticModelAction(_ => { });

startContext.AssertExpectedInvocationCounts(expectedSemanticModelCount: 1);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterSymbolAction()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
sut.RegisterSymbolAction(_ => { });

startContext.AssertExpectedInvocationCounts(expectedSymbolCount: 1);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterNodeAction()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
sut.RegisterNodeAction<SyntaxKind>(CSharpGeneratedCodeRecognizer.Instance, _ => { });

startContext.AssertExpectedInvocationCounts(expectedNodeCount: 0); // RegisterNodeAction doesn't use DummyCompilationStartAnalysisContext to register but a newly created context
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterSemanticModel_ReportsIssue()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
sut.RegisterSemanticModelAction(x => x.ReportIssue(diagnostic));

startContext.RaisedDiagnostic.Should().NotBeNull().And.BeSameAs(diagnostic);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegisterCompilationEnd_ReportsIssue()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
sut.RegisterCompilationEndAction(x => x.ReportIssue(CSharpGeneratedCodeRecognizer.Instance, diagnostic));

startContext.RaisedDiagnostic.Should().NotBeNull().And.BeSameAs(diagnostic);
}

[TestMethod]
public void SonarCompilationStartAnalysisContext_RegistSymbol_ReportsIssue()
{
var context = new DummyAnalysisContext(TestContext);
var startContext = new DummyCompilationStartAnalysisContext(context);
var sut = new SonarCompilationStartAnalysisContext(new(context, DummyMainDescriptor), startContext);
var diagnostic = Diagnostic.Create(DiagnosticDescriptorFactory.CreateUtility("TEST", "Test report"), context.Tree.GetRoot().GetLocation());
sut.RegisterSymbolAction(x => x.ReportIssue(CSharpGeneratedCodeRecognizer.Instance, diagnostic));

startContext.RaisedDiagnostic.Should().NotBeNull().And.BeSameAs(diagnostic);
}

private static CompilationStartAnalysisContext MockCompilationStartAnalysisContext(DummyAnalysisContext context)
{
var mock = new Mock<CompilationStartAnalysisContext>(context.Model.Compilation, context.Options, CancellationToken.None);
Expand Down Expand Up @@ -200,6 +279,59 @@ private class DummyCodeBlockStartAnalysisContext<TSyntaxKind> : CodeBlockStartAn
throw new NotImplementedException();
}

private class DummyCompilationStartAnalysisContext : CompilationStartAnalysisContext
{
private readonly DummyAnalysisContext context;
private int compilationEndCount;
private int semanticModelCount;
private int symbolCount;
private int nodeCount;

public Diagnostic RaisedDiagnostic { get; private set; }

public DummyCompilationStartAnalysisContext(DummyAnalysisContext context) : base(context.Model.Compilation, context.Options, default) =>
this.context = context;

public void AssertExpectedInvocationCounts(int expectedCompilationEndCount = 0, int expectedSemanticModelCount = 0, int expectedSymbolCount = 0, int expectedNodeCount = 0)
{
compilationEndCount.Should().Be(expectedCompilationEndCount);
semanticModelCount.Should().Be(expectedSemanticModelCount);
symbolCount.Should().Be(expectedSymbolCount);
nodeCount.Should().Be(expectedNodeCount);
}

public override void RegisterCodeBlockAction(Action<CodeBlockAnalysisContext> action) =>
throw new NotImplementedException();

public override void RegisterCodeBlockStartAction<TLanguageKindEnum>(Action<CodeBlockStartAnalysisContext<TLanguageKindEnum>> action) =>
throw new NotImplementedException();

public override void RegisterCompilationEndAction(Action<CompilationAnalysisContext> action)
{
compilationEndCount++;
action(new CompilationAnalysisContext(context.Model.Compilation, context.Options, reportDiagnostic: x => RaisedDiagnostic = x, isSupportedDiagnostic: _ => true, CancellationToken.None));
}

public override void RegisterSemanticModelAction(Action<SemanticModelAnalysisContext> action)
{
semanticModelCount++;
action(new SemanticModelAnalysisContext(context.Model, context.Options, reportDiagnostic: x => RaisedDiagnostic = x, isSupportedDiagnostic: _ => true, CancellationToken.None));
}

public override void RegisterSymbolAction(Action<SymbolAnalysisContext> action, ImmutableArray<SymbolKind> symbolKinds)
{
symbolCount++;
action(new SymbolAnalysisContext(Mock.Of<ISymbol>(), context.Model.Compilation, context.Options,
reportDiagnostic: x => RaisedDiagnostic = x, isSupportedDiagnostic: _ => true, CancellationToken.None));
}

public override void RegisterSyntaxNodeAction<TLanguageKindEnum>(Action<SyntaxNodeAnalysisContext> action, ImmutableArray<TLanguageKindEnum> syntaxKinds) =>
nodeCount++;

public override void RegisterSyntaxTreeAction(Action<SyntaxTreeAnalysisContext> action) =>
throw new NotImplementedException();
}

[DiagnosticAnalyzer(LanguageNames.CSharp)]
private class DummyAnalyzerForGenerated : SonarDiagnosticAnalyzer
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public void Properties_ArePropagated()
var context = new CodeBlockAnalysisContext(codeBlock, owningSymbol, model, options, _ => { }, _ => true, cancel);
var sut = new SonarCodeBlockReportingContext(AnalysisScaffolding.CreateSonarAnalysisContext(), context);

sut.Tree.Should().Be(codeBlock.SyntaxTree);
sut.Compilation.Should().Be(model.Compilation);
sut.Options.Should().Be(options);
sut.Tree.Should().BeSameAs(codeBlock.SyntaxTree);
sut.Compilation.Should().BeSameAs(model.Compilation);
sut.Options.Should().BeSameAs(options);
sut.Cancel.Should().Be(cancel);
sut.CodeBlock.Should().Be(codeBlock);
sut.OwningSymbol.Should().Be(owningSymbol);
sut.SemanticModel.Should().Be(model);
sut.CodeBlock.Should().BeSameAs(codeBlock);
sut.OwningSymbol.Should().BeSameAs(owningSymbol);
sut.SemanticModel.Should().BeSameAs(model);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public void Properties_ArePropagated()
var context = new Mock<CodeBlockStartAnalysisContext<SyntaxKind>>(codeBlock, owningSymbol, model, options, cancel).Object;
var sut = new SonarCodeBlockStartAnalysisContext<SyntaxKind>(AnalysisScaffolding.CreateSonarAnalysisContext(), context);

sut.Tree.Should().Be(codeBlock.SyntaxTree);
sut.Compilation.Should().Be(model.Compilation);
sut.Options.Should().Be(options);
sut.Tree.Should().BeSameAs(codeBlock.SyntaxTree);
sut.Compilation.Should().BeSameAs(model.Compilation);
sut.Options.Should().BeSameAs(options);
sut.Cancel.Should().Be(cancel);
sut.CodeBlock.Should().Be(codeBlock);
sut.OwningSymbol.Should().Be(owningSymbol);
sut.SemanticModel.Should().Be(model);
sut.CodeBlock.Should().BeSameAs(codeBlock);
sut.OwningSymbol.Should().BeSameAs(owningSymbol);
sut.SemanticModel.Should().BeSameAs(model);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public void Properties_ArePropagated()
var context = new CompilationAnalysisContext(model.Compilation, options, _ => { }, _ => true, cancel);
var sut = new SonarCompilationReportingContext(AnalysisScaffolding.CreateSonarAnalysisContext(), context);

sut.Tree.Should().Be(tree);
sut.Compilation.Should().Be(model.Compilation);
sut.Options.Should().Be(options);
sut.Tree.Should().BeSameAs(tree);
sut.Compilation.Should().BeSameAs(model.Compilation);
sut.Options.Should().BeSameAs(options);
sut.Cancel.Should().Be(cancel);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public void Properties_ArePropagated()
var context = new Mock<CompilationStartAnalysisContext>(model.Compilation, options, cancel).Object;
var sut = new SonarCompilationStartAnalysisContext(AnalysisScaffolding.CreateSonarAnalysisContext(), context);

sut.Tree.Should().Be(tree);
sut.Compilation.Should().Be(model.Compilation);
sut.Options.Should().Be(options);
sut.Tree.Should().BeSameAs(tree);
sut.Compilation.Should().BeSameAs(model.Compilation);
sut.Options.Should().BeSameAs(options);
sut.Cancel.Should().Be(cancel);
}
}

0 comments on commit bb07dd6

Please sign in to comment.