Skip to content

Don't report MSTEST0065 on collection types that declare their own equality#9978

Merged
Evangelink merged 8 commits into
mainfrom
dev/amauryleve/mstest0065-skip-iequatable-collections
Jul 16, 2026
Merged

Don't report MSTEST0065 on collection types that declare their own equality#9978
Evangelink merged 8 commits into
mainfrom
dev/amauryleve/mstest0065-skip-iequatable-collections

Conversation

@Evangelink

@Evangelink Evangelink commented Jul 15, 2026

Copy link
Copy Markdown
Member

Fixes #9971

Problem

MSTEST0065 (AvoidAssertAreEqualOnCollections) fires purely because the compared type implements IEnumerable<T>. It doesn't check whether the type also defines its own equality. For a type that is both a collection and meaningfully IEquatable<T> (or overrides object.Equals), Assert.AreEqual is the correct call — but the rule flagged it anyway, producing false positives that forced people to disable the whole rule.

Approach

The analyzer fundamentally can't read intent: Assert.AreEqual(a, b) on a collection that is also IEquatable<T> is genuinely ambiguous (type equality vs. element/content equality). So the question isn't "how do we detect intent" but "which way do we err when we can't tell?".

For a default-on Warning, false positives are more expensive than false negatives (a noisy warning gets suppressed wholesale). When a type implements IEquatable<self> or overrides object.Equals, the author deliberately opted into a custom, non-reference equality that Assert.AreEqual honors via EqualityComparer<T>.Default — the strongest static signal we're not in accidental-reference-equality territory. So we suppress there.

Changes

AvoidAssertAreEqualOnCollectionsAnalyzer no longer reports when the compared type declares its own equality. Precise semantics (mirroring (comparer ?? EqualityComparer<T>.Default).Equals(...)):

  • Opt-out keys off the selected generic type argument T, not the argument's runtime collection type — widening (Assert.AreEqual<object>(c, c)) still reports.
  • Opt-out is skipped only for a genuinely custom comparer. A null/default/default(IEqualityComparer<T>) comparer or an explicit EqualityComparer<T>.Default argument is equivalent to the parameterless overload (Assert falls back to the default comparer — Assert.AreEqual.cs:146), so those keep the opt-out.
  • IEquatable<T> detection requires the exact self type argument. IEquatable<T> is invariant, so a collection implementing only IEquatable<Base> (used as a derived type) still uses reference equality via EqualityComparer<Derived>.Default and is correctly reported; explicit-interface implementations count.
  • object.Equals detection follows the override chain to its root and requires it to be System.Object.Equals (a derived override of a base new virtual bool Equals(object) does not count), and stops before System.ValueType/System.Enum so struct collections with no equality of their own still report.
  • Type-parameter constraints: where T : IEquatable<T> (self) or a class constraint overriding object.Equals suppress; a bare where T : IEnumerable<...>, or where T : ISelf where ISelf : IEquatable<ISelf> (a concrete T need not implement IEquatable<T>), still report. Transitive type-parameter constraints (where T : U where U : ...) are followed, keeping T as the equality target.

Known, accepted slippage

Types like ImmutableArray<T> implement IEquatable<T> but compare by the underlying array reference, so they're no longer flagged even though element-wise is usually what you'd want. There's no static way to tell "IEquatable meaning content" from "IEquatable meaning identity", and chasing it just reintroduces intent-guessing.

Verification

  • Analyzer + test project build clean.
  • Full AvoidAssertAreEqualOnCollectionsAnalyzerTests: 49/49 pass (covers widened-to-object, custom/null/default/default(...)/explicit-default comparer, IEquatable<self> incl. explicit-interface and IEquatable<Base>-only, object.Equals override, new virtual Equals base, struct with/without own equality, and the various direct and transitive type-parameter constraint forms — for both AreEqual and AreNotEqual).

…uality

Suppress MSTEST0065 when the compared type implements IEquatable<itself>
or overrides object.Equals. In that case the author has deliberately
opted into a custom, non-reference equality that Assert.AreEqual honors
via EqualityComparer<T>.Default, so suggesting a sequence/structural
comparison second-guesses a deliberate decision.

Fixes #9971

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 15, 2026 15:08
@Evangelink Evangelink added the state/needs-review Awaiting review from the team. label Jul 15, 2026
@Evangelink
Evangelink enabled auto-merge (squash) July 15, 2026 15:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Suppresses MSTEST0065 for collection types with custom equality semantics, addressing #9971.

Changes:

  • Detects self-typed IEquatable<T> implementations and object.Equals overrides.
  • Adds the IEquatable<T> well-known type name.
  • Adds analyzer tests for equality scenarios.
Show a summary per file
File Description
AvoidAssertAreEqualOnCollectionsAnalyzer.cs Adds custom-equality detection.
WellKnownTypeNames.cs Registers IEquatable<T>.
AvoidAssertAreEqualOnCollectionsAnalyzerTests.cs Tests diagnostic suppression and reporting.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 5
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

Review Summary

This PR correctly suppresses MSTEST0045 for collection types that declare their own equality semantics (via IEquatable<self> or object.Equals override), fixing issue #9971. The implementation is clean and well-tested.

# Dimension Verdict
1 Algorithmic Correctness ✅ Clean
2 Threading & Concurrency N/A
3 Security & IPC Contract Safety N/A
4 Public API & Binary Compatibility ✅ Clean — no public API change
5 Performance & Allocations ⚠️ Minor (see inline)
6 Cross-TFM Compatibility ✅ Handled — equatableSymbol is nullable for old TFMs
7 Error Handling N/A
8 Diagnostics & Observability N/A
9 Naming & Coding Style ✅ Clean
10 Documentation & Comments ✅ Good inline comments
11 Test Quality ✅ Four test cases covering key scenarios
12 Localization N/A
13 Configuration & Options N/A
14 Resource Management N/A
15 Backward Compatibility ✅ Fewer false positives only
16 Code Organization ✅ Clean
17 Extensibility N/A
18 Dependency Management N/A
19 Build Infrastructure N/A
20 CLI Options N/A
21 Agentic Workflows N/A
22 PowerShell Hygiene N/A

Overall: Looks good. One minor performance note below.

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
…onstraints, override root

- Base the IEquatable/Equals opt-out on the selected generic type argument T
  (the equality contract EqualityComparer<T>.Default actually uses), so widening
  to a base type (AreEqual<object>) no longer loses the diagnostic.
- Skip the opt-out when an explicit comparer argument is supplied.
- Honor IEquatable<T> (and self-equating class) type-parameter constraints.
- Follow the override chain to its root and require object.Equals, so a derived
  override of a base 'new virtual bool Equals(object)' is not mistaken for one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 15, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
@github-actions

This comment has been minimized.

…quals walk

- Type-parameter constraints: keep T as the equality target. A where T : ISelf
  constraint where ISelf implements IEquatable<ISelf> no longer suppresses, since a
  concrete T need not implement IEquatable<T>. Only IEquatable<T> (self) constraints
  or class constraints overriding object.Equals suppress.
- OverridesObjectEquals now stops before System.ValueType/System.Enum, so a struct
  collection with no equality of its own is still reported (ValueType.Equals is not
  the author's intentional equality).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 15, 2026 15:54
…dd coverage

- Document that transitive type-parameter constraints are intentionally not followed.
- Clarify the equatableSymbol==null comment (only IEquatable<self> becomes a no-op).
- Add tests: explicit-interface IEquatable implementation (suppresses) and
  AreNotEqual widened-to-object (reports).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
Copilot AI review requested due to automatic review settings July 15, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
@github-actions

This comment has been minimized.

…traints, comment fixes

- HasCustomComparerArgument: a null-literal or explicit EqualityComparer<T>.Default
  comparer now keeps the equality opt-out (both dispatch to the default comparer per
  Assert.AreEqual.cs), only a genuinely custom comparer re-enables the diagnostic.
- Type-parameter constraints are now traversed transitively (where T : U where U : ...),
  keeping T as the IEquatable equality target while honoring inherited object.Equals.
- Reworded the struct fixture comment (no backing field) to describe ValueType's
  reflection-based field-wise equality accurately.
- Added tests: custom comparer (reports), null/default comparer (suppress), transitive
  class constraint (suppress), IEquatable<Base>-only used as Derived (reports, since
  IEquatable<T> is invariant).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 15, 2026 16:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
… as the default

HasCustomComparerArgument now uses IOperation.ConstantValue instead of matching only
a null literal, so default, default(IEqualityComparer<T>), and const null fields are all
recognized as the default comparer and keep the equality opt-out. Added a default(...)
comparer test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 15, 2026 16:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
@github-actions

This comment has been minimized.

Peel only built-in conversions (not user-defined) when classifying the comparer
argument, matching GetCollectionArgumentType and the runtime value. A user-defined
conversion executes and can change the value, so it must stay opaque.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 16, 2026 07:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Analyzers/MSTest.Analyzers/AvoidAssertAreEqualOnCollectionsAnalyzer.cs Outdated
@github-actions

This comment has been minimized.

…ected T

IEqualityComparer<in T> is contravariant, so EqualityComparer<Base>.Default compiles as
the comparer for a Derived argument but dispatches to Base's equality. Require the
property's type argument to equal the selected T so such a base comparer is treated as
custom and MSTEST0065 still fires. Added a variance regression test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 78d66f93-e4cf-45d3-bdde-07ee9f67833d
Copilot AI review requested due to automatic review settings July 16, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #9978

GradeTestNotes
A (90–100) new AvoidAssertAreEqualOnCollectionsAnalyzerTests.
WhenUsingAssertAreEqualOnCollectionWithBaseTypeDefaultComparer_
ReportDiagnostic
Well-crafted edge-case test; explanatory comment on contravariance; assertion verifies exact diagnostic type and type argument.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · 35.8 AIC · ⌖ 6.38 AIC · ⊞ 8.9K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
Member Author

/backport to rel/4.3

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to rel/4.3: https://github.com/microsoft/testfx/actions/runs/29490461211

@Evangelink
Evangelink merged commit 71bcd3a into main Jul 16, 2026
46 checks passed
@Evangelink
Evangelink deleted the dev/amauryleve/mstest0065-skip-iequatable-collections branch July 16, 2026 11:10
Evangelink added a commit that referenced this pull request Jul 16, 2026
…uality by @Evangelink in #9978 (backport to rel/4.3) (#10003)

Co-authored-by: Amaury Levé <amauryleve@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-review Awaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Collections that implement IEquatable<T> should be allowed to be compared with Assert.AreEqual

3 participants