Skip to content

Add unsafe modifier migration code fixer#131002

Draft
EgorBo wants to merge 13 commits into
mainfrom
unsafe-migrator-4
Draft

Add unsafe modifier migration code fixer#131002
EgorBo wants to merge 13 commits into
mainfrom
unsafe-migrator-4

Conversation

@EgorBo

@EgorBo EgorBo commented Jul 17, 2026

Copy link
Copy Markdown
Member

This PR adds new analyzers and codefixer (not yet shipping) that assists in migrating to the new unsafe-v2 rules ("unsafe evolution") and are supposed to be idempotent. This PR doesn't add a code-fixer that introduces new unsafe scopes/or unsafe-expressions as that ons is quite complex and will be added separately. So I tried to just rely on Roslyn's existing diagnostics and only introduced IL5005 and IL5006 (see below).

New analyzers/code-fixers

  • AddUnsafeToExternCodeFixProvider fixes CS9389 by marking an extern member unsafe by default. Developers can replace it with safe after auditing the interop boundary.

  • RemoveInvalidUnsafeCodeFixProvider fixes CS9377 and unsafe-specific CS0106 diagnostics by removing the compiler-reported meaningless or invalid unsafe modifier. (e.g. from class level)

  • UnsafeMemberMissingSafetyDocumentationAnalyzer (IL5005) reports unsafe members without a <safety> XML comment. Members with pointer or function-pointer signatures are excluded because they were already effectively caller-unsafe under the legacy rules.

    • Basically, we assume that e.g. a unsafe void Foo() is coming from legacy rules where it used that unsafe to open a global unsafe scope if there is no <safety> comment.
  • RemoveUndocumentedUnsafeCodeFixProvider a companion code-fixer for IL5005 defined above ^ by removing the undocumented unsafe modifier (we assume it's the unsafe-v1's global unsafe scope unsafe and not caller-unsafe).

  • PointerSignatureRequiresUnsafeAnalyzer (IL5006) reports members with pointer or function-pointer signatures that are missing unsafe. A <safety> XML comment suppresses the diagnostic when the signature is intentionally safe, e.g. an API that never dereferences the pointer - ArgumentNullException.ThrowIfNull(void*).

  • AddUnsafeToPointerSignatureCodeFixProvider fixes IL5006 ^ by adding the missing unsafe modifier.

  • UnsafeMemberAnalysis and UnsafeModifierCodeFixHelpers contain the shared declaration, pointer-signature, documentation, and modifier-editing logic used by the focused components above.

Codes

  • CS9389 [Roslyn] - An extern member must be explicitly marked unsafe or safe.
  • CS9377 [Roslyn] - The unsafe modifier has no effect at this location under the updated memory safety rules.
  • CS0106 [Roslyn] - The unsafe modifier is not valid for this item. This PR's fixer only activates when unsafe is the invalid modifier.
  • IL5005 [This PR] - An unsafe member has no <safety> XML documentation. Pointer and function-pointer signatures are excluded for legacy compatibility.
  • IL5006 [This PR] - A member with a pointer or function-pointer signature is missing unsafe, unless a <safety> comment documents why it is safe. (e.g. when in unsafe-v1 such methods relied on unsafe on the class level).

For follow ups

  • Implement a code fixer for unsafe-context diagnostics CS9360, CS9361, CS9362, CS9363, and CS9376. It should introduce unsafe { /* SAFETY: Audit */ } blocks or unsafe(/* SAFETY: Audit */ ...) expressions

  • Synchronize intentional caller-unsafe contracts across overrides, interface implementations, and partial declarations. This includes Roslyn's unsafe-to-safe mismatch diagnostics CS9364, CS9365, and CS9366, partial modifier mismatches CS0764 and CS9390, and the opposite migration case where an unsafe interface/base contract should be propagated to an implementation that is currently unannotated.

  • Handle explicit/extended-layout fields and compiler-generated backing fields. CS9392 requires an explicit unsafe or safe marker, while CS9388 reports invalid safe usage. Migration should default to unsafe unless the developer has explicitly audited the field as safe.

  • Figure out what to do with LibraryImport. Roslyn currently does not treat it as extern, safe cannot be applied to it, and its generated implementation is not compiled under the updated memory-safety rules. See safe keyword on LibraryImport roslyn#84555

EgorBo and others added 12 commits July 13, 2026 11:22
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
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
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c4ec9d91-2b56-40d3-b938-b386154f464c
Copilot AI review requested due to automatic review settings July 17, 2026 21:59
@EgorBo
EgorBo requested a review from sbomer as a code owner July 17, 2026 21:59
@EgorBo
EgorBo temporarily deployed to copilot-pat-pool July 17, 2026 22:00 — with GitHub Actions Inactive
@EgorBo
EgorBo temporarily deployed to copilot-pat-pool July 17, 2026 22:00 — with GitHub Actions Inactive
@github-actions github-actions Bot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Jul 17, 2026
@dotnet-policy-service dotnet-policy-service Bot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 17, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@EgorBo
EgorBo had a problem deploying to copilot-pat-pool July 17, 2026 22:01 — with GitHub Actions Failure
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/illink
See info in area-owners.md if you want to be subscribed.

@EgorBo
EgorBo temporarily deployed to copilot-pat-pool July 17, 2026 22:02 — with GitHub Actions Inactive
@EgorBo
EgorBo temporarily deployed to copilot-pat-pool July 17, 2026 22:02 — with GitHub Actions Inactive
@EgorBo
EgorBo temporarily deployed to copilot-pat-pool July 17, 2026 22:02 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds initial “unsafe modifier migration” support to the ILLink analyzer/codefix tooling (DEBUG-only), wiring a new IL5005-style diagnostic and a code fix that normalizes unsafe modifiers based on signature/docs, plus build plumbing to enable the feature via EnableUnsafeMigration.

Changes:

  • Introduces UnsafeMigrationAnalyzer (IL5005) and UnsafeModifierMigrationCodeFixProvider to report/fix unsafe modifier normalization when EnableUnsafeMigration is enabled.
  • Adds supporting shared diagnostic IDs/categories/strings and exposes EnableUnsafeMigration as a compiler-visible MSBuild property.
  • Extends the existing analyzer/codefix test suite with targeted tests for the new diagnostic and fixer.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/RequiresUnsafeCodeFixTests.cs Adds verifier helpers and new tests for the unsafe modifier migration diagnostic and code fix.
src/tools/illink/src/ILLink.Shared/SharedStrings.resx Adds title/message resources for the new unsafe migration diagnostic.
src/tools/illink/src/ILLink.Shared/DiagnosticId.cs Adds UnsafeModifierMigration (DEBUG-only) and maps it to a new diagnostic category.
src/tools/illink/src/ILLink.Shared/DiagnosticCategory.cs Adds Unsafe diagnostic category (DEBUG-only).
src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationAnalyzer.cs New analyzer that reports a single “file needs unsafe modifier migration” diagnostic when updates are detected.
src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationAnalysis.cs New semantic analysis that computes which declarations should/shouldn’t have unsafe based on rules (docs/pointers/interop).
src/tools/illink/src/ILLink.RoslynAnalyzer/MSBuildPropertyOptionNames.cs Adds EnableUnsafeMigration MSBuild property name (DEBUG-only constant).
src/tools/illink/src/ILLink.RoslynAnalyzer/build/Microsoft.NET.ILLink.Analyzers.props Exposes EnableUnsafeMigration as a compiler-visible property.
src/tools/illink/src/ILLink.CodeFix/UnsafeModifierMigrationCodeFixProvider.cs New code fix provider to apply modifier normalization across a document when enabled.
src/tools/illink/src/ILLink.CodeFix/Resources.resx Adds the code fix title resource string.
eng/liveILLink.targets Wires EnableUnsafeMigration into LiveILLink, enabling unsafe analyzer and setting the updated-memory-safety-rules feature flag.

Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationAnalysis.cs Outdated
Comment thread src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/RequiresUnsafeCodeFixTests.cs Outdated
@EgorBo
EgorBo marked this pull request as draft July 17, 2026 22:06
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c4ec9d91-2b56-40d3-b938-b386154f464c
Copilot AI review requested due to automatic review settings July 17, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/RequiresUnsafeCodeFixTests.cs:130

  • DiagnosticDescriptor.HelpLinkUri is a nullable string and DiagnosticDescriptors.GetDiagnosticDescriptor(...) passes helpLinkUri through directly. For these new diagnostics DiagnosticIdExtensions.GetHelpUri returns null, so descriptor.HelpLinkUri will be null and Assert.Empty(descriptor.HelpLinkUri) will fail. Use a null check (or string.IsNullOrEmpty) instead of Assert.Empty.
                DiagnosticDescriptor descriptor = DiagnosticDescriptors.GetDiagnosticDescriptor(diagnosticId);
                Assert.Equal("Unsafe", descriptor.Category);
                Assert.Empty(descriptor.HelpLinkUri);
            }

Comment on lines +1272 to +1280
<data name="UnsafeMemberMissingSafetyDocumentationTitle" xml:space="preserve">
<value>Unsafe member is missing safety documentation</value>
</data>
<data name="UnsafeMemberMissingSafetyDocumentationMessage" xml:space="preserve">
<value>Remove the 'unsafe' modifier or document the caller's safety requirements with a &lt;safety&gt; XML comment.</value>
</data>
<data name="PointerSignatureRequiresUnsafeTitle" xml:space="preserve">
<value>Pointer signature requires an unsafe modifier</value>
</data>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Tools-ILLink .NET linker development as well as trimming analyzers linkable-framework Issues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants