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

feat: warn about ambiguous properties/fields #107

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
24 changes: 24 additions & 0 deletions docs/rules/DAP046.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# DAP046

Dapper uses normalization for the type properties, and this can cause problems when the properties are not unique.
Recomendation is to use a uniquelly normalized name for each property, which would not conflict with other normalized properties names.

Bad:

``` c#
public class MyType
{
public string First_Name { get; set; }
public string FirstName { get; set; }
}
```

Good:

``` c#
public class MyType
{
public string FirstName { get; set; }
public string FirstNaming { get; set; }
}
```
24 changes: 24 additions & 0 deletions docs/rules/DAP047.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# DAP047

Dapper uses normalization for the type fields, and this can cause problems when the fields are not unique.
Recomendation is to use a uniquelly normalized name for each field, which would not conflict with other normalized fields names.

Bad:

``` c#
public class MyType
{
public string _firstName;
public string first_name;
}
```

Good:

``` c#
public class MyType
{
public string firstName;
public string firstNaming;
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public static readonly DiagnosticDescriptor
UseColumnAttributeNotSpecified = LibraryWarning("DAP043", "[Column] has no effect", "Attach the [UseColumnAttribute] attribute to make Dapper consider [Column]"),
CancellationNotSupported = LibraryWarning("DAP044", "Cancellation not supported", "Vanilla Dapper does not support cancellation parameters in this scenario"),
CancellationDuplicated = LibraryWarning("DAP045", "Duplicate cancellation", "Multiple parameter values cannot define cancellation"),
AmbiguousProperties = LibraryWarning("DAP046", "Ambiguous properties", "Properties have same name '{0}' after normalization and can be conflated"),
AmbiguousFields = LibraryWarning("DAP047", "Ambiguous fields", "Fields have same name '{0}' after normalization and can be conflated"),

// SQL parse specific
GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"),
Expand Down
67 changes: 59 additions & 8 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,13 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq
}

// check the types
var resultType = invoke.GetResultType(flags);
var resultType = invoke.GetResultType(flags);
if (resultType is not null && IdentifyDbType(resultType, out _) is null) // don't warn if handled as an inbuilt
{
var resultMap = MemberMap.CreateForResults(resultType, location);
if (resultMap is not null)
{
if (resultMap.Members.Length != 0)
{
foreach (var member in resultMap.Members)
{
ValidateMember(member, onDiagnostic);
}
}
ValidateMembers(resultMap, onDiagnostic);

// check for single-row value-type usage
if (flags.HasAny(OperationFlags.SingleRow) && !flags.HasAny(OperationFlags.AtLeastOne)
Expand Down Expand Up @@ -856,6 +850,63 @@ enum ParameterMode
? null : new(rowCountHint, rowCountHintMember?.Member.Name, batchSize, cmdProps);
}

static void ValidateMembers(MemberMap memberMap, Action<Diagnostic> onDiagnostic)
{
if (memberMap.Members.Length == 0)
{
return;
}

var normalizedPropertyNames = new Dictionary<string, List<Location>>();
var normalizedFieldNames = new Dictionary<string, List<Location>>();

foreach (var member in memberMap!.Members)
{
ValidateMember(member, onDiagnostic);

// build collection of duplicate normalized memberNames with memberLocations
Location? memberLoc;
if ((memberLoc = member.GetLocation()) is not null)
{
var normalizedName = StringHashing.Normalize(member.CodeName);

switch (member.SymbolKind)
{
case SymbolKind.Property:
if (!normalizedPropertyNames.ContainsKey(normalizedName)) normalizedPropertyNames[normalizedName] = new();
normalizedPropertyNames[normalizedName].Add(memberLoc);
break;

case SymbolKind.Field:
if (!normalizedFieldNames.ContainsKey(normalizedName)) normalizedFieldNames[normalizedName] = new();
normalizedFieldNames[normalizedName].Add(memberLoc);
break;
}
}
}

ReportNormalizedMembersNamesCollisions(normalizedFieldNames, Diagnostics.AmbiguousFields);
ReportNormalizedMembersNamesCollisions(normalizedPropertyNames, Diagnostics.AmbiguousProperties);

void ReportNormalizedMembersNamesCollisions(Dictionary<string, List<Location>> nameMemberLocations, DiagnosticDescriptor diagnosticDescriptor)
{
if (nameMemberLocations.Count == 0) return;

foreach (var entry in nameMemberLocations)
{
if (entry.Value?.Count <= 1)
{
continue;
}

onDiagnostic?.Invoke(Diagnostic.Create(diagnosticDescriptor,
location: entry.Value.First(),
additionalLocations: entry.Value.Skip(1),
messageArgs: entry.Key));
}
}
}

static void ValidateMember(ElementMember member, Action<Diagnostic>? reportDiagnostic)
{
ValidateColumnAttribute();
Expand Down
11 changes: 11 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ public string DbName

public ElementMemberKind Kind { get; }

public SymbolKind SymbolKind => Member.Kind;

public bool IsRowCount => (Kind & ElementMemberKind.RowCount) != 0;
public bool IsRowCountHint => (Kind & ElementMemberKind.RowCountHint) != 0;
public bool IsCancellation => (Kind & ElementMemberKind.Cancellation) != 0;
Expand Down Expand Up @@ -570,6 +572,15 @@ public override bool Equals(object obj) => obj is ElementMember other

public Location? GetLocation()
{
// `ISymbol.Locations` gives the best location of member
// (i.e. for property it will be NAME of an element without modifiers \ getters and setters),
// but it also returns implicitly defined members -> so we need to double check if that can be used
if (Member.Locations.Length > 0)
{
var sourceLocation = Member.Locations.FirstOrDefault(loc => loc.IsInSource);
if (sourceLocation is not null) return sourceLocation;
}

foreach (var node in Member.DeclaringSyntaxReferences)
{
if (node.GetSyntax().GetLocation() is { } loc)
Expand Down
7 changes: 3 additions & 4 deletions test/Dapper.AOT.Test/Verifiers/DAP021.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ class Data
void SomeMethod(DbConnection conn)
=> conn.Execute("someproc", this);

{|#1:public int Id {get;set;}|}
public int {|#1:Id|} {get;set;}

[{|#0:DbValue(Name = "Id")|}] // location should prefer the attrib when exists; dup is primary
public int OtherId {get;set;}


[{|#3:DbValue(Name = "X")|}]
public int Y {get;set;}

[{|#2:DbValue(Name = "X")|}] // dup is primary
public int Z {get;set;}

{|#5:public int casing {get;set;}|}
{|#4:public int CASING {get;set;}|} // dup is primary
public int {|#5:casing|} {get;set;}
public int {|#4:CASING|} {get;set;} // dup is primary

public int fixedCase {get;set;}
[DbValue(Name = "AnotherName")]
Expand Down
2 changes: 1 addition & 1 deletion test/Dapper.AOT.Test/Verifiers/DAP045.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task DapperVanillaNotSupported(DbConnection conn, int id, Cancellat
{
_ = conn.ExecuteAsync("some_proc", cancellationToken);
_ = conn.ExecuteAsync("some_proc", new { id, x = cancellationToken });
_ = conn.ExecuteAsync("some_proc", new { id, x = cancellationToken, {|#0:y = cancellationToken|}});
_ = conn.ExecuteAsync("some_proc", new { id, x = cancellationToken, {|#0:y|} = cancellationToken});
}
}
""",
Expand Down
43 changes: 43 additions & 0 deletions test/Dapper.AOT.Test/Verifiers/DAP046.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Dapper.CodeAnalysis;
using System.Threading.Tasks;
using Xunit;
using static Dapper.CodeAnalysis.DapperAnalyzer;

namespace Dapper.AOT.Test.Verifiers;

public class DAP046 : Verifier<DapperAnalyzer>
{
[Theory]
[InlineData("[DapperAot(true)]")]
[InlineData("[DapperAot(false)]")]
public Task AmbiguousProperties(string dapperAotAttribute) => CSVerifyAsync($$"""
using Dapper;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

public class Data
{
public string firstName; // THIS IS FINE AND SHOULD NOT REPORT DIAGNOSTIC -> IT IS A FIELD,

public string {|#0:First_Name|} { get; set; }
public string {|#1:FirstName|} { get; set; }
}

{{dapperAotAttribute}}
class WithoutAot
{
public void DapperCode(DbConnection conn)
{
_ = conn.Query<Data>("select_proc");
}
}
""",
DefaultConfig,
[
Diagnostic(Diagnostics.AmbiguousProperties)
.WithArguments("firstname")
.WithLocation(0).WithLocation(1)
]
);
}
43 changes: 43 additions & 0 deletions test/Dapper.AOT.Test/Verifiers/DAP047.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Dapper.CodeAnalysis;
using System.Threading.Tasks;
using Xunit;
using static Dapper.CodeAnalysis.DapperAnalyzer;

namespace Dapper.AOT.Test.Verifiers;

public class DAP047 : Verifier<DapperAnalyzer>
{
[Theory]
[InlineData("[DapperAot(true)]")]
[InlineData("[DapperAot(false)]")]
public Task AmbiguousFields(string dapperAotAttribute) => CSVerifyAsync($$"""
using Dapper;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

public class Data
{
public string FirstName { get; set; } // THIS IS FINE AND SHOULD NOT REPORT DIAGNOSTIC -> IT IS A PROPERTY,

public string {|#0:first_name|};
public string {|#1:_firstName|};
}

{{dapperAotAttribute}}
class WithoutAot
{
public void DapperCode(DbConnection conn)
{
_ = conn.Query<Data>("select_proc");
}
}
""",
DefaultConfig,
[
Diagnostic(Diagnostics.AmbiguousFields)
.WithArguments("firstname")
.WithLocation(0).WithLocation(1)
]
);
}
Loading