Skip to content

Commit

Permalink
Add Analyzers to Orleans (#5589)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjorkstromm authored and ReubenBond committed May 9, 2019
1 parent 94c5cab commit b9ae772
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 3 deletions.
34 changes: 32 additions & 2 deletions Orleans.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.202
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4CD3AA9E-D937-48CA-BB6C-158E12257D23}"
EndProject
Expand Down Expand Up @@ -226,6 +226,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Transactions.TestKi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.TelemetryConsumers.Linux", "src\TelemetryConsumers\Orleans.TelemetryConsumers.Linux\Orleans.TelemetryConsumers.Linux.csproj", "{49E24301-040C-4C23-A985-AF30005A413C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Analyzers", "src\Orleans.Analyzers\Orleans.Analyzers.csproj", "{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Analyzers.Tests\Analyzers.Tests.csproj", "{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1376,6 +1380,30 @@ Global
{49E24301-040C-4C23-A985-AF30005A413C}.Release|x64.Build.0 = Release|Any CPU
{49E24301-040C-4C23-A985-AF30005A413C}.Release|x86.ActiveCfg = Release|Any CPU
{49E24301-040C-4C23-A985-AF30005A413C}.Release|x86.Build.0 = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|x64.ActiveCfg = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|x64.Build.0 = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|x86.ActiveCfg = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Debug|x86.Build.0 = Debug|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|Any CPU.Build.0 = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|x64.ActiveCfg = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|x64.Build.0 = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|x86.ActiveCfg = Release|Any CPU
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F}.Release|x86.Build.0 = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|x64.ActiveCfg = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|x64.Build.0 = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|x86.ActiveCfg = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Debug|x86.Build.0 = Debug|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|Any CPU.Build.0 = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|x64.ActiveCfg = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|x64.Build.0 = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|x86.ActiveCfg = Release|Any CPU
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1490,6 +1518,8 @@ Global
{531A89F5-DD05-4D14-A330-6548359A231A} = {3189037B-208D-40A1-A561-169D77D9BB5A}
{B8ABE746-C76F-4E0E-86A5-744E85A55CE8} = {3189037B-208D-40A1-A561-169D77D9BB5A}
{49E24301-040C-4C23-A985-AF30005A413C} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A}
{8CE65361-7EFA-41B2-AE13-D6CC2A68105F} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23}
{B94232D8-B8C2-45D8-AE09-7D8A0B0D88ED} = {A6573187-FD0D-4DF7-91D1-03E07E470C0A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952}
Expand Down
3 changes: 2 additions & 1 deletion Test.cmd
Expand Up @@ -39,7 +39,8 @@ set TESTS=^
%CMDHOME%\test\Transactions\Orleans.Transactions.Tests,^
%CMDHOME%\test\Transactions\Orleans.Transactions.Azure.Test,^
%CMDHOME%\test\TestInfrastructure\Orleans.TestingHost.Tests,^
%CMDHOME%\test\DependencyInjection.Tests
%CMDHOME%\test\DependencyInjection.Tests,^
%CMDHOME%\test\Analyzers.Tests

if []==[%TEST_FILTERS%] set TEST_FILTERS=-trait Category=BVT -trait Category=SlowBVT

Expand Down
58 changes: 58 additions & 0 deletions src/Orleans.Analyzers/AlwaysInterleaveDiagnosticAnalyzer.cs
@@ -0,0 +1,58 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Orleans.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AlwaysInterleaveDiagnosticAnalyzer : DiagnosticAnalyzer
{
private const string AlwaysInterleaveAttributeName = "Orleans.Concurrency.AlwaysInterleaveAttribute";

public const string DiagnosticId = "ORLEANS0001";
public const string Title = "[AlwaysInterleave] must only be used on the grain interface method and not the grain class method.";
public const string MessageFormat = Title;
public const string Category = "Usage";

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(
AnalyzeSyntax,
SyntaxKind.MethodDeclaration);
}

private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
var alwaysInterleaveAttribute = context.Compilation.GetTypeByMetadataName(AlwaysInterleaveAttributeName);

var syntax = (MethodDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(syntax);

if (symbol.ContainingType.TypeKind == TypeKind.Interface)
{
// TODO: Check that interface inherits from IGrain
return;
}

foreach (var attribute in symbol.GetAttributes())
{
if (!attribute.AttributeClass.Equals(alwaysInterleaveAttribute))
{
return;
}

var syntaxReference = attribute.ApplicationSyntaxReference;

context.ReportDiagnostic(
Diagnostic.Create(Rule, Location.Create(syntaxReference.SyntaxTree, syntaxReference.Span)));
}
}
}
}
13 changes: 13 additions & 0 deletions src/Orleans.Analyzers/Orleans.Analyzers.csproj
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Microsoft.Orleans.Analyzers</PackageId>
<Title>Microsoft Orleans Analyzers</Title>
<Description>C# Analyzers for Microsoft Orleans.</Description>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />
</ItemGroup>

</Project>
58 changes: 58 additions & 0 deletions test/Analyzers.Tests/AlwaysInterleaveDiagnosticAnalyzerTests.cs
@@ -0,0 +1,58 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Orleans.Analyzers;
using Xunit;

namespace Analyzers.Tests
{
[TestCategory("BVT"), TestCategory("Analyzer")]
public class AlwaysInterleaveDiagnosticAnalyzerTest : DiagnosticAnalyzerTestBase<AlwaysInterleaveDiagnosticAnalyzer>
{
protected override Task<(Diagnostic[], string)> GetDiagnosticsAsync(string source, params string[] extraUsings)
=> base.GetDiagnosticsAsync(source, extraUsings.Concat(new[] { "Orleans.Concurrency" }).ToArray());

[Fact]
public async Task AlwaysInterleave_Analyzer_NoWarningsIfAttributeIsNotUsed() => await this.AssertNoDiagnostics(@"
class C
{
Task M() => Task.CompletedTask;
}
");

[Fact]
public async Task AlwaysInterleave_Analyzer_NoWarningsIfAttributeIsUsedOnInterface() => await this.AssertNoDiagnostics(@"
public interface I : IGrain
{
[AlwaysInterleave]
Task<string> M();
}
");

[Fact]
public async Task AlwaysInterleave_Analyzer_WarningIfAttributeisUsedOnGrainClass()
{
var (diagnostics, source) = await this.GetDiagnosticsAsync(@"
public interface I : IGrain
{
Task<int> Method();
}
public class C : I
{
[AlwaysInterleave]
public Task<int> Method() => Task.FromResult(0);
}
");

var diagnostic = diagnostics.Single();

Assert.Equal(AlwaysInterleaveDiagnosticAnalyzer.DiagnosticId, diagnostic.Id);
Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
Assert.Equal(AlwaysInterleaveDiagnosticAnalyzer.MessageFormat, diagnostic.GetMessage());

var span = diagnostic.Location.SourceSpan;
Assert.Equal("AlwaysInterleave", source.Substring(span.Start, span.End - span.Start));
}
}
}
22 changes: 22 additions & 0 deletions test/Analyzers.Tests/Analyzers.Tests.csproj
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="xunit" Version="$(xUnitVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelVersion)" />
<DotNetCliToolReference Include="dotnet-xunit" Version="$(xUnitVersion)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(SourceRoot)src\Orleans.Analyzers\Orleans.Analyzers.csproj" />
<ProjectReference Include="$(SourceRoot)src\Orleans.Core.Abstractions\Orleans.Core.Abstractions.csproj" />
<ProjectReference Include="$(SourceRoot)test\TestInfrastructure\TestExtensions\TestExtensions.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="Analyzers.Tests.xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions test/Analyzers.Tests/Analyzers.Tests.xunit.runner.json
@@ -0,0 +1,8 @@
{
"appDomain": "ifAvailable",
"diagnosticMessages": true,
"parallelizeAssembly": false,
"parallelizeTestCollections": false,
"methodDisplay": "classAndMethod",
"shadowCopy": false
}
101 changes: 101 additions & 0 deletions test/Analyzers.Tests/DiagnosticAnalyzerTestBase.cs
@@ -0,0 +1,101 @@
// Derived from Entity Framework Core Analyzer Tests
// https://github.com/aspnet/EntityFrameworkCore/blob/cbefe76162b16c1122629dbf6c85becbbb5cdc8e/test/EFCore.Analyzers.Tests/TestUtilities/DiagnosticAnalyzerTestBase.cs
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
using Xunit;

namespace Analyzers.Tests
{
public abstract class DiagnosticAnalyzerTestBase<TDiagnosticAnalyzer>
where TDiagnosticAnalyzer : DiagnosticAnalyzer, new()
{
private static readonly string[] Usings = new[] {
"System",
"System.Threading.Tasks",
"Orleans"
};

protected virtual DiagnosticAnalyzer CreateDiagnosticAnalyzer() => new TDiagnosticAnalyzer();

protected async Task AssertNoDiagnostics(string source, params string[] extraUsings)
{
var (diagnostics, _) = await this.GetDiagnosticsAsync(source, extraUsings);
Assert.Empty(diagnostics);
}

protected virtual async Task<(Diagnostic[], string)> GetDiagnosticsAsync(string source, params string[] extraUsings)
{
var sb = new StringBuilder();
foreach (var @using in Usings.Concat(extraUsings))
{
sb.AppendLine($"using {@using};");
}
sb.AppendLine(source);

var sourceText = sb.ToString();
return (await this.GetDiagnosticsFullSourceAsync(sourceText), sourceText);
}

protected async Task<Diagnostic[]> GetDiagnosticsFullSourceAsync(string source)
{
var compilation = await CreateProject(source).GetCompilationAsync();
var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error);

Assert.Empty(errors);

var analyzer = this.CreateDiagnosticAnalyzer();
var compilationWithAnalyzers
= compilation
.WithOptions(
compilation.Options.WithSpecificDiagnosticOptions(
analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default)))
.WithAnalyzers(ImmutableArray.Create(analyzer));

var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();

return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
}

private static Project CreateProject(string source)
{
const string fileName = "Test.cs";

var projectId = ProjectId.CreateNewId(debugName: "TestProject");
var documentId = DocumentId.CreateNewId(projectId, fileName);

var assemblies = new[]
{
typeof(Task).Assembly,
typeof(Orleans.Grain).Assembly
};

var metadataReferences = assemblies
.SelectMany(x => x.GetReferencedAssemblies())
.Select(Assembly.Load)
.Concat(assemblies)
.Select(x => MetadataReference.CreateFromFile(x.Location))
.Cast<MetadataReference>()
.ToList();

var solution = new AdhocWorkspace()
.CurrentSolution
.AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp)
.AddMetadataReferences(projectId, metadataReferences)
.AddDocument(documentId, fileName, SourceText.From(source));

return solution.GetProject(projectId)
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
}
}

0 comments on commit b9ae772

Please sign in to comment.