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

Implement S6562: Always set the DateTimeKind when creating a new DateTime object #7527

Merged
merged 12 commits into from
Jul 4, 2023
Prev Previous commit
Next Next commit
Improve performance by checking the type name first
  • Loading branch information
csaba-sagi-sonarsource committed Jul 3, 2023
commit c40dafd7fecd3e0d21a5773831f8b5ede7c9110d
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public override IEnumerable<SyntaxNode> EnumMembers(SyntaxNode @enum) =>
public override SyntaxToken? InvocationIdentifier(SyntaxNode invocation) =>
invocation == null ? null : Cast<InvocationExpressionSyntax>(invocation).GetMethodCallIdentifier();

public override SyntaxToken? ObjectCreationTypeIdentifier(SyntaxNode objectCreation) =>
objectCreation == null ? null : Cast<ObjectCreationExpressionSyntax>(objectCreation).GetObjectCreationTypeIdentifier();

public override ImmutableArray<SyntaxToken> LocalDeclarationIdentifiers(SyntaxNode node) =>
Cast<LocalDeclarationStatementSyntax>(node).Declaration.Variables.Select(x => x.Identifier).ToImmutableArray();

Expand Down
29 changes: 5 additions & 24 deletions analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSyntaxHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,31 +185,11 @@ public static bool ContainsMethodInvocation(this BaseMethodDeclarationSyntax met
}
}

public static SyntaxToken? GetMethodCallIdentifier(this InvocationExpressionSyntax invocation)
{
if (invocation == null)
{
return null;
}
var expression = invocation.Expression;
switch (expression.Kind())
{
case SyntaxKind.IdentifierName:
// method()
return ((IdentifierNameSyntax)expression).Identifier;
public static SyntaxToken? GetMethodCallIdentifier(this InvocationExpressionSyntax invocation) =>
invocation == null ? null : GetIdentifier(invocation.Expression);

case SyntaxKind.SimpleMemberAccessExpression:
// foo.method()
return ((MemberAccessExpressionSyntax)expression).Name.Identifier;

case SyntaxKind.MemberBindingExpression:
// foo?.method()
return ((MemberBindingExpressionSyntax)expression).Name.Identifier;

default:
return null;
}
}
public static SyntaxToken? GetObjectCreationTypeIdentifier(this ObjectCreationExpressionSyntax objectCreation) =>
objectCreation == null ? null : GetIdentifier(objectCreation.Type);

public static bool IsMethodInvocation(this InvocationExpressionSyntax invocation, KnownType type, string methodName, SemanticModel semanticModel) =>
invocation.Expression.NameIs(methodName) &&
Expand Down Expand Up @@ -250,6 +230,7 @@ public static bool HasBodyOrExpressionBody(this AccessorDeclarationSyntax node)
DelegateDeclarationSyntax { Identifier: var identifier } => identifier,
DestructorDeclarationSyntax { Identifier: var identifier } => identifier,
EnumMemberDeclarationSyntax { Identifier: var identifier } => identifier,
IdentifierNameSyntax { Identifier: var identifier } => identifier,
IndexerDeclarationSyntax { ThisKeyword: var thisKeyword } => thisKeyword,
InvocationExpressionSyntax
{
Expand Down
17 changes: 17 additions & 0 deletions analyzers/src/SonarAnalyzer.CSharp/Rules/AlwaysSetDateTimeKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,21 @@ namespace SonarAnalyzer.Rules.CSharp;
public sealed class AlwaysSetDateTimeKind : AlwaysSetDateTimeKindBase<SyntaxKind>
{
protected override ILanguageFacade<SyntaxKind> Language => CSharpFacade.Instance;

protected override SyntaxKind ObjectCreationExpression => SyntaxKind.ObjectCreationExpression;

protected override string[] ValidNames { get; } = new[] { "DateTime" };

protected override void Initialize(SonarAnalysisContext context)
{
base.Initialize(context);
context.RegisterNodeAction(c =>
{
if (IsDateTimeConstructorWithoutKindParameter(c.Node, c.SemanticModel))
{
c.ReportIssue(Diagnostic.Create(Rule, c.Node.GetLocation()));
}
},
SyntaxKindEx.ImplicitObjectCreationExpression);
}
}
1 change: 1 addition & 0 deletions analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public abstract class SyntaxFacade<TSyntaxKind>
public abstract SyntaxNode CastExpression(SyntaxNode cast);
public abstract IEnumerable<SyntaxNode> EnumMembers(SyntaxNode @enum);
public abstract SyntaxToken? InvocationIdentifier(SyntaxNode invocation);
public abstract SyntaxToken? ObjectCreationTypeIdentifier(SyntaxNode objectCreation);
public abstract ImmutableArray<SyntaxToken> LocalDeclarationIdentifiers(SyntaxNode node);
public abstract ImmutableArray<SyntaxToken> FieldDeclarationIdentifiers(SyntaxNode node);
public abstract TSyntaxKind[] ModifierKinds(SyntaxNode node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,27 @@ public abstract class AlwaysSetDateTimeKindBase<TSyntaxKind> : SonarDiagnosticAn
{
private const string DiagnosticId = "S6562";

protected abstract TSyntaxKind ObjectCreationExpression { get; }
protected abstract string[] ValidNames { get; }

protected override string MessageFormat => "Provide the \"DateTimeKind\" when creating this object.";

protected AlwaysSetDateTimeKindBase() : base(DiagnosticId) { }

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(Language.GeneratedCodeRecognizer, c =>
{
if (c.SemanticModel.GetSymbolInfo(c.Node).Symbol is IMethodSymbol ctor
&& ctor.IsInType(KnownType.System_DateTime)
&& !ctor.Parameters.Any(x => x.IsType(KnownType.System_DateTimeKind)))
if (Language.Syntax.ObjectCreationTypeIdentifier(c.Node) is { } identifier
&& ValidNames.Any(x => x.Equals(identifier.ValueText, Language.NameComparison))
&& IsDateTimeConstructorWithoutKindParameter(c.Node, c.SemanticModel))
{
c.ReportIssue(Diagnostic.Create(Rule, c.Node.GetLocation()));
}
},
Language.SyntaxKind.ObjectCreationExpressions);
ObjectCreationExpression);

protected static bool IsDateTimeConstructorWithoutKindParameter(SyntaxNode objectCreation, SemanticModel semanticModel) =>
semanticModel.GetSymbolInfo(objectCreation).Symbol is IMethodSymbol ctor
&& ctor.IsInType(KnownType.System_DateTime)
&& !ctor.Parameters.Any(x => x.IsType(KnownType.System_DateTimeKind));
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ public override IEnumerable<SyntaxNode> EnumMembers(SyntaxNode @enum) =>
public override SyntaxToken? InvocationIdentifier(SyntaxNode invocation) =>
invocation == null ? null : Cast<InvocationExpressionSyntax>(invocation).GetMethodCallIdentifier();

public override SyntaxToken? ObjectCreationTypeIdentifier(SyntaxNode objectCreation) =>
objectCreation == null ? null : Cast<ObjectCreationExpressionSyntax>(objectCreation).GetObjectCreationTypeIdentifier();

public override ImmutableArray<SyntaxToken> LocalDeclarationIdentifiers(SyntaxNode node) =>
Cast<LocalDeclarationStatementSyntax>(node).Declarators.SelectMany(d => d.Names.Select(n => n.Identifier)).ToImmutableArray();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,32 +104,23 @@ public static bool IsAnyKind(this SyntaxTrivia syntaxTrivia, params SyntaxKind[]
public static bool AnyOfKind(this IEnumerable<SyntaxNode> nodes, SyntaxKind kind) =>
nodes.Any(n => n.RawKind == (int)kind);

public static SyntaxToken? GetMethodCallIdentifier(this InvocationExpressionSyntax invocation)
{
if (invocation == null ||
invocation.Expression == null)
{
return null;
}
public static SyntaxToken? GetMethodCallIdentifier(this InvocationExpressionSyntax invocation) =>
invocation == null
|| invocation.Expression == null
? null
: GetIdentifier(invocation.Expression);

public static SyntaxToken? GetObjectCreationTypeIdentifier(this ObjectCreationExpressionSyntax objectCreation) =>
objectCreation == null
|| objectCreation.Type == null
? null
: GetIdentifier(objectCreation.Type);

var expressionType = invocation.Expression.Kind();
// in vb.net when using the null - conditional operator (e.g.handle?.IsClosed), the parser
// will generate a SimpleMemberAccessExpression and not a MemberBindingExpressionSyntax like for C#
switch (expressionType)
{
case SyntaxKind.IdentifierName:
return ((IdentifierNameSyntax)invocation.Expression).Identifier;
case SyntaxKind.SimpleMemberAccessExpression:
return ((MemberAccessExpressionSyntax)invocation.Expression).Name.Identifier;
default:
return null;
}
}
public static bool IsMethodInvocation(this InvocationExpressionSyntax expression, KnownType type, string methodName, SemanticModel semanticModel) =>
semanticModel.GetSymbolInfo(expression).Symbol is IMethodSymbol methodSymbol &&
methodSymbol.IsInType(type) &&
// vbnet is case insensitive
methodName.Equals(methodSymbol.Name, System.StringComparison.InvariantCultureIgnoreCase);
methodName.Equals(methodSymbol.Name, StringComparison.InvariantCultureIgnoreCase);

public static bool IsOnBase(this ExpressionSyntax expression) =>
IsOn(expression, SyntaxKind.MyBaseExpression);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ namespace SonarAnalyzer.Rules.VisualBasic;
public sealed class AlwaysSetDateTimeKind : AlwaysSetDateTimeKindBase<SyntaxKind>
{
protected override ILanguageFacade<SyntaxKind> Language => VisualBasicFacade.Instance;

protected override SyntaxKind ObjectCreationExpression => SyntaxKind.ObjectCreationExpression;

protected override string[] ValidNames { get; } = new[] { "DateTime", "Date" };
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using MyAlias = System.DateTime;

public class Program
{
Expand All @@ -14,6 +15,8 @@ public void Noncompliant()
dt = new DateTime(1994, 07, 05, 16, 23, 00, 42); // Noncompliant
dt = new DateTime(1994, 07, 05, 16, 23, 00, 42, new GregorianCalendar()); // Noncompliant
dt = new DateTime(1994, 07, 05, 16, 23, 00, 42, new GregorianCalendar()); // Noncompliant
dt = new MyAlias(); // FN
dt = new System.DateTime(); // Noncompliant
}

public void Compliant()
Expand All @@ -26,3 +29,16 @@ public void Compliant()
dt = new DateTime(1994, 07, 05, 16, 23, 00, 42, DateTimeKind.Unspecified);
}
}

public class FakeDateTime
{
private class DateTime
{

}

private void Compliant()
{
var dt = new DateTime();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
Imports System.Globalization
Imports MyAlias = System.DateTime

Public Class Program

Public Sub Noncompliant()
Dim dt = New DateTime() ' Noncompliant
Dim dt = New DateTime() ' Noncompliant {{Provide the "DateTimeKind" when creating this object.}}
dt = New Date() ' Noncompliant
dt = New dATEtIME() ' Noncompliant
dt = New DateTime(1623) ' Noncompliant
Expand All @@ -14,6 +15,8 @@ Public Class Program
dt = New DateTime(1994, 7, 5, 16, 23, 0, 42) ' Noncompliant
dt = New DateTime(1994, 7, 5, 16, 23, 0, 42, New GregorianCalendar()) ' Noncompliant
dt = New DateTime(1994, 7, 5, 16, 23, 0, 42, New GregorianCalendar()) ' Noncompliant
dt = New MyAlias() ' FN
dt = New System.DateTime() ' Noncompliant
End Sub

Public Sub Compiant()
Expand All @@ -26,3 +29,15 @@ Public Class Program
End Sub

End Class

Class FakeDateTime

Private Class DateTime

End Class

Private Sub Compliant()
Dim dt = New DateTime()
End Sub

End Class