[http-client-csharp] Dedup operators between generated and customization partials#10639
Conversation
…ion partials Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/6ebd5fd9-d214-4a9a-bafd-72c4999ed242 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
…eSymbolProvider-based test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/0420672c-19ed-4fa7-bfbe-c1c48af7e741 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
commit: |
jorgerangel-msft
left a comment
There was a problem hiding this comment.
@copilot lets also add a test in TypeProviderTests to validate that the resulting canonicalview of a generated type who was customized with operators only has the custom operators
Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/62f6ef1e-a963-4cfb-8c42-4c8c058a268f Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Added |
…ion partials (microsoft#10639) Customization partials that declare `==`, `!=`, or `implicit`/`explicit` operators on a type whose generated partial also emits those operators (e.g. extensible enums) trigger CS0111 because the dedup path in `MethodSignatureBase.SignatureComparer` never matches operator methods. Root cause: the comparer's early `Name` equality check fails for operators because the two sides use different naming conventions: | Operator | Generated (`ExtensibleEnumProvider` / `MrwSerializationTypeDefinition`) | Customization (`NamedTypeSymbolProvider` via Roslyn `ToDisplayString`) | | --- | --- | --- | | `==`, `!=` | `"=="`, `"!="` | `"operator =="`, `"operator !="` | | `implicit operator T(...)` | `string.Empty` | `"T"` (return type name) | | `explicit operator T(...)` | `"T"` | `"T"` | Only the explicit-operator case happens to align, which is why MRW-serialization dedup tests pass while extensible-enum operators slip through. ### Changes - **`MethodSignatureBase.SignatureComparer.Equals`** — when both signatures carry the `Operator` modifier: - User-defined operators (`==`, `!=`, `+`, …): normalize the name by stripping a leading `"operator "` prefix before comparing, so `"operator =="` matches `"=="` while `==` and `!=` remain distinguishable. - Conversion operators (`implicit`/`explicit`): skip the `Name` comparison entirely. Modifiers + return type + parameter types fully identify a conversion operator (C# disallows duplicates). - Existing return-type and explicit-vs-implicit modifier checks are preserved. - **`MethodSignatureComparerTests`** — new unit tests covering: `==` generated/customization name normalization, `==` vs `!=` not equal, implicit-cast `string.Empty` matching customization return-type name, and implicit vs explicit not equal. - **`NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated`** — end-to-end test that uses the existing TestData/`Helpers.GetCompilationFromDirectoryAsync()` infrastructure to load a `WithOperators` customization partial, parse its operator signatures via `NamedTypeSymbolProvider`, and assert they match generated-side signatures (`==`, `!=`, `implicit operator T(string)`) through `MethodSignatureBase.SignatureComparer`. - **`TypeProviderTests.CanonicalViewDedupesCustomOperators`** — end-to-end test that builds a generated `TypeProvider` with `==`, `!=`, and `implicit` operator `MethodProvider`s, loads a matching customization partial via TestData, and asserts that `CanonicalView.Methods` contains exactly the three operators all sourced from `CustomCodeView` (i.e., the generated copies are filtered out). ### Example ```csharp // Generated partial (from ExtensibleEnumProvider) public readonly partial struct MyEnum { public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right); public static implicit operator MyEnum(string value) => new MyEnum(value); } // Customization partial — previously triggered CS0111 on both operators; // now correctly suppresses the duplicates from the generated partial. public readonly partial struct MyEnum : IEquatable<MyEnum> { public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right); public static implicit operator MyEnum(string value) => new MyEnum(value); } ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
…ion partials (microsoft#10639) Customization partials that declare `==`, `!=`, or `implicit`/`explicit` operators on a type whose generated partial also emits those operators (e.g. extensible enums) trigger CS0111 because the dedup path in `MethodSignatureBase.SignatureComparer` never matches operator methods. Root cause: the comparer's early `Name` equality check fails for operators because the two sides use different naming conventions: | Operator | Generated (`ExtensibleEnumProvider` / `MrwSerializationTypeDefinition`) | Customization (`NamedTypeSymbolProvider` via Roslyn `ToDisplayString`) | | --- | --- | --- | | `==`, `!=` | `"=="`, `"!="` | `"operator =="`, `"operator !="` | | `implicit operator T(...)` | `string.Empty` | `"T"` (return type name) | | `explicit operator T(...)` | `"T"` | `"T"` | Only the explicit-operator case happens to align, which is why MRW-serialization dedup tests pass while extensible-enum operators slip through. - **`MethodSignatureBase.SignatureComparer.Equals`** — when both signatures carry the `Operator` modifier: - User-defined operators (`==`, `!=`, `+`, …): normalize the name by stripping a leading `"operator "` prefix before comparing, so `"operator =="` matches `"=="` while `==` and `!=` remain distinguishable. - Conversion operators (`implicit`/`explicit`): skip the `Name` comparison entirely. Modifiers + return type + parameter types fully identify a conversion operator (C# disallows duplicates). - Existing return-type and explicit-vs-implicit modifier checks are preserved. - **`MethodSignatureComparerTests`** — new unit tests covering: `==` generated/customization name normalization, `==` vs `!=` not equal, implicit-cast `string.Empty` matching customization return-type name, and implicit vs explicit not equal. - **`NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated`** — end-to-end test that uses the existing TestData/`Helpers.GetCompilationFromDirectoryAsync()` infrastructure to load a `WithOperators` customization partial, parse its operator signatures via `NamedTypeSymbolProvider`, and assert they match generated-side signatures (`==`, `!=`, `implicit operator T(string)`) through `MethodSignatureBase.SignatureComparer`. - **`TypeProviderTests.CanonicalViewDedupesCustomOperators`** — end-to-end test that builds a generated `TypeProvider` with `==`, `!=`, and `implicit` operator `MethodProvider`s, loads a matching customization partial via TestData, and asserts that `CanonicalView.Methods` contains exactly the three operators all sourced from `CustomCodeView` (i.e., the generated copies are filtered out). ```csharp // Generated partial (from ExtensibleEnumProvider) public readonly partial struct MyEnum { public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right); public static implicit operator MyEnum(string value) => new MyEnum(value); } // Customization partial — previously triggered CS0111 on both operators; // now correctly suppresses the duplicates from the generated partial. public readonly partial struct MyEnum : IEquatable<MyEnum> { public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right); public static implicit operator MyEnum(string value) => new MyEnum(value); } ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
… as distinct in conversion-operator dedup (#10706) Fixes #10705 The operator-dedup path added in #10639 uses `CSharpType.AreNamesEqual` to compare conversion-operator return types. `AreNamesEqual` only compares the type `Name`/`FullyQualifiedName` and the generic `Arguments` list — it never reads `IsNullable`. Because the `CSharpType` constructor explicitly unwraps `Nullable<T>` for value types and stores nullability on a separate flag, `T` and `T?` produce identical results from `AreNamesEqual`. The practical effect (seen in azure-sdk-for-net PR [#59283](Azure/azure-sdk-for-net#59283)): a customization partial that declares `implicit operator T(string)` causes the dedup comparer to drop **both** the matching generated operator **and** the generated `implicit operator T?(string)`. When those were the only members in the generated partial, the file is emitted empty and the writer drops it — silently removing the nullable conversion operator from the SDK's public surface. ### Change In `MethodSignatureBaseEqualityComparer.Equals` (operator branch) compare `ReturnType.IsNullable` alongside `AreNamesEqual`. Conversion operators returning `T` and `T?` are now correctly treated as distinct. ### Tests Added `ImplicitConversionOperators_NullableAndNonNullableReturnTypes_AreNotEqual` to `MethodSignatureComparerTests`. All 290 tests in the affected suites pass. Co-authored-by: Josh Love <joshlove@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Customization partials that declare
==,!=, orimplicit/explicitoperators on a type whose generated partial also emits those operators (e.g. extensible enums) trigger CS0111 because the dedup path inMethodSignatureBase.SignatureComparernever matches operator methods.Root cause: the comparer's early
Nameequality check fails for operators because the two sides use different naming conventions:ExtensibleEnumProvider/MrwSerializationTypeDefinition)NamedTypeSymbolProvidervia RoslynToDisplayString)==,!="==","!=""operator ==","operator !="implicit operator T(...)string.Empty"T"(return type name)explicit operator T(...)"T""T"Only the explicit-operator case happens to align, which is why MRW-serialization dedup tests pass while extensible-enum operators slip through.
Changes
MethodSignatureBase.SignatureComparer.Equals— when both signatures carry theOperatormodifier:==,!=,+, …): normalize the name by stripping a leading"operator "prefix before comparing, so"operator =="matches"=="while==and!=remain distinguishable.implicit/explicit): skip theNamecomparison entirely. Modifiers + return type + parameter types fully identify a conversion operator (C# disallows duplicates).MethodSignatureComparerTests— new unit tests covering:==generated/customization name normalization,==vs!=not equal, implicit-caststring.Emptymatching customization return-type name, and implicit vs explicit not equal.NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated— end-to-end test that uses the existing TestData/Helpers.GetCompilationFromDirectoryAsync()infrastructure to load aWithOperatorscustomization partial, parse its operator signatures viaNamedTypeSymbolProvider, and assert they match generated-side signatures (==,!=,implicit operator T(string)) throughMethodSignatureBase.SignatureComparer.TypeProviderTests.CanonicalViewDedupesCustomOperators— end-to-end test that builds a generatedTypeProviderwith==,!=, andimplicitoperatorMethodProviders, loads a matching customization partial via TestData, and asserts thatCanonicalView.Methodscontains exactly the three operators all sourced fromCustomCodeView(i.e., the generated copies are filtered out).Example