Skip to content

Commit 2538a59

Browse files
committed
Add a code fix provider for invalid DuckType null checks
1 parent 47d8c5e commit 2538a59

File tree

5 files changed

+799
-0
lines changed

5 files changed

+799
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// <copyright file="DuckDiagnostics.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using Microsoft.CodeAnalysis;
7+
8+
namespace Datadog.Trace.Tools.Analyzers.DuckTypeAnalyzer;
9+
10+
/// <summary>
11+
/// Helper class for holding various diagnostics on ducks.
12+
/// </summary>
13+
public class DuckDiagnostics
14+
{
15+
/// <summary>
16+
/// The DiagnosticID for duck type null check rule.
17+
/// </summary>
18+
public const string DuckTypeNullCheckDiagnosticId = "DDDUCK001";
19+
20+
internal static readonly DiagnosticDescriptor ADuckIsNeverNullRule = new(
21+
DuckTypeNullCheckDiagnosticId,
22+
title: "Checking IDuckType for null",
23+
messageFormat: "{0}",
24+
category: "CodeQuality",
25+
defaultSeverity: DiagnosticSeverity.Error,
26+
isEnabledByDefault: true,
27+
description: "The IDuckType is almost always never null, check the Instance for null to ensure we have access to the ducked object.");
28+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// <copyright file="DuckTypeNullCheckAnalyzer.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System.Collections.Immutable;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using Microsoft.CodeAnalysis.Operations;
12+
13+
namespace Datadog.Trace.Tools.Analyzers.DuckTypeAnalyzer
14+
{
15+
/// <summary>
16+
/// Checks fo r null checks against IDuckType instances.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public sealed class DuckTypeNullCheckAnalyzer : DiagnosticAnalyzer
20+
{
21+
private const string DatadogIDuckTypeInterface = "Datadog.Trace.DuckTyping.IDuckType";
22+
23+
/// <summary>
24+
/// We exclude certain namespaces from this rule:
25+
/// - Activity because we do a log of "as" pattern matching against various runtime implementations and expect null for some
26+
/// </summary>
27+
private static readonly ImmutableArray<string> ExcludedNamespacePrefixes =
28+
ImmutableArray.Create(
29+
"Datadog.Trace.Activity");
30+
31+
/// <inheritdoc/>
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
33+
=> ImmutableArray.Create(DuckDiagnostics.ADuckIsNeverNullRule);
34+
35+
/// <inheritdoc/>
36+
public override void Initialize(AnalysisContext context)
37+
{
38+
// not checking any generated code
39+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
40+
context.EnableConcurrentExecution();
41+
42+
context.RegisterCompilationStartAction(static compilationContext =>
43+
{
44+
var duckType = compilationContext.Compilation.GetTypeByMetadataName(DatadogIDuckTypeInterface);
45+
if (duckType is null)
46+
{
47+
return;
48+
}
49+
50+
compilationContext.RegisterOperationAction(
51+
ctx => AnalyzeBinaryNullCheck(ctx, duckType),
52+
OperationKind.BinaryOperator);
53+
54+
compilationContext.RegisterOperationAction(
55+
ctx => AnalyzeIsPatternNullCheck(ctx, duckType),
56+
OperationKind.IsPattern);
57+
});
58+
}
59+
60+
private static void AnalyzeBinaryNullCheck(OperationAnalysisContext context, INamedTypeSymbol duckType)
61+
{
62+
// if (duckType == null) or if (duckType != null) is what we looking for (operands can be swapped)
63+
// both are technically incorrect
64+
var bin = (IBinaryOperation)context.Operation;
65+
66+
// make sure it is == or !=
67+
if (bin.OperatorKind != BinaryOperatorKind.Equals &&
68+
bin.OperatorKind != BinaryOperatorKind.NotEquals)
69+
{
70+
return;
71+
}
72+
73+
// find which side we need, really unsure if anyone has ever written null == duckType :)
74+
var leftIsNull = IsNullLiteral(bin.LeftOperand);
75+
var rightIsNull = IsNullLiteral(bin.RightOperand);
76+
if (!leftIsNull && !rightIsNull)
77+
{
78+
return;
79+
}
80+
81+
// Look at the non-null side and unwrap casts/boxing to object/dynamic
82+
// candidate here is: ConversionOperation Type: object
83+
// it is an implicit cast / box to object (unlike the `is null` pattern)
84+
// so we need to undo that before we can check if it is a IDuckType
85+
var candidate = leftIsNull ? bin.RightOperand : bin.LeftOperand;
86+
87+
// When we have == or != null it seems that the candidate.Type is just an Object, we need to get the DuckType from it
88+
// we can query the SemanticModel to get the actual type, but that proved to be very slow
89+
// so we can unwrap it instead
90+
var type = UnwrapForType(candidate);
91+
92+
if (type is null || !ImplementsDuckType(type, duckType) || IsExcluded(type))
93+
{
94+
return;
95+
}
96+
97+
Report(context);
98+
}
99+
100+
private static void AnalyzeIsPatternNullCheck(OperationAnalysisContext context, INamedTypeSymbol duckType)
101+
{
102+
var isPattern = (IIsPatternOperation)context.Operation;
103+
104+
if (!IsNullPattern(isPattern.Pattern))
105+
{
106+
return;
107+
}
108+
109+
// this just falls through to the default op case as it isn't boxed / casted to object
110+
var type = UnwrapForType(isPattern.Value);
111+
if (type is null || !ImplementsDuckType(type, duckType) || IsExcluded(type))
112+
{
113+
return;
114+
}
115+
116+
Report(context);
117+
}
118+
119+
private static void Report(OperationAnalysisContext context)
120+
{
121+
var diagnostic = Diagnostic.Create(
122+
DuckDiagnostics.ADuckIsNeverNullRule,
123+
context.Operation.Syntax.GetLocation());
124+
context.ReportDiagnostic(diagnostic);
125+
}
126+
127+
private static ITypeSymbol? UnwrapForType(IOperation op)
128+
{
129+
op = Unwrap(op);
130+
131+
// The type here will be the actual type like Datadog.Trace.DuckTyping.IDuckType
132+
return op.Type;
133+
}
134+
135+
private static IOperation Unwrap(IOperation op)
136+
{
137+
while (true)
138+
{
139+
switch (op)
140+
{
141+
case IConversionOperation c when c.IsImplicit:
142+
// implicit meaning that there wasn't a (object) cast
143+
// this happens automatically in the `==` and `!=` operators
144+
// c.Operand here is something like => ParameterReferenceOperation Type: Datadog.Trace.DuckTyping.IDuckType
145+
op = c.Operand;
146+
continue;
147+
148+
case IConversionOperation c
149+
when c.Type is { SpecialType: SpecialType.System_Object } ||
150+
c.Type is { TypeKind: TypeKind.Dynamic }:
151+
// Explicit cast/as to object or dynamic — unwrap so we can see the original type
152+
op = c.Operand;
153+
continue;
154+
155+
default:
156+
return op;
157+
}
158+
}
159+
}
160+
161+
private static bool ImplementsDuckType(ITypeSymbol type, INamedTypeSymbol duckType)
162+
{
163+
// where T : IDuckType
164+
// where T : IFoo, IDuckType
165+
// where T : IFoo
166+
// IFoo : IDuckType (in different file)
167+
if (type is ITypeParameterSymbol tp)
168+
{
169+
foreach (var c in tp.ConstraintTypes)
170+
{
171+
if (ImplementsDuckType(c, duckType))
172+
{
173+
return true;
174+
}
175+
}
176+
177+
return false;
178+
}
179+
180+
// NOTE: IDuckType? == IDuckType which surprised me
181+
if (SymbolEqualityComparer.Default.Equals(type, duckType))
182+
{
183+
return true;
184+
}
185+
186+
// IFoo : IBar
187+
// IBar : IDuckType
188+
// NOTE: IFoo : IBar (where IBar implements IDuckType) is handled by the AllInterfaces check below
189+
foreach (var i in type.AllInterfaces)
190+
{
191+
if (SymbolEqualityComparer.Default.Equals(i, duckType))
192+
{
193+
return true;
194+
}
195+
}
196+
197+
return false;
198+
}
199+
200+
private static bool IsExcluded(ITypeSymbol type)
201+
{
202+
var ns = GetNamespace(type);
203+
if (ns is null)
204+
{
205+
return false;
206+
}
207+
208+
foreach (var prefix in ExcludedNamespacePrefixes)
209+
{
210+
if (ns.StartsWith(prefix, System.StringComparison.Ordinal))
211+
{
212+
return true;
213+
}
214+
}
215+
216+
return false;
217+
}
218+
219+
private static string? GetNamespace(ITypeSymbol type)
220+
{
221+
if (type is ITypeParameterSymbol tp)
222+
{
223+
foreach (var c in tp.ConstraintTypes)
224+
{
225+
var nam = GetNamespace(c);
226+
if (nam is not null)
227+
{
228+
return nam;
229+
}
230+
}
231+
232+
return null;
233+
}
234+
235+
// Avoid allocations from "global::"
236+
var ns = type.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
237+
if (ns is null)
238+
{
239+
return null;
240+
}
241+
242+
return ns.StartsWith("global::", System.StringComparison.Ordinal) ? ns.Substring(8) : ns;
243+
}
244+
245+
private static bool IsNullLiteral(IOperation operation)
246+
{
247+
var op = Unwrap(operation);
248+
if (op is ILiteralOperation lit && lit.ConstantValue.HasValue)
249+
{
250+
return lit.ConstantValue.Value is null;
251+
}
252+
253+
return false;
254+
}
255+
256+
private static bool IsNullPattern(IPatternOperation pattern)
257+
{
258+
// is null
259+
if (pattern is IConstantPatternOperation cp && IsNullLiteral(cp.Value))
260+
{
261+
return true;
262+
}
263+
264+
// is not null => Negated(Constant(null))
265+
if (pattern is INegatedPatternOperation neg &&
266+
neg.Pattern is IConstantPatternOperation cp2 &&
267+
IsNullLiteral(cp2.Value))
268+
{
269+
return true;
270+
}
271+
272+
return false;
273+
}
274+
}
275+
}

0 commit comments

Comments
 (0)