Skip to content

Feature/readonly collections and adapters#140

Open
kasyanovandrii wants to merge 72 commits into
loresoft:mainfrom
kasyanovandrii:feature/readonly-collections-and-adapters
Open

Feature/readonly collections and adapters#140
kasyanovandrii wants to merge 72 commits into
loresoft:mainfrom
kasyanovandrii:feature/readonly-collections-and-adapters

Conversation

@kasyanovandrii
Copy link
Copy Markdown

@kasyanovandrii kasyanovandrii commented May 11, 2026

What's new

New packages

  • Equatable.Generator.DataContract — adapter generator that reads [DataMember] attributes
    (System.Runtime.Serialization). Only properties annotated with [DataMember] are included in equality;
    [IgnoreDataMember] and unannotated properties are excluded. EQ0022 is emitted for any public property with no
    annotation, requiring the intent to be made explicit.
  • Equatable.Generator.MessagePack — adapter generator that reads [Key(n)] attributes. Only [Key] properties are
    included; [IgnoreMember] and unannotated properties are excluded. EQ0023 is emitted for unannotated properties.

New features

  • [DictionaryEquality(sequential: true)] — key-sorted dictionary comparison. Both sides are sorted by key before
    comparing, producing deterministic equality regardless of insertion order. Useful for snapshot testing and diagnostic
    logging. Propagates into nested dictionary values.
  • Direction overrides — [HashSetEquality] on List or T[] produces order-insensitive comparison; [SequenceEquality]
    on HashSet enforces order-sensitive comparison.
  • Nested collection comparer propagation — a single annotation on the outer property propagates the chosen comparer
    kind into all nested levels. Dictionary<K, Dictionary<K2,V>>, Dictionary<K, List>, and three-level nesting are all
    handled with a single annotation.
  • MultiDimensionalArrayEqualityComparer — structural equality for T[,], T[,,], and higher-rank arrays, applied
    automatically as the default. Checks rank, dimension lengths, and element values in row-major order.
  • IReadOnlyDictionary<K,V> support — dictionary comparers now accept any IReadOnlyDictionary<K,V>, not only
    Dictionary<K,V>.
  • Base class delegation — the generated Equals method calls base.Equals() when the base class is also an
    equatable-generated type, including across adapter boundaries (e.g. [Equatable] derived from [DataContractEquatable]).
  • Analyzer diagnostics
    • EQ0014 — equality attribute on a multi-dimensional array (rank ≥ 2), where the comparer cannot be overridden
    • EQ0015 — [SequenceEquality] or [HashSetEquality] on a dictionary type
    • EQ0020 — [DataContractEquatable] without [DataContract]
    • EQ0021 — [MessagePackEquatable] without [MessagePackObject]
    • EQ0022 — unannotated public property on a [DataContractEquatable] type
    • EQ0023 — unannotated public property on a [MessagePackEquatable] type

Bug fixes

  • Empty collections hash differently from null — a sentinel value ensures GetHashCode(empty) != GetHashCode(null),
    satisfying the hash contract.
  • HashSetEqualityComparer.GetHashCode is order-independent — uses a commutative accumulation strategy, consistent with
    SetEquals-based Equals.
  • [DictionaryEquality(sequential: true)] propagates correctly — key-sorted mode is applied to nested dictionary
    values, not only the outermost level.
  • Value types without == — EqualityComparer.Default is used instead of direct operator comparison.

Improvements

  • Equatable.Generator.DataContract and Equatable.Generator.MessagePack are independent NuGet packages — include only
    what the project requires.
  • IsPublicInstanceProperty extracted as a shared helper, eliminating duplication across adapter generators.
  • Allocation-free hash code computation for dictionary comparers.

a.kasyanov and others added 30 commits May 11, 2026 21:51
…attributes

- Support IReadOnlyDictionary<TKey,TValue> in [DictionaryEquality] — generator
  now recognises both IDictionary and IReadOnlyDictionary via AllInterfaces check;
  generated DictionaryEquals helper uses IEnumerable<KeyValuePair<TKey,TValue>>
  as the common base and pattern-matches to IReadOnlyDictionary/IDictionary for
  O(1) TryGetValue lookup without casting
- Add ReadOnlyDictionaryEqualityComparer<TKey,TValue> to Equatable.Comparers for
  use with [EqualityComparer] on nested IReadOnlyDictionary properties
- Replace OrderBy in DictionaryHashCode and HashSetHashCode with commutative
  sum-of-HashCode.Combine approach — order-independent and allocation-free;
  same fix applied to DictionaryEqualityComparer and HashSetEqualityComparer
- Add [DataContractEquatable] adapter attribute — reacts to [DataMember]/
  [IgnoreDataMember] instead of [IgnoreEquality]; feeds into the same
  EquatableWriter pipeline
- Add [MessagePackEquatable] adapter attribute — reacts to MessagePack
  [Key]/[IgnoreMember] attributes
- Add entity and generator snapshot tests for all new scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…d collection composition

- Add MultiDimensionalArrayEqualityComparer<T> for int[,]/T[,] with zero-allocation Array.GetEnumerator() iteration
- Extend GetBaseEquatableType and HasEquatableAttribute to recognise DataContractEquatable and MessagePackEquatable bases so derived classes emit base.Equals()/base.GetHashCode()
- Fix IsIncludedDataContract namespace check to require full System.Runtime.Serialization chain
- Auto-compose nested collection comparers (SequenceEqualityComparer, DictionaryEqualityComparer, HashSetEqualityComparer) recursively from a single top-level attribute; explicit [EqualityComparer] overrides at any level
- Add ComparerTypes.Expression path in writer for fully-composed comparer instance expressions
- Extend analyzer to cover DataContractEquatable/MessagePackEquatable types and accept arrays for [SequenceEquality]
- Add pinned MetadataReferences for DataMemberAttribute and KeyAttribute assemblies to fix load-order fragility in tests
- Add property-based tests (FsCheck) and snapshot tests for all new scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Core EquatableGenerator no longer knows about DataContract or MessagePack.
RegisterProvider is now public static so adapter generators can call it directly.
GetBaseEquatableType detects any *EquatableAttribute in Equatable.Attributes
namespace instead of hard-coding adapter names.

New packages:
- Equatable.SourceGenerator.DataContract — DataContractEquatableGenerator
- Equatable.SourceGenerator.MessagePack — MessagePackEquatableGenerator
- Equatable.Generator.DataContract — NuGet wrapper + DataContractEquatableAttribute
- Equatable.Generator.MessagePack — NuGet wrapper + MessagePackEquatableAttribute

DataContractEquatableAttribute and MessagePackEquatableAttribute moved out of
Equatable.Generator into their respective adapter packages.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…kObject

DataContractEquatableAnalyzer (EQ0020): warns when [DataContractEquatable] is
present on a class without [DataContract] — DataMember attributes are silently
ignored by DataContractSerializer without it.

MessagePackEquatableAnalyzer (EQ0021): warns when [MessagePackEquatable] is
present on a class without [MessagePackObject] — Key attributes are ignored by
the MessagePack serializer without it.

Also add [DataContract]/[MessagePackObject] to all test entities and generator
test sources to reflect real-world usage accurately.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…r duplication

Both adapter generators repeated the same indexer/accessibility guard.
Moved to EquatableGenerator.IsPublicInstanceProperty (public static) so
adapters call the shared helper instead of copying the logic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Structs that don't define op_Equality (e.g. Nullable<T> wrapping a plain
struct) would cause a CS0019 compile error in generated code because the
generator unconditionally emitted == for all value types.

HasEqualityOperator() now checks the underlying type for op_Equality before
choosing ComparerTypes.ValueType, falling back to EqualityComparer<T>.Default.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
[DictionaryEquality] defaults to order-independent equality (key-lookup
Equals, sum-of-pairs hash). [DictionaryEquality(ordered: true)] opts into
order-sensitive equality: SequenceEqual on the key-value pair sequence for
Equals, sequential HashCode.Add per pair for GetHashCode.

Both modes are guaranteed in sync:
- Plain types: single ComparerTypes.OrderedDictionary enum value drives
  both the Equals and GetHashCode switch arms in the writer.
- Nested types: BuildCollectionComparerExpression now always emits an
  OrderedDictionaryEqualityComparer / OrderedReadOnlyDictionaryEqualityComparer
  instance for ordered properties, so both Equals and GetHashCode go through
  the same comparer object and can never diverge.

Adds OrderedDictionaryEqualityComparer<TKey,TValue> and
OrderedReadOnlyDictionaryEqualityComparer<TKey,TValue> to Equatable.Comparers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ode, enable property tests

- Fix equality contract violation: GetHashCode used order-dependent logic while
  Equals was order-independent for dictionaries and hashsets
- DictionaryEqualityComparer/ReadOnlyDictionaryEqualityComparer: GetHashCode now
  uses commutative SUM (insertion-order independent, consistent with TryGetValue Equals)
- HashSetEqualityComparer: GetHashCode now uses commutative SUM (consistent with SetEquals)
- Add [DictionaryEquality(sequential: true)] mode: both Equals and GetHashCode use
  OrderBy(key) for deterministic key-sorted comparison
- OrderedDictionary comparers: fix OrderBy sort comparer to use hash tiebreaker
  when IComparer<TKey> treats distinct keys as equal (e.g. culture-sensitive string sort)
- Migrate property-based tests from FsCheck v2 to FsCheck.Xunit.v3 (xUnit v3 compatible)
- Add Arbitraries class with custom generators for HashSet<T> (not auto-generated in v3)
- Fix property test semantics for SetOfLists/SetOfDicts (reference equality for inner elements)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… hashset comparers

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ish null from empty

DictionaryHashCode and HashSetHashCode previously initialized hashCode to 0,
causing null and empty collections to produce the same hash (0). This violates
the expectation that null != empty in hash-based collections. Using 1 as the
sentinel for empty matches the pattern already established in EqualityHelper.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… comparers

DictionaryEqualityComparer, ReadOnlyDictionaryEqualityComparer, and
HashSetEqualityComparer all initialized GetHashCode to 0, causing null and
empty collections to produce the same hash. This violates the Equals/GetHashCode
contract: Equals correctly returns false for (null, empty), so GetHashCode must
also differ to avoid unnecessary hash table collisions.

Starting from 1 ensures empty collections are always distinguishable from null
(which returns 0). Added comments explaining why the seed is 1 and what problem
it solves.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…l hash check

Rename all property-based test methods across four files to follow the
Invariant_Subject_Condition pattern, making the tested invariant explicit
in the name rather than describing the operation.

Fix incorrect EqualImpliesSameHashCode property in ReadOnlyDictionaryComparerProperties
and OrderedDictionaryComparerProperties: was asserting Equals(a,b) ↔ hash(a)==hash(b)
(bidirectional), which fails on hash collisions. Corrected to one-directional
Prop.When(Equals(a,b), hash(a)==hash(b)), which is the actual contract.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…o EquatableGenerator

- Add InferCollectionComparer public hook: infers structural collection comparers
  (DictionaryEqualityComparer, SequenceEqualityComparer, ReadOnlyDictionaryEqualityComparer)
  for adapter generators so [Key] / [DataMember] properties don't require explicit
  equality attributes. Recurses into nested types via BuildElementComparerExpression.
- Add IsEquatableGeneratorAttribute: recognises *EquatableAttribute across all three
  adapter namespaces (Equatable.Attributes, .DataContract, .MessagePack) so derived
  classes correctly delegate to base.Equals() regardless of which adapter annotates the base.
- Wire InferCollectionComparer into DataContractEquatableGenerator and
  MessagePackEquatableGenerator as postProcessProperty delegate.
- Update DataContractEquatableAnalyzer and MessagePackEquatableAnalyzer to use
  IsEquatableGeneratorAttribute for base-class detection.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…agePack

Add OrderDataContractNested and SerializedRecordNested — partial classes annotated
with [DataContractEquatable] / [MessagePackEquatable] carrying nested collection
properties (Dict<K,List<V>>, Dict<K,Dict<K2,V>>, List<Dict<K,V>>,
IReadOnlyDictionary<K,List<V>>) used by entity-level integration tests.

Update existing OrderDataContract and SerializedRecord entities as needed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ctions, multi-dim arrays

- ComparerGetHashCodeTest: covers null→0, empty→non-zero, empty≠null, and
  equal-implies-same-hash for all five comparers (Dictionary, ReadOnlyDictionary,
  HashSet, Sequence, OrderedDictionary).
- MultiDimensionalArrayEqualityComparerTest: full coverage of
  MultiDimensionalArrayEqualityComparer — rank checks, dimension checks, 2D/3D
  content equality, custom comparer, GetHashCode row-major order sensitivity.
- NestedDictionaryEqualityComparerTest: explicitly-composed nested comparers
  (Dict<K,Dict>, Dict<K,List>, Dict<K,HashSet>, 3-level, ReadOnlyDict<K,List>).
- Add custom-comparer constructor path tests (Path C) to HashSetEqualityComparerTest
  and SequenceEqualityComparerTest.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…erage

- Extract shared AnalyzerTestHelper from EquatableAnalyzerTest.
- Add DataContractAnalyzerTest: EQ0020 (missing [DataContract]) and derived-class checks.
- Add MessagePackAnalyzerTest: EQ0021 (missing [MessagePackObject]) and derived-class checks.
- Add 7 tests to EquatableAnalyzerTest covering ISet<T>, IReadOnlySet<T>, int[], int[,]
  missing-attribute diagnostics (EQ0002) and valid [HashSetEquality]/[SequenceEquality]
  on those types.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…r coverage

- AdapterGeneratorTestBase: shared base class with BuildReferences and
  GetGeneratedOutput helpers for DataContract and MessagePack generator tests.
- DataContractGeneratorTest: 7 snapshot tests covering basic generation,
  derived-from-annotated-base delegation, unannotated-base inclusion,
  explicit comparer override, inferred collection comparers, nested collection
  comparers, and zero-property edge case.
- MessagePackGeneratorTest: same 7 scenarios for MessagePack.
- EquatableGeneratorTest: add GenerateEquatableWithNoPublicProperties snapshot
  (Equals reduces to !(other is null)).
- EquatableWriterTest: add 4 snapshot tests for record, sealed, derived, and
  nested-class writer paths previously uncovered.
- Accept all new verified snapshots.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ed entities

- OrderDataContractNestedTest: 13 tests verifying [DataContractEquatable]-generated
  Equals/GetHashCode on Dict<K,List<V>>, Dict<K,Dict<K2,V>>, List<Dict<K,V>>,
  IReadOnlyDictionary<K,List<V>> — structural inner comparison, insertion-order
  independence for dicts, order-sensitivity for lists, null vs empty discrimination.
- SerializedRecordNestedTest: same 13 scenarios for [MessagePackEquatable].
- DictionaryHashCodeTest: documents and verifies order-independent hash algorithm.
- Add OrderDataContractProperties and SerializedRecordProperties to the property
  test project, and update project file accordingly.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
All 7 MessagePack test scenarios now use identical entity class names,
property names, and property types as their DataContract counterparts
(OrderDataContract, DerivedContract/BaseContract, ConcreteRecord/UnannotatedBase,
OrderedContract, ContractWithCollections, ContractWithNestedCollections, AllIgnored).
Only the serialization attributes differ: [Key(n)]/[IgnoreMember]/[MessagePackObject]/
[MessagePackEquatable] replace [DataMember(Order=n)]/[IgnoreDataMember]/[DataContract]/
[DataContractEquatable]. Snapshots regenerated and accepted.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When [DictionaryEquality(sequential:true)] is applied, OrderedDictionaryEqualityComparer
now propagates to all nested dictionary levels rather than only the outermost one.
Adds nested-comparer tests to OrderedDictionaryEqualityComparerTest and snapshot tests
verifying the generated output for Dictionary<K, Dictionary<K2, V>>.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… on HashSet<T>

Fixes two missing cases for explicit comparer overrides that reverse natural order-sensitivity:

1. [HashSetEquality] on T[] (array): ValidateComparer now accepts rank-1 arrays for HashSet kind;
   new BuildHashSetArrayComparerExpression emits HashSetEqualityComparer instead of SequenceEqualityComparer.

2. [SequenceEquality] on HashSet<T>: already validated and emitted correctly (no code change needed);
   documented with explicit snapshot tests.

Adds 6 snapshot tests (base + DataContract + MessagePack × 2 directions).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…d collection levels

When [HashSetEquality] or [SequenceEquality] is explicitly annotated on a property,
the chosen comparer class now propagates into every nested enumerable/array level rather
than only the outermost. Similarly, [DictionaryEquality(sequential:true)] already
propagated dictKind to nested dicts; tests now cover 3-level nesting, mixed dict+list,
and the unordered case symmetrically with the enumKind tests.

- EquatableGenerator: thread enumKind through all nested collection builder calls;
  remove IsEnumKindOverride guard so all explicit annotations always propagate
- ValidateComparer: accept IArrayTypeSymbol{Rank:1} as valid target for [HashSetEquality]
- BuildHashSetArrayComparerExpression: new helper for [HashSetEquality] on T[]
- NestedCollectionsProperties: rename ListOfSets_InnerOrderDoesNotMatter →
  ListOfSets_InnerOrderMatters now that [SequenceEquality] propagates into inner sets
- New snapshot tests: GenerateHashSetEqualityPropagatesIntoNestedCollections,
  GenerateSequenceEqualityPropagatesIntoNestedCollections,
  GenerateDictionaryEqualityPropagatesIntoNestedDictionaries (+ DataContract/MessagePack variants)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Assert a.Equals(b) == b.Equals(a) across all nested collection shapes
(Dict/List/HashSet/Array combinations) in both property test projects.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ection overrides

Documents DataContractEquatable/MessagePackEquatable adapters, nested
collection comparer propagation (single annotation propagates all levels),
direction override examples (HashSetEquality on List, SequenceEquality on
HashSet), and custom comparer pattern with clear table format.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…xamples

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…licit override explanation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… getting started

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
a.kasyanov and others added 20 commits May 14, 2026 12:10
…nal array

Multi-dimensional arrays (rank ≥ 2) always use MultiDimensionalArrayEqualityComparer
regardless of any annotation. Any collection or equality attribute on such a property
has no effect (or worse, [EqualityComparer] silently bypasses the multi-dim comparer
and produces reference equality). EQ0014 turns this silent wrong behavior into a
visible compile-time warning.

Changes:
- DiagnosticDescriptors.cs: add EQ0014 descriptor
- EquatableAnalyzer.cs: register EQ0014 in SupportedDiagnostics; skip EQ0002 for
  multi-dim arrays (they have a default); fire EQ0014 for any collection/equality
  attribute on rank ≥ 2 arrays
- EquatableAnalyzerTest.cs: fix existing test (int[,] with no attribute → no warning);
  add 6 new tests covering EQ0014 for SequenceEquality, HashSetEquality, EqualityComparer,
  ReferenceEquality, 3D arrays, and the no-attribute baseline
- README.md: add build-time diagnostics section documenting all EQ00xx codes with
  special explanation of EQ0014 and why override is impossible on multi-dim arrays

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…sed on a dictionary type

Applying an enumerable attribute to a Dictionary<K,V> treats it as a flat sequence of
KeyValuePair entries, discarding key-lookup semantics. This is almost always wrong.
EQ0015 fires for [SequenceEquality] and [HashSetEquality] on any type that implements
IDictionary<K,V> or IReadOnlyDictionary<K,V>; [DictionaryEquality] is the correct choice.

Changes:
- DiagnosticDescriptors.cs: add EQ0015 descriptor
- EquatableAnalyzer.cs: register EQ0015; detect SequenceEquality/HashSetEquality on
  dictionary types and report EQ0015
- EquatableAnalyzerTest.cs: 5 new tests covering Dictionary<K,V>, IDictionary<K,V>,
  IReadOnlyDictionary<K,V>, and the valid [DictionaryEquality] baseline
- README.md: add EQ0015 to the diagnostics table and add an explanation section

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…mple

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ullet list

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ages too

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…able types

Adapter generators only include properties that carry the serialisation inclusion
attribute ([DataMember] / [Key(n)]). All other public properties are silently skipped.
Accidental omissions are easy to miss, so EQ0022/EQ0023 force the intent to be
explicit: either add the inclusion attribute or suppress with [IgnoreDataMember] /
[IgnoreMember] / [IgnoreEquality].

Also clarified in README:
- EQ0001/EQ0002 apply to [Equatable] only; adapters auto-infer comparers
- EQ0022/EQ0023 explanation with code example showing both paths to suppress

Changes:
- DataContractEquatableAnalyzer.cs: add EQ0022, check every public property
- MessagePackEquatableAnalyzer.cs: add EQ0023, same pattern
- DataContractAnalyzerTest.cs: 4 new tests for EQ0022 (fires, IgnoreDataMember, IgnoreEquality, multiple)
- MessagePackAnalyzerTest.cs: 4 new tests for EQ0023 (fires, IgnoreMember, IgnoreEquality, multiple)
- README.md: update diagnostics table and add explanations for all groups

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…r inference, overrides

- Document which attributes include/exclude properties per adapter (table)
- Explain EQ0022/EQ0023 in context of silent exclusion
- Show that collection comparers are inferred — no [SequenceEquality] etc. needed
- Show explicit attribute overrides still work on adapter-included properties
- Fix package table: [DataContractEquatable] uses System.Runtime.Serialization, not WCF/protobuf-net specifically

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ntract, MessagePack

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…distinction with EQ022x reference

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…es, and improvements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… declarative approach; add struct support

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…y with Key(n)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ctions still use reference equality

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…nts IEquatable, not by special treatment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ary docs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@kasyanovandrii
Copy link
Copy Markdown
Author

@pwelter34, @loresoft — PR with adapter generators, nested collection propagation, multi-dimensional array support, and new analyzer diagnostics (EQ0014/0015/0022/0023). Would appreciate a review when you have time.

a.kasyanov and others added 9 commits May 14, 2026 17:48
…adapter generators

Properties annotated with both a serialisation attribute ([DataMember] or
[Key(n)]) and [IgnoreEquality] are now correctly excluded from generated
Equals / GetHashCode. Previously IsIncludedDataContract and
IsIncludedMessagePack only checked the serialisation-specific exclusion
attributes; the [IgnoreEquality] check was missing from both adapters.

Adds snapshot tests proving the fix and a README section documenting the
[DataMember(Order=n)][IgnoreEquality] pattern with a DataContract and a
MessagePack example.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ashing

The inline OrderedDictionaryHashCode helper in EquatableWriter iterated
entries without OrderBy, while OrderedDictionaryEquals sorted by key.
An equal pair with different insertion order would produce the same Equals
result but different hash codes — a direct hash contract violation.

Also adds missing multi-entry swapped-values tests for DictionaryEqualityComparer:
- SwappedValues_UnequalDictionaries_EqualIsFalse — contract test (always passes)
- SwappedValues_ProduceDifferentHashInPractice — regression guard for systematic
  collisions in the commutative sum hash

Note: the inline helper is currently dead code (the generator always emits an
explicit OrderedReadOnlyDictionaryEqualityComparer expression for ordered dicts).
The fix prevents confusion and is correct if the path is ever reached.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…Writer

OrderedDictionaryEquals and OrderedDictionaryHashCode were never reachable:
the generator always emits an explicit OrderedReadOnlyDictionaryEqualityComparer
expression for ordered-dictionary properties, so ComparerTypes.OrderedDictionary
is never left on the property model when the writer is called.

Dead code removed; no behaviour change.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… different algorithm

Explains that [DictionaryEquality(sequential: true)] produces the same equality
result as plain [DictionaryEquality] for all inputs. The O(n log n) cost is only
justified when a custom keyComparer implements both IEqualityComparer<K> and
IComparer<K> and you need deterministic sort order for snapshot testing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tial dictionary equality

README: concrete example showing StringComparer.OrdinalIgnoreCase driving both
key equality and sort order in OrderedDictionaryEqualityComparer, with a note
that the dictionary's own internal comparer is not visible to generated code.

Tests: four cases covering case-insensitive equality, insertion-order-independent
hash with custom comparer, case-variant hash equivalence, and the contrast with
the default ordinal comparer where case variants are distinct keys.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ot just case

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…'s internal comparer

y.TryGetValue uses the dictionary's own internal comparer, not this.KeyComparer.
When a custom keyComparer is supplied, GetHashCode already uses it — so two entries
equal under KeyComparer produced the same hash but Equals returned false, a direct
hash contract violation.

Fix: build a temporary Dictionary keyed by KeyComparer, then TryGetValue against
that. O(n) cost preserved. Same fix applied to ReadOnlyDictionaryEqualityComparer.

Property tests added to DictionaryComparerProperties and ReadOnlyDictionaryComparerProperties
to permanently guard the invariant: for any custom keyComparer, Equals(x,y) implies
GetHashCode(x) == GetHashCode(y).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…mparer correctly

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ed to prod

Drops OrderedDictionaryEqualityComparer, OrderedReadOnlyDictionaryEqualityComparer,
the sequential: true parameter on DictionaryEqualityAttribute, all generator code
paths that routed to ComparerTypes.OrderedDictionary, and all related tests,
snapshot files, and README documentation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant