Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyzer: Added binding analyzer, updated entity interface content analyzer, fixed argument exception #1968

Merged
merged 8 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public static void ReportProblems(CompilationAnalysisContext context, EntityInte

foreach (var node in interfaceChildNodes)
{
if (!node.IsKind(SyntaxKind.MethodDeclaration))
// Only methods and implemented interfaces are allowed in an entity interface.
if (!node.IsKind(SyntaxKind.MethodDeclaration) && !node.IsKind(SyntaxKind.BaseList))
amdeel marked this conversation as resolved.
Show resolved Hide resolved
{
var diagnostic = Diagnostic.Create(NotAMethodRule, node.GetLocation(), node);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BindingAnalyzer
{
public const string DiagnosticId = "DF0112";

private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.BindingAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.BindingAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
private const string Category = SupportedCategories.Orchestrator;
public const DiagnosticSeverity Severity = DiagnosticSeverity.Warning;

public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, Severity, isEnabledByDefault: true);

public static bool RegisterDiagnostic(CompilationAnalysisContext context, SyntaxNode method)
{
var diagnosedIssue = false;

var parameterList = method.ChildNodes().First(x => x.IsKind(SyntaxKind.ParameterList));
amdeel marked this conversation as resolved.
Show resolved Hide resolved

foreach (SyntaxNode descendant in parameterList.DescendantNodes())
{
if (descendant is AttributeSyntax attribute)
{
var attributeName = attribute.Name.ToString();
if (attributeName != "OrchestrationTrigger")
{
var diagnostic = Diagnostic.Create(Rule, attribute.GetLocation(), attributeName);

context.ReportDiagnostic(diagnostic);

diagnosedIssue = true;
}
}
}

return diagnosedIssue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
TimerAnalyzer.V1Rule,
TimerAnalyzer.V2Rule,
CancellationTokenAnalyzer.Rule,
BindingAnalyzer.Rule,
MethodInvocationAnalyzer.Rule);
}
}
Expand Down Expand Up @@ -63,7 +64,8 @@ private void RegisterAnalyzers(CompilationAnalysisContext context)
| IOTypesAnalyzer.RegisterDiagnostic(context, semanticModel, methodDeclaration)
| ThreadTaskAnalyzer.RegisterDiagnostic(context, semanticModel, methodDeclaration)
| TimerAnalyzer.RegisterDiagnostic(context, semanticModel, methodDeclaration)
| CancellationTokenAnalyzer.RegisterDiagnostic(context, methodDeclaration))
| CancellationTokenAnalyzer.RegisterDiagnostic(context, methodDeclaration)
| BindingAnalyzer.RegisterDiagnostic(context, methodDeclaration))
{
methodInvocationAnalyzer.RegisterDiagnostics(context, methodInformation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ private void FindInvokedMethods(SemanticModel semanticModel, MethodInformation p
&& invokedMethodDeclaration is MethodDeclarationSyntax
&& SyntaxNodeUtils.TryGetISymbol(semanticModel, invocation, out ISymbol invokedSymbol))
{

if (this.orchestratorMethodDeclarations.TryGetValue(invokedSymbol, out MethodInformation existingMethodInformation))
{
existingMethodInformation.Invocations.Add(invocation);
Expand All @@ -67,18 +66,21 @@ private void FindInvokedMethods(SemanticModel semanticModel, MethodInformation p
}
else
{
var invokedMethodInformation = new MethodInformation()
if (SyntaxNodeUtils.TryGetSemanticModelForSyntaxTree(semanticModel, invokedMethodDeclaration, out SemanticModel invocationModel))
{
SemanticModel = semanticModel,
Declaration = invokedMethodDeclaration,
DeclarationSymbol = invokedSymbol,
Invocations = new List<InvocationExpressionSyntax>() { invocation },
Parents = new HashSet<MethodInformation>(new List<MethodInformation>() { parentMethodInformation }),
};
var invokedMethodInformation = new MethodInformation()
{
SemanticModel = invocationModel,
Declaration = invokedMethodDeclaration,
DeclarationSymbol = invokedSymbol,
Invocations = new List<InvocationExpressionSyntax>() { invocation },
Parents = new HashSet<MethodInformation>(new List<MethodInformation>() { parentMethodInformation }),
};

this.orchestratorMethodDeclarations.Add(invokedSymbol, invokedMethodInformation);
this.orchestratorMethodDeclarations.Add(invokedSymbol, invokedMethodInformation);

FindInvokedMethods(semanticModel, invokedMethodInformation);
FindInvokedMethods(semanticModel, invokedMethodInformation);
}
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/WebJobs.Extensions.DurableTask.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/WebJobs.Extensions.DurableTask.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@
<data name="AwaitAnalyzerTitle" xml:space="preserve">
<value>Code used in an orchestrator must not use await on non-Durable Functions methods.</value>
</data>
<data name="BindingAnalyzerMessageFormat" xml:space="preserve">
<value>Using binding '{0}' with an OrchestrationTrigger method violates deterministic code constraints.</value>
</data>
<data name="BindingAnalyzerTitle" xml:space="preserve">
<value>OrchestrationTrigger methods must not use any other binding.</value>
</data>
<data name="CancellationTokenAnalyzerMessageFormat" xml:space="preserve">
<value>CancellationToken should not be used as an orchestrator function parameter.</value>
</data>
Expand Down
28 changes: 24 additions & 4 deletions src/WebJobs.Extensions.DurableTask.Analyzers/SyntaxNodeUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,30 @@ public static bool TryGetSemanticModelForSyntaxTree(SemanticModel model, SyntaxN
newModel = null;
return false;
}

newModel = model.SyntaxTree == node.SyntaxTree
? model
: model.Compilation.GetSemanticModel(node.SyntaxTree);

try
{
var compilation = model.Compilation;
if (!compilation.ContainsSyntaxTree(node.SyntaxTree))
{
var newComplilation = model.Compilation.AddSyntaxTrees(node.SyntaxTree);
newModel = newComplilation.GetSemanticModel(node.SyntaxTree);
}
else
{
newModel = model.SyntaxTree == node.SyntaxTree
? model
: model.Compilation.GetSemanticModel(node.SyntaxTree);
}
}
catch( ArgumentException e) when (e.Message.Contains("Inconsistent language versions"))
{
// model.Compilation.AddSyntaxTrees(node.SyntaxTree) can sometimes throw an ArgumentException with this message if the SyntaxTree
// that is being added has an inconsistent language version with the compilation.
newModel = null;
return false;
}

return newModel != null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<PropertyGroup>
<PackageId>Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers</PackageId>
<PackageVersion>0.4.0</PackageVersion>
<PackageVersion>0.4.1</PackageVersion>
<Authors>Microsoft</Authors>
<PackageLicenseUrl>https://go.microsoft.com/fwlink/?linkid=2028464</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/Azure/azure-functions-durable-extension</PackageProjectUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ public static async Task Run(
}
}

public interface IEntityExample
public interface IEntityExample : IEntityMultipleInterfaces
{
public static void methodTest();
}

public interface IEntityMultipleInterfaces
{
}

}";

VerifyCSharpDiagnostic(test);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestHelper;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Analyzers.Test.Orchestrator
{
[TestClass]
public class BindingAnalyzerTersts : CodeFixVerifier
{
private static readonly string DiagnosticId = BindingAnalyzer.DiagnosticId;
private static readonly DiagnosticSeverity Severity = DiagnosticSeverity.Warning;

[TestMethod]
public void Binding_NoDiagnosticTestCase()
{
var test = @"
using System;
using System.Threading;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;

namespace VSSample
{
public static class HelloSequence
{
[FunctionName(""BindingAnalyzerTestCases"")]
public static async Task Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
}
}
}";

VerifyCSharpDiagnostic(test);
}

[TestMethod]
public void Binding_DurableClientTest()
{
var test = @"
using System;
using System.Threading;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;

namespace VSSample
{
public static class HelloSequence
{
[FunctionName(""BindingAnalyzerTestCases"")]
public static async Task Run(
[OrchestrationTrigger] IDurableOrchestrationContext context, [DurableClient] IDurableClient client)
{
}
}
}";
var expectedDiagnostics = new DiagnosticResult
{
Id = DiagnosticId,
Message = string.Format(Resources.BindingAnalyzerMessageFormat, "DurableClient"),
Severity = Severity,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 13, 75)
}
};

VerifyCSharpDiagnostic(test, expectedDiagnostics);
}

protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new DeterministicMethodAnalyzer();
}
}
}