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

Type-handler registered at runtime

`SqlMapper.AddTypeHandler` mutates a process-global registry when it executes. The generator
runs at compile time, so it cannot see that call - and generated code therefore binds the value
the way it would have without any handler at all: usually a raw `p.Value = ...`, which the
provider rejects ("No mapping exists from object type ..."), or a read that never reaches your
`Parse`.

``` csharp
SqlMapper.AddTypeHandler(new LocalDateHandler()); // DAP053
```

**The fix**: declare the handler instead, so the generator can bake the dispatch:

``` csharp
[module: TypeHandler(typeof(LocalDate), typeof(LocalDateHandler))]
```

The declarative form is per-assembly rather than process-global, is deterministic (no
startup-ordering races), and shows up in review. An existing handler written against vanilla
Dapper (`SqlMapper.TypeHandler<T>` / `SqlMapper.ITypeHandler`) can be named as-is: generated
code adapts it to Dapper.AOT's `IDbValueHandler<T>`.

A handler written for Dapper.AOT directly implements `IDbValueHandler<T>`, usually by
inheriting `DbValueHandler<T>`:

``` csharp
public sealed class LocalDateHandler : DbValueHandler<LocalDate>
{
protected override void Configure(DbParameter parameter) => parameter.DbType = DbType.Date;
protected override void SetValueCore(DbParameter parameter, LocalDate value)
=> parameter.Value = new DateTime(value.Year, value.Month, value.Day);
protected override LocalDate Parse(object? value) { /* ... */ }
}
```

Note the limit of this diagnostic: it only sees `AddTypeHandler` calls in the compilation being
built. A registration performed by a referenced library, or through a helper that hides the
call, cannot be reported - so its absence is not proof that generated code sees every handler.
33 changes: 33 additions & 0 deletions docs/rules/DAP054.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# DAP054

Type-handler cannot be used

A `[TypeHandler(typeof(TValue), typeof(THandler))]` registration named a handler that generated
code cannot use, so the registration is **ignored** - and values of that type bind as if it were
not there, which is usually the raw `p.Value = ...` the handler existed to replace.

The reasons, and what each means:

| message | what to do |
| --- | --- |
| implements neither `IDbValueHandler<T>` nor `SqlMapper.ITypeHandler` | inherit `DbValueHandler<TValue>`, or name a vanilla Dapper handler (generated code adapts it) |
| handles '`X`', not '`Y`' | the handler and the registered value type disagree - fix whichever is wrong |
| it has no public parameterless constructor | generated code constructs the handler itself; give it one, or use a handler that needs no state |
| it is abstract / it is static | name a concrete, instantiable type |
| it is not accessible to generated code | make it `public` or `internal` (generated code lives in your own assembly) |
| the handled type is not accessible to generated code | same, for `TValue` |

``` csharp
[module: TypeHandler(typeof(LocalDate), typeof(LocalDateHandler))] // DAP054 if the handler is wrong

public sealed class LocalDateHandler : DbValueHandler<LocalDate> // <-- the shape it wants
{
protected override void SetValueCore(DbParameter parameter, LocalDate value)
=> parameter.Value = new DateTime(value.Year, value.Month, value.Day);
protected override LocalDate Parse(object? value) { /* ... */ }
}
```

This is a warning rather than an error because the code still compiles and runs - it just binds
without the handler. It is reported precisely because that failure is otherwise silent, which is
the thing the declarative registration exists to remove.
23 changes: 23 additions & 0 deletions docs/rules/DAP055.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# DAP055

Duplicate type-handler

Two different handlers were registered for the same type. There is no correct way to choose
between them, so the **first registration wins** - module scope before assembly scope, source
order within each - and the later one is ignored.

``` csharp
[module: TypeHandler(typeof(LocalDate), typeof(FirstHandler))]
[module: TypeHandler(typeof(LocalDate), typeof(SecondHandler))] // DAP055: SecondHandler is ignored
```

**The fix**: delete whichever registration is wrong. If both are wanted for different reasons -
say, one for reads and one for writes - that belongs in a single handler, since generated code
dispatches both directions through the same instance.

Registering the *same* handler twice is not reported: it is redundant, but the outcome is
identical either way.

A dropped duplicate is not also checked for usability, so a registration that is both duplicated
and unusable gets this one message rather than this plus DAP054 - the second registration is
ignored regardless of whether it would have worked.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public static readonly DiagnosticDescriptor
DapperAotTupleParameter = LibraryInfo("DAP014", "Tuple-type parameter", "Tuple-type parameters are not currently supported"),
UntypedParameter = LibraryInfo("DAP015", "Untyped parameter", "The parameter type could not be resolved"),
GenericTypeParameter = LibraryInfo("DAP016", "Generic type parameter", "Generic type parameters ({0}) are not currently supported"),
DuplicateTypeHandler = LibraryWarning("DAP055", "Duplicate type-handler",
"Type '{0}' has more than one handler registered ('{1}' and '{2}'); '{2}' will be ignored"),
UnusableTypeHandler = LibraryWarning("DAP054", "Type-handler cannot be used",
"'{0}' cannot be used as a type-handler for '{1}' because {2}; the registration is ignored, and values of '{1}' bind as if it were not there"),
RuntimeTypeHandlerRegistration = LibraryWarning("DAP053", "Type-handler registered at runtime",
"SqlMapper.AddTypeHandler for '{0}' is invisible to Dapper.AOT, so generated code binds the value without it; declare it instead with [module: TypeHandler(typeof({0}), typeof({1}))]"),
FeatureNeedsNewerDapper = LibraryInfo("DAP052", "Feature requires a newer Dapper", "Dapper.AOT support for {0} needs '{1}', which the referenced Dapper version does not expose; the call-site is left on vanilla Dapper (which will not work under native AOT) - update the Dapper package to enable this"),
NestedInGenericType = LibraryWarning("DAP051", "Type is only generic by containment", "Type '{0}' is generic only because it is declared inside generic type '{1}'; if it does not need the enclosing type parameters, move it to non-generic scope"),
NonPublicType = LibraryInfo("DAP017", "Non-accessible type", "Type '{0}' is not accessible; {1} types are not currently supported"),
Expand Down
108 changes: 108 additions & 0 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,59 @@ private void OnDapperAotHit()
}
}
}
/// <summary>
/// A <c>[TypeHandler(...)]</c> naming something generated code cannot use is otherwise
/// silently skipped - which is the exact failure mode the declarative form exists to
/// remove, so it has to be said out loud.
/// </summary>
private static void ReportTypeHandlerProblems(CompilationAnalysisContext ctx)
{
// module then assembly, in that order, so "the first one wins" means the same thing
// here as it does in the generator
var claimed = new Dictionary<string, INamedTypeSymbol>(StringComparer.Ordinal);
Report(ctx.Compilation.SourceModule.GetAttributes());
Report(ctx.Compilation.Assembly.GetAttributes());

void Report(ImmutableArray<AttributeData> attributes)
{
foreach (var attribute in attributes)
{
if (attribute.AttributeClass is not { Name: Types.TypeHandlerAttribute, Arity: 0 }
|| !Inspection.IsDapperAttribute(attribute)
|| attribute.ConstructorArguments.Length != 2) continue;

if (attribute.ConstructorArguments[0].Value is not ITypeSymbol valueType
|| attribute.ConstructorArguments[1].Value is not INamedTypeSymbol handlerType) continue;

var location = attribute.ApplicationSyntaxReference is { } syntax
? Location.Create(syntax.SyntaxTree, syntax.Span) : Location.None;

var key = valueType.ToDisplayString();
if (claimed.TryGetValue(key, out var incumbent))
{
// an exact repeat is harmless - same handler, same outcome - but two
// *different* handlers for one type has no correct resolution, so say
// which one is being dropped
if (!SymbolEqualityComparer.Default.Equals(incumbent, handlerType))
{
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateTypeHandler, location,
Display(valueType), Display(incumbent), Display(handlerType)));
}
continue; // it is ignored either way; do not also grade it
}
claimed.Add(key, handlerType);

if (DapperInterceptorGenerator.ClassifyTypeHandler(handlerType, valueType, ctx.Compilation.Assembly, out var problem) is not null
|| problem is null) continue;

ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnusableTypeHandler, location,
Display(handlerType), Display(valueType), problem));
}
}

static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
}

private void OnDapperAotMiss(Location location)
{
if (Thread.VolatileRead(ref _dapperHits) == 0 // fast short-circuit if we know we're all good
Expand All @@ -88,6 +141,7 @@ internal void OnCompilationEndAction(CompilationAnalysisContext ctx)
{
try
{
ReportTypeHandlerProblems(ctx);
lock (_missedOpportunities)
{
var count = _missedOpportunities.Count;
Expand Down Expand Up @@ -121,6 +175,7 @@ public void OnOperation(OperationAnalysisContext ctx)
switch (ctx.Operation.Kind)
{
case OperationKind.Invocation when ctx.Operation is IInvocationOperation invoke:
DetectRuntimeTypeHandlerRegistration(ctx, invoke);
int index = 0;
foreach (var p in invoke.TargetMethod.Parameters)
{
Expand Down Expand Up @@ -184,6 +239,59 @@ public void OnOperation(OperationAnalysisContext ctx)
}
}

/// <summary>
/// A runtime <c>SqlMapper.AddTypeHandler</c> registration cannot be seen by the generator,
/// so generated code silently ignores it; point at the declarative form instead. Only what
/// is registered in *this* compilation is visible here - a registration made by a
/// referenced library is not - so this reduces the silent-wrongness surface, it does not
/// close it.
/// </summary>
private static void DetectRuntimeTypeHandlerRegistration(in OperationAnalysisContext ctx, IInvocationOperation invoke)
{
var method = invoke.TargetMethod;
if (method is not { Name: "AddTypeHandler", IsStatic: true, ContainingType: { Name: "SqlMapper", ContainingNamespace: { Name: "Dapper", ContainingNamespace.IsGlobalNamespace: true } } }) return;

var parseState = new ParseState(ctx);
if (!IsEnabled(in parseState, invoke, Types.DapperAotAttribute, out _)) return; // vanilla-only code is fine as-is

// AddTypeHandler<T>(TypeHandler<T>) or AddTypeHandler(Type, ITypeHandler)
ITypeSymbol? valueType = method.TypeArguments.Length == 1 ? method.TypeArguments[0] : null;
if (valueType is null)
{
foreach (var arg in invoke.Arguments)
{
if (arg.Value is ITypeOfOperation typeOf) { valueType = typeOf.TypeOperand; break; }
}
}
if (valueType is null) return; // cannot name it; saying nothing beats guessing

foreach (var handler in DapperInterceptorGenerator.GetTypeHandlers(ctx.Compilation))
{
if (string.Equals(handler.ValueTypeName, CodeWriter.GetAppendTypeName(valueType), StringComparison.Ordinal))
{
return; // already declared; the runtime call is redundant but harmless
}
}

// name the *concrete* handler being registered, not the parameter's declared type,
// so the suggested attribute can be pasted as-is
var handlerType = "TheHandler";
foreach (var arg in invoke.Arguments)
{
var value = arg.Value;
while (value is IConversionOperation conversion) value = conversion.Operand;
if (value is not ITypeOfOperation && value.Type is INamedTypeSymbol named)
{
handlerType = Display(named);
break;
}
}
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.RuntimeTypeHandlerRegistration,
invoke.Syntax.GetLocation(), Display(valueType), handlerType));

static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
}

private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sqlSource, OperationFlags flags)
{
Action<Diagnostic> onDiagnostic = ctx.ReportDiagnostic;
Expand Down
Loading
Loading