Skip to content

Conversation

@bouwkast
Copy link
Collaborator

@bouwkast bouwkast commented Aug 28, 2025

Summary of changes

This introduces a new Roslyn analyzer to flag instances where we are directly checking if an IDuckType is or isn't null.
In most cases this is incorrect and the correct equality check would be to check if the IDuckType.Instance is or isn't null.

The analyzer flags all instances of the former as a new rule called DDDUCK001 🦆 .

In addition to the analyzer, I've added a code fix provider that rewrites the flagged expression to use ?.Instance instead. This allows us to correct the issue and still check for null on the base IDuckType just to be safe.

It is important to note though that not every instance of this detection is 100% accurate - there are and will be cases where checking if the IDuckType is null is the correct thing to do.

However, using the conditional accessor should still be safe, but for transparency I've added some current cases to a pramga and to tracer/GlobalSuppressions.cs.

To ensure that we do not potentially disrupt developer workflows drastically with this the analyzer is flagged as a Warning, it will be bumped to an Error at a later point.

Reason for change

Help enforce us to not accidentally check the IDuckType for null and instead check the Instance for null.

Using a null conditional (?.) also means that we are still checking if the IDuckType is null on the off chance that it is.

Implementation details

The analyzer is based off of our other analyzers (somewhat) and largely was based on stepping through the code in the tests in debug mode (works well in VS).

It will match on the following expressions: ==, !=, is, is not.
This is only when the left and right hand sides implement an IDuckType and the other side is the null keyword.

I went through each of the current violations of the analyzer and either applied the code fix in VS automatically or ignored them on a case-by-case basis.

Test coverage

  • Tests have been added for the following usage patterns (code fix and analyzer included)
    • binary and is patterns: duckType != null / duckType is not null
    • generic constraints: where T : IDuckType
    • (object)duckType == null -> duckType?.Instance == null

Other details

There was some ChatGPT usage here around the parenthesis / casting and namespace exclusions as that really tripped me up.

@bouwkast
Copy link
Collaborator Author

Build failures are due to the detected errors, some I already have PRs for but will aim to address them before merging this

@bouwkast bouwkast force-pushed the steven/r-and-d-august-2025 branch from 852a4af to 2538a59 Compare August 28, 2025 21:44
Copy link
Member

@andrewlock andrewlock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

return false;
}

// NOTE: IDuckType? == IDuckType which surprised me
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's because IDuckType is effectively a reference type by default, so the ? is an annotation on that rather than making the struct nullable.

That's why this is bad (it has to box to an object):

public void MyMethod(IDuckType duck);

And this is good (no boxing)

public void MyMethod<T>(T duck);
    where T : IDuckType

Copy link
Collaborator Author

@bouwkast bouwkast Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh very interesting thanks!

@bouwkast bouwkast force-pushed the steven/r-and-d-august-2025 branch from a57af5c to 146c511 Compare September 24, 2025 20:28
@bouwkast bouwkast requested review from a team as code owners September 24, 2025 20:28
@bouwkast bouwkast requested a review from link04 September 24, 2025 20:28
@datadog-official

This comment has been minimized.

@dd-trace-dotnet-ci-bot
Copy link

dd-trace-dotnet-ci-bot bot commented Sep 24, 2025

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing the following branches/commits:

Execution-time benchmarks measure the whole time it takes to execute a program. And are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are shown in red. The following thresholds were used for comparing the execution times:

  • Welch test with statistical test for significance of 5%
  • Only results indicating a difference greater than 5% and 5 ms are considered.

Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard.

Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph).

gantt
    title Execution time (ms) FakeDbCommand (.NET Framework 4.8) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (72ms)  : 71, 73
     .   : milestone, 72,
    master - mean (72ms)  : 70, 73
     .   : milestone, 72,

    section Baseline
    This PR (7448) - mean (68ms)  : 66, 70
     .   : milestone, 68,
    master - mean (68ms)  : 67, 70
     .   : milestone, 68,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (1,050ms)  : 1004, 1097
     .   : milestone, 1050,
    master - mean (1,051ms)  : 995, 1106
     .   : milestone, 1051,

Loading
gantt
    title Execution time (ms) FakeDbCommand (.NET Core 3.1) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (107ms)  : 105, 109
     .   : milestone, 107,
    master - mean (107ms)  : 105, 108
     .   : milestone, 107,

    section Baseline
    This PR (7448) - mean (107ms)  : 104, 109
     .   : milestone, 107,
    master - mean (106ms)  : 104, 109
     .   : milestone, 106,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (759ms)  : 727, 790
     .   : milestone, 759,
    master - mean (745ms)  : 728, 763
     .   : milestone, 745,

Loading
gantt
    title Execution time (ms) FakeDbCommand (.NET 6) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (104ms)  : 101, 107
     .   : milestone, 104,
    master - mean (101ms)  : 99, 102
     .   : milestone, 101,

    section Baseline
    This PR (7448) - mean (104ms)  : 100, 108
     .   : milestone, 104,
    master - mean (100ms)  : 98, 102
     .   : milestone, 100,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (847ms)  : crit, 771, 923
     .   : crit, milestone, 847,
    master - mean (771ms)  : 716, 826
     .   : milestone, 771,

Loading
gantt
    title Execution time (ms) FakeDbCommand (.NET 8) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (111ms)  : crit, 108, 113
     .   : crit, milestone, 111,
    master - mean (93ms)  : 92, 94
     .   : milestone, 93,

    section Baseline
    This PR (7448) - mean (108ms)  : 105, 111
     .   : milestone, 108,
    master - mean (92ms)  : 90, 94
     .   : milestone, 92,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (737ms)  : crit, 709, 765
     .   : crit, milestone, 737,
    master - mean (663ms)  : 651, 675
     .   : milestone, 663,

Loading
gantt
    title Execution time (ms) HttpMessageHandler (.NET Framework 4.8) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (215ms)  : 204, 225
     .   : milestone, 215,
    master - mean (207ms)  : 197, 218
     .   : milestone, 207,

    section Baseline
    This PR (7448) - mean (203ms)  : 194, 212
     .   : milestone, 203,
    master - mean (204ms)  : 192, 216
     .   : milestone, 204,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (1,232ms)  : 1149, 1315
     .   : milestone, 1232,
    master - mean (1,215ms)  : 1146, 1284
     .   : milestone, 1215,

Loading
gantt
    title Execution time (ms) HttpMessageHandler (.NET Core 3.1) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (310ms)  : 290, 329
     .   : milestone, 310,
    master - mean (296ms)  : 280, 311
     .   : milestone, 296,

    section Baseline
    This PR (7448) - mean (302ms)  : 287, 318
     .   : milestone, 302,
    master - mean (292ms)  : 276, 309
     .   : milestone, 292,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (990ms)  : 942, 1039
     .   : milestone, 990,
    master - mean (983ms)  : 936, 1030
     .   : milestone, 983,

Loading
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (304ms)  : 287, 321
     .   : milestone, 304,
    master - mean (297ms)  : 284, 310
     .   : milestone, 297,

    section Baseline
    This PR (7448) - mean (302ms)  : 286, 317
     .   : milestone, 302,
    master - mean (298ms)  : 282, 314
     .   : milestone, 298,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (1,029ms)  : 971, 1088
     .   : milestone, 1029,
    master - mean (1,041ms)  : 977, 1105
     .   : milestone, 1041,

Loading
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8) 
    dateFormat  X
    axisFormat %s
    todayMarker off
    section Bailout
    This PR (7448) - mean (289ms)  : 273, 304
     .   : milestone, 289,
    master - mean (287ms)  : 272, 302
     .   : milestone, 287,

    section Baseline
    This PR (7448) - mean (290ms)  : 274, 305
     .   : milestone, 290,
    master - mean (289ms)  : 274, 305
     .   : milestone, 289,

    section CallTarget+Inlining+NGEN
    This PR (7448) - mean (934ms)  : 819, 1048
     .   : milestone, 934,
    master - mean (917ms)  : 829, 1005
     .   : milestone, 917,

Loading

This will help ease onto the analyzer / code fix
and not accidentally cause more developer
friction.
var stringAttr = new Mock<IDynamoDbAttributeValue>();
stringAttr.Setup(a => a.S).Returns("testvalue");
var keys = new Mock<IDynamoDbKeysObject>();
keys.Setup(k => k.Instance).Returns(new object()); // not important of what it returns as long as it isn't null
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done due to the new (correct) check if the keys.Instance is null

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AWS/Shared/SpanPointers.cs

@bouwkast
Copy link
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

@bouwkast
Copy link
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

@bouwkast
Copy link
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

@bouwkast bouwkast merged commit 028cfa1 into master Sep 30, 2025
153 checks passed
@bouwkast bouwkast deleted the steven/r-and-d-august-2025 branch September 30, 2025 22:13
@github-actions github-actions bot added this to the vNext-v3 milestone Sep 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants