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

Warn if eventsubscribers don't use var for parameters defined as var. #676

Merged
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
35 changes: 35 additions & 0 deletions BusinessCentral.LinterCop.Test/Rule0065.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace BusinessCentral.LinterCop.Test;

public class Rule0065
{
private string _testCaseDir = "";

[SetUp]
public void Setup()
{
_testCaseDir = Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName,
"TestCases", "Rule0065");
}

[Test]
[TestCase("1")]
public async Task HasDiagnostic(string testCase)
{
var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "HasDiagnostic", $"{testCase}.al"))
.ConfigureAwait(false);

var fixture = RoslynFixtureFactory.Create<Rule0065CheckEventSubscriberVarKeyword>();
fixture.HasDiagnostic(code, Rule0065CheckEventSubscriberVarKeyword.DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id);
}

[Test]
[TestCase("1")]
public async Task NoDiagnostic(string testCase)
{
var code = await File.ReadAllTextAsync(Path.Combine(_testCaseDir, "NoDiagnostic", $"{testCase}.al"))
.ConfigureAwait(false);

var fixture = RoslynFixtureFactory.Create<Rule0065CheckEventSubscriberVarKeyword>();
fixture.NoDiagnosticAtMarker(code, Rule0065CheckEventSubscriberVarKeyword.DiagnosticDescriptors.Rule0065EventSubscriberVarCheck.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
codeunit 50000 MyCodeunit
{
[IntegrationEvent(false, false)]
local procedure MyProcedure(var MyTable: Record MyTable; var MyTable3: Record MyTable; var MyTable2: Record MyTable)
begin
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, MyProcedure, '', false, false)]
local procedure MyProcedure2(var MyTable: Record MyTable; var Mytable2: Record MyTable; [|MyTable3|]: Record MyTable)
begin

end;
}

table 50000 MyTable
{
Caption = '', Locked = true;

fields
{
field(1; MyField; Integer)
{
Caption = '', Locked = true;
DataClassification = ToBeClassified;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
codeunit 50000 MyCodeunit
{
[IntegrationEvent(false, false)]
local procedure MyProcedure(var MyTable: Record MyTable; mytable2: Record MyTable)
begin
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, MyProcedure, '', false, false)]
local procedure MyProcedure2(var [|MyTable|]: Record MyTable; MyTable2: Record MyTable)
begin

end;
}

table 50000 MyTable
{
Caption = '', Locked = true;

fields
{
field(1; MyField; Integer)
{
Caption = '', Locked = true;
DataClassification = ToBeClassified;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Microsoft.Dynamics.Nav.CodeAnalysis;
#nullable enable
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using Microsoft.Dynamics.Nav.CodeAnalysis.Utilities;
using System.Collections.Immutable;
using System.Reflection;

#nullable enable
namespace BusinessCentral.LinterCop.Design
{
public delegate bool Filter(IGrouping<int, Rule0044AnalyzeTransferFields.Field> fieldGroup);
Expand Down Expand Up @@ -305,7 +305,7 @@ private Dictionary<string, TableExtensionSyntax> GetTableExtensions(Compilation

private Table GetTableWithFieldsByTableName(Compilation compilation, string tableName, Dictionary<string, TableExtensionSyntax>? tableExtensions = null)
{
IApplicationObjectTypeSymbol tableSymbol = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName).FirstOrDefault();
IApplicationObjectTypeSymbol? tableSymbol = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Table, tableName).FirstOrDefault();

Table table = new Table(tableName);

Expand Down Expand Up @@ -337,7 +337,7 @@ private Table GetTableWithFieldsByTableName(Compilation compilation, string tabl

private string? GetSourceTableByPageName(Compilation compilation, string pageName)
{
IApplicationObjectTypeSymbol pageSymbol = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Page, pageName).FirstOrDefault();
IApplicationObjectTypeSymbol? pageSymbol = compilation.GetApplicationObjectTypeSymbolsByNameAcrossModules(SymbolKind.Page, pageName).FirstOrDefault();

if (pageSymbol != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#nullable enable
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using Microsoft.Dynamics.Nav.CodeAnalysis.InternalSyntax;

namespace BusinessCentral.LinterCop.Design;

[DiagnosticAnalyzer]
public class Rule0065CheckEventSubscriberVarKeyword : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create<DiagnosticDescriptor>(DiagnosticDescriptors.Rule0065EventSubscriberVarCheck);

public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(CheckForEventSubscriberVar, SymbolKind.Method);
}

private void CheckForEventSubscriberVar(SymbolAnalysisContext context)
{
var methodSymbol = (IMethodSymbol)context.Symbol;
var eventSubscriberAttribute = methodSymbol.Attributes
.FirstOrDefault(attr => attr.AttributeKind == AttributeKind.EventSubscriber);

if (eventSubscriberAttribute == null)
{
return;
}

var method = GetReferencedEventPublisherMethodSymbol(context, eventSubscriberAttribute);
if (method == null)
{
return;
}

var publisherParameters = method.Parameters;

foreach (var subscriberParameter in methodSymbol.Parameters)
{
var publisherParameter = publisherParameters.FirstOrDefault(p => p.Name.Equals(subscriberParameter.Name, StringComparison.OrdinalIgnoreCase));
if (publisherParameter == null)
{
continue;
}

if (publisherParameter.IsVar && !subscriberParameter.IsVar)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.Rule0065EventSubscriberVarCheck,
subscriberParameter.GetLocation(),
new object[] { subscriberParameter.Name}));
}
}
}

private static IMethodSymbol? GetReferencedEventPublisherMethodSymbol(SymbolAnalysisContext context, IAttributeSymbol eventSubscriberAttribute)
{
var applicationObject = eventSubscriberAttribute.GetReferencedApplicationObject();
if (applicationObject == null)
{
return null;
}

if (eventSubscriberAttribute.Arguments.Length < 3)
{
return null;
}

var eventName = eventSubscriberAttribute.Arguments[2].ValueText;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (eventName == null)
{
return null;
}

return applicationObject.GetFirstMethod(eventName, context.Compilation);
}

public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor Rule0065EventSubscriberVarCheck = new(
id: LinterCopAnalyzers.AnalyzerPrefix + "0065",
title: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckTitle"),
messageFormat: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckFormat"),
category: "Design",
defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true,
description: LinterCopAnalyzers.GetLocalizableString("Rule0065EventSubscriberVarCheckDescription"),
helpLinkUri: "https://github.com/StefanMaron/BusinessCentral.LinterCop/wiki/LC0065");
}
}
43 changes: 34 additions & 9 deletions BusinessCentral.LinterCop/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.Dynamics.Nav.CodeAnalysis;
#nullable enable
using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using System.Text.RegularExpressions;
using System.Threading;


namespace BusinessCentral.LinterCop
Expand All @@ -21,27 +21,52 @@ internal static class Extensions

internal static bool IsValidCamelCase(this string str) => !string.IsNullOrEmpty(str) && Extensions.CamelCaseRegex.IsMatch(str);

internal static IdentifierNameSyntax GetIdentifierNameSyntax(
internal static IdentifierNameSyntax? GetIdentifierNameSyntax(
this SyntaxNodeAnalysisContext context)
{
if (context.Node.IsKind(SyntaxKind.IdentifierName))
return (IdentifierNameSyntax) context.Node;
return !context.Node.IsKind(SyntaxKind.IdentifierNameOrEmpty) ? (IdentifierNameSyntax) null : ((IdentifierNameOrEmptySyntax) context.Node).IdentifierName;
return (IdentifierNameSyntax?) context.Node;
return !context.Node.IsKind(SyntaxKind.IdentifierNameOrEmpty) ? (IdentifierNameSyntax?) null : ((IdentifierNameOrEmptySyntax) context.Node).IdentifierName;
}

internal static bool TryGetSymbolFromIdentifier(
SyntaxNodeAnalysisContext syntaxNodeAnalysisContext,
IdentifierNameSyntax identifierName,
SymbolKind symbolKind,
out ISymbol symbol)
out ISymbol? symbol)
{
symbol = (ISymbol) null;
symbol = (ISymbol?) null;
SymbolInfo symbolInfo = syntaxNodeAnalysisContext.SemanticModel.GetSymbolInfo((ExpressionSyntax) identifierName, new CancellationToken());
ISymbol symbol1 = symbolInfo.Symbol;
ISymbol? symbol1 = symbolInfo.Symbol;
if ((symbol1 != null ? (symbol1.Kind != symbolKind ? 1 : 0) : 1) != 0)
return false;
return false;
symbol = symbolInfo.Symbol;
return true;
}

internal static IMethodSymbol? GetFirstMethod(
this IApplicationObjectTypeSymbol applicationObject,
string memberName,
Compilation compilation)
{
foreach (ISymbol member in applicationObject.GetMembers(memberName))
{
if (member.Kind == SymbolKind.Method)
return (IMethodSymbol) member;
}
foreach (var extensionsAcrossModule in compilation.GetApplicationObjectExtensionTypeSymbolsAcrossModules(applicationObject))
{
foreach (var member in extensionsAcrossModule.GetMembers(memberName))
{
if (member.Kind == SymbolKind.Method)
{
IMethodSymbol firstMethod = (IMethodSymbol) member;
return firstMethod;
}
}
}
return null;
}

}
}
10 changes: 10 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.Designer.cs

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

9 changes: 9 additions & 0 deletions BusinessCentral.LinterCop/LinterCopAnalyzers.resx
Original file line number Diff line number Diff line change
Expand Up @@ -690,4 +690,13 @@
<data name="Rule0064UseTableFieldToolTipInsteadOfPageFieldToolTipDescription" xml:space="preserve">
<value>Use table field ToolTip instead of page field ToolTip.</value>
</data>
<data name="Rule0065EventSubscriberVarCheckTitle" xml:space="preserve">
<value>Event subscriber var keyword mismatch</value>
</data>
<data name="Rule0065EventSubscriberVarCheckFormat" xml:space="preserve">
<value>Parameter '{0}' must use the 'var' keyword if the publisher parameter is 'var'.</value>
</data>
<data name="Rule0065EventSubscriberVarCheckDescription" xml:space="preserve">
<value>Ensures that event subscriber methods use 'var' keyword for parameters as defined by event publisher.</value>
</data>
</root>