diff --git a/docs/site/src/content/docs/diagnostics/index.md b/docs/site/src/content/docs/diagnostics/index.md index b6ddabca004..f26a81062ae 100644 --- a/docs/site/src/content/docs/diagnostics/index.md +++ b/docs/site/src/content/docs/diagnostics/index.md @@ -110,3 +110,7 @@ Analyzer help links use `https://aka.ms/orleans/diagnostics` with the diagnostic ## ORLEANS0026 [Invalid invokable base type mapping](orleans0026.md) — Error. A custom grain-call return type mapping cannot generate a valid invokable request. + +## ORLEANS0027 + +[Grain interface member removed from source](orleans0027.md) — Warning. The manifest retains an RPC signature which is absent from source. diff --git a/docs/site/src/content/docs/diagnostics/orleans0016.md b/docs/site/src/content/docs/diagnostics/orleans0016.md index 08bc298b164..2f2c75fdebc 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0016.md +++ b/docs/site/src/content/docs/diagnostics/orleans0016.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0016: Grain interface is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0016 when a grain interface is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Verify the interface identity and version, then apply **Add to OrleansContracts. If the interface was restored accidentally, remove it from source or introduce a separately named replacement instead of reusing a retired identity. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Deployable RPC contracts should remain in the manifest. If the project intentionally does not maintain a contract manifest, disable the contract analyzer for the project instead of suppressing individual interfaces. diff --git a/docs/site/src/content/docs/diagnostics/orleans0017.md b/docs/site/src/content/docs/diagnostics/orleans0017.md index a96a5e70afa..983451d8b08 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0017.md +++ b/docs/site/src/content/docs/diagnostics/orleans0017.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0017: Grain interface version mismatch" description: Understand and resolve ORLEANS0017 when a grain interface version differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The manifest no longer describes the numeric version used by runtime compatibili Determine whether the source or manifest changed unintentionally. Restore the previous source version, or review the rolling-upgrade implications and apply **Update version in OrleansContracts.txt** when the new version is intentional. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Use suppression only during a short-lived staged edit. Do not release with a source and manifest version mismatch. diff --git a/docs/site/src/content/docs/diagnostics/orleans0018.md b/docs/site/src/content/docs/diagnostics/orleans0018.md index 312f137789d..0ff2be41704 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0018.md +++ b/docs/site/src/content/docs/diagnostics/orleans0018.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0018: Grain interface member not declared" description: Understand and resolve ORLEANS0018 when an RPC method signature is missing from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -17,13 +17,17 @@ ms.topic: reference An ordinary grain-interface method has no matching contract signature in `OrleansContracts.txt`. Method identity, generic arity, parameter types and order, and return type are part of the signature. Parameter names are not. +The method identity is the source `[Id]` value, the source `[Alias]` value, or the generated xxHash32 ID used by the Orleans code generator. Recording a generated ID in the manifest does not add an attribute or change the runtime identity. + ## Impact Older activations can receive an unknown RPC, and changed identities or payload types can cause dispatch or serialization failures during a rolling upgrade. ## How to fix -Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the new signature but does not increment `[Version]`. +Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the existing effective wire identity and does not increment `[Version]` or modify source attributes. + +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0019.md b/docs/site/src/content/docs/diagnostics/orleans0019.md index dd6b3a83cce..f258d8b0b37 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0019.md +++ b/docs/site/src/content/docs/diagnostics/orleans0019.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0019: Removed grain interface is not retired" description: Understand and resolve ORLEANS0019 when OrleansContracts.txt contains an active interface that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The deletion or identity-changing rename is not recorded as intentional, and the Restore the interface if its removal was accidental. Otherwise apply **Mark as *RETIRED* in OrleansContracts.txt**. Preserve retired declarations as contract history. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppression is appropriate only when the manifest intentionally contains contracts owned by another compilation. Prefer one manifest per project so ownership remains explicit. diff --git a/docs/site/src/content/docs/diagnostics/orleans0020.md b/docs/site/src/content/docs/diagnostics/orleans0020.md index 7f181a14e74..aa4568d27fb 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0020.md +++ b/docs/site/src/content/docs/diagnostics/orleans0020.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0020: OrleansContracts.txt is missing" description: Understand and resolve ORLEANS0020 when contract compatibility analysis is enabled without a manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -11,7 +11,7 @@ ms.topic: reference | --- | --- | | Category | Orleans.Versioning | | Severity | Info | -| Code fix | Not available | +| Code fix | Available | ## Cause @@ -23,7 +23,7 @@ The analyzer has no baseline, so it cannot detect RPC identity, signature, versi ## How to fix -Create `OrleansContracts.txt` at `OrleansContractsPath`, add it to source control, and rebuild. Apply the resulting diagnostics' code fixes to populate interface, method, and class declarations. +Apply **Regenerate OrleansContracts.txt** to create and populate the complete project manifest. Use **Fix all in solution** to create manifests for every affected project, then add the generated files to source control and review the baseline using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0021.md b/docs/site/src/content/docs/diagnostics/orleans0021.md index 193859716a8..a8dd3d0d394 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0021.md +++ b/docs/site/src/content/docs/diagnostics/orleans0021.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0021: Duplicate grain interface declaration" description: Understand and resolve ORLEANS0021 when OrleansContracts.txt declares an interface identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats an interface CLR name or a non-empty `GrainInterfaceType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective interface identity. The effective identity is `GrainInterfaceType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The manifest is ambiguous. The parser retains the first declaration, so compatib ## How to fix -Merge the declarations into one canonical entry. Keep one active declaration when the interface exists, or one retired declaration when it has been removed. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainInterfaceType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0022.md b/docs/site/src/content/docs/diagnostics/orleans0022.md index 389b0af22da..e23e1fa69b1 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0022.md +++ b/docs/site/src/content/docs/diagnostics/orleans0022.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0022: Grain class is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0022 when a concrete grain class is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The implementation identity is not protected by contract review. A CLR rename wi Verify the class's durable grain type, add `[GrainType]` when it must remain independent of the CLR name, and apply **Add to OrleansContracts.txt**. The code fix adds or reactivates the class declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a grain class intentionally excluded from deployment-contract tracking. diff --git a/docs/site/src/content/docs/diagnostics/orleans0023.md b/docs/site/src/content/docs/diagnostics/orleans0023.md index 4b16080040e..3fffe564cf4 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0023.md +++ b/docs/site/src/content/docs/diagnostics/orleans0023.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0023: Grain class identity mismatch" description: Understand and resolve ORLEANS0023 when a grain class GrainType differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Restore the previous `[GrainType]` when the change was accidental. Update the ma The **Update grain class alias in OrleansContracts.txt** code fix accepts the source identity as the new baseline. Review the identity change before applying it. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a deliberate, documented identity migration. Updating the reviewed baseline is preferable to retaining a suppression. diff --git a/docs/site/src/content/docs/diagnostics/orleans0024.md b/docs/site/src/content/docs/diagnostics/orleans0024.md index 45f3403e944..ecccb232232 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0024.md +++ b/docs/site/src/content/docs/diagnostics/orleans0024.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0024: Removed grain class is not retired" description: Understand and resolve ORLEANS0024 when OrleansContracts.txt contains an active grain class that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The removal or identity-changing rename is not recorded, and the old grain ident Restore the class if its removal was accidental. Otherwise apply **Mark grain class as *RETIRED* in OrleansContracts.txt** and preserve the declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only when the manifest intentionally includes classes owned by another compilation. Prefer separate project manifests. diff --git a/docs/site/src/content/docs/diagnostics/orleans0025.md b/docs/site/src/content/docs/diagnostics/orleans0025.md index 642cc3de57b..23fe3dd8eca 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0025.md +++ b/docs/site/src/content/docs/diagnostics/orleans0025.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0025: Duplicate grain class declaration" description: Understand and resolve ORLEANS0025 when OrleansContracts.txt declares a grain identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats a grain class CLR name or a non-empty `GrainType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective grain class identity. The effective identity is `GrainType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The grain identity history becomes ambiguous, and the parser accepts only the fi ## How to fix -Merge or remove duplicates, retaining one canonical active or retired declaration. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md new file mode 100644 index 00000000000..94c1a4e8771 --- /dev/null +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -0,0 +1,37 @@ +--- +title: "ORLEANS0027: Grain interface member removed from source" +description: Understand and resolve ORLEANS0027 when OrleansContracts.txt retains an RPC method which is absent from source. +ms.date: 08/27/2026 +ms.topic: reference +--- + +# ORLEANS0027: Grain interface member removed from source + +| Property | Value | +| --- | --- | +| Category | Orleans.Versioning | +| Severity | Warning | +| Code fix | Not available | + +## Cause + +`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. The manifest identity is an explicit `[Id]` or `[Alias]` value when present in source; otherwise, it is the generated method ID already used by Orleans on the wire. + +## Impact + +Removing an RPC method can break calls from older clients or activations during a rolling upgrade. Regeneration retains the historical signature so the wire-contract removal remains visible and requires an explicit decision. + +## How to fix + +Restore the source method when the removal was accidental. When the removal is intentional, review the mixed-version deployment impact, increment the interface version when appropriate, and explicitly remove the retained signature from `OrleansContracts.txt`. + +See [Orleans contract compatibility analyzer](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + +## Suppress the diagnostic + +Prefer updating the reviewed manifest after accepting the contract removal. Suppress only for a manifest intentionally shared with another compilation. + +```ini +[*] +dotnet_diagnostic.ORLEANS0027.severity = none +``` diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index af2da8618d4..ba74ed48b00 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -1,7 +1,7 @@ --- title: Orleans contract compatibility analyzer description: Track grain RPC contracts during development to identify changes which can break rolling upgrades. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: concept-article --- @@ -36,40 +36,69 @@ The path can also be set in `Directory.Build.props` to apply a repository conven ## Create and update the manifest -After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file. Create the file, include it in source control, and apply the Orleans code fixes to add missing interface and class entries. Apply the code fixes again after adding RPC methods. +After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file at the first contract declaration. Apply **Regenerate OrleansContracts.txt** to create and populate the manifest for the project. -Code fixes add the generated-file header, preserve the file's line endings, and write entries in stable ordinal order. The resulting file is deterministic regardless of the order in which fixes are applied. +The regeneration code fix rebuilds every active interface, method, and grain-class entry from the project compilation. It preserves existing `*RETIRED*` entries and marks declarations which are no longer in source as retired. The generated header, line endings, and ordinal entry order are deterministic. ### Regenerate the manifest -Use the analyzer code fixes to regenerate the active contracts: +Apply **Regenerate OrleansContracts.txt** from `ORLEANS0016`, `ORLEANS0017`, `ORLEANS0018`, `ORLEANS0019`, `ORLEANS0020`, `ORLEANS0022`, `ORLEANS0023`, or `ORLEANS0024`. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. -1. Preserve every `*RETIRED*` declaration from the existing manifest. Retired identities are historical data and cannot be reconstructed from current source. -2. Create an empty `OrleansContracts.txt`, then restore the retired declarations. -3. Build the project. -4. Apply each `ORLEANS0016` and `ORLEANS0022` code fix to add active interfaces and grain classes. -5. Build again and apply each `ORLEANS0018` code fix to add interface methods. -6. Review the resulting identity, version, and signature diff before committing it. +`ORLEANS0027` intentionally retains a removed method signature, so regeneration isn't offered for that diagnostic. If it is the only remaining diagnostic, restore the source method or explicitly delete the retained signature after reviewing and accepting the wire-compatibility break. -For routine contract changes, keep the existing manifest and apply the reported update or retirement code fix instead of rebuilding it from scratch. +Agents and command-line workflows can regenerate manifests without an IDE: + +```dotnetcli +dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +``` + +Run the command from the repository root. Replace `PATH_TO_PROJECT_OR_SOLUTION` with the path to the owning `.csproj` to regenerate one manifest, or a `.sln`/`.slnx` path to regenerate manifests in every affected project. The `--severity info` option includes `ORLEANS0020`, allowing the command to create a missing manifest. + +Regeneration edits `OrleansContracts.txt` files only. It does not add or change `[Alias]`, `[Id]`, `[GrainType]`, or `[GrainInterfaceType]` attributes in source. Attribute-like syntax in the manifest records the effective identity which Orleans already uses at runtime. + +For a method without `[Id]` or `[Alias]`, the manifest records the same generated xxHash32 method ID which the Orleans code generator already uses on the wire. The preceding comment records the CLR signature so reviewers can map the wire ID back to source. A one-time upgrade from an older manifest format can therefore replace a CLR method name with its existing generated ID; this records the current wire contract and does not change it. + +After the command completes: + +1. Inspect `git diff -- "*OrleansContracts.txt"` and account for every changed identity, version, and method signature. +2. Preserve all `*RETIRED*` declarations and retained removed-method signatures unless the compatibility break is intentional. +3. Run `dotnet build PATH_TO_PROJECT_OR_SOLUTION` and resolve all Orleans contract diagnostics. `ORLEANS0027` remains until a removed method is restored or its retained signature is explicitly deleted after compatibility review. + +Add the generated file to source control and review its diff before committing. Treat every changed contract line as a potential wire-compatibility change: + +- A changed `GrainInterfaceType`, `GrainType`, method identity, parameter type, or return type changes a wire identity or signature. +- A removed source contract becomes `*RETIRED*`, preserving its identity history and preventing accidental reuse. +- A removed RPC method remains in the manifest and reports `ORLEANS0027` until the method is restored or the wire break is explicitly accepted by removing the retained signature. +- A `[Version]` change affects version-aware routing and must align with the rolling-upgrade design. +- A CLR comment-only change records a refactor while the explicit Orleans identity remains stable. + +Coding agents should regenerate the manifest instead of hand-editing active entries, retain retired history, and explain the compatibility impact of each contract diff in the change description. ## Manifest format Interface methods are indented beneath their interface: ```text -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICartGrain [Version(1)] - AddAsync(Contoso.Grains.Item) -> Task - GetAsync() -> Task + # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task + 15793847(Contoso.Grains.Item) -> Task + # Contoso.Grains.ICartGrain.GetAsync() -> Task + 857AC6B2() -> Task class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Each declaration includes both its Orleans identity and CLR type name. A diff which changes both values is a breaking identity change. A diff which changes only the CLR type name preserves the Orleans identity. +Each declaration includes both its Orleans identity and CLR type name. A diff which changes the Orleans identity changes the wire contract. A diff which changes only the CLR type name preserves an explicit Orleans identity. Explicit identities remain visible alongside their CLR names: @@ -77,13 +106,13 @@ Explicit identities remain visible alongside their CLR names: # Contoso.Grains.ICartGrain interface [GrainInterfaceType("cart")] Contoso.Grains.ICartGrain [Version(1)] # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task - add(Contoso.Grains.Item) -> Task + [Alias("add")] add(Contoso.Grains.Item) -> Task # Contoso.Grains.CartGrain class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Comments record CLR names only when they differ from the stable identity. Comments are informational and aren't part of contract matching. +`[Alias("...")]` on a manifest method records that the identity comes from a source `[Alias]` attribute. An unmarked eight-digit hexadecimal method identity is the generated wire ID. Comments record CLR names when they improve traceability; comments are informational and aren't part of contract matching. `*RETIRED*` marks an intentionally removed contract: @@ -93,7 +122,7 @@ Comments record CLR names only when they differ from the stable identity. Commen *RETIRED* class [GrainType("legacy")] Contoso.Grains.LegacyGrain ``` -Don't delete retired entries. They preserve the contract history and prevent a removed identity from being unintentionally reused. +Retired entries preserve contract history and prevent a removed identity from being unintentionally reused. ## Refactor-safe identities @@ -104,7 +133,7 @@ The analyzer uses Orleans identities before CLR names: - or identifies grain methods. - identifies serialized parameter and return types. -When these identities remain unchanged, renaming a CLR class, interface, method, parameter, or aliased data type doesn't require a manifest update. Changing an Orleans identity remains a contract change and produces a diagnostic. +When these identities remain unchanged, a CLR class, interface, method, parameter, or aliased data type can be renamed while preserving the wire identity. Changing an Orleans identity produces a contract diff and diagnostic. Without an explicit stable identity, Orleans derives the identity from the CLR type name. Renaming the CLR type therefore changes the derived identity and the contract. @@ -122,9 +151,10 @@ Without an explicit stable identity, Orleans derives the identity from the CLR t | [`ORLEANS0023`](../../diagnostics/orleans0023.md) | Warning | A grain class identity differs from the manifest. | | [`ORLEANS0024`](../../diagnostics/orleans0024.md) | Warning | A removed grain class isn't marked `*RETIRED*`. | | [`ORLEANS0025`](../../diagnostics/orleans0025.md) | Warning | A grain class is declared more than once. | +| [`ORLEANS0027`](../../diagnostics/orleans0027.md) | Warning | An RPC method remains in the manifest after it is removed from source. | Standard `.editorconfig` diagnostic configuration can change these severities. Prefer fixing contract drift instead of suppressing diagnostics. ## Scope -The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. It doesn't prove behavioral compatibility or validate persisted state schemas. Continue to follow the [backward compatibility guidelines](backward-compatibility-guidelines.md) and test mixed-version deployments before production rollout. +The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. Behavioral compatibility and persisted state schemas require separate review using the [backward compatibility guidelines](backward-compatibility-guidelines.md) and mixed-version deployment tests. diff --git a/docs/site/src/content/docs/toc.yml b/docs/site/src/content/docs/toc.yml index 6c7037e1fe1..ac0f03b8883 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -435,6 +435,8 @@ items: href: diagnostics/orleans0025.md - name: ORLEANS0026 href: diagnostics/orleans0026.md + - name: ORLEANS0027 + href: diagnostics/orleans0027.md - name: API context and examples href: resources/api-reference-guide.md - name: Best practices diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 14bcf5a3150..181ecb811a4 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -1,32 +1,38 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardGrain")] Orleans.Dashboard.Core.IDashboardGrain [Version(0)] - GetClusterTracing() -> Task>> - GetCounters(string[]) -> Task> - GetGrainState(string?, string?) -> Task> - GetGrainTracing(string) -> Task>>> - GetGrainTypes(string[]) -> Task> - GetSiloTracing(string) -> Task>> - InitializeAsync() -> Task - SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task - TopGrainMethods(int, string[]) -> Task>> + [Alias("GetClusterTracing")] GetClusterTracing() -> Task>> + [Alias("GetCounters")] GetCounters(string[]) -> Task> + [Alias("GetGrainState")] GetGrainState(string?, string?) -> Task> + [Alias("GetGrainTracing")] GetGrainTracing(string) -> Task>>> + [Alias("GetGrainTypes")] GetGrainTypes(string[]) -> Task> + [Alias("GetSiloTracing")] GetSiloTracing(string) -> Task>> + [Alias("InitializeAsync")] InitializeAsync() -> Task + [Alias("SubmitTracing")] SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task + [Alias("TopGrainMethods")] TopGrainMethods(int, string[]) -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardRemindersGrain")] Orleans.Dashboard.Core.IDashboardRemindersGrain [Version(0)] - GetReminders(int, int) -> Task> + [Alias("GetReminders")] GetReminders(int, int) -> Task> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainProxy")] Orleans.Dashboard.Core.ISiloGrainProxy [Version(0)] - GetMetadata() -> Task>> + [Alias("GetMetadata")] GetMetadata() -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainService")] Orleans.Dashboard.Core.ISiloGrainService [Version(0)] - Enable(bool) -> Task - GetCounters() -> Task> - GetExtendedProperties() -> Task>> - GetLifecycleStages() -> Task> - GetRuntimeStatistics() -> Task> - ReportCounters(Orleans.Concurrency.Immutable) -> Task - SetVersion(string, string) -> Task + [Alias("Enable")] Enable(bool) -> Task + [Alias("GetCounters")] GetCounters() -> Task> + [Alias("GetExtendedProperties")] GetExtendedProperties() -> Task>> + [Alias("GetLifecycleStages")] GetLifecycleStages() -> Task> + [Alias("GetRuntimeStatistics")] GetRuntimeStatistics() -> Task> + [Alias("ReportCounters")] ReportCounters(Orleans.Concurrency.Immutable) -> Task + [Alias("SetVersion")] SetVersion(string, string) -> Task class [GrainType("dashboard")] Orleans.Dashboard.Implementation.Grains.DashboardGrain diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 721094b23ba..2520dcbd010 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -7,6 +7,7 @@ + diff --git a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md index 28ca7fa6bfc..30fabca3063 100644 --- a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md @@ -16,3 +16,4 @@ ORLEANS0022 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grai ORLEANS0023 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class alias mismatch ORLEANS0024 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain class not marked as *RETIRED* ORLEANS0025 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate grain class declaration in file +ORLEANS0027 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain interface member remains in OrleansContracts.txt diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 11d31f780a8..888ed990198 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; +using Orleans.CodeGenerator; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -33,6 +34,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer public const string RuleId0023 = "ORLEANS0023"; public const string RuleId0024 = "ORLEANS0024"; public const string RuleId0025 = "ORLEANS0025"; + public const string RuleId0027 = "ORLEANS0027"; // Property bag keys for code fixes internal const string InterfaceNamePropertyKey = "InterfaceName"; @@ -79,6 +81,17 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0018)); + private static readonly DiagnosticDescriptor RemovedMemberRule = new( + id: RuleId0027, + title: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedMessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Orleans.Versioning", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedDescription), Resources.ResourceManager, typeof(Resources)), + helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0027), + customTags: WellKnownDiagnosticTags.CompilationEnd); + private static readonly DiagnosticDescriptor RemovedInterfaceNotRetiredRule = new( id: RuleId0019, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredTitle), Resources.ResourceManager, typeof(Resources)), @@ -163,7 +176,8 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer GrainClassNotDeclaredRule, GrainClassAliasMismatchRule, RemovedGrainClassNotRetiredRule, - DuplicateGrainClassDeclarationRule); + DuplicateGrainClassDeclarationRule, + RemovedMemberRule); public override void Initialize(AnalysisContext context) { @@ -191,18 +205,19 @@ private void OnCompilationStart(CompilationStartAnalysisContext context) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); GrainInterfaceData? data = null; + SourceText? sourceText = null; List? fileParseErrors = null; if (grainInterfacesFile is not null) { - var sourceText = grainInterfacesFile.GetText(context.CancellationToken); + sourceText = grainInterfacesFile.GetText(context.CancellationToken); if (sourceText is not null) { (data, fileParseErrors) = GrainInterfaceFileParser.Parse(sourceText, grainInterfacesFile.Path); } } - var impl = new Impl(context.Compilation, data, grainInterfacesFile, fileParseErrors); + var impl = new Impl(context.Compilation, data, grainInterfacesFile, sourceText, fileParseErrors); context.RegisterSymbolAction(impl.AnalyzeNamedType, SymbolKind.NamedType); context.RegisterCompilationEndAction(impl.OnCompilationEnd); @@ -212,21 +227,26 @@ private sealed class Impl { private readonly GrainInterfaceData? _data; private readonly AdditionalText? _grainInterfacesFile; + private readonly SourceText? _grainInterfacesFileText; private readonly List? _fileParseErrors; private readonly ConcurrentDictionary _visitedInterfaces = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _visitedClasses = new(StringComparer.Ordinal); + private readonly ConcurrentBag _removedMemberDiagnostics = new(); private readonly INamedTypeSymbol? _iAddressableType; private readonly INamedTypeSymbol? _aliasAttributeType; private readonly INamedTypeSymbol? _versionAttributeType; + private Location? _firstContractLocation; public Impl( Compilation compilation, GrainInterfaceData? data, AdditionalText? grainInterfacesFile, + SourceText? grainInterfacesFileText, List? fileParseErrors) { _data = data; _grainInterfacesFile = grainInterfacesFile; + _grainInterfacesFileText = grainInterfacesFileText; _fileParseErrors = fileParseErrors; _iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); @@ -262,13 +282,24 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) if (_data is null) { _visitedInterfaces.TryAdd(interfaceName, true); + RecordContractLocation(namedType); // We'll report file missing at compilation end return; } // Check if interface is declared in the file + var explicitGrainInterfaceType = GetStringAttributeValue( + namedType, + Constants.GrainInterfaceTypeAttributeFullyQualifiedName); var grainInterfaceType = GetGrainInterfaceType(namedType); - var declaredInterface = FindDeclaredInterface(interfaceName, grainInterfaceType); + var declaredInterface = FindDeclaredInterface( + interfaceName, + grainInterfaceType, + explicitGrainInterfaceType is null + || string.Equals( + explicitGrainInterfaceType, + GetDefaultGrainInterfaceType(namedType), + StringComparison.Ordinal)); if (declaredInterface is null) { _visitedInterfaces.TryAdd(interfaceName, true); @@ -288,7 +319,7 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) return; } - _visitedInterfaces.TryAdd(declaredInterface.Name, true); + _visitedInterfaces.TryAdd(GetDeclarationKey(declaredInterface), true); // Check if retired if (declaredInterface.IsRetired) @@ -339,38 +370,23 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) // Alias mismatch - could add a separate diagnostic for this } + var sourceMembers = namedType.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .ToArray(); + // Check members - foreach (var member in namedType.GetMembers().OfType()) + foreach (var member in sourceMembers) { - if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) - { - continue; - } - var memberSignature = GetMethodSignature(member); var memberAlias = GetAliasFromAttribute(member); - if (!declaredInterface.Members.ContainsKey(memberSignature) - && !declaredInterface.Members.Keys.Any(signature => - { - if (string.Equals( - NormalizeStoredMemberSignature(signature, declaredInterface.Name), - memberSignature, - StringComparison.Ordinal)) - { - return true; - } - - var normalized = NormalizeLegacyMethodSignature(signature); - var clrSignature = GetClrMethodSignature(member); - var containingTypePrefix = - $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; - return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) - || string.Equals( - normalized, - NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), - StringComparison.Ordinal); - })) + if (!declaredInterface.Members.Values.Any(declaredMember => + GrainInterfaceVersionAnalyzer.IsMatchingMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) { // Member not found - interface has changed var properties = ImmutableDictionary.Empty @@ -387,10 +403,38 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) location, properties, memberSignature, - interfaceName)); + interfaceName, + GetClrMethodSignature(member))); } } } + + if (_grainInterfacesFile is not null && _grainInterfacesFileText is { } sourceText) + { + foreach (var declaredMember in declaredInterface.Members.Values) + { + if (sourceMembers.Any(member => + GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) + { + continue; + } + + var properties = ImmutableDictionary.Empty + .Add(InterfaceNamePropertyKey, interfaceName) + .Add(GrainInterfaceTypePropertyKey, grainInterfaceType) + .Add(MemberNamePropertyKey, declaredMember.Signature); + _removedMemberDiagnostics.Add(Diagnostic.Create( + RemovedMemberRule, + declaredMember.GetLocation(sourceText, _grainInterfacesFile.Path), + properties, + declaredMember.Signature, + interfaceName)); + } + } } public void OnCompilationEnd(CompilationAnalysisContext context) @@ -404,60 +448,66 @@ public void OnCompilationEnd(CompilationAnalysisContext context) } } + foreach (var diagnostic in _removedMemberDiagnostics) + { + context.ReportDiagnostic(diagnostic); + } + // Report file missing if any grain interfaces were found but no file exists if (_data is null && (_visitedInterfaces.Count > 0 || _visitedClasses.Count > 0)) { context.ReportDiagnostic(Diagnostic.Create( OrleansContractsFileMissingRule, - Location.None, + _firstContractLocation ?? Location.None, Constants.OrleansContractsFileName)); } // Check for removed interfaces (in file but not in code) if (_data is not null && _grainInterfacesFile is not null) { - var sourceText = _grainInterfacesFile.GetText(context.CancellationToken); - if (sourceText is not null) + if (_grainInterfacesFileText is { } sourceText) { - foreach (var kvp in _data.Interfaces) + foreach (var declaredInterface in _data.Interfaces) { - if (kvp.Value.IsRetired) + if (declaredInterface.IsRetired) { continue; } - if (!_visitedInterfaces.ContainsKey(kvp.Key)) + if (!_visitedInterfaces.ContainsKey(GetDeclarationKey(declaredInterface))) { // Interface in file but not in code - needs to be retired - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredInterface.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(InterfaceNamePropertyKey, kvp.Key); + .Add(InterfaceNamePropertyKey, declaredInterface.Name) + .Add(GrainInterfaceTypePropertyKey, declaredInterface.GrainInterfaceType); context.ReportDiagnostic(Diagnostic.Create( RemovedInterfaceNotRetiredRule, location, properties, - kvp.Key)); + declaredInterface.Name)); } } - foreach (var kvp in _data.Classes) + foreach (var declaredClass in _data.Classes) { - if (kvp.Value.IsRetired || _visitedClasses.ContainsKey(kvp.Key)) + if (declaredClass.IsRetired || _visitedClasses.ContainsKey(GetDeclarationKey(declaredClass))) { continue; } - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredClass.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(ClassNamePropertyKey, kvp.Key); + .Add(ClassNamePropertyKey, declaredClass.Name) + .Add(ActualAliasPropertyKey, declaredClass.Alias); context.ReportDiagnostic(Diagnostic.Create( RemovedGrainClassNotRetiredRule, location, properties, - kvp.Key)); + declaredClass.Name)); } } } @@ -470,6 +520,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n if (_data is null) { _visitedClasses.TryAdd(className, true); + RecordContractLocation(namedType); return; } @@ -487,7 +538,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n return; } - _visitedClasses.TryAdd(declaredClass.Name, true); + _visitedClasses.TryAdd(GetDeclarationKey(declaredClass), true); if (declaredClass.Alias is null || string.Equals(codeAlias, declaredClass.Alias, StringComparison.Ordinal)) { @@ -520,43 +571,68 @@ private bool IsRpcContract(INamedTypeSymbol type) && type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, _iAddressableType)); } - private DeclaredGrainInterface? FindDeclaredInterface(string interfaceName, string? grainInterfaceType) + private void RecordContractLocation(INamedTypeSymbol type) + { + var location = type.Locations.FirstOrDefault(candidate => candidate.IsInSource); + if (location is not null) + { + Interlocked.CompareExchange(ref _firstContractLocation, location, null); + } + } + + private DeclaredGrainInterface? FindDeclaredInterface( + string interfaceName, + string? grainInterfaceType, + bool allowLegacyNameMatch) { if (grainInterfaceType is not null) { - var stableMatch = _data!.Interfaces.Values.FirstOrDefault(candidate => - string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal)); + var stableMatch = _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), grainInterfaceType, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Interfaces.TryGetValue(interfaceName, out var legacyMatch) - && legacyMatch.GrainInterfaceType is null - ? legacyMatch - : null; + if (allowLegacyNameMatch) + { + return _data.Interfaces.FirstOrDefault(candidate => + candidate.GrainInterfaceType is null + && string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); + } + + return null; } - return _data!.Interfaces.TryGetValue(interfaceName, out var result) ? result : null; + return _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); } private DeclaredGrainClass? FindDeclaredClass(string className, string? alias) { if (alias is not null) { - var stableMatch = _data!.Classes.Values.FirstOrDefault(candidate => - string.Equals(candidate.Alias, alias, StringComparison.Ordinal)); + var stableMatch = _data!.Classes.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), alias, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Classes.TryGetValue(className, out var exactNameMatch) ? exactNameMatch : null; + return _data.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } - return _data!.Classes.TryGetValue(className, out var result) ? result : null; + return _data!.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } + private static string GetDeclarationKey(DeclaredGrainInterface declaration) + => declaration.GrainInterfaceType ?? GetDefaultGrainInterfaceType(declaration.Name); + + private static string GetDeclarationKey(DeclaredGrainClass declaration) + => declaration.Alias ?? GetDefaultGrainType(declaration.Name); + private ushort GetVersionFromAttribute(INamedTypeSymbol type) { if (_versionAttributeType is null) @@ -610,21 +686,68 @@ internal static string GetGrainType(INamedTypeSymbol type) return grainType; } - var name = type.MetadataName.ToLowerInvariant(); - var arityIndex = name.IndexOf('`'); - var arity = arityIndex >= 0 ? name.Substring(arityIndex) : string.Empty; - if (arityIndex >= 0) + return GetDefaultGrainType(type); + } + + internal static string GetDefaultGrainType(INamedTypeSymbol type) + { + var name = type.Name.ToLowerInvariant(); + + const string GrainSuffix = "grain"; + if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) + { + name = name.Substring(0, name.Length - GrainSuffix.Length); + } + + var arity = 0; + for (var current = type; current is not null; current = current.ContainingType) + { + arity += current.Arity; + } + + return arity > 0 ? $"{name}`{arity}" : name; + } + + internal static string GetDefaultGrainType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = typeName.Substring(simpleNameStart); + var genericStart = simpleName.IndexOf('<'); + if (genericStart >= 0) { - name = name.Substring(0, arityIndex); + simpleName = simpleName.Substring(0, genericStart); } + var name = simpleName.ToLowerInvariant(); const string GrainSuffix = "grain"; if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) { name = name.Substring(0, name.Length - GrainSuffix.Length); } - return name + arity; + var arity = 0; + var searchIndex = 0; + while ((genericStart = typeName.IndexOf('<', searchIndex)) >= 0) + { + var genericEnd = typeName.IndexOf('>', genericStart + 1); + if (genericEnd < 0) + { + break; + } + + arity++; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (typeName[index] == ',') + { + arity++; + } + } + + searchIndex = genericEnd + 1; + } + + return arity > 0 ? $"{name}`{arity}" : name; } internal static string GetGrainInterfaceType(INamedTypeSymbol type) @@ -634,14 +757,14 @@ internal static string GetGrainInterfaceType(INamedTypeSymbol type) return grainInterfaceType; } - return GetRuntimeTypeName(type); + return GetDefaultGrainInterfaceType(type); } - private static string GetRuntimeTypeName(INamedTypeSymbol type) + internal static string GetDefaultGrainInterfaceType(INamedTypeSymbol type) { if (type.ContainingType is { } containingType) { - return $"{GetRuntimeTypeName(containingType)}+{type.MetadataName}"; + return $"{GetDefaultGrainInterfaceType(containingType)}+{type.MetadataName}"; } return type.ContainingNamespace.IsGlobalNamespace @@ -649,12 +772,52 @@ private static string GetRuntimeTypeName(INamedTypeSymbol type) : $"{type.ContainingNamespace.ToDisplayString()}.{type.MetadataName}"; } + internal static string GetDefaultGrainInterfaceType(string typeName) + { + var segments = typeName.Split('.'); + var firstGenericSegment = Array.FindIndex(segments, segment => segment.IndexOf('<') >= 0); + if (firstGenericSegment < 0) + { + return typeName; + } + + for (var index = firstGenericSegment; index < segments.Length; index++) + { + var genericStart = segments[index].IndexOf('<'); + if (genericStart < 0) + { + continue; + } + + var genericEnd = segments[index].LastIndexOf('>'); + var arity = 1; + for (var characterIndex = genericStart + 1; characterIndex < genericEnd; characterIndex++) + { + if (segments[index][characterIndex] == ',') + { + arity++; + } + } + + segments[index] = $"{segments[index].Substring(0, genericStart)}`{arity}"; + } + + return string.Join(".", segments.Take(firstGenericSegment)) + + (firstGenericSegment > 0 ? "." : string.Empty) + + string.Join("+", segments.Skip(firstGenericSegment)); + } + internal static string GetMethodSignature(IMethodSymbol method) { - var sb = new StringBuilder(); var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); - sb.Append(methodId ?? methodAlias ?? method.Name); + return GetMethodSignature(method, methodId ?? methodAlias ?? MethodIdProvider.Create(method)); + } + + private static string GetMethodSignature(IMethodSymbol method, object methodId) + { + var sb = new StringBuilder(); + sb.Append(methodId); if (method.Arity > 0) { sb.Append('`'); @@ -706,6 +869,11 @@ internal static bool RequiresClrComment(IMethodSymbol method) { var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName)?.ToString(); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); + if (methodId is null && methodAlias is null) + { + return true; + } + if (methodId is not null && !string.Equals(methodId, method.Name, StringComparison.Ordinal) || methodAlias is not null && !string.Equals(methodAlias, method.Name, StringComparison.Ordinal)) { @@ -744,6 +912,116 @@ internal static string NormalizeStoredMemberSignature(string signature, string i return Regex.Replace(result, @"alias\(""([^""]+)""\)", "$1"); } + internal static bool IsMatchingMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + var memberSignature = GetMethodSignature(member); + var sourceMethodId = GetAttributeValue(member, Constants.IdAttributeFullyQualifiedName); + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); + if (storedAlias is not null) + { + if (!string.Equals( + storedAlias, + sourceMethodAlias, + StringComparison.Ordinal)) + { + return false; + } + + var storedIdentity = storedAlias + GrainInterfaceFileParser.GetMethodAritySuffix(storedSignature); + var parameterListStart = memberSignature.IndexOf('('); + if (parameterListStart < 0 + || !string.Equals( + storedIdentity, + memberSignature.Substring(0, parameterListStart), + StringComparison.Ordinal)) + { + return false; + } + + var canonicalStoredSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedSignature, + storedAlias); + if (string.Equals( + NormalizeStoredMemberSignature(canonicalStoredSignature, declaredInterfaceName), + memberSignature, + StringComparison.Ordinal)) + { + return true; + } + + return string.Equals( + GetNormalizedSignatureSuffix(storedSignature), + GetNormalizedSignatureSuffix(GetClrMethodSignature(member)), + StringComparison.Ordinal); + } + + var normalizedStoredSignature = NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName); + if (sourceMethodAlias is not null) + { + return false; + } + + if (sourceMethodId is not null) + { + return string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal); + } + + if (string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal)) + { + return true; + } + + if (string.Equals( + normalizedStoredSignature, + GetMethodSignature(member, member.Name), + StringComparison.Ordinal)) + { + return true; + } + + var normalized = NormalizeLegacyMethodSignature(storedSignature); + var clrSignature = GetClrMethodSignature(member); + var containingTypePrefix = + $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; + return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) + || string.Equals( + normalized, + NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), + StringComparison.Ordinal); + } + + internal static bool IsMatchingHistoricalMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + if (IsMatchingMember(declaredInterfaceName, storedSignature, storedAlias, member)) + { + return true; + } + + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); + return storedAlias is null + && sourceMethodAlias is not null + && string.Equals( + NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), + GetMethodSignature(member), + StringComparison.Ordinal); + } + + private static string GetNormalizedSignatureSuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? NormalizeLegacyMethodSignature(signature) + : NormalizeLegacyMethodSignature(signature.Substring(parameterListStart)); + } + private static string GetContractTypeName(ITypeSymbol type) { if (type is IArrayTypeSymbol array) @@ -844,9 +1122,9 @@ private static bool HasMeaningfulTypeAlias(ITypeSymbol type) /// internal sealed class GrainInterfaceData { - public Dictionary Interfaces { get; } = new(StringComparer.Ordinal); + public List Interfaces { get; } = new(); - public Dictionary Classes { get; } = new(StringComparer.Ordinal); + public List Classes { get; } = new(); } /// @@ -888,6 +1166,12 @@ public DeclaredGrainMember(string signature) public string Signature { get; } public string? Alias { get; set; } public TextSpan Span { get; set; } + + public Location GetLocation(SourceText sourceText, string filePath) + { + var lineSpan = sourceText.Lines.GetLinePositionSpan(Span); + return Location.Create(filePath, Span, lineSpan); + } } /// @@ -925,13 +1209,13 @@ internal static class GrainInterfaceFileParser // Or with retired: *RETIRED* interface [GrainInterfaceType("x")] Namespace.IInterface [Version(N)] // The name can include generic type parameters like IMyGrain or IMyGrain private static readonly Regex InterfacePattern = new( - @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)\s*\[Version\((?\d+)\)\]$", + @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)\s*\[Version\((?\d+)\)\]$", RegexOptions.Compiled); // Grain class line: class [GrainType("x")] Namespace.GrainClass // Or with retired: *RETIRED* class [GrainType("x")] Namespace.GrainClass private static readonly Regex GrainClassPattern = new( - @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)$", + @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)$", RegexOptions.Compiled); // Member line: [Alias("x")] Namespace.IInterface.Method(params) -> ReturnType @@ -979,22 +1263,97 @@ internal static bool TryGetGrainClassName(string line, out string name) return false; } + internal static bool TryGetGrainClassType(string line, out string grainType) + { + var match = GrainClassPattern.Match(StripClrComment(line)); + if (match.Success && match.Groups["alias"].Success) + { + grainType = match.Groups["alias"].Value; + return true; + } + + grainType = string.Empty; + return false; + } + internal static bool TryGetContractName(string line, out string name) => TryGetGrainClassName(line, out name) || TryGetInterfaceName(line, out name); internal static bool TryGetMemberSignature(string line, out string signature) + { + if (TryGetMemberDeclaration(line, out signature, out var alias)) + { + signature = GetCanonicalMemberSignature(signature, alias); + return true; + } + + signature = string.Empty; + return false; + } + + internal static bool TryGetMemberDeclaration(string line, out string signature, out string? alias) { var match = MemberPattern.Match(StripClrComment(line)); if (match.Success) { signature = match.Groups["signature"].Value; + alias = match.Groups["alias"].Success ? match.Groups["alias"].Value : null; return true; } signature = string.Empty; + alias = null; return false; } + internal static string GetCanonicalMemberSignature(string signature, string? alias) + { + if (alias is null) + { + return signature; + } + + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? signature + : alias + GetMethodAritySuffix(signature) + signature.Substring(parameterListStart); + } + + internal static string GetMethodAritySuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + if (parameterListStart < 0) + { + return string.Empty; + } + + var methodStart = signature.LastIndexOf('.', parameterListStart - 1) + 1; + var methodName = signature.Substring(methodStart, parameterListStart - methodStart); + var arityMarker = methodName.LastIndexOf('`'); + if (arityMarker >= 0) + { + return methodName.Substring(arityMarker); + } + + var genericStart = methodName.IndexOf('<'); + var genericEnd = methodName.LastIndexOf('>'); + if (genericStart < 0 || genericEnd <= genericStart) + { + return string.Empty; + } + + var arity = 1; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (methodName[index] == ',') + { + arity++; + } + } + + return $"`{arity}"; + } + internal static string GetClrComment(string line) { const string Prefix = " # CLR: "; @@ -1032,8 +1391,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe currentInterface = null; var name = grainClassMatch.Groups["name"].Value; var alias = grainClassMatch.Groups["alias"].Success ? grainClassMatch.Groups["alias"].Value : null; - if (data.Classes.ContainsKey(name) - || alias is not null && data.Classes.Values.Any(candidate => string.Equals(candidate.Alias, alias, StringComparison.Ordinal))) + var identity = alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(name); + if (data.Classes.Any(candidate => + string.Equals( + candidate.Alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(candidate.Name), + identity, + StringComparison.Ordinal))) { errors ??= new List(); var location = Location.Create( @@ -1044,12 +1407,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe continue; } - data.Classes[name] = new DeclaredGrainClass(name) + data.Classes.Add(new DeclaredGrainClass(name) { Alias = alias, IsRetired = grainClassMatch.Groups["retired"].Success, Span = textLine.Span - }; + }); continue; } @@ -1067,9 +1430,13 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe } var isRetired = interfaceMatch.Groups["retired"].Success; - if (data.Interfaces.ContainsKey(name) - || grainInterfaceType is not null - && data.Interfaces.Values.Any(candidate => string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal))) + var identity = grainInterfaceType ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(name); + if (data.Interfaces.Any(candidate => + string.Equals( + candidate.GrainInterfaceType + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(candidate.Name), + identity, + StringComparison.Ordinal))) { // Duplicate declaration errors ??= new List(); @@ -1092,7 +1459,7 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe IsRetired = isRetired, Span = textLine.Span }; - data.Interfaces[name] = currentInterface; + data.Interfaces.Add(currentInterface); continue; } diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 4c6203480f2..e31560066f0 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -24,11 +24,22 @@ namespace Orleans.Analyzers; public class GrainInterfaceVersionCodeFix : CodeFixProvider { private const string DefaultNewLine = "\n"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = nameof(RegenerateOrleansContractsFileAsync); + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private const string RegenerationCommand = + "dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024"; private static readonly string[] GeneratedHeader = [ - "# This file is auto-generated by the Orleans contract analyzer.", - "# Update source contracts, then regenerate this file by following:", - "# https://aka.ms/orleans/OrleansContracts.txt" + "# This file is generated by the Orleans contract analyzer.", + "# To regenerate, run this command from the repository root after replacing", + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:", + $"# {RegenerationCommand}", + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION", + "# The regeneration command edits this manifest only; it does not change source attributes.", + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.", + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", + "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( @@ -36,15 +47,26 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider GrainInterfaceVersionAnalyzer.RuleId0017, // Version mismatch GrainInterfaceVersionAnalyzer.RuleId0018, // Member not declared GrainInterfaceVersionAnalyzer.RuleId0019, // Removed interface not retired + GrainInterfaceVersionAnalyzer.RuleId0020, // Contracts file missing GrainInterfaceVersionAnalyzer.RuleId0022, // Grain class not declared GrainInterfaceVersionAnalyzer.RuleId0023, // Grain class alias mismatch GrainInterfaceVersionAnalyzer.RuleId0024); // Removed grain class not retired - // Note: We don't use BatchFixer because each fix may need to coordinate updates to the same file - public sealed override FixAllProvider? GetFixAllProvider() => null; + public sealed override FixAllProvider GetFixAllProvider() => OrleansContractsFixAllProvider.Instance; public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { + var validDiagnostics = context.Diagnostics.Where(HasRequiredProperties).ToImmutableArray(); + if (!validDiagnostics.IsEmpty) + { + context.RegisterCodeFix( + CodeAction.Create( + title: RegenerateCodeActionTitle, + createChangedSolution: cancellationToken => RegenerateOrleansContractsFileAsync(context.Document.Project, cancellationToken), + equivalenceKey: RegenerateCodeActionEquivalenceKey), + validDiagnostics); + } + foreach (var diagnostic in context.Diagnostics) { switch (diagnostic.Id) @@ -76,6 +98,660 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } + private static bool HasRequiredProperties(Diagnostic diagnostic) + => diagnostic.Id switch + { + GrainInterfaceVersionAnalyzer.RuleId0016 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0017 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ActualVersionPropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0018 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0019 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0020 => true, + GrainInterfaceVersionAnalyzer.RuleId0022 + or GrainInterfaceVersionAnalyzer.RuleId0023 + or GrainInterfaceVersionAnalyzer.RuleId0024 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ClassNamePropertyKey), + _ => false + }; + + private static bool HasProperty(Diagnostic diagnostic, string propertyName) + => diagnostic.Properties.TryGetValue(propertyName, out var value) + && !string.IsNullOrEmpty(value); + + private static async Task RegenerateOrleansContractsFileAsync( + Project project, + CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return project.Solution; + } + + var contractsFile = FindContractsDocument(project); + var existingText = contractsFile is null + ? null + : await contractsFile.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newLine = existingText is null ? DefaultNewLine : GetNewLine(existingText); + var lines = new List(); + var activeInterfaceIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionInterfaceNames = new HashSet(StringComparer.Ordinal); + var activeInterfaces = new Dictionary(StringComparer.Ordinal); + var activeInterfacesByName = new Dictionary(StringComparer.Ordinal); + var activeClassIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionClassNames = new HashSet(StringComparer.Ordinal); + var iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); + var generatedCodeTrees = new Dictionary(); + var semanticModels = new Dictionary(); + + foreach (var type in GetAllSourceTypes( + compilation.Assembly.GlobalNamespace, + compilation, + generatedCodeTrees, + semanticModels) + .OrderBy(GetFullyQualifiedName, StringComparer.Ordinal)) + { + if (type.TypeKind == TypeKind.Interface + && iAddressableType is not null + && !SymbolEqualityComparer.Default.Equals(type, iAddressableType) + && type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, iAddressableType))) + { + AppendInterface( + lines, + type, + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName); + } + else if (type.TypeKind == TypeKind.Class && !type.IsAbstract && type.IsGrainClass()) + { + AppendGrainClass(lines, type, activeClassIdentities, activeConventionClassNames); + } + } + + if (existingText is not null) + { + AppendHistoricalContracts( + lines, + existingText.ToString(), + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName, + activeClassIdentities, + activeConventionClassNames); + } + + var content = SortContractEntries(string.Join(newLine, lines), newLine); + var newText = SourceText.From(content, Utf8NoBom); + if (contractsFile is not null) + { + return project.Solution.WithAdditionalDocumentText(contractsFile.Id, newText); + } + + var filePath = GetConfiguredContractsPath(project); + return project.Solution.AddAdditionalDocument( + DocumentId.CreateNewId(project.Id), + Path.GetFileName(filePath), + newText, + filePath: filePath); + } + + private static TextDocument? FindContractsDocument(Project project) + { + var configuredPath = GetConfiguredContractsPath(project); + var configuredDocument = project.AdditionalDocuments.FirstOrDefault(document => + PathsEqual(document.FilePath ?? document.Name, configuredPath)); + if (configuredDocument is not null) + { + return configuredDocument; + } + + foreach (var additionalFile in project.AnalyzerOptions.AdditionalFiles) + { + if (!Path.GetFileName(additionalFile.Path) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase) + && (!project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(additionalFile) + .TryGetValue("build_metadata.AdditionalFiles.OrleansContractsFile", out var value) + || !string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var document = project.AdditionalDocuments.FirstOrDefault(candidate => + string.Equals(candidate.FilePath, additionalFile.Path, StringComparison.OrdinalIgnoreCase)); + if (document is not null) + { + return document; + } + } + + return project.AdditionalDocuments.FirstOrDefault(document => + Path.GetFileName(document.FilePath ?? document.Name) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + } + + private static bool PathsEqual(string left, string right) + { + left = NormalizePathSeparators(left); + right = NormalizePathSeparators(right); + if (Path.IsPathRooted(left) && Path.IsPathRooted(right)) + { + left = Path.GetFullPath(left); + right = Path.GetFullPath(right); + } + + return string.Equals(left, right, StringComparison.OrdinalIgnoreCase); + } + + private static string GetConfiguredContractsPath(Project project) + { + if (project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( + "build_property.OrleansContractsPath", + out var configuredPath) + && !string.IsNullOrWhiteSpace(configuredPath)) + { + configuredPath = NormalizePathSeparators(configuredPath); + if (Path.IsPathRooted(configuredPath)) + { + return configuredPath; + } + + if (Path.GetDirectoryName(project.FilePath) is { } projectDirectory) + { + return Path.Combine(projectDirectory, configuredPath); + } + + return configuredPath; + } + + return Path.GetDirectoryName(project.FilePath) is { } directory + ? Path.Combine(directory, Constants.OrleansContractsFileName) + : Constants.OrleansContractsFileName; + } + + private static string NormalizePathSeparators(string path) + => path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + + private static IEnumerable GetAllSourceTypes( + INamespaceSymbol @namespace, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + foreach (var type in @namespace.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes( + type, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + + foreach (var childNamespace in @namespace.GetNamespaceMembers()) + { + foreach (var result in GetAllSourceTypes( + childNamespace, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + } + + private static IEnumerable GetSourceTypeAndNestedTypes( + INamedTypeSymbol type, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + if (!type.IsImplicitlyDeclared + && !IsGeneratedCode(type, compilation, generatedCodeTrees, semanticModels) + && type.Locations.Any(location => location.IsInSource)) + { + yield return type; + } + + foreach (var nestedType in type.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes( + nestedType, + compilation, + generatedCodeTrees, + semanticModels)) + { + yield return result; + } + } + } + + private static bool IsGeneratedCode( + INamedTypeSymbol type, + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) + { + var declarations = type.DeclaringSyntaxReferences; + return !declarations.IsEmpty + && declarations.All(declaration => + IsGeneratedCode(declaration.SyntaxTree, generatedCodeTrees) + || HasGeneratedCodeAttribute(declaration.GetSyntax(), compilation, semanticModels)); + } + + private static bool HasGeneratedCodeAttribute( + SyntaxNode declaration, + Compilation compilation, + Dictionary semanticModels) + { + if (!semanticModels.TryGetValue(declaration.SyntaxTree, out var semanticModel)) + { + semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); + semanticModels[declaration.SyntaxTree] = semanticModel; + } + + return declaration.AncestorsAndSelf() + .OfType() + .SelectMany(type => type.AttributeLists) + .SelectMany(list => list.Attributes) + .Select(attribute => semanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol) + .Select(constructor => constructor?.ContainingType.ToDisplayString()) + .Any(attributeType => attributeType is + "System.CodeDom.Compiler.GeneratedCodeAttribute" + or "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); + } + + private static bool IsGeneratedCode( + SyntaxTree syntaxTree, + Dictionary generatedCodeTrees) + { + if (generatedCodeTrees.TryGetValue(syntaxTree, out var result)) + { + return result; + } + + var fileName = Path.GetFileName(syntaxTree.FilePath); + result = fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase); + if (!result) + { + var text = syntaxTree.GetText(); + var prefixLength = Math.Min(text.Length, 2048); + result = text.ToString(new TextSpan(0, prefixLength)) + .IndexOf("= 0; + } + + generatedCodeTrees[syntaxTree] = result; + return result; + } + + private static void AppendInterface( + List lines, + INamedTypeSymbol type, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName) + { + AppendBlockSeparator(lines); + var interfaceName = GetFullyQualifiedName(type); + var explicitInterfaceType = GetGrainInterfaceTypeFromAttributes(type); + var interfaceType = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(type); + activeInterfaceIdentities.Add(interfaceType); + activeInterfaces[interfaceType] = type; + activeInterfacesByName[interfaceName] = type; + if (explicitInterfaceType is null + || string.Equals( + explicitInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(type), + StringComparison.Ordinal)) + { + activeConventionInterfaceNames.Add(interfaceName); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitInterfaceType, type)) + { + lines.Add($"# {interfaceName}"); + } + + lines.Add($"interface [GrainInterfaceType(\"{interfaceType}\")] {interfaceName} [Version({GetVersionFromAttributes(type)})]"); + foreach (var member in type.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .OrderBy(GrainInterfaceVersionAnalyzer.GetMethodSignature, StringComparer.Ordinal)) + { + if (GrainInterfaceVersionAnalyzer.RequiresClrComment(member)) + { + lines.Add($" # {GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member)}"); + } + + lines.Add($" {FormatStoredMember( + GrainInterfaceVersionAnalyzer.GetMethodSignature(member), + GetAliasFromAttributes(member))}"); + } + } + + private static void AppendGrainClass( + List lines, + INamedTypeSymbol type, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + AppendBlockSeparator(lines); + var className = GetFullyQualifiedName(type); + var explicitGrainType = GetGrainTypeFromAttributes(type); + var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(type); + activeClassIdentities.Add(grainType); + if (explicitGrainType is null + || string.Equals( + explicitGrainType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainType(type), + StringComparison.Ordinal)) + { + activeConventionClassNames.Add(className); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, type)) + { + lines.Add($"# {className}"); + } + + lines.Add($"class [GrainType(\"{grainType}\")] {className}"); + } + + private static void AppendHistoricalContracts( + List result, + string existingContent, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + var lines = existingContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + for (var index = 0; index < lines.Length; index++) + { + var declaration = lines[index].Trim(); + var isInterface = GrainInterfaceFileParser.TryGetInterfaceName(declaration, out var contractName); + var isClass = !isInterface && GrainInterfaceFileParser.TryGetGrainClassName(declaration, out contractName); + if (!isInterface && !isClass) + { + continue; + } + + string? explicitIdentity = null; + if (isInterface && GrainInterfaceFileParser.TryGetGrainInterfaceType(declaration, out var interfaceType)) + { + explicitIdentity = interfaceType; + } + else if (isClass && GrainInterfaceFileParser.TryGetGrainClassType(declaration, out var grainType)) + { + explicitIdentity = grainType; + } + + var isActive = isInterface + ? explicitIdentity is null + ? activeInterfaceIdentities.Contains( + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName)) + || activeConventionInterfaceNames.Contains(contractName) + : activeInterfaceIdentities.Contains(explicitIdentity) + : explicitIdentity is null + ? activeClassIdentities.Contains(GrainInterfaceVersionAnalyzer.GetDefaultGrainType(contractName)) + || activeConventionClassNames.Contains(contractName) + : activeClassIdentities.Contains(explicitIdentity); + var blockEnd = index + 1; + while (blockEnd < lines.Length) + { + if (GrainInterfaceFileParser.TryGetContractName(lines[blockEnd], out _)) + { + break; + } + + if (lines[blockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && blockEnd + 1 < lines.Length + && GrainInterfaceFileParser.TryGetContractName(lines[blockEnd + 1], out _)) + { + break; + } + + blockEnd++; + } + + if (!isActive) + { + AppendBlockSeparator(result); + if (index > 0 + && lines[index - 1].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && !IsGeneratedHeaderLine(lines[index - 1].TrimStart())) + { + result.Add(lines[index - 1].Trim()); + } + + for (var blockIndex = index; blockIndex < blockEnd; blockIndex++) + { + result.Add(blockIndex == index + && !declaration.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal) + ? $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {declaration}" + : lines[blockIndex]); + } + } + else if (isInterface) + { + var interfaceIdentity = explicitIdentity + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName); + if (activeInterfaces.TryGetValue(interfaceIdentity, out var activeInterface) + || activeConventionInterfaceNames.Contains(contractName) + && activeInterfacesByName.TryGetValue(contractName, out activeInterface)) + { + MergeHistoricalMembers( + result, + lines, + index, + blockEnd, + contractName, + activeInterface); + } + } + + index = blockEnd - 1; + } + } + + private static void MergeHistoricalMembers( + List result, + string[] historicalLines, + int historicalDeclarationIndex, + int historicalBlockEnd, + string historicalInterfaceName, + INamedTypeSymbol activeInterface) + { + var generatedDeclarationIndex = -1; + var activeInterfaceIdentity = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(activeInterface); + var activeInterfaceName = GetFullyQualifiedName(activeInterface); + for (var index = 0; index < result.Count; index++) + { + if (!GrainInterfaceFileParser.TryGetInterfaceName(result[index], out var generatedInterfaceName)) + { + continue; + } + + var generatedIdentity = GrainInterfaceFileParser.TryGetGrainInterfaceType( + result[index], + out var generatedInterfaceType) + ? generatedInterfaceType + : GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(generatedInterfaceName); + if (string.Equals(generatedIdentity, activeInterfaceIdentity, StringComparison.Ordinal) + || string.Equals(generatedInterfaceName, activeInterfaceName, StringComparison.Ordinal)) + { + generatedDeclarationIndex = index; + break; + } + } + + if (generatedDeclarationIndex < 0) + { + return; + } + + var generatedBlockEnd = generatedDeclarationIndex + 1; + var generatedMembers = new HashSet(StringComparer.Ordinal); + while (generatedBlockEnd < result.Count) + { + if (GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd], out _) + || result[generatedBlockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && generatedBlockEnd + 1 < result.Count + && GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd + 1], out _)) + { + break; + } + + if (GrainInterfaceFileParser.TryGetMemberSignature(result[generatedBlockEnd], out var memberSignature)) + { + generatedMembers.Add(GetHistoricalMemberKey(memberSignature, historicalInterfaceName)); + } + + generatedBlockEnd++; + } + + string? pendingComment = null; + for (var index = historicalDeclarationIndex + 1; index < historicalBlockEnd; index++) + { + var line = historicalLines[index]; + if (line.TrimStart().StartsWith("# ", StringComparison.Ordinal)) + { + pendingComment = line.Trim(); + continue; + } + + if (!GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var memberSignature, + out var memberAlias)) + { + pendingComment = null; + continue; + } + + if (activeInterface.GetMembers() + .OfType() + .Any(member => member.MethodKind == MethodKind.Ordinary + && !member.IsStatic + && GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( + historicalInterfaceName, + memberSignature, + memberAlias, + member))) + { + pendingComment = null; + continue; + } + + var normalizedSignature = GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature( + GrainInterfaceFileParser.GetCanonicalMemberSignature(memberSignature, memberAlias), + historicalInterfaceName); + var historicalMemberKey = GetHistoricalMemberKey(normalizedSignature, historicalInterfaceName); + if (generatedMembers.Add(historicalMemberKey)) + { + if (pendingComment is not null) + { + result.Insert(generatedBlockEnd++, $" {pendingComment}"); + } + + result.Insert(generatedBlockEnd++, $" {FormatStoredMember(historicalMemberKey, memberAlias)}"); + } + + pendingComment = null; + } + } + + private static string GetHistoricalMemberKey(string signature, string interfaceName) + => GrainInterfaceVersionAnalyzer.NormalizeLegacyMethodSignature( + GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature(signature, interfaceName)); + + private static void AppendBlockSeparator(List lines) + { + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[lines.Count - 1])) + { + lines.RemoveAt(lines.Count - 1); + } + + if (lines.Count > 0) + { + lines.Add(string.Empty); + } + } + + private static string GetFullyQualifiedName(INamedTypeSymbol type) + => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + private sealed class OrleansContractsFixAllProvider : FixAllProvider + { + public static OrleansContractsFixAllProvider Instance { get; } = new(); + + public override Task GetFixAsync(FixAllContext fixAllContext) + { + if (!string.Equals( + fixAllContext.CodeActionEquivalenceKey, + RegenerateCodeActionEquivalenceKey, + StringComparison.Ordinal)) + { + return Task.FromResult(null); + } + + var title = fixAllContext.Scope == FixAllScope.Solution + ? "Regenerate OrleansContracts.txt in solution" + : "Regenerate OrleansContracts.txt in project"; + return Task.FromResult(CodeAction.Create( + title, + cancellationToken => RegenerateFixAllAsync(fixAllContext, cancellationToken), + RegenerateCodeActionEquivalenceKey)); + } + + private static async Task RegenerateFixAllAsync( + FixAllContext fixAllContext, + CancellationToken cancellationToken) + { + var solution = fixAllContext.Solution; + var projectIds = new List(); + if (fixAllContext.Scope == FixAllScope.Solution) + { + foreach (var project in solution.Projects.Where(project => project.Language == LanguageNames.CSharp)) + { + if (!(await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false)).IsEmpty) + { + projectIds.Add(project.Id); + } + } + } + else + { + projectIds.Add(fixAllContext.Project.Id); + } + + foreach (var projectId in projectIds) + { + cancellationToken.ThrowIfCancellationRequested(); + if (solution.GetProject(projectId) is { } project) + { + solution = await RegenerateOrleansContractsFileAsync(project, cancellationToken).ConfigureAwait(false); + } + } + + return solution; + } + } + private static void RegisterAddInterfaceCodeFix(CodeFixContext context, Diagnostic diagnostic) { if (!diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey, out var interfaceName) || @@ -131,12 +807,20 @@ private static void RegisterAddMemberCodeFix(CodeFixContext context, Diagnostic } diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberClrSignaturePropertyKey, out var memberClrSignature); + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberAliasPropertyKey, out var memberAlias); diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.AddToOrleansContractsFileTitle, - createChangedSolution: ct => AddMemberToFileAsync(context.Document, interfaceName!, grainInterfaceType, memberSignature!, memberClrSignature, ct), + createChangedSolution: ct => AddMemberToFileAsync( + context.Document, + interfaceName!, + grainInterfaceType, + memberSignature!, + memberAlias, + memberClrSignature, + ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0018), diagnostic); } @@ -149,10 +833,11 @@ private static void RegisterRetireInterfaceCodeFix(CodeFixContext context, Diagn return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainInterfaceTitle, - createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, ct), + createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, grainInterfaceType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0019), diagnostic); } @@ -198,10 +883,11 @@ private static void RegisterRetireGrainClassCodeFix(CodeFixContext context, Diag return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.ActualAliasPropertyKey, out var grainType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainClassTitle, - createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, ct), + createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, grainType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0024), diagnostic); } @@ -237,8 +923,7 @@ private static async Task AddGrainClassToFileAsync( var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(classSymbol); var classClrComment = GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, classSymbol) ? className : null; var classLine = $"class [GrainType(\"{grainType}\")] {className}"; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return solution; @@ -263,7 +948,7 @@ private static async Task AddGrainClassToFileAsync( var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom)); } } @@ -280,7 +965,7 @@ private static async Task AddGrainClassToFileAsync( content = SortContractEntries(content + classLine, newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task UpdateGrainClassAliasInFileAsync( @@ -290,8 +975,7 @@ private static async Task UpdateGrainClassAliasInFileAsync( CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -323,17 +1007,17 @@ private static async Task UpdateGrainClassAliasInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task RetireGrainClassInFileAsync( Document document, string className, + string? grainType, CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -350,8 +1034,7 @@ private static async Task RetireGrainClassInFileAsync( for (var i = 0; i < lines.Length; i++) { var trimmedLine = lines[i].Trim(); - if (GrainInterfaceFileParser.TryGetGrainClassName(trimmedLine, out var declaredName) - && string.Equals(declaredName, className, StringComparison.Ordinal) + if (IsMatchingGrainClass(trimmedLine, className, grainType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { lines[i] = $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {trimmedLine}"; @@ -362,7 +1045,7 @@ private static async Task RetireGrainClassInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task AddInterfaceToFileAsync( @@ -423,7 +1106,7 @@ private static async Task AddInterfaceToFileAsync( var interfaceLine = sb.ToString(); // Build member lines - var memberLines = new List<(string Signature, string? ClrSignature)>(); + var memberLines = new List<(string Signature, string? Alias, string? ClrSignature)>(); foreach (var member in symbol.GetMembers().OfType()) { if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) @@ -434,14 +1117,14 @@ private static async Task AddInterfaceToFileAsync( var memberSignature = GrainInterfaceVersionAnalyzer.GetMethodSignature(member); memberLines.Add(( memberSignature, + GetAliasFromAttributes(member), GrainInterfaceVersionAnalyzer.RequiresClrComment(member) ? GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member) : null)); } // Find or create the OrleansContracts.txt file - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is not null) { @@ -458,7 +1141,7 @@ private static async Task AddInterfaceToFileAsync( i = SetClrCommentBefore(updatedLines, i, interfaceClrComment); updatedLines[i] = interfaceLine; var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); - var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8); + var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, reactivatedText); } } @@ -479,11 +1162,11 @@ private static async Task AddInterfaceToFileAsync( { newContent += $"{newLine} # {member.ClrSignature}"; } - newContent += $"{newLine} {member.Signature}"; + newContent += $"{newLine} {FormatStoredMember(member.Signature, member.Alias)}"; } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); solution = solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } else @@ -500,18 +1183,15 @@ private static async Task AddInterfaceToFileAsync( { AppendLine(content, $" # {member.ClrSignature}", DefaultNewLine); } - AppendLine(content, $" {member.Signature}", DefaultNewLine); + AppendLine(content, $" {FormatStoredMember(member.Signature, member.Alias)}", DefaultNewLine); } - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Encoding.UTF8); - var projectDir = Path.GetDirectoryName(project.FilePath); - var filePath = projectDir is not null - ? Path.Combine(projectDir, Constants.OrleansContractsFileName) - : Constants.OrleansContractsFileName; + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Utf8NoBom); + var filePath = GetConfiguredContractsPath(project); solution = solution.AddAdditionalDocument( DocumentId.CreateNewId(project.Id), - Constants.OrleansContractsFileName, + Path.GetFileName(filePath), newText, filePath: filePath); } @@ -529,8 +1209,7 @@ private static async Task UpdateVersionInFileAsync( var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -578,7 +1257,7 @@ private static async Task UpdateVersionInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -587,14 +1266,14 @@ private static async Task AddMemberToFileAsync( string interfaceName, string? grainInterfaceType, string memberSignature, + string? memberAlias, string? memberClrSignature, CancellationToken cancellationToken) { var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -624,7 +1303,7 @@ private static async Task AddMemberToFileAsync( || trimmedLine.StartsWith("#", StringComparison.Ordinal) || GrainInterfaceFileParser.TryGetContractName(trimmedLine, out _))) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); insertedMember = true; } @@ -640,7 +1319,7 @@ private static async Task AddMemberToFileAsync( // If we didn't insert the member yet, append it at the end if (foundInterface && !insertedMember) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); } // Remove trailing newline added by AppendLine @@ -651,20 +1330,20 @@ private static async Task AddMemberToFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } private static async Task RetireInterfaceInFileAsync( Document document, string interfaceName, + string? grainInterfaceType, CancellationToken cancellationToken) { var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -686,8 +1365,7 @@ private static async Task RetireInterfaceInFileAsync( var trimmedLine = line.Trim(); // Check if this line contains the interface declaration - if (GrainInterfaceFileParser.TryGetInterfaceName(trimmedLine, out var declaredInterfaceName) - && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal) && + if (IsMatchingInterface(trimmedLine, interfaceName, grainInterfaceType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { // Add *RETIRED* prefix @@ -706,7 +1384,7 @@ private static async Task RetireInterfaceInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -734,12 +1412,30 @@ private static bool IsMatchingInterface(string line, string interfaceName, strin { return GrainInterfaceFileParser.TryGetGrainInterfaceType(line, out var declaredGrainInterfaceType) ? string.Equals(declaredGrainInterfaceType, grainInterfaceType, StringComparison.Ordinal) - : string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); + : string.Equals( + grainInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(interfaceName), + StringComparison.Ordinal) + && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } return string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } + private static bool IsMatchingGrainClass(string line, string className, string? grainType) + { + if (!GrainInterfaceFileParser.TryGetGrainClassName(line, out var declaredClassName) + || !string.Equals(declaredClassName, className, StringComparison.Ordinal)) + { + return false; + } + + return grainType is null + ? !GrainInterfaceFileParser.TryGetGrainClassType(line, out _) + : GrainInterfaceFileParser.TryGetGrainClassType(line, out var declaredGrainType) + && string.Equals(declaredGrainType, grainType, StringComparison.Ordinal); + } + private static bool IdentityDiffersFromClrName(string? identity, string fullName) { if (identity is null) @@ -823,13 +1519,20 @@ private static string SortContractEntries(string content, string newLine) } preamble.Add(line); } - else if (GrainInterfaceFileParser.TryGetMemberSignature(line, out var memberSignature)) + else if (GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var storedMemberSignature, + out var memberAlias)) { + var memberSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedMemberSignature, + memberAlias); var inlineComment = GrainInterfaceFileParser.GetClrComment(line); var normalizedMemberLine = NormalizeMemberLine(memberSignature, currentBlock.Name); currentBlock.Members.Add(( normalizedMemberLine, normalizedMemberLine, + memberAlias, pendingComment ?? (inlineComment.Length > 0 ? NormalizeComment(inlineComment) : null))); pendingComment = null; } @@ -882,7 +1585,7 @@ private static string SortContractEntries(string content, string newLine) { result.Add($" {member.ClrComment}"); } - result.Add($" {member.Line}"); + result.Add($" {FormatStoredMember(member.Line, member.Alias)}"); } result.AddRange(block.OtherLines); } @@ -893,6 +1596,10 @@ private static string SortContractEntries(string content, string newLine) private static bool IsGeneratedHeaderLine(string line) => GeneratedHeader.Contains(line, StringComparer.Ordinal) || line is "# OrleansContracts.txt" + or "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope." + or "# This file is auto-generated by the Orleans contract analyzer." + or "# Update source contracts, then regenerate this file by following:" + or "# https://aka.ms/orleans/OrleansContracts.txt" or "# This file tracks grain interface versions for compatibility during rolling upgrades." or "# Format:" or "# # Namespace.GrainClass" @@ -941,6 +1648,7 @@ private static void AppendLine(StringBuilder builder, string value, string newLi private static void AppendMember( StringBuilder builder, string memberSignature, + string? memberAlias, string? memberClrSignature, string newLine) { @@ -949,9 +1657,14 @@ private static void AppendMember( AppendLine(builder, $" # {memberClrSignature}", newLine); } - AppendLine(builder, $" {memberSignature}", newLine); + AppendLine(builder, $" {FormatStoredMember(memberSignature, memberAlias)}", newLine); } + private static string FormatStoredMember(string memberSignature, string? memberAlias) + => memberAlias is null + ? memberSignature + : $"[Alias(\"{memberAlias}\")] {memberSignature}"; + private static ushort GetVersionFromAttributes(ISymbol symbol) { foreach (var attribute in symbol.GetAttributes()) @@ -1031,7 +1744,7 @@ public ContractBlock(string name, string declaration, string? clrComment) public string? ClrComment { get; } - public List<(string Signature, string Line, string? ClrComment)> Members { get; } = new(); + public List<(string Signature, string Line, string? Alias, string? ClrComment)> Members { get; } = new(); public List OtherLines { get; } = new(); } diff --git a/src/Orleans.Analyzers/Orleans.Analyzers.csproj b/src/Orleans.Analyzers/Orleans.Analyzers.csproj index 4675a5de0d5..d68d00de2e5 100644 --- a/src/Orleans.Analyzers/Orleans.Analyzers.csproj +++ b/src/Orleans.Analyzers/Orleans.Analyzers.csproj @@ -19,6 +19,11 @@ + + + + + diff --git a/src/Orleans.Analyzers/Resources.Designer.cs b/src/Orleans.Analyzers/Resources.Designer.cs index c02c461d0fa..5b3f26601ba 100644 --- a/src/Orleans.Analyzers/Resources.Designer.cs +++ b/src/Orleans.Analyzers/Resources.Designer.cs @@ -409,6 +409,24 @@ internal static string GrainInterfaceMemberNotDeclaredTitle { return ResourceManager.GetString("GrainInterfaceMemberNotDeclaredTitle", resourceCulture); } } + + internal static string GrainInterfaceMemberRemovedDescription { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedDescription", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedMessageFormat { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedMessageFormat", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedTitle { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedTitle", resourceCulture); + } + } /// /// Looks up a localized string similar to All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades.. diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 4cbbf0c2f81..6cfbcce2090 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -218,7 +218,7 @@ Grain interface is not active in OrleansContracts.txt - Grain interface '{0}' does not have an active declaration in OrleansContracts.txt + Grain interface '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades. @@ -227,7 +227,7 @@ Grain interface version mismatch - Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code + Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [Version] attribute on the grain interface must match the version declared in OrleansContracts.txt. @@ -236,16 +236,25 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}' + Grain interface source member '{2}' with wire signature '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. + + Grain interface member removed from source + + + Grain interface member '{0}' is declared in OrleansContracts.txt for interface '{1}' but is absent from source. Restore the method or explicitly remove the manifest entry after reviewing the wire break. See https://aka.ms/orleans/OrleansContracts.txt for details. + + + Removing a grain interface method can break calls during a rolling upgrade. OrleansContracts.txt retains the signature until the removal is explicitly accepted. + Removed grain interface not marked as retired - Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain interface, mark it as *RETIRED* in OrleansContracts.txt to document that it has been intentionally removed. @@ -254,7 +263,7 @@ OrleansContracts.txt file is missing - The project contains grain interfaces but no {0} file to track them + The project contains Orleans contracts but no {0} file. From the repository root, replace PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path and run 'dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024'. Review the generated baseline. See https://aka.ms/orleans/OrleansContracts.txt for details. Add an OrleansContracts.txt file to track Orleans contracts for compatibility during rolling upgrades. @@ -281,7 +290,7 @@ Grain class is not active in OrleansContracts.txt - Grain class '{0}' does not have an active declaration in OrleansContracts.txt + Grain class '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All concrete grain classes should have an active declaration in OrleansContracts.txt to protect grain type identities during renames. @@ -290,7 +299,7 @@ Grain class alias mismatch - Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code + Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [GrainType] attribute on a grain class must match the alias declared in OrleansContracts.txt. @@ -299,7 +308,7 @@ Removed grain class not marked as retired - Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain class, mark it as *RETIRED* in OrleansContracts.txt to preserve its contract history. diff --git a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props index 7d4869b7bd2..bb67bff9c88 100644 --- a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props +++ b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props @@ -5,5 +5,6 @@ + diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index b3edafef674..2df212605c9 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -1,7 +1,15 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension")] Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension [Version(0)] - OnError(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task - OnPublished(Orleans.BroadcastChannel.InternalChannelId, object) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnError(InternalChannelId streamId, Exception exception) -> Task + 73F72B20(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnPublished(InternalChannelId streamId, object item) -> Task + B1E55518(Orleans.BroadcastChannel.InternalChannelId, object) -> Task diff --git a/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs new file mode 100644 index 00000000000..cd74257e6ba --- /dev/null +++ b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs @@ -0,0 +1,62 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Orleans.CodeGenerator.Hashing; + +namespace Orleans.CodeGenerator; + +internal static class MethodIdProvider +{ + public static string Create(IMethodSymbol method) + { + var signature = Format(method); + var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(signature)); + var result = new StringBuilder(hash.Length * 2); + foreach (var value in hash) + { + result.Append(value.ToString("X2")); + } + + return result.ToString(); + } + + private static string Format(IMethodSymbol method) + { + var result = new StringBuilder(); + result.Append(method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + result.Append('.'); + result.Append(method.Name); + + if (method.IsGenericMethod) + { + result.Append('<'); + for (var index = 0; index < method.TypeArguments.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + result.Append(method.TypeArguments[index].Name); + } + + result.Append('>'); + } + + result.Append('('); + for (var index = 0; index < method.Parameters.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + var parameterType = method.Parameters[index].Type; + result.Append(parameterType is ITypeParameterSymbol + ? parameterType.Name + : parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + result.Append(')'); + return result.ToString(); + } +} diff --git a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs index a7660c72e5b..6c1ee36f49e 100644 --- a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs +++ b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs @@ -1,7 +1,5 @@ -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Orleans.CodeGenerator.Hashing; using Orleans.CodeGenerator.SyntaxGeneration; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Orleans.CodeGenerator.SyntaxGeneration.SymbolExtensions; @@ -26,62 +24,7 @@ internal static class GeneratedCodeUtilities } internal static string CreateHashedMethodId(IMethodSymbol methodSymbol) - { - var methodSignature = Format(methodSymbol); - var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(methodSignature)); - return $"{HexConverter.ToString(hash)}"; - - static string Format(IMethodSymbol methodInfo) - { - var result = new StringBuilder(); - result.Append(methodInfo.ContainingType.ToDisplayName()); - result.Append('.'); - result.Append(methodInfo.Name); - - if (methodInfo.IsGenericMethod) - { - result.Append('<'); - var first = true; - foreach (var typeArgument in methodInfo.TypeArguments) - { - if (!first) result.Append(','); - else first = false; - result.Append(typeArgument.Name); - } - - result.Append('>'); - } - - { - result.Append('('); - var parameters = methodInfo.Parameters; - var first = true; - foreach (var parameter in parameters) - { - if (!first) - { - result.Append(','); - } - - var parameterType = parameter.Type; - switch (parameterType) - { - case ITypeParameterSymbol _: - result.Append(parameterType.Name); - break; - default: - result.Append(parameterType.ToDisplayName()); - break; - } - - first = false; - } - } - - result.Append(')'); - return result.ToString(); - } - } + => MethodIdProvider.Create(methodSymbol); internal static string? GetAlias(LibraryTypes libraryTypes, ISymbol symbol) => (string?)symbol.GetAttribute(libraryTypes.AliasAttribute)?.ConstructorArguments.First().Value; diff --git a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj index 6fb0ee8e325..805e0e1461f 100644 --- a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj +++ b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index be4406a54a9..1f7df80e614 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -1,10 +1,18 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Core.Internal.IGrainManagementExtension")] Orleans.Core.Internal.IGrainManagementExtension [Version(0)] - DeactivateOnIdle() -> ValueTask - MigrateOnIdle() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.DeactivateOnIdle() -> ValueTask + 1B9614D1() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.MigrateOnIdle() -> ValueTask + 4CC93B45() -> ValueTask interface [GrainInterfaceType("Orleans.IGrain")] Orleans.IGrain [Version(0)] @@ -23,14 +31,20 @@ interface [GrainInterfaceType("Orleans.IGrainWithStringKey")] Orleans.IGrainWith interface [GrainInterfaceType("Orleans.ISystemTarget")] Orleans.ISystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.Runtime.IAsyncEnumerableGrainExtension")] Orleans.Runtime.IAsyncEnumerableGrainExtension [Version(0)] - DisposeAsync(System.Guid) -> ValueTask - MoveNext`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - MoveNext`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request) -> ValueTask<(EnumerationResult Status, object? Value)> + 370CD5AB`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.DisposeAsync(Guid requestId) -> ValueTask + 3C6D7209(System.Guid) -> ValueTask + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + 8678B466`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId) -> ValueTask<(EnumerationResult Status, object? Value)> + A7FA7E30`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + E60EA75B`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> interface [GrainInterfaceType("Orleans.Runtime.ICancellationSourcesExtension")] Orleans.Runtime.ICancellationSourcesExtension [Version(0)] - CancelRemoteToken(System.Guid) -> Task + # Orleans.Runtime.ICancellationSourcesExtension.CancelRemoteToken(Guid tokenId) -> Task + 50F75C16(System.Guid) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainExtension")] Orleans.Runtime.IGrainExtension [Version(0)] diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index f90b29fe0a1..f41e5afc4d7 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -1,80 +1,133 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.ClientObservers.IClientGatewayObserver")] Orleans.ClientObservers.IClientGatewayObserver [Version(0)] - StopSendingToGateway(Orleans.Runtime.SiloAddress) -> void + # Orleans.ClientObservers.IClientGatewayObserver.StopSendingToGateway(SiloAddress gateway) -> void + AFB768FD(Orleans.Runtime.SiloAddress) -> void interface [GrainInterfaceType("Orleans.IMembershipTableSystemTarget")] Orleans.IMembershipTableSystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.ISiloControl")] Orleans.ISiloControl [Version(0)] - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection() -> Task - ForceRuntimeStatisticsCollection() -> Task - GetActivationCount() -> Task - GetActiveGrains(Orleans.Runtime.GrainType) -> Task> - GetDetailedGrainReport(Orleans.Runtime.GrainId) -> Task - GetDetailedGrainStatistics(string[]) -> Task> - GetGrainStatistics() -> Task>> - GetRuntimeStatistics() -> Task - GetSimpleGrainStatistics() -> Task - MigrateRandomActivations(Orleans.Runtime.SiloAddress, int) -> Task - Ping(string) -> Task - SendControlCommandToProvider`1(string, int, object?) -> Task + # Orleans.ISiloControl.ForceRuntimeStatisticsCollection() -> Task + 0C7DBD0C() -> Task + # Orleans.ISiloControl.Ping(string message) -> Task + 1422B0B7(string) -> Task + # Orleans.ISiloControl.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + 355CA3FA`1(string, int, object?) -> Task + # Orleans.ISiloControl.GetDetailedGrainReport(GrainId grainId) -> Task + 45172562(Orleans.Runtime.GrainId) -> Task + # Orleans.ISiloControl.ForceActivationCollection(TimeSpan ageLimit) -> Task + 45D07D09(System.TimeSpan) -> Task + # Orleans.ISiloControl.GetSimpleGrainStatistics() -> Task + 6DE16EF7() -> Task + # Orleans.ISiloControl.GetActiveGrains(GrainType grainType) -> Task> + 85797C87(Orleans.Runtime.GrainType) -> Task> + # Orleans.ISiloControl.GetDetailedGrainStatistics(string[]? types) -> Task> + B0F4C24B(string[]) -> Task> + # Orleans.ISiloControl.GetActivationCount() -> Task + C4C370A5() -> Task + # Orleans.ISiloControl.MigrateRandomActivations(SiloAddress target, int count) -> Task + E8327F0B(Orleans.Runtime.SiloAddress, int) -> Task + # Orleans.ISiloControl.GetRuntimeStatistics() -> Task + F18EAF24() -> Task + # Orleans.ISiloControl.ForceGarbageCollection() -> Task + F388CED1() -> Task + # Orleans.ISiloControl.GetGrainStatistics() -> Task>> + FF707A30() -> Task>> interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerMonitor")] Orleans.Placement.Rebalancing.IActivationRebalancerMonitor [Version(0)] - Report(RebalancingReport) -> Task + [Alias("Report")] Report(RebalancingReport) -> Task interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerWorker")] Orleans.Placement.Rebalancing.IActivationRebalancerWorker [Version(0)] - GetReport() -> ValueTask - ResumeRebalancing() -> Task - SuspendRebalancing(System.TimeSpan?) -> Task + [Alias("GetReport")] GetReport() -> ValueTask + [Alias("ResumeRebalancing")] ResumeRebalancing() -> Task + [Alias("SuspendRebalancing")] SuspendRebalancing(System.TimeSpan?) -> Task interface [GrainInterfaceType("Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget")] Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget [Version(0)] - AcceptExchangeRequest(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask - FlushBuffers() -> ValueTask - GetActivationCount() -> ValueTask - GetGrainCallFrequencies() -> ValueTask> - ResetCounters() -> ValueTask - SetActivationCountOffset(int) -> ValueTask - TriggerExchangeRequest() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.FlushBuffers() -> ValueTask + 11731652() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.SetActivationCountOffset(int activationCountOffset) -> ValueTask + 135356E5(int) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.ResetCounters() -> ValueTask + 21852A09() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.AcceptExchangeRequest(AcceptExchangeRequest request) -> ValueTask + 9D8EDC44(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetActivationCount() -> ValueTask + 9FB525F3() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.TriggerExchangeRequest() -> ValueTask + A6EE4757() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetGrainCallFrequencies() -> ValueTask> + C4497899() -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.IClusterManifestSystemTarget")] Orleans.Runtime.IClusterManifestSystemTarget [Version(0)] - GetClusterManifest() -> ValueTask - GetClusterManifestUpdate(Orleans.Metadata.MajorMinorVersion) -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifest() -> ValueTask + 40D39F85() -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifestUpdate(MajorMinorVersion previousVersion) -> ValueTask + 4EFCA109(Orleans.Metadata.MajorMinorVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IDeploymentLoadPublisher")] Orleans.Runtime.IDeploymentLoadPublisher [Version(0)] - UpdateRuntimeStatistics(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task + # Orleans.Runtime.IDeploymentLoadPublisher.UpdateRuntimeStatistics(SiloAddress siloAddress, SiloRuntimeStatistics siloStats) -> Task + C5255F0C(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationExtension")] Orleans.Runtime.IGrainCallCancellationExtension [Version(0)] - CancelRequestAsync(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationExtension.CancelRequestAsync(GrainId senderGrainId, CorrelationId messageId) -> ValueTask + FA239824(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IManagementGrain")] Orleans.Runtime.IManagementGrain [Version(0)] - ForceActivationCollection(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection(Orleans.Runtime.SiloAddress[]) -> Task - ForceRuntimeStatisticsCollection(Orleans.Runtime.SiloAddress[]) -> Task - GetActivationAddress(Orleans.Runtime.IAddressable) -> ValueTask - GetActiveGrains(Orleans.Runtime.GrainType) -> ValueTask> - GetDetailedGrainStatistics(string[], Orleans.Runtime.SiloAddress[]) -> Task - GetDetailedHosts(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedGrainStatistics(string[]? types, SiloAddress[]? hostsIds) -> Task + 0A1C0D82(string[], Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetGrainCallFrequencies(SiloAddress[]? hostsIds) -> Task> + 0F06E027(Orleans.Runtime.SiloAddress[]) -> Task> + # Orleans.Runtime.IManagementGrain.GetRuntimeStatistics(SiloAddress[] hostsIds) -> Task + 2D761B36(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActivationAddress(IAddressable reference) -> ValueTask + 317D82B6(Orleans.Runtime.IAddressable) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(SiloAddress[] hostsIds, TimeSpan ageLimit) -> Task + 329F9A1B(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics(SiloAddress[] hostsIds) -> Task + 3CFF788C(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActiveGrains(GrainType type) -> ValueTask> + 3DB7923B(Orleans.Runtime.GrainType) -> ValueTask> + # Orleans.Runtime.IManagementGrain.GetHosts(bool onlyActive) -> Task> + 4C0864C2(bool) -> Task> + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(TimeSpan ageLimit) -> Task + 54E6D1D1(System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.ResetGrainCallFrequencies(SiloAddress[]? hostsIds) -> ValueTask + 54FE0FEC(Orleans.Runtime.SiloAddress[]) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceGarbageCollection(SiloAddress[] hostsIds) -> Task + 5922EB76(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics() -> Task + ACCE9D6A() -> Task # Orleans.Runtime.IManagementGrain.GetGrainActivationCount(GrainReference grainReference) -> Task - GetGrainActivationCount(GrainRef) -> Task - GetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> Task> - GetHosts(bool) -> Task> - GetRuntimeStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetSimpleGrainStatistics() -> Task - GetSimpleGrainStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetTotalActivationCount() -> Task - ResetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> ValueTask - SendControlCommandToProvider`1(string, int, object?) -> Task + AEDE93F6(GrainRef) -> Task + # Orleans.Runtime.IManagementGrain.ForceRuntimeStatisticsCollection(SiloAddress[] siloAddresses) -> Task + B761B345(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedHosts(bool onlyActive) -> Task + CC6CCBC3(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetTotalActivationCount() -> Task + D7365B43() -> Task + # Orleans.Runtime.IManagementGrain.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + F67965CC`1(string, int, object?) -> Task interface [GrainInterfaceType("Orleans.Runtime.IMembershipService")] Orleans.Runtime.IMembershipService [Version(0)] - MembershipChangeNotification(Orleans.Runtime.MembershipTableSnapshot) -> Task - Ping(int) -> Task - ProbeIndirectly(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.ProbeIndirectly(SiloAddress target, TimeSpan probeTimeout, int probeNumber) -> Task + 0F85FAAF(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.MembershipChangeNotification(MembershipTableSnapshot snapshot) -> Task + 22A02D46(Orleans.Runtime.MembershipTableSnapshot) -> Task + # Orleans.Runtime.IMembershipService.Ping(int pingNumber) -> Task + 39AB7071(int) -> Task interface [GrainInterfaceType("Orleans.Storage.IMemoryStorageGrain")] Orleans.Storage.IMemoryStorageGrain [Version(0)] - DeleteStateAsync`1(string, string?) -> Task - ReadStateAsync`1(string) -> Task> - WriteStateAsync`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.ReadStateAsync(string grainStoreKey) -> Task?> + 45659318`1(string) -> Task> + # Orleans.Storage.IMemoryStorageGrain.WriteStateAsync(string grainStoreKey, IGrainState grainState) -> Task + 7CC6CA25`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.DeleteStateAsync(string grainStoreKey, string? eTag) -> Task + B7CADD03`1(string, string?) -> Task diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 756611fbcb2..49d70fce8d4 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -1,11 +1,19 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.DurableJobs.IDurableJobReceiverExtension")] Orleans.DurableJobs.IDurableJobReceiverExtension [Version(0)] - HandleDurableJobAsync(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask + # Orleans.DurableJobs.IDurableJobReceiverExtension.HandleDurableJobAsync(IJobRunContext context, CancellationToken attemptCancellationToken) -> ValueTask + 703DB2D4(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget")] Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget [Version(0)] - CancelAsync(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task + # Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget.CancelAsync(DurableJob job, CancellationToken requestCancellationToken) -> Task + 4D559F22(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task class [GrainType("localdurablejobmanager")] Orleans.DurableJobs.LocalDurableJobManager diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index c3cd2d29887..db96365c6b1 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -1,11 +1,21 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.EventSourcing.ILogConsistencyProtocolParticipant")] Orleans.EventSourcing.ILogConsistencyProtocolParticipant [Version(0)] - DeactivateProtocolParticipant() -> Task - PostActivateProtocolParticipant() -> Task - PreActivateProtocolParticipant() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PreActivateProtocolParticipant() -> Task + 0DB087C8() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PostActivateProtocolParticipant() -> Task + 22FD7D72() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.DeactivateProtocolParticipant() -> Task + A36FC884() -> Task interface [GrainInterfaceType("Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway")] Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway [Version(0)] - RelayMessage(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task + # Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway.RelayMessage(GrainId id, ILogConsistencyProtocolMessage payload) -> Task + C86A1066(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index bb17fc7aff6..524150db701 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -1,5 +1,11 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("memorystorage")] Orleans.Storage.MemoryStorageGrain diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index 2db089842d6..f877ceebf16 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -1,25 +1,44 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.IRemindable")] Orleans.IRemindable [Version(0)] - ReceiveReminder(string, Orleans.Runtime.TickStatus) -> Task + # Orleans.IRemindable.ReceiveReminder(string reminderName, TickStatus status) -> Task + 6461BF2F(string, Orleans.Runtime.TickStatus) -> Task interface [GrainInterfaceType("Orleans.IReminderService")] Orleans.IReminderService [Version(0)] - GetReminder(Orleans.Runtime.GrainId, string) -> Task - GetReminders(Orleans.Runtime.GrainId) -> Task> - RegisterOrUpdateReminder(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task - Start() -> Task - Stop() -> Task - UnregisterReminder(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.RegisterOrUpdateReminder(GrainId grainId, string reminderName, TimeSpan dueTime, TimeSpan period) -> Task + 1281C86D(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task + # Orleans.IReminderService.GetReminders(GrainId grainId) -> Task> + 419EB51E(Orleans.Runtime.GrainId) -> Task> + # Orleans.IReminderService.Start() -> Task + 5CF78F8A() -> Task + # Orleans.IReminderService.UnregisterReminder(IGrainReminder reminder) -> Task + A7AF84A8(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.GetReminder(GrainId grainId, string reminderName) -> Task + AC622EEB(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderService.Stop() -> Task + DCFCA00D() -> Task interface [GrainInterfaceType("Orleans.IReminderTableGrain")] Orleans.IReminderTableGrain [Version(0)] - ReadRow(Orleans.Runtime.GrainId, string) -> Task - ReadRows(Orleans.Runtime.GrainId) -> Task - ReadRows(uint, uint) -> Task - RemoveRow(Orleans.Runtime.GrainId, string, string) -> Task - TestOnlyClearTable() -> Task - UpsertRow(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.ReadRows(uint begin, uint end) -> Task + 13558B55(uint, uint) -> Task + # Orleans.IReminderTableGrain.UpsertRow(ReminderEntry entry) -> Task + 873299B5(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.TestOnlyClearTable() -> Task + 8EBE0523() -> Task + # Orleans.IReminderTableGrain.ReadRow(GrainId grainId, string reminderName) -> Task + ECA791DE(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderTableGrain.ReadRows(GrainId grainId) -> Task + EEEF6FCA(Orleans.Runtime.GrainId) -> Task + # Orleans.IReminderTableGrain.RemoveRow(GrainId grainId, string reminderName, string eTag) -> Task + FF391E0B(Orleans.Runtime.GrainId, string, string) -> Task class [GrainType("localreminderservice")] Orleans.Runtime.ReminderService.LocalReminderService diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 75bdb2c2fe4..99064daf66b 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -1,6 +1,12 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("activationmigrationmanager")] Orleans.Runtime.ActivationMigrationManager @@ -13,7 +19,8 @@ class [GrainType("deploymentloadpublisher")] Orleans.Runtime.DeploymentLoadPubli class [GrainType("developmentleaseprovider")] Orleans.Runtime.Development.DevelopmentLeaseProviderGrain interface [GrainInterfaceType("Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain")] Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain [Version(0)] - Reset() -> Task + # Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain.Reset() -> Task + 847FCE12() -> Task class [GrainType("graincallcancellationmanager")] Orleans.Runtime.GrainCallCancellationManager @@ -26,25 +33,27 @@ class [GrainType("distributedremotegraindirectory")] Orleans.Runtime.GrainDirect class [GrainType("graindirectorypartition")] Orleans.Runtime.GrainDirectory.GrainDirectoryPartition interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryClient")] Orleans.Runtime.GrainDirectory.IGrainDirectoryClient [Version(0)] - GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> - RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> + [Alias("GetRegisteredActivations")] GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> + [Alias("RecoverRegisteredActivations")] RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition")] Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition [Version(0)] - AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask - DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> - GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask - LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> - RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> + [Alias("AcknowledgeSnapshotTransferAsync")] AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask + [Alias("DeregisterAsync")] DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> + [Alias("GetSnapshotAsync")] GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask + [Alias("LookupAsync")] LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> + [Alias("RegisterAsync")] RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks")] Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks [Version(0)] - CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> - CheckIntegrityAsync() -> ValueTask - RecoverAndCheckIntegrityAsync() -> ValueTask - WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask + [Alias("CheckActivationsAsync")] CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> + [Alias("CheckIntegrityAsync")] CheckIntegrityAsync() -> ValueTask + [Alias("RecoverAndCheckIntegrityAsync")] RecoverAndCheckIntegrityAsync() -> ValueTask + [Alias("WaitForMembershipVersionAsync")] WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IRemoteClientDirectory")] Orleans.Runtime.GrainDirectory.IRemoteClientDirectory [Version(0)] - GetClientRoutes(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> - OnUpdateClientRoutes(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.OnUpdateClientRoutes(ImmutableDictionary ConnectedClients, long Version)> update) -> Task + 972F9953(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.GetClientRoutes(ImmutableDictionary knownRoutes) -> Task ConnectedClients, long Version)>> + A6E49CD1(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> class [GrainType("localgraindirectoryclientcompatibility")] Orleans.Runtime.GrainDirectory.LocalGrainDirectoryClientCompatibility @@ -53,24 +62,32 @@ class [GrainType("localgraindirectorypartitioncompatibility")] Orleans.Runtime.G class [GrainType("remotegraindirectory")] Orleans.Runtime.GrainDirectory.RemoteGrainDirectory interface [GrainInterfaceType("Orleans.Runtime.IActivationMigrationManagerSystemTarget")] Orleans.Runtime.IActivationMigrationManagerSystemTarget [Version(0)] - AcceptMigratingGrains(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IActivationMigrationManagerSystemTarget.AcceptMigratingGrains(List migratingGrains) -> ValueTask + 29E9E63F(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.ICatalog")] Orleans.Runtime.ICatalog [Version(0)] - DeleteActivations(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task + # Orleans.Runtime.ICatalog.DeleteActivations(List activationAddresses, DeactivationReasonCode reasonCode, string reasonText) -> Task + C4A56D7C(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationManagerSystemTarget")] Orleans.Runtime.IGrainCallCancellationManagerSystemTarget [Version(0)] - CancelCallsAsync(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationManagerSystemTarget.CancelCallsAsync(List cancellationRequests) -> ValueTask + AF79F3FA(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IGrainTimerInvoker")] Orleans.Runtime.IGrainTimerInvoker [Version(0)] - InvokeCallbackAsync() -> Task + # Orleans.Runtime.IGrainTimerInvoker.InvokeCallbackAsync() -> Task + 3F6C2672() -> Task interface [GrainInterfaceType("Orleans.Runtime.IRemoteGrainDirectory")] Orleans.Runtime.IRemoteGrainDirectory [Version(0)] - AcceptSplitPartition(System.Collections.Generic.List) -> Task - LookUpMany(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> - RegisterMany(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.LookUpMany(List<(GrainId GrainId, int Version)> grainAndETagList) -> Task> + 7DF50601(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> + # Orleans.Runtime.IRemoteGrainDirectory.AcceptSplitPartition(List singleActivations) -> Task + 9ABE3793(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.RegisterMany(List addresses) -> Task + CD06EAEE(System.Collections.Generic.List) -> Task interface [GrainInterfaceType("Orleans.Runtime.ISiloManifestSystemTarget")] Orleans.Runtime.ISiloManifestSystemTarget [Version(0)] - GetSiloManifest() -> ValueTask + # Orleans.Runtime.ISiloManifestSystemTarget.GetSiloManifest() -> ValueTask + 1857A4C8() -> ValueTask class [GrainType("management")] Orleans.Runtime.Management.ManagementGrain @@ -79,7 +96,7 @@ class [GrainType("membershipsystemtarget")] Orleans.Runtime.MembershipService.Me class [GrainType("membershiptablesystemtarget")] Orleans.Runtime.MembershipService.MembershipTableSystemTarget interface [GrainInterfaceType("Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget")] Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget [Version(0)] - GetSiloMetadata() -> Task + [Alias("GetSiloMetadata")] GetSiloMetadata() -> Task class [GrainType("silometadatasystemtarget")] Orleans.Runtime.MembershipService.SiloMetadata.SiloMetadataSystemTarget @@ -96,13 +113,21 @@ interface [GrainInterfaceType("Orleans.Runtime.TestHooks.ITestHooksSystemTarget" class [GrainType("testhookssystemtarget")] Orleans.Runtime.TestHooks.TestHooksSystemTarget interface [GrainInterfaceType("Orleans.Runtime.Versions.IVersionStoreGrain")] Orleans.Runtime.Versions.IVersionStoreGrain [Version(0)] - GetCompatibilityStrategies() -> Task> - GetCompatibilityStrategy() -> Task - GetSelectorStrategies() -> Task> - GetSelectorStrategy() -> Task - SetCompatibilityStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetCompatibilityStrategy(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetSelectorStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task - SetSelectorStrategy(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(GrainInterfaceType interfaceType, CompatibilityStrategy strategy) -> Task + 1B7F13C8(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(GrainInterfaceType interfaceType, VersionSelectorStrategy strategy) -> Task + 3E6DDE3E(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(CompatibilityStrategy strategy) -> Task + 67A0B5AA(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategy() -> Task + 67EF9A39() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategies() -> Task> + 7261373F() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategies() -> Task> + 743D88ED() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategy() -> Task + 8A72848A() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(VersionSelectorStrategy strategy) -> Task + E7532DE3(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task class [GrainType("versionstore")] Orleans.Runtime.Versions.VersionStoreGrain diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index de3f623bf05..60fe142410c 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -1,10 +1,18 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Providers.IMemoryStreamQueueGrain")] Orleans.Providers.IMemoryStreamQueueGrain [Version(0)] - Dequeue(int) -> Task> - Enqueue(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Enqueue(MemoryMessageData data) -> Task + 74D60341(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Dequeue(int maxCount) -> Task> + 7A8F8C1A(int) -> Task> class [GrainType("memorystreamqueue")] Orleans.Providers.MemoryStreamQueueGrain @@ -14,43 +22,70 @@ class [GrainType("stream.checkpoint.configured")] Orleans.Streams.ConfiguredStre interface [GrainInterfaceType("Orleans.Streams.IConfiguredStreamCheckpointerGrain")] Orleans.Streams.IConfiguredStreamCheckpointerGrain [Version(0)] interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingAgent")] Orleans.Streams.IPersistentStreamPullingAgent [Version(0)] - Initialize() -> Task - Shutdown() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Initialize() -> Task + 06009D9C() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Shutdown() -> Task + 620FF905() -> Task interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingManager")] Orleans.Streams.IPersistentStreamPullingManager [Version(0)] - ExecuteCommand(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task - Initialize() -> Task - StartAgents() -> Task - Stop() -> Task - StopAgents() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Initialize() -> Task + 455AB850() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StartAgents() -> Task + 54E9E970() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StopAgents() -> Task + BBD50CFF() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.ExecuteCommand(PersistentStreamProviderCommand command, object? arg) -> Task + DE756D95(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Stop() -> Task + F4B5B5AA() -> Task interface [GrainInterfaceType("Orleans.Streams.IPubSubRendezvousGrain")] Orleans.Streams.IPubSubRendezvousGrain [Version(0)] - ConsumerCount(Orleans.Runtime.QualifiedStreamId) -> Task - DiagGetConsumers(Orleans.Runtime.QualifiedStreamId) -> Task - FaultSubscription(Orleans.Runtime.GuidId) -> Task - GetAllSubscriptions(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - ProducerCount(Orleans.Runtime.QualifiedStreamId) -> Task - RegisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RegisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - UnregisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task - UnregisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task - Validate() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.Validate() -> Task + 20AA72BF() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.FaultSubscription(GuidId subscriptionId) -> Task + 2821FCF5(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ProducerCount(QualifiedStreamId streamId) -> Task + 29B61035(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 5E7E20BC(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ConsumerCount(QualifiedStreamId streamId) -> Task + 5F72C5CF(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.GetAllSubscriptions(QualifiedStreamId streamId, GrainId streamConsumer) -> Task> + 7DBE84FA(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.DiagGetConsumers(QualifiedStreamId streamId) -> Task + 8A033955(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + 974334B6(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task> + B5FFB7F3(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task + C017B47D(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamCheckpointerGrain")] Orleans.Streams.IStreamCheckpointerGrain [Version(0)] - Load(System.Threading.CancellationToken) -> ValueTask - Update(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Update(string offset, string expectedCheckpoint, CancellationToken cancellationToken) -> ValueTask + 7AB50A87(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Load(CancellationToken cancellationToken) -> ValueTask + DE3727A1(System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.Streams.IStreamConsumerExtension")] Orleans.Streams.IStreamConsumerExtension [Version(0)] - CompleteStream(Orleans.Runtime.GuidId) -> Task - DeliverBatch(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverImmutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverMutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - ErrorInStream(Orleans.Runtime.GuidId, System.Exception) -> Task - GetSequenceToken(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverMutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 31840DDE(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.CompleteStream(GuidId subscriptionId) -> Task + 49F94A48(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.ErrorInStream(GuidId subscriptionId, Exception exc) -> Task + 4C676CAF(Orleans.Runtime.GuidId, System.Exception) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverImmutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 6D8FAEB2(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverBatch(GuidId subscriptionId, QualifiedStreamId streamId, IBatchContainer item, StreamHandshakeToken? handshakeToken) -> Task + B9CFF2C9(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.GetSequenceToken(GuidId subscriptionId) -> Task + C265B3CB(Orleans.Runtime.GuidId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamProducerExtension")] Orleans.Streams.IStreamProducerExtension [Version(0)] - AddSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RemoveSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IStreamProducerExtension.AddSubscriber(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 1341E3D4(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IStreamProducerExtension.RemoveSubscriber(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + B98BA876(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task class [GrainType("persistentstreampullingagent")] Orleans.Streams.PersistentStreamPullingAgent diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index eb810b77708..d1d33c7d442 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -1,13 +1,25 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.TestingHost.IStorageFaultGrain")] Orleans.TestingHost.IStorageFaultGrain [Version(0)] - AddFaultOnClear(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnRead(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnWrite(Orleans.Runtime.GrainId, System.Exception) -> Task - OnClear(Orleans.Runtime.GrainId) -> Task - OnRead(Orleans.Runtime.GrainId) -> Task - OnWrite(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnRead(GrainId grainId, Exception exception) -> Task + 1150D526(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnClear(GrainId grainId, Exception exception) -> Task + 1A607A31(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnRead(GrainId grainId) -> Task + 5D91E1AF(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnWrite(GrainId grainId, Exception exception) -> Task + B9852E6E(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnClear(GrainId grainId) -> Task + C94BA77C(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnWrite(GrainId grainId) -> Task + E8594820(Orleans.Runtime.GrainId) -> Task class [GrainType("storagefault")] Orleans.TestingHost.StorageFaultGrain diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index cb8e54cb075..1fcbada73e3 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -1,19 +1,29 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("consistencytest")] Orleans.Transactions.TestKit.Consistency.ConsistencyTestGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain")] Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain [Version(0)] - Run(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task + # Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain.Run(ConsistencyTestOptions options, int depth, string stack, int max, DateTime stopAfter) -> Task + 2EB318CB(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task # Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain class [GrainType("txn-correctness-DoubleStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain")] Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain [Version(0)] - Get() -> Task> - Ping() -> Task - SetBit(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.SetBit(int newValue) -> Task + 0183C2F5(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Ping() -> Task + 9A5740F1() -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Get() -> Task> + B821F3B1() -> Task> # Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain class [GrainType("txn-correctness-MaxStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain @@ -37,68 +47,104 @@ class [GrainType("exclusivelocktransactiontest")] Orleans.Transactions.TestKit.E class [GrainType("faultinjectiontransactioncoordinator")] Orleans.Transactions.TestKit.FaultInjectionTransactionCoordinatorGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateAttributionGrain")] Orleans.Transactions.TestKit.ICreateAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 3EFBDD5D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain")] Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + C9B8ECB8(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain")] Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain [Version(0)] - ReadThenWrite(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - ReadThenWriteWithExclusiveLock(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWrite(ITransactionTestGrain grain, int value) -> Task + 148E55F3(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWriteWithExclusiveLock(IExclusiveLockTransactionTestGrain grain, int value) -> Task + F880C5FF(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain")] Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain [Version(0)] - Add(int) -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Get() -> Task + 16E53FE3() -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Add(int numberToAdd) -> Task + 81B05CD8(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Set(int newValue) -> Task + BD3AA4D0(int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain [Version(0)] - MultiGrainAddAndFaultInjection(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 70FF7C60(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainAddAndFaultInjection(List grains, int numberToAdd, FaultInjectionControl? faultInjection) -> Task + E67D54A5(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain [Version(0)] - Add(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Set(int newValue) -> Task + 8389970A(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Add(int numberToAdd, FaultInjectionControl? faultInjectionControl) -> Task + A4CAE05C(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Deactivate() -> Task + A6C1652E() -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Get() -> Task + C752DF7D() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IJoinAttributionGrain")] Orleans.Transactions.TestKit.IJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.IJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + B1619F67(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INoAttributionGrain")] Orleans.Transactions.TestKit.INoAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INoAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7E3A79(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INotAllowedAttributionGrain")] Orleans.Transactions.TestKit.INotAllowedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INotAllowedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 891D027E(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISupportedAttributionGrain")] Orleans.Transactions.TestKit.ISupportedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISupportedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7DBC0A(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISuppressAttributionGrain")] Orleans.Transactions.TestKit.ISuppressAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISuppressAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 5A02311D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCommitterTestGrain")] Orleans.Transactions.TestKit.ITransactionCommitterTestGrain [Version(0)] - Commit(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task + # Orleans.Transactions.TestKit.ITransactionCommitterTestGrain.Commit(ITransactionCommitOperation operation) -> Task + C44BE2A4(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCoordinatorGrain")] Orleans.Transactions.TestKit.ITransactionCoordinatorGrain [Version(0)] - AddAndThrow(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - MultiGrainAdd(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task - MultiGrainAdd(System.Collections.Generic.List, int) -> Task - MultiGrainAddAndThrow(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task - MultiGrainDouble(System.Collections.Generic.List) -> Task - MultiGrainDoubleByRWRW(System.Collections.Generic.List, int) -> Task - MultiGrainDoubleByWRWR(System.Collections.Generic.List, int) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task - MultiGrainSetBit(System.Collections.Generic.List, int) -> Task - OrphanCallTransaction() -> Task - UpdateViolated(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAddAndThrow(List grain, List grains, int numberToAdd) -> Task + 2760260D(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(List grains, int numberToAdd) -> Task + 3A6B9237(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.UpdateViolated(ITransactionTestGrain grains, int numberToAdd) -> Task + 485592B2(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDouble(List grains) -> Task + 5FC2E7A1(System.Collections.Generic.List) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSetBit(List grains, int bitIndex) -> Task + 5FF4F216(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 78D54907(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(ITransactionCommitterTestGrain committer, ITransactionCommitOperation operation, List grains, int numberToAdd) -> Task + 8EE5E563(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByRWRW(List grains, int numberToAdd) -> Task + 9EFEA7F3(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByWRWR(List grains, int numberToAdd) -> Task + B4376B4D(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.AddAndThrow(ITransactionTestGrain grain, int numberToAdd) -> Task + D3EF444F(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.OrphanCallTransaction() -> Task + EDCC120B() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionTestGrain")] Orleans.Transactions.TestKit.ITransactionTestGrain [Version(0)] - Add(int) -> Task - AddAndThrow(int) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task - SetAndThrow(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.AddAndThrow(int numberToAdd) -> Task + 25B066B5(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.SetAndThrow(int numberToSet) -> Task + 35C87F81(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Deactivate() -> Task + 35D6FD32() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Get() -> Task + 8DAA79AA() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Set(int newValue) -> Task + CE9EC80B(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Add(int numberToAdd) -> Task + DC07DAEA(int) -> Task class [GrainType("joinattribution")] Orleans.Transactions.TestKit.JoinAttributionGrain diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 6521d3b3a11..02e544c9e58 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -1,15 +1,29 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionManagerExtension")] Orleans.Transactions.Abstractions.ITransactionManagerExtension [Version(0)] - Ping(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task - PrepareAndCommit(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task - Prepared(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Prepared(string resourceId, Guid transactionId, DateTime timestamp, ParticipantId resource, TransactionalStatus status) -> Task + 12BEFA17(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Ping(string resourceId, Guid transactionId, DateTime timeStamp, ParticipantId resource) -> Task + AC4A9AEB(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.PrepareAndCommit(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, List writeResources, int totalParticipants) -> Task + B024EFA6(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionalResourceExtension")] Orleans.Transactions.Abstractions.ITransactionalResourceExtension [Version(0)] - Abort(string, System.Guid) -> Task - Cancel(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task - CommitReadOnly(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task - Confirm(string, System.Guid, System.DateTime) -> Task - Prepare(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.CommitReadOnly(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp) -> Task + 1BB071FE(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Prepare(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, ParticipantId transactionManager) -> Task + 2ADCC608(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Confirm(string resourceId, Guid transactionId, DateTime timeStamp) -> Task + 5DDDE6F0(string, System.Guid, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Cancel(string resourceId, Guid transactionId, DateTime timeStamp, TransactionalStatus status) -> Task + 80028AB9(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Abort(string resourceId, Guid transactionId) -> Task + BD051D23(string, System.Guid) -> Task diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index 9189e7b03fa..e152c048447 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -23,10 +24,18 @@ namespace Analyzers.Tests; public class GrainInterfaceVersionAnalyzerTest { private const string OrleansContractsFileName = "OrleansContracts.txt"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = "RegenerateOrleansContractsFileAsync"; private const string GeneratedHeader = - "# This file is auto-generated by the Orleans contract analyzer.\n" + - "# Update source contracts, then regenerate this file by following:\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\n\n"; + "# This file is generated by the Orleans contract analyzer.\n" + + "# To regenerate, run this command from the repository root after replacing\n" + + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:\n" + + "# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024\n" + + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION\n" + + "# The regeneration command edits this manifest only; it does not change source attributes.\n" + + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.\n" + + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; private static readonly string[] Usings = new[] { @@ -354,6 +363,7 @@ IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); var diagnostic = diagnostics.First(d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); Assert.Contains("NewMethod", diagnostic.GetMessage()); + Assert.Contains("8E43BF4F() -> Task", diagnostic.GetMessage()); } [Fact] @@ -520,6 +530,162 @@ interface IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); } + [Fact] + public async Task RemovedMember_ReportsDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + var diagnostic = Assert.Single( + diagnostics, + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains("RemovedAsync() -> Task", diagnostic.GetMessage()); + Assert.Equal(OrleansContractsFileName, Path.GetFileName(diagnostic.Location.GetLineSpan().Path)); + } + + [Fact] + public async Task LegacyAliasedMemberRename_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task NewName(string renamed); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.OldName(string original) -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task UnmarkedSameNameAlias_ReportsManifestUpgrade() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""Method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberIdentityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""new-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task ExplicitId_DoesNotMatchLegacyMethodName() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task RemovedAlias_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberGenericArityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + #endregion #region ORLEANS0019 - Removed Interface Not Retired @@ -576,6 +742,9 @@ public interface IMyGrain : IGrain var diagnostic = Assert.Single(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0020); Assert.Contains(OrleansContractsFileName, diagnostic.GetMessage()); + Assert.True(diagnostic.Location.IsInSource); + Assert.Contains("dotnet format PATH_TO_PROJECT_OR_SOLUTION", diagnostic.GetMessage()); + Assert.Contains("https://aka.ms/orleans/OrleansContracts.txt", diagnostic.GetMessage()); } [Fact] @@ -953,188 +1122,1093 @@ public sealed class NewRequest { } [Alias(""response"")] public sealed class NewResponse { } -[GrainInterfaceType(""stable-interface"")] -public interface INewGrain : IGrain +[GrainInterfaceType(""stable-interface"")] +public interface INewGrain : IGrain +{ + [Alias(""stable-method"")] + Task NewMethod(NewRequest renamedParameter); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + Assert.Empty(diagnostics); + Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); + Assert.Contains( + " [Alias(\"stable-method\")] stable-method(request) -> Task", + contractsFile); + Assert.Contains("# IOldGrain", contractsFile); + Assert.Contains("# IOldGrain.OldMethod", contractsFile); + } + + [Fact] + public async Task StableInterfaceIdentityChange_IsBreaking() + { + const string source = @" +[GrainInterfaceType(""new-identity"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = @" +[GrainInterfaceType(""old-identity"")] IMyGrain [Version(0)] +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0016); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0019); + } + + [Fact] + public async Task StableGrainTypeChange_IsBreaking() + { + const string source = @" +[GrainType(""new-identity"")] +public class MyGrain : Grain, IGrainWithStringKey +{ +} +"; + const string contractsFile = "class [GrainType(\"old-identity\")] MyGrain"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0023); + } + + [Fact] + public async Task MethodRenameWithoutAlias_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task OldName(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task NewName(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains(diagnostics, diagnostic => diagnostic.GetMessage().Contains("NewName", StringComparison.Ordinal)); + Assert.Contains("# IMyGrain.OldName() -> Task", contractsFile); + } + + [Fact] + public async Task MethodReturnTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string value); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int value); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterOrderChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string name, int count); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int count, string name); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterRename_PreservesGeneratedIdentity() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string oldName); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string newName); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodIdChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + [Id(1)] + Task Update(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + [Id(2)] + Task Update(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task PayloadAliasChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +[Alias(""request"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + const string newSource = @" +[Alias(""request-v2"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task InterfaceRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public interface IOldGrain : IGrain { }"; + const string newSource = "public interface INewGrain : IGrain { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0016, + GrainInterfaceVersionAnalyzer.RuleId0019); + } + + [Fact] + public async Task GrainClassRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public class OldGrain : Grain, IGrainWithStringKey { }"; + const string newSource = "public class NewGrain : Grain, IGrainWithStringKey { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0022); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0022, + GrainInterfaceVersionAnalyzer.RuleId0024); + } + + #endregion + + #region Code Fix Tests Infrastructure + + private async Task<(Solution ChangedSolution, DocumentId? AdditionalDocumentId)> ApplyCodeFixAsync( + string source, + string? grainInterfacesFileContent, + string expectedDiagnosticId, + string? codeActionTitle = null, + string? configuredContractsPath = null, + string? generatedSource = null) + { + var project = CreateProjectWithAdditionalFilesForCodeFix( + source, + grainInterfacesFileContent, + configuredContractsPath, + generatedSource); + var document = project.Documents.First(); + var compilation = await project.GetCompilationAsync(); + + Assert.NotNull(compilation); + var errors = compilation!.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); + Assert.Empty(errors); + + var analyzer = new GrainInterfaceVersionAnalyzer(); + + // Build analyzer options with additional files + var additionalFiles = grainInterfacesFileContent is not null + ? ImmutableArray.Create(new TestAdditionalText(OrleansContractsFileName, grainInterfacesFileContent)) + : ImmutableArray.Empty; + + var analyzerOptions = CreateAnalyzerOptions(additionalFiles, analyzerEnabled: true); + + var compilationWithAnalyzers = compilation + .WithOptions(compilation.Options.WithSpecificDiagnosticOptions( + analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) + .WithAnalyzers(ImmutableArray.Create(analyzer), analyzerOptions); + + var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); + var diagnostic = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnosticId); + + Assert.NotNull(diagnostic); + + // Apply code fix + var codeFixer = new GrainInterfaceVersionCodeFix(); + var actions = new List(); + var context = new CodeFixContext( + document, + diagnostic!, + (action, _) => actions.Add(action), + CancellationToken.None); + + await codeFixer.RegisterCodeFixesAsync(context); + Assert.NotEmpty(actions); + + var action = codeActionTitle is null + ? actions.FirstOrDefault(candidate => !string.Equals(candidate.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)) + ?? actions.First() + : actions.Single(candidate => string.Equals(candidate.Title, codeActionTitle, StringComparison.Ordinal)); + var operations = await action.GetOperationsAsync(CancellationToken.None); + var changedSolution = operations.OfType().Single().ChangedSolution; + + var additionalDocumentId = changedSolution.GetProject(project.Id)?.AdditionalDocumentIds.FirstOrDefault(); + + return (changedSolution, additionalDocumentId); + } + + private async Task ApplyCodeFixAndGetContractsAsync( + string source, + string contractsFileContent, + string expectedDiagnosticId) + { + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, contractsFileContent, expectedDiagnosticId); + Assert.NotNull(additionalDocumentId); + + var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId!); + Assert.NotNull(changedDocument); + + return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + } + + private static void AssertContainsGeneratedMethod( + string content, + string clrSignature, + string contractSignatureSuffix) + { + var pattern = + $"(?m)^ # {Regex.Escape(clrSignature)}\\r?$\\n" + + $" [0-9A-F]{{8}}{Regex.Escape(contractSignatureSuffix)}\\r?$"; + Assert.Matches(pattern, content); + } + + private static void AssertDiagnosticIds(IEnumerable diagnostics, params string[] expected) + => Assert.Equal( + expected.OrderBy(id => id, StringComparer.Ordinal), + diagnostics.Select(diagnostic => diagnostic.Id).OrderBy(id => id, StringComparer.Ordinal)); + + private static Project CreateProjectWithAdditionalFilesForCodeFix( + string source, + string? grainInterfacesFileContent, + string? configuredContractsPath = null, + string? generatedSource = null) + { + const string fileName = "Test.cs"; + + // Prepend usings + var sb = new StringBuilder(); + foreach (var @using in Usings) + { + sb.AppendLine($"using {@using};"); + } + sb.AppendLine(source); + var fullSource = sb.ToString(); + + var projectId = ProjectId.CreateNewId(debugName: "TestProject"); + var documentId = DocumentId.CreateNewId(projectId, fileName); + + var assemblies = new[] + { + typeof(Task).Assembly, + typeof(Orleans.IGrain).Assembly, + typeof(Orleans.Grain).Assembly, + typeof(Attribute).Assembly, + typeof(int).Assembly, + typeof(object).Assembly, + }; + + var metadataReferences = assemblies + .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) + .Concat(assemblies) + .Distinct() + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .Cast() + .ToList(); + + var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); + metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) + .AddMetadataReferences(projectId, metadataReferences) + .AddDocument(documentId, fileName, SourceText.From(fullSource)); + + if (generatedSource is not null) + { + solution = solution.AddDocument( + DocumentId.CreateNewId(projectId, "Generated.g.cs"), + "Generated.g.cs", + SourceText.From(generatedSource), + filePath: "Generated.g.cs"); + } + + // Add additional document if content is provided + if (grainInterfacesFileContent is not null) + { + var contractsDocumentPath = configuredContractsPath is null + ? OrleansContractsFileName + : configuredContractsPath.Replace('/', Path.DirectorySeparatorChar); + var contractsDocumentName = Path.GetFileName(contractsDocumentPath); + var additionalDocumentId = DocumentId.CreateNewId(projectId, contractsDocumentName); + solution = solution.AddAdditionalDocument( + additionalDocumentId, + contractsDocumentName, + SourceText.From(grainInterfacesFileContent), + filePath: contractsDocumentPath); + } + + if (configuredContractsPath is not null) + { + var analyzerConfigDirectory = Path.GetDirectoryName(configuredContractsPath); + var analyzerConfigPath = string.IsNullOrEmpty(analyzerConfigDirectory) + ? Path.Combine(Path.GetTempPath(), ".globalconfig") + : Path.Combine(analyzerConfigDirectory, ".globalconfig"); + solution = solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId, ".globalconfig"), + ".globalconfig", + SourceText.From( + $"is_global = true{Environment.NewLine}" + + $"build_property.OrleansContractsPath = {configuredContractsPath}{Environment.NewLine}"), + filePath: analyzerConfigPath); + } + + return solution.GetProject(projectId)! + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + #endregion + + #region Code Fix Tests - Regenerate + + [Fact] + public async Task CodeFix_RegenerateMissingFile_CreatesCompleteManifest() + { + const string source = @" +[Version(2)] +[GrainInterfaceType(""cart"")] +public interface ICartGrain : IGrain +{ + [Alias(""read"")] + Task GetAsync(int itemId); +} + +[GrainType(""cart"")] +public class CartGrain : Grain, IGrainWithStringKey +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + Assert.NotNull(additionalDocumentId); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"cart\")] ICartGrain [Version(2)]", content); + Assert.Contains(" [Alias(\"read\")] read(int) -> Task", content); + Assert.Contains("class [GrainType(\"cart\")] CartGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesConfiguredManifestPath() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var configuredPathWithAlternateSeparators = configuredPath.Replace('\\', '/'); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPathWithAlternateSeparators); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesFilenameOnlyConfiguredManifestPath() + { + const string configuredPath = "rpc-contracts.txt"; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + + [Fact] + public async Task CodeFix_RegenerateExistingFile_UsesConfiguredCustomFilename() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); Task Pong(); }", + "interface IMyGrain [Version(0)]\n Ping() -> Task\n", + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + var project = changedSolution.GetProject(changedSolution.ProjectIds.Single())!; + var document = Assert.Single(project.AdditionalDocuments); + Assert.Equal(configuredPath, document.FilePath); + Assert.Equal("rpc-contracts.txt", document.Name); + AssertContainsGeneratedMethod( + (await document.GetTextAsync(TestContext.Current.CancellationToken)).ToString(), + "IMyGrain.Pong() -> Task", + "() -> Task"); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesContractHistory() + { + const string source = @" +public interface ICurrentGrain : IGrain +{ + Task ReadAsync(); +} +"; + const string contractsFile = @" +interface ILegacyGrain [Version(1)] + WriteAsync(int) -> Task + +*RETIRED* class [GrainType(""retired"")] RetiredGrain +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"ICurrentGrain\")] ICurrentGrain [Version(0)]", content); + AssertContainsGeneratedMethod(content, "ICurrentGrain.ReadAsync() -> Task", "() -> Task"); + Assert.Contains("*RETIRED* interface ILegacyGrain [Version(1)]", content); + Assert.Contains(" WriteAsync(int) -> Task", content); + Assert.Contains("*RETIRED* class [GrainType(\"retired\")] RetiredGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DistinguishesClrNamesFromExplicitIdentities() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface LegacyName : IGrain +{ +} +"; + const string contractsFile = @" +interface [GrainInterfaceType(""LegacyName"")] OldContract [Version(0)] +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] LegacyName [Version(0)]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"LegacyName\")] OldContract [Version(0)]", + content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesLegacyIdentityWhenExplicitIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("*RETIRED* interface IMyGrain [Version(0)]", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesLegacyIdentityAfterClrRename() + { + const string source = @" +[GrainInterfaceType(""OldName"")] +public interface NewName : IGrain +{ +} + +public interface ITriggerGrain : IGrain +{ +} +"; + const string contractsFile = "interface OldName [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"OldName\")] NewName [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface OldName", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_ExcludesGeneratedGrainProxies() + { + const string source = @" +public interface IMyGrain : IGrain +{ + Task Ping(); +} +"; + const string generatedSource = @" +public sealed class GeneratedProxy : IMyGrain +{ + public System.Threading.Tasks.Task Ping() => System.Threading.Tasks.Task.CompletedTask; +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"IMyGrain\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("GeneratedProxy", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesAliasedGeneratedCodeAttribute() + { + const string source = @" +using GC = System.CodeDom.Compiler.GeneratedCodeAttribute; + +public interface IMyGrain : IGrain +{ +} + +[GC(""Test"", ""1.0"")] +public sealed class GeneratedProxy : IMyGrain +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("GeneratedProxy", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DoesNotTreatSimilarlyNamedAttributeAsGeneratedCode() + { + const string source = @" +public sealed class CustomGeneratedCodeAttribute : Attribute +{ +} + +[CustomGeneratedCode] +public sealed class MyGrain : Grain, IGrainWithStringKey { - [Alias(""stable-method"")] - Task NewMethod(NewRequest renamedParameter); } "; - var contractsFile = await ApplyCodeFixAndGetContractsAsync( - oldSource, - "# OrleansContracts.txt\n", - GrainInterfaceVersionAnalyzer.RuleId0016); - var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); - Assert.Empty(diagnostics); - Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); - Assert.Contains( - " stable-method(request) -> Task", - contractsFile); - Assert.Contains("# IOldGrain", contractsFile); - Assert.Contains("# IOldGrain.OldMethod", contractsFile); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); } [Fact] - public async Task StableInterfaceIdentityChange_IsBreaking() + public async Task CodeFix_RegenerateProject_IncludesSourcePartialGrainWithGeneratedPartial() { const string source = @" -[GrainInterfaceType(""new-identity"")] -public interface IMyGrain : IGrain +public partial class MyGrain : Grain, IGrainWithStringKey { } "; - const string contractsFile = @" -[GrainInterfaceType(""old-identity"")] IMyGrain [Version(0)] + const string generatedSource = @" +[System.CodeDom.Compiler.GeneratedCode(""Test"", ""1.0"")] +public partial class MyGrain +{ +} "; - var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0016); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); + + var sourceText = (await changedSolution.Projects.Single().Documents + .Single(document => document.Name == "Test.cs") + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("[Alias(", sourceText); + Assert.DoesNotContain("[Id(", sourceText); + Assert.DoesNotContain("[GrainType(", sourceText); + Assert.DoesNotContain("[GrainInterfaceType(", sourceText); } [Fact] - public async Task StableGrainTypeChange_IsBreaking() + public async Task CodeFix_RegenerateProject_RecognizesLegacyGrainTypeAfterClrRename() { const string source = @" -[GrainType(""new-identity"")] -public class MyGrain : Grain, IGrainWithStringKey +[GrainType(""old"")] +public class NewGrain : Grain, IGrainWithStringKey +{ +} + +public interface ITriggerGrain : IGrain { } "; - const string contractsFile = "class [GrainType(\"old-identity\")] MyGrain"; + const string contractsFile = "class OldGrain"; - var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); - Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0023); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"old\")] NewGrain", content); + Assert.DoesNotContain("*RETIRED* class OldGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); } - #endregion - - #region Code Fix Tests Infrastructure - - private async Task<(Solution ChangedSolution, DocumentId? AdditionalDocumentId)> ApplyCodeFixAsync( - string source, - string? grainInterfacesFileContent, - string expectedDiagnosticId) + [Fact] + public async Task CodeFix_RegenerateProject_RoundTripsContractsNestedInGenericTypes() { - var project = CreateProjectWithAdditionalFilesForCodeFix(source, grainInterfacesFileContent); - var document = project.Documents.First(); - var compilation = await project.GetCompilationAsync(); - - Assert.NotNull(compilation); - var errors = compilation!.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error); - Assert.Empty(errors); + const string source = @" +public class Outer +{ + public interface IInnerGrain : IGrain + { + Task Ping(T value); + } - var analyzer = new GrainInterfaceVersionAnalyzer(); + public class InnerGrain : Grain, IGrainWithStringKey + { + } +} +"; - // Build analyzer options with additional files - var additionalFiles = grainInterfacesFileContent is not null - ? ImmutableArray.Create(new TestAdditionalText(OrleansContractsFileName, grainInterfacesFileContent)) - : ImmutableArray.Empty; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); - var analyzerOptions = CreateAnalyzerOptions(additionalFiles, analyzerEnabled: true); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains( + "interface [GrainInterfaceType(\"Outer`1+IInnerGrain\")] Outer.IInnerGrain [Version(0)]", + content); + Assert.Contains("class [GrainType(\"inner`1\")] Outer.InnerGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } - var compilationWithAnalyzers = compilation - .WithOptions(compilation.Options.WithSpecificDiagnosticOptions( - analyzer.SupportedDiagnostics.ToDictionary(d => d.Id, d => ReportDiagnostic.Default))) - .WithAnalyzers(ImmutableArray.Create(analyzer), analyzerOptions); + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMemberSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; - var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); - var diagnostic = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnosticId); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - Assert.NotNull(diagnostic); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + AssertContainsGeneratedMethod(content, "IMyGrain.ExistingAsync() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMyGrain.NewAsync() -> Task", "() -> Task"); + Assert.Contains(" RemovedAsync() -> Task", content); - // Apply code fix - var codeFixer = new GrainInterfaceVersionCodeFix(); - var actions = new List(); - var context = new CodeFixContext( - document, - diagnostic!, - (action, _) => actions.Add(action), - CancellationToken.None); + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } - await codeFixer.RegisterCodeFixesAsync(context); - Assert.NotEmpty(actions); + [Fact] + public async Task CodeFix_RegenerateProject_PreservesAliasesAndDeduplicatesLegacyMembers() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + [Alias(""removed"")] IMyGrain.RemovedAsync(string value) -> Task + removed(string) -> Task +"; - var operations = await actions.First().GetOperationsAsync(CancellationToken.None); - var changedSolution = operations.OfType().Single().ChangedSolution; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - var additionalDocumentId = changedSolution.GetProject(project.Id)?.AdditionalDocumentIds.FirstOrDefault(); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Equal(1, content.Split(new[] { "removed(string) -> Task" }, StringSplitOptions.None).Length - 1); + Assert.Contains("[Alias(\"removed\")] removed(string) -> Task", content); - return (changedSolution, additionalDocumentId); + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.Single(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } - private async Task ApplyCodeFixAndGetContractsAsync( - string source, - string contractsFileContent, - string expectedDiagnosticId) + [Fact] + public async Task CodeFix_RegenerateProject_DeduplicatesSemanticallyEquivalentLegacyMembers() { - var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync(source, contractsFileContent, expectedDiagnosticId); - Assert.NotNull(additionalDocumentId); + const string source = @" +namespace Models +{ + public sealed class Request { } +} - var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId!); - Assert.NotNull(changedDocument); +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(Models.Request request); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method(Request value) -> Task +"; - return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + AssertContainsGeneratedMethod( + content, + "IMyGrain.Method(Request request) -> Task", + "(Models.Request) -> Task"); + Assert.DoesNotContain(" Method(Request) -> Task", content); + Assert.DoesNotContain( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } - private static Project CreateProjectWithAdditionalFilesForCodeFix(string source, string? grainInterfacesFileContent) + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMembersOnNestedLegacyInterfaces() { - const string fileName = "Test.cs"; - - // Prepend usings - var sb = new StringBuilder(); - foreach (var @using in Usings) - { - sb.AppendLine($"using {@using};"); - } - sb.AppendLine(source); - var fullSource = sb.ToString(); + const string source = @" +public class Outer +{ + [Version(1)] + public interface IInnerGrain : IGrain + { + Task ExistingAsync(); + Task NewAsync(); + } +} +"; + const string contractsFile = @" +interface Outer.IInnerGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; - var projectId = ProjectId.CreateNewId(debugName: "TestProject"); - var documentId = DocumentId.CreateNewId(projectId, fileName); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); - var assemblies = new[] + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" RemovedAsync() -> Task", content); + Assert.Contains( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task FixAll_RegenerateSolution_UpdatesEveryProjectWithDiagnostics() + { + var firstProject = CreateProjectWithAdditionalFilesForCodeFix( + "public interface IFirstGrain : IGrain { Task Ping(); }", + "# OrleansContracts.txt\n"); + var solution = firstProject.Solution; + var secondProjectId = ProjectId.CreateNewId("SecondProject"); + var secondDocumentId = DocumentId.CreateNewId(secondProjectId, "Second.cs"); + var secondContractsId = DocumentId.CreateNewId(secondProjectId, OrleansContractsFileName); + var secondSource = string.Join( + Environment.NewLine, + Usings.Select(@using => $"using {@using};").Append( + "public interface ISecondGrain : IGrain { Task Pong(); }")); + solution = solution + .AddProject(secondProjectId, "SecondProject", "SecondProject", LanguageNames.CSharp) + .AddMetadataReferences(secondProjectId, firstProject.MetadataReferences) + .WithProjectCompilationOptions(secondProjectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddDocument(secondDocumentId, "Second.cs", SourceText.From(secondSource)) + .AddAdditionalDocument(secondContractsId, OrleansContractsFileName, SourceText.From("# OrleansContracts.txt\n")); + + firstProject = solution.GetProject(firstProject.Id)!; + var diagnostics = new Dictionary> { - typeof(Task).Assembly, - typeof(Orleans.IGrain).Assembly, - typeof(Orleans.Grain).Assembly, - typeof(Attribute).Assembly, - typeof(int).Assembly, - typeof(object).Assembly, + [firstProject.Id] = new[] { CreateFixAllDiagnostic() }, + [secondProjectId] = new[] { CreateFixAllDiagnostic() } }; + var codeFixer = new GrainInterfaceVersionCodeFix(); + var context = new FixAllContext( + firstProject.Documents.First(), + codeFixer, + FixAllScope.Solution, + RegenerateCodeActionEquivalenceKey, + codeFixer.FixableDiagnosticIds, + new TestFixAllDiagnosticProvider(diagnostics), + TestContext.Current.CancellationToken); - var metadataReferences = assemblies - .SelectMany(x => x.GetReferencedAssemblies().Select(Assembly.Load)) - .Concat(assemblies) - .Distinct() - .Select(x => MetadataReference.CreateFromFile(x.Location)) - .Cast() - .ToList(); - - var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll"))); - metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); - - var solution = new AdhocWorkspace() - .CurrentSolution - .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) - .AddMetadataReferences(projectId, metadataReferences) - .AddDocument(documentId, fileName, SourceText.From(fullSource)); - - // Add additional document if content is provided - if (grainInterfacesFileContent is not null) - { - var additionalDocumentId = DocumentId.CreateNewId(projectId, OrleansContractsFileName); - solution = solution.AddAdditionalDocument(additionalDocumentId, OrleansContractsFileName, SourceText.From(grainInterfacesFileContent)); - } - - return solution.GetProject(projectId)! - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var action = await codeFixer.GetFixAllProvider().GetFixAsync(context); + Assert.NotNull(action); + var operations = await action!.GetOperationsAsync(TestContext.Current.CancellationToken); + var changedSolution = operations.OfType().Single().ChangedSolution; + var firstContent = (await changedSolution.GetProject(firstProject.Id)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var secondContent = (await changedSolution.GetProject(secondProjectId)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + + Assert.Contains("interface [GrainInterfaceType(\"IFirstGrain\")] IFirstGrain [Version(0)]", firstContent); + Assert.Contains("interface [GrainInterfaceType(\"ISecondGrain\")] ISecondGrain [Version(0)]", secondContent); + } + + private static Diagnostic CreateFixAllDiagnostic() + => Diagnostic.Create( + new DiagnosticDescriptor( + GrainInterfaceVersionAnalyzer.RuleId0016, + "Contract missing", + "Contract missing", + "Orleans.Versioning", + DiagnosticSeverity.Warning, + isEnabledByDefault: true), + Location.None); + + private sealed class TestFixAllDiagnosticProvider( + IReadOnlyDictionary> diagnostics) : FixAllContext.DiagnosticProvider + { + public override Task> GetDocumentDiagnosticsAsync( + Document document, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetProjectDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetAllDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult( + diagnostics.TryGetValue(project.Id, out var result) + ? result + : Enumerable.Empty()); } #endregion @@ -1318,7 +2392,7 @@ public interface IMyGrain : IGrain // Should contain the new interface Assert.Contains("IMyGrain [Version(1)]", content); - Assert.Contains("\n DoSomething() -> Task", content); + AssertContainsGeneratedMethod(content, "IMyGrain.DoSomething() -> Task", "() -> Task"); Assert.DoesNotContain("Utility", content); } @@ -1347,7 +2421,10 @@ public interface IMyGrain : IGrain Assert.DoesNotContain("[Alias(", content); Assert.Contains("IMyGrain [Version(2)]", content); - Assert.Contains("\n DoSomething(string) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.DoSomething(string name) -> Task", + "(string) -> Task"); } [Fact] @@ -1425,22 +2502,12 @@ public interface IMiddle : IGrain var alphaInterface = content.IndexOf("IAlpha [Version(1)]", StringComparison.Ordinal); var middleInterface = content.IndexOf("IMiddle [Version(1)]", StringComparison.Ordinal); var zuluInterface = content.IndexOf("IZulu [Version(1)]", StringComparison.Ordinal); - var alphaMember = content.IndexOf(" Alpha() -> Task", StringComparison.Ordinal); - var zetaMember = content.IndexOf(" Zeta() -> Task", StringComparison.Ordinal); - Assert.True(alphaInterface < middleInterface); Assert.True(middleInterface < zuluInterface); - Assert.True(alphaMember < zetaMember); - Assert.Equal( - GeneratedHeader + - "interface IAlpha [Version(1)]\n" + - " Method() -> Task\n\n" + - "interface [GrainInterfaceType(\"IMiddle\")] IMiddle [Version(1)]\n" + - " Alpha() -> Task\n" + - " Zeta() -> Task\n\n" + - "interface IZulu [Version(1)]\n" + - " Method() -> Task\n", - content); + AssertContainsGeneratedMethod(content, "IMiddle.Alpha() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMiddle.Zeta() -> Task", "() -> Task"); + Assert.Contains("interface IAlpha [Version(1)]\n Method() -> Task", content); + Assert.Contains("interface IZulu [Version(1)]\n Method() -> Task", content); } [Fact] @@ -1533,6 +2600,26 @@ public interface INewGrain : IGrain Assert.Equal(1, content.Split(new[] { "stable-interface" }, StringSplitOptions.None).Length - 1); } + [Fact] + public async Task CodeFix_AddInterface_PreservesLegacyDeclarationWhenIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016); + + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("interface IMyGrain [Version(0)]", content); + } + #endregion #region Code Fix Tests - ORLEANS0017 Update Version @@ -1655,7 +2742,10 @@ IMyGrain [Version(1)] var content = changedText.ToString(); // Should contain the new member - Assert.Contains("\n NewMethod(int) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.NewMethod(int value) -> Task", + "(int) -> Task"); } [Fact] @@ -1684,7 +2774,7 @@ IMyGrain [Version(1)] var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); var content = changedText.ToString(); - Assert.Contains("\n new-method(int) -> Task", content); + Assert.Contains("\n [Alias(\"new-method\")] new-method(int) -> Task", content); Assert.Contains(" # IMyGrain.NewMethod(int value) -> Task", content); } @@ -1706,7 +2796,9 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal(GeneratedHeader + "interface IMyGrain [Version(1)]\n NewMethod() -> Task\n", content); + Assert.Equal( + GeneratedHeader + "interface IMyGrain [Version(1)]\n [Alias(\"NewMethod\")] NewMethod() -> Task\n", + content); } [Fact] @@ -1726,9 +2818,11 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal( - GeneratedHeader + "interface IMyGrain [Version(1)]\n ReadStateAsync`1(T) -> Task\n", - content); + Assert.StartsWith(GeneratedHeader + "interface IMyGrain [Version(1)]\n", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.ReadStateAsync(T value) -> Task", + "`1(T) -> Task"); } [Fact] @@ -1908,6 +3002,32 @@ IFoo [Version(1)] Assert.DoesNotContain("*RETIRED* interface IFooBar", content); } + [Fact] + public async Task CodeFix_RetireInterface_UsesStableIdentityWhenClrNamesMatch() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string grainInterfacesFile = @" +interface [GrainInterfaceType(""current-id"")] IMyGrain [Version(0)] +interface [GrainInterfaceType(""old-id"")] IMyGrain [Version(0)] +"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + grainInterfacesFile, + GrainInterfaceVersionAnalyzer.RuleId0019); + + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface [GrainInterfaceType(\"current-id\")]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"old-id\")] IMyGrain [Version(0)]", + content); + } + #endregion #region Inherited Interfaces @@ -2049,10 +3169,8 @@ public interface IMyGrain : IGrain } "; const string contractsFile = "# OrleansContracts.txt\r\nIMyGrain [Version(1)]\r\nIMyGrain.DoSomething() -> Task\r\n"; - const string expectedContractsFile = - "# This file is auto-generated by the Orleans contract analyzer.\r\n" + - "# Update source contracts, then regenerate this file by following:\r\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\r\n\r\n" + + var expectedContractsFile = + GeneratedHeader.Replace("\n", "\r\n", StringComparison.Ordinal) + "interface IMyGrain [Version(2)]\r\n" + " DoSomething() -> Task\r\n"; var properties = ImmutableDictionary.Empty @@ -2066,7 +3184,9 @@ public interface IMyGrain : IGrain var codeFixer = new GrainInterfaceVersionCodeFix(); await codeFixer.RegisterCodeFixesAsync(context); - var action = Assert.Single(actions); + var action = Assert.Single( + actions, + action => !string.Equals(action.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)); var operations = await action.GetOperationsAsync(TestContext.Current.CancellationToken); var changedSolution = Assert.Single(operations.OfType()).ChangedSolution; var changedProject = changedSolution.GetProject(context.Document.Project.Id); @@ -2074,10 +3194,12 @@ public interface IMyGrain : IGrain var additionalDocumentId = Assert.Single(changedProject!.AdditionalDocumentIds); var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId); Assert.NotNull(changedDocument); - var actualContractsFile = (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); + var actualContractsFile = changedText.ToString(); Assert.Equal(expectedContractsFile, actualContractsFile); Assert.DoesNotContain("\n", actualContractsFile.Replace("\r\n", string.Empty, StringComparison.Ordinal)); + Assert.Empty(changedText.Encoding?.GetPreamble() ?? []); } [Theory]