Skip to content

Commit

Permalink
Add analyzer action for reporting diagnostics in additional files
Browse files Browse the repository at this point in the history
Addresses #44131
The added analyzer APIs are similar to the existing APIs for syntax tree callback. This commit adds the compiler APIs + tests

Future enhancement: Add analyzer action for reporting diagnostics in analyzer config files
  • Loading branch information
mavasani committed Jun 22, 2020
1 parent 5ae370f commit 450cf6a
Show file tree
Hide file tree
Showing 44 changed files with 1,151 additions and 279 deletions.
24 changes: 24 additions & 0 deletions src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12588,6 +12588,30 @@ class C
Assert.True(options.TryGetValue("key3", out val));
Assert.Equal("value3", val);
}

[Theory, CombinatorialData]
public void TestAdditionalFileAnalyzer(bool registerFromInitialize)
{
var srcDirectory = Temp.CreateDirectory();

var source = "class C { }";
var srcFile = srcDirectory.CreateFile("a.cs");
srcFile.WriteAllText(source);

var additionalText = "Additional Text";
var additionalFile = srcDirectory.CreateFile("b.txt");
additionalFile.WriteAllText(additionalText);

var diagnosticSpan = new TextSpan(2, 2);
var analyzer = new AdditionalFileAnalyzer(registerFromInitialize, diagnosticSpan);

var output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, includeCurrentAssemblyAsAnalyzerReference: false,
additionalFlags: new[] { "/additionalfile:" + additionalFile.Path },
analyzers: analyzer);
Assert.Contains("b.txt(1,3): warning ID0001", output, StringComparison.Ordinal);

CleanupAllGeneratedFiles(srcDirectory.Path);
}
}

[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ public class C
public void AnalyzerDriverIsSafeAgainstAnalyzerExceptions()
{
var compilation = CreateCompilationWithMscorlib45(TestResource.AllInOneCSharpCode);
var options = new AnalyzerOptions(new[] { new TestAdditionalText() }.ToImmutableArray<AdditionalText>());

ThrowingDiagnosticAnalyzer<SyntaxKind>.VerifyAnalyzerEngineIsSafeAgainstExceptions(analyzer =>
compilation.GetAnalyzerDiagnostics(new[] { analyzer }, null));
compilation.GetAnalyzerDiagnostics(new[] { analyzer }, options));
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Cci;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics.CSharp;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
Expand Down Expand Up @@ -3518,5 +3520,133 @@ public B(int a) { }
Diagnostic("ID0001", "B").WithLocation(7, 12)
});
}

[Theory, CombinatorialData]
public async Task TestAdditionalFileAnalyzer(bool registerFromInitialize)
{
var tree = CSharpSyntaxTree.ParseText(string.Empty);
var compilation = CreateCompilationWithMscorlib45(new[] { tree });
compilation.VerifyDiagnostics();

AdditionalText additionalFile = new TestAdditionalText("Additional File Text");
var options = new AnalyzerOptions(ImmutableArray.Create(additionalFile));
var diagnosticSpan = new TextSpan(2, 2);
var analyzer = new AdditionalFileAnalyzer(registerFromInitialize, diagnosticSpan);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer);

var diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerDiagnosticsAsync(CancellationToken.None);
verifyDiagnostics(diagnostics);

diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerAdditionalFileDiagnosticsAsync(additionalFile, CancellationToken.None);
verifyDiagnostics(diagnostics);

var analysisResult = await compilation.WithAnalyzers(analyzers, options).GetAnalysisResultAsync(CancellationToken.None);
verifyDiagnostics(analysisResult.GetAllDiagnostics());
verifyDiagnostics(analysisResult.NonSourceFileDiagnostics[additionalFile][analyzer]);

void verifyDiagnostics(ImmutableArray<Diagnostic> diagnostics)
{
var diagnostic = Assert.Single(diagnostics);
Assert.Equal(analyzer.Descriptor.Id, diagnostic.Id);
Assert.Equal(LocationKind.ExternalFile, diagnostic.Location.Kind);
var location = (ExternalFileLocation)diagnostic.Location;
Assert.Equal(additionalFile.Path, location.FilePath);
Assert.Equal(diagnosticSpan, location.SourceSpan);
}
}

[Theory, CombinatorialData]
public async Task TestMultipleAdditionalFileAnalyzers(bool registerFromInitialize, bool additionalFilesHaveSamePaths, bool firstAdditionalFileHasNullPath)
{
var tree = CSharpSyntaxTree.ParseText(string.Empty);
var compilation = CreateCompilationWithMscorlib45(new[] { tree });
compilation.VerifyDiagnostics();

var path1 = firstAdditionalFileHasNullPath ? null : @"c:\file.txt";
var path2 = additionalFilesHaveSamePaths ? path1 : @"file2.txt";

AdditionalText additionalFile1 = new TestAdditionalText("Additional File1 Text", path: path1);
AdditionalText additionalFile2 = new TestAdditionalText("Additional File2 Text", path: path2);
var additionalFiles = ImmutableArray.Create(additionalFile1, additionalFile2);
var options = new AnalyzerOptions(additionalFiles);

var diagnosticSpan = new TextSpan(2, 2);
var analyzer1 = new AdditionalFileAnalyzer(registerFromInitialize, diagnosticSpan, id: "ID0001");
var analyzer2 = new AdditionalFileAnalyzer(registerFromInitialize, diagnosticSpan, id: "ID0002");
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer1, analyzer2);

var diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerDiagnosticsAsync(CancellationToken.None);
verifyDiagnostics(diagnostics, analyzers, additionalFiles);

diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerAdditionalFileDiagnosticsAsync(additionalFile1, CancellationToken.None);
verifyDiagnostics(diagnostics, analyzers, ImmutableArray.Create(additionalFile1));
diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerAdditionalFileDiagnosticsAsync(additionalFile2, CancellationToken.None);
verifyDiagnostics(diagnostics, analyzers, ImmutableArray.Create(additionalFile2));

var singleAnalyzerArray = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer1);
diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerAdditionalFileDiagnosticsAsync(additionalFile1, singleAnalyzerArray, CancellationToken.None);
verifyDiagnostics(diagnostics, singleAnalyzerArray, ImmutableArray.Create(additionalFile1));
diagnostics = await compilation.WithAnalyzers(analyzers, options).GetAnalyzerAdditionalFileDiagnosticsAsync(additionalFile2, singleAnalyzerArray, CancellationToken.None);
verifyDiagnostics(diagnostics, singleAnalyzerArray, ImmutableArray.Create(additionalFile2));

var analysisResult = await compilation.WithAnalyzers(analyzers, options).GetAnalysisResultAsync(CancellationToken.None);
verifyDiagnostics(analysisResult.GetAllDiagnostics(), analyzers, additionalFiles);

if (!additionalFilesHaveSamePaths)
{
verifyDiagnostics(getReportedDiagnostics(analysisResult, analyzer1, additionalFile1), singleAnalyzerArray, ImmutableArray.Create(additionalFile1));
verifyDiagnostics(getReportedDiagnostics(analysisResult, analyzer1, additionalFile2), singleAnalyzerArray, ImmutableArray.Create(additionalFile2));
singleAnalyzerArray = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer2);
verifyDiagnostics(getReportedDiagnostics(analysisResult, analyzer2, additionalFile1), singleAnalyzerArray, ImmutableArray.Create(additionalFile1));
verifyDiagnostics(getReportedDiagnostics(analysisResult, analyzer2, additionalFile2), singleAnalyzerArray, ImmutableArray.Create(additionalFile2));
}

static ImmutableArray<Diagnostic> getReportedDiagnostics(AnalysisResult analysisResult, DiagnosticAnalyzer analyzer, AdditionalText additionalFile)
{
if (analysisResult.NonSourceFileDiagnostics.TryGetValue(additionalFile, out var diagnosticsMap) &&
diagnosticsMap.TryGetValue(analyzer, out var diagnostics))
{
return diagnostics;
}

return ImmutableArray<Diagnostic>.Empty;
}

void verifyDiagnostics(ImmutableArray<Diagnostic> diagnostics, ImmutableArray<DiagnosticAnalyzer> analyzers, ImmutableArray<AdditionalText> additionalFiles)
{
foreach (AdditionalFileAnalyzer analyzer in analyzers)
{
var fileIndex = 0;
foreach (var additionalFile in additionalFiles)
{
var applicableDiagnostics = diagnostics.WhereAsArray(
d => d.Id == analyzer.Descriptor.Id && PathUtilities.Comparer.Equals(d.Location.GetLineSpan().Path, additionalFile.Path));
if (additionalFile.Path == null)
{
Assert.Empty(applicableDiagnostics);
continue;
}

var expectedCount = additionalFilesHaveSamePaths ? additionalFiles.Length : 1;
Assert.Equal(expectedCount, applicableDiagnostics.Length);

foreach (var diagnostic in applicableDiagnostics)
{
Assert.Equal(LocationKind.ExternalFile, diagnostic.Location.Kind);
var location = (ExternalFileLocation)diagnostic.Location;
Assert.Equal(diagnosticSpan, location.SourceSpan);
}

fileIndex++;
if (!additionalFilesHaveSamePaths || fileIndex == additionalFiles.Length)
{
diagnostics = diagnostics.RemoveRange(applicableDiagnostics);
}
}
}

Assert.Empty(diagnostics);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;

Expand All @@ -23,23 +24,25 @@ public void InitializeTest()
var parseOptions = new CSharpParseOptions(kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.None)
.WithFeatures(new[] { new KeyValuePair<string, string>("IOperation", "true") });
var compilation = CreateCompilation(code, parseOptions: parseOptions);

Verify(compilation, nameof(AnalysisContext.RegisterCodeBlockAction));
Verify(compilation, nameof(AnalysisContext.RegisterCodeBlockStartAction));
Verify(compilation, nameof(AnalysisContext.RegisterCompilationAction));
Verify(compilation, nameof(AnalysisContext.RegisterCompilationStartAction));
Verify(compilation, nameof(AnalysisContext.RegisterOperationAction));
Verify(compilation, nameof(AnalysisContext.RegisterOperationBlockAction));
Verify(compilation, nameof(AnalysisContext.RegisterSemanticModelAction));
Verify(compilation, nameof(AnalysisContext.RegisterSymbolAction));
Verify(compilation, nameof(AnalysisContext.RegisterSyntaxNodeAction));
Verify(compilation, nameof(AnalysisContext.RegisterSyntaxTreeAction));
var options = new AnalyzerOptions(new[] { new TestAdditionalText() }.ToImmutableArray<AdditionalText>());

Verify(compilation, options, nameof(AnalysisContext.RegisterCodeBlockAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterCodeBlockStartAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterCompilationAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterCompilationStartAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterOperationAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterOperationBlockAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterSemanticModelAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterSymbolAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterSyntaxNodeAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterSyntaxTreeAction));
Verify(compilation, options, nameof(AnalysisContext.RegisterAdditionalFileAction));
}

private static void Verify(Compilation compilation, string context)
private static void Verify(Compilation compilation, AnalyzerOptions options, string context)
{
var analyzer = new Analyzer(s => context == s);
var diagnostics = compilation.GetAnalyzerDiagnostics(new DiagnosticAnalyzer[] { analyzer });
var diagnostics = compilation.GetAnalyzerDiagnostics(new DiagnosticAnalyzer[] { analyzer }, options);

Assert.Equal(1, diagnostics.Length);
Assert.True(diagnostics[0].Descriptor.Description.ToString().IndexOf(analyzer.Info.GetContext()) >= 0);
Expand Down Expand Up @@ -74,6 +77,7 @@ public override void Initialize(AnalysisContext c)
c.RegisterSymbolAction(b => ThrowIfMatch(nameof(c.RegisterSymbolAction), new AnalysisContextInfo(b.Compilation, b.Symbol)), SymbolKind.NamedType);
c.RegisterSyntaxNodeAction(b => ThrowIfMatch(nameof(c.RegisterSyntaxNodeAction), new AnalysisContextInfo(b.SemanticModel.Compilation, b.Node)), SyntaxKind.ReturnStatement);
c.RegisterSyntaxTreeAction(b => ThrowIfMatch(nameof(c.RegisterSyntaxTreeAction), new AnalysisContextInfo(b.Compilation, b.Tree)));
c.RegisterAdditionalFileAction(b => ThrowIfMatch(nameof(c.RegisterAdditionalFileAction), new AnalysisContextInfo(b.Compilation, b.AdditionalFile)));
}

private void ThrowIfMatch(string context, AnalysisContextInfo info)
Expand Down
3 changes: 3 additions & 0 deletions src/Compilers/Core/Portable/CodeAnalysisResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,9 @@
<data name="InvalidTree" xml:space="preserve">
<value>Syntax tree doesn't belong to the underlying 'Compilation'.</value>
</data>
<data name="InvalidNonSourceFile" xml:space="preserve">
<value>Non-source file doesn't belong to the underlying 'CompilationWithAnalyzers'.</value>
</data>
<data name="ResourceStreamEndedUnexpectedly" xml:space="preserve">
<value>Resource stream ended at {0} bytes, expected {1} bytes.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public override TextSpan SourceSpan
}
}

public string FilePath => _lineSpan.Path;

public override FileLinePositionSpan GetLineSpan()
{
return _lineSpan;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

This comment has been minimized.

Copy link
@jaredpar

jaredpar Jul 10, 2020

Member

In the future I'd prefer we add nullable annotations and new features in different commits. It makes it significantly easier to review that way because we can separate out new code and features with annotation changes.


using System.Text;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeAnalysis.Diagnostics
{
Expand All @@ -12,14 +13,15 @@ namespace Microsoft.CodeAnalysis.Diagnostics
/// </summary>
internal struct AnalysisContextInfo
{
private readonly Compilation _compilation;
private readonly IOperation _operation;
private readonly ISymbol _symbol;
private readonly SyntaxTree _tree;
private readonly SyntaxNode _node;
private readonly Compilation? _compilation;
private readonly IOperation? _operation;
private readonly ISymbol? _symbol;
private readonly SyntaxTree? _tree;
private readonly AdditionalText? _nonSourceFile;
private readonly SyntaxNode? _node;

public AnalysisContextInfo(Compilation compilation) :
this(compilation: compilation, operation: null, symbol: null, tree: null, node: null)
this(compilation: compilation, operation: null, symbol: null, tree: null, node: null, nonSourceFile: null)
{
}

Expand All @@ -29,41 +31,48 @@ public AnalysisContextInfo(SemanticModel model) :
}

public AnalysisContextInfo(Compilation compilation, ISymbol symbol) :
this(compilation: compilation, operation: null, symbol: symbol, tree: null, node: null)
this(compilation: compilation, operation: null, symbol: symbol, tree: null, node: null, nonSourceFile: null)
{
}

public AnalysisContextInfo(Compilation compilation, SyntaxTree tree) :
this(compilation: compilation, operation: null, symbol: null, tree: tree, node: null)
this(compilation: compilation, operation: null, symbol: null, tree: tree, node: null, nonSourceFile: null)
{
}

public AnalysisContextInfo(Compilation compilation, AdditionalText nonSourceFile) :
this(compilation: compilation, operation: null, symbol: null, tree: null, node: null, nonSourceFile)
{
}

public AnalysisContextInfo(Compilation compilation, SyntaxNode node) :
this(compilation: compilation, operation: null, symbol: null, tree: node.SyntaxTree, node: node)
this(compilation: compilation, operation: null, symbol: null, tree: node.SyntaxTree, node, nonSourceFile: null)
{
}

public AnalysisContextInfo(Compilation compilation, IOperation operation) :
this(compilation: compilation, operation: operation, symbol: null, tree: operation.Syntax.SyntaxTree, node: operation.Syntax)
this(compilation: compilation, operation: operation, symbol: null, tree: operation.Syntax.SyntaxTree, node: operation.Syntax, nonSourceFile: null)
{
}

public AnalysisContextInfo(Compilation compilation, ISymbol symbol, SyntaxNode node) :
this(compilation: compilation, operation: null, symbol: symbol, tree: node.SyntaxTree, node: node)
this(compilation: compilation, operation: null, symbol: symbol, tree: node.SyntaxTree, node, nonSourceFile: null)
{
}

public AnalysisContextInfo(
Compilation compilation,
IOperation operation,
ISymbol symbol,
SyntaxTree tree,
SyntaxNode node)
Compilation? compilation,
IOperation? operation,
ISymbol? symbol,
SyntaxTree? tree,
SyntaxNode? node,
AdditionalText? nonSourceFile)
{
_compilation = compilation;
_operation = operation;
_symbol = symbol;
_tree = tree;
_nonSourceFile = nonSourceFile;
_node = node;
}

Expand Down Expand Up @@ -91,6 +100,11 @@ public string GetContext()
sb.AppendLine($"{nameof(SyntaxTree)}: {_tree.FilePath}");
}

if (_nonSourceFile?.Path != null)
{
sb.AppendLine($"{nameof(AdditionalText)}: {_nonSourceFile.Path}");
}

if (_node != null)
{
var text = _tree?.GetText();
Expand Down
Loading

0 comments on commit 450cf6a

Please sign in to comment.