Add unsafe evolution migration code fixers#130611
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c3ccf7c6-8a32-4c31-9e31-12f0f1d2a026
There was a problem hiding this comment.
Pull request overview
This PR adds a DEBUG-only “unsafe evolution” migration analyzer + code fixers to ILLink, introducing two new IL* diagnostics (IL5005/IL5006) to (1) remove legacy/invalid unsafe modifiers and (2) migrate unsafe usages by adding explicit unsafe contexts and safety placeholders. It also wires up additional MSBuild properties and live-ILLink behavior to enable the migration experience, and adds/extends Roslyn analyzer/codefix tests.
Changes:
- Add
UnsafeMigrationAnalyzer+UnsafeMigrationAnalysisto detect modifier removals, required declaration updates, and unsafe-operation sites (behind#if DEBUG). - Add two new code fix providers (modifier removal + usage migration) and supporting document/AST rewrite helpers (behind
#if DEBUG). - Add new diagnostic strings/IDs and update build/targets/tests to enable and validate the migration workflow.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/RequiresUnsafeCodeFixTests.cs | Adds test helpers and extensive test coverage for IL5005/IL5006 codefix behavior. |
| src/tools/illink/src/ILLink.Shared/SharedStrings.resx | Adds user-facing title/message strings for the new unsafe migration diagnostics. |
| src/tools/illink/src/ILLink.Shared/DiagnosticId.cs | Adds DEBUG-only diagnostic IDs and introduces an “Unsafe” category range mapping. |
| src/tools/illink/src/ILLink.Shared/DiagnosticCategory.cs | Adds DEBUG-only Unsafe category constant. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationAnalyzer.cs | New analyzer that reports file-level migration diagnostics when migration is enabled. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationAnalysis.cs | New analysis engine for modifier removals, contract/declaration updates, and unsafe operation locations. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/MSBuildPropertyOptionNames.cs | Adds DEBUG-only MSBuild property names used to toggle unsafe migration behavior. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/build/Microsoft.NET.ILLink.Analyzers.props | Exposes additional compiler-visible MSBuild properties used by the migration flow. |
| src/tools/illink/src/ILLink.CodeFix/UnsafeUsageMigrationCodeFixProvider.cs | New code fix provider that migrates unsafe usage sites (wrap blocks/expressions, etc.). |
| src/tools/illink/src/ILLink.CodeFix/UnsafeUsageDocumentFixer.cs | New document fixer implementing the migration transformations and heuristics. |
| src/tools/illink/src/ILLink.CodeFix/UnsafeModifierMigrationCodeFixProvider.cs | New code fix provider that removes legacy unsafe modifiers when eligible. |
| src/tools/illink/src/ILLink.CodeFix/UnsafeCodeFixHelpers.cs | New shared helpers for adding unsafe contexts and safety documentation. |
| src/tools/illink/src/ILLink.CodeFix/Resources.resx | Adds code fix titles for the new migration fixers. |
| eng/liveILLink.targets | Updates live-ILLink wiring to support enabling unsafe migration and design-time analyzer referencing. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c3ccf7c6-8a32-4c31-9e31-12f0f1d2a026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c3ccf7c6-8a32-4c31-9e31-12f0f1d2a026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c3ccf7c6-8a32-4c31-9e31-12f0f1d2a026
- Parse unsafe(...) with explicit preview + updated-memory-safety-rules options - Use SyntaxFactory elastic newline trivia instead of hardcoded CRLF literals - Remove redundant single-arm switch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 826b613c-e6ee-4cc6-a6a7-b06038520aad
The IL5006 document fixer could emit code that fails to compile:
- unsafe(...) expressions at statement/void/assignment-target positions (only valid as a value sub-expression) now fall back to unsafe { } blocks
- out-var/pattern-var declarations (Foo(out int y);) are forward-declared before the unsafe block so they stay in scope
- out-vars in a local declaration initializer (int x = Foo(out int y);) fall back to the unsafe(...) expression form, keeping the variable in scope
- only interior (not leading/trailing) directives block wrapping a statement
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 826b613c-e6ee-4cc6-a6a7-b06038520aad
- Split multi-variable event field declarations with mixed safe/unsafe contracts so 'unsafe' is applied only to the variables that require it (avoids CS9365 where a safe interface event would be made illegally unsafe)
- Drop the statement-wrap branch that ignored requiresExpression, so using/await/directive statements that need an unsafe(...) expression fall back to whole-body wrapping instead of an unsafe { } block that would change scope
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 826b613c-e6ee-4cc6-a6a7-b06038520aad
When splitting a multi-variable event field, keep each variable node's trivia (e.g. inline comments like 'event Action E1 /* keep */, E2;') instead of stripping it; only the outer declaration trivia is normalized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 826b613c-e6ee-4cc6-a6a7-b06038520aad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ee40608f-4e94-47e1-8763-843d916bab40
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "74d7482707561a9b391043d905abee5cc4c733c9",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "53a3837195f7d8e7d714306fab5679562b2261d2",
"last_reviewed_commit": "74d7482707561a9b391043d905abee5cc4c733c9",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "53a3837195f7d8e7d714306fab5679562b2261d2",
"last_recorded_worker_run_id": "29686415576",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "74d7482707561a9b391043d905abee5cc4c733c9",
"review_id": 4730737351
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: The change implements automated migration tooling (analyzer IL5005/IL5006 plus code-fix providers) for the unsafe-evolution C# language proposal, letting the runtime repo mechanically strip legacy/invalid unsafe modifiers and add required unsafe blocks, expressions, and <safety> documentation. This is a real, well-motivated need: migrating a large codebase to the new memory-safety rules by hand would be impractical and error-prone.
Approach: The approach is sound and appropriately scoped. All new code is wrapped in #if DEBUG and gated behind the opt-in EnableUnsafeMigration MSBuild property, so it adds no shipping public API surface and cannot affect production builds or normal analyzer runs. It reuses the existing ILLink analyzer/code-fix infrastructure (diagnostic IDs, categories, resx strings, CSharpCodeFixVerifier) rather than inventing parallel machinery, and the IL5005/5006 IDs and Unsafe category are correctly slotted into DiagnosticId/DiagnosticCategory without disturbing the existing AOT range mapping. The logic handles a broad matrix of C# constructs (partial members, interfaces, accessors, stackalloc, async, top-level statements, scoped refs), which the extensive test file exercises.
Summary: <safety>/skiplocalsinit handling, and confirm the tool's output on representative source is what the proposal intends.
Detailed Findings
✅ Scope & Risk — Correctly isolated tooling change
Every new type and every touched enum/category/resx addition is under #if DEBUG and further gated by the EnableUnsafeMigration compiler-visible property (verified in Microsoft.NET.ILLink.Analyzers.props, MSBuildPropertyOptionNames.cs, and both code-fix providers' RegisterCodeFixesAsync). No ref/ assembly or shipping public API is introduced, so the API-approval gate does not apply. Production analyzer behavior is unchanged.
✅ Codebase Fit — Consistent with existing ILLink patterns
Diagnostic IDs (5005/5006), the new Unsafe category, and the resx entries follow existing conventions. The GetDiagnosticCategory switch inserts the two new IDs before the >= 3050 and <= 6000 => AOT arm so they resolve to Unsafe rather than being swallowed by the AOT range — this ordering is correct and covered by the UnsafeMigrationDiagnostics_HaveUnsafeCategoryWithoutHelpLinks test.
✅ Test Quality — Broad behavioral coverage
The new RequiresUnsafeCodeFixTests.cs adds ~44 focused test methods covering modifier removal/retention, whole-body vs. statement vs. expression wrapping, forward declarations, ref locals, async/yield exclusions, and the migration-disabled case. Using CSharpCodeFixVerifier with before/after sources is the right harness for code-fix validation. Note: I could not build or run these tests in this environment; the human reviewer should confirm they pass in CI.
💡 Brittle syntax-node detection (inline)
One low-severity, non-blocking maintainability note is left inline in UnsafeMigrationAnalysis.cs regarding the GetType().Name == "UnsafeExpressionSyntax" string check used to recognize the preview unsafe(...) expression node.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 192.5 AIC · ⌖ 12.1 AIC · ⊞ 10K
| private static bool IsInUnsafeContext(SyntaxNode node) | ||
| => node.AncestorsAndSelf().Any(static ancestor => | ||
| ancestor is UnsafeStatementSyntax || | ||
| ancestor.GetType().Name == "UnsafeExpressionSyntax" || |
There was a problem hiding this comment.
💡 This uses a stringly-typed check (ancestor.GetType().Name == "UnsafeExpressionSyntax") to detect the preview unsafe(...) expression node, presumably because the strongly-typed syntax type isn't available in the referenced Roslyn version. That's a reasonable pragmatic workaround, but it is brittle: it silently breaks if the node type is renamed, and it won't match derived/wrapped forms. Consider adding a short comment explaining why the reflection-by-name check is used and, if feasible, guarding it with a SyntaxKind check so the intent survives a Roslyn bump. Non-blocking given this is DEBUG-only tooling.
|
decided to split into many small analyzers/fixers and the first batch is in #131002 |
Spec: https://github.com/dotnet/csharplang/blob/main/proposals/unsafe-evolution.md
What it does:
unsafemodifiers.<safety>comment. It's a sign that thisunsafewas from unsafe-v1.unsafewhen required by interop, partial members, overrides, or interface contracts.unsafeand<safety>placeholders to unannotated interop declarations.How to run it from the repo root:
I was using the latest daily SDK in
C:\prj\dotnet-latest, not sure the bootstrapped one contains all the new C# features.