Skip to content

CodeFixer: move unsafe from method modifier to method body#128304

Open
EgorBo wants to merge 9 commits into
dotnet:mainfrom
EgorBo:unsafe-to-scope
Open

CodeFixer: move unsafe from method modifier to method body#128304
EgorBo wants to merge 9 commits into
dotnet:mainfrom
EgorBo:unsafe-to-scope

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented May 17, 2026

Adds a code fixer (analyzer IL5005 + IL5006, both info severity, gated behind MSBuild property EnableUnsafeV2MigrationAnalyzer) that mechanically migrates a BCL assembly to the unsafe-evolution semantics.

Run with (example for STJ):

.\dotnet.cmd format analyzers .\src\libraries\System.Text.Json\System.Text.Json.slnx `
    --diagnostics IL5005 IL5006 --severity info --no-restore

Recommend reviewing diffs with whitespace disabled.

What it does

IL5005 — unsafe on a type or delegate declaration

Target Action
class, struct, interface, record, record struct, delegate Remove unsafe unconditionally (per spec: has no meaning under updated rules).

IL5006 — unsafe on a member declaration

Next, we move unsafe from method modifer to wrap the entire method body

Member Modifier Body
Static ctor Always remove Wrap if non-empty
Destructor Always remove Wrap if non-empty
Instance ctor with : base(...) / : this(...) Keep (modifier opens unsafe context for the initializer) Wrap if non-empty
Instance ctor without initializer Remove iff signature has no pointer / function-pointer Wrap if non-empty
Method / local function / operator / conversion operator Remove iff signature has no pointer / function-pointer Wrap if non-empty
Property / indexer Per-accessor decision Per-accessor; trivial field-forwarders (=> _x / => _x = value) left alone
Event with accessors Per-event-type decision Per-accessor wrap (no trivial shortcut)
Event field (event Action E;) Remove iff event type has no pointer n/a (no body)

What it gives up on

Case Why Outcome
Body contains #if / #elif / #else / #endif A multi-TFM dotnet format run produces different active branches per TFM, which makes dotnet format write <<<<<<< Unmerged change markers into the source. Modifier removed , body left alone. Developer wraps manually. (we probably can do a best effort here still)
Body already contains an unsafe { ... } block Presumed already audited. Modifier removed (if removable), no extra wrap.
Auto-property / trivial field-forwarder accessor No unsafe operation possible. Modifier removed only.
Member without explicit unsafe, inheriting from unsafe class C We only see members that carry the unsafe token themselves. Migrating implicit-context members would need data-flow analysis. Out of scope; developer chases the resulting compile errors.
Constructor with pointer expressions in its initializer (: base(deref(p))) Initializer sits outside the body — we can't wrap it. We conservatively keep the unsafe modifier on any ctor that has an initializer at all (initializer is an unsafe context under unsafe-v2 per spec).
Iterator bodies (yield inside) unsafe { yield ... } is CS1629. Best-effort: we still wrap. Developer moves unsafe inwards by hand.
scoped locals assigned to outer parameters Wrapping the whole body in unsafe { ... } puts the local in a narrower scope than the parameter, triggering CS9080. Example: Developer moves the wrap tighter, or hoists the local.
Lambdas / anonymous methods Current Roslyn parser rejects unsafe as a lambda modifier; the analyzer never sees one. Untouched.
Fields with unsafe Out of scope of this fixer. Untouched.

The scoped case:

public void M2(scoped ReadOnlySpan<char> name)
{
    unsafe
    {
        scoped Span<char> tmp;
        tmp = stackalloc char[8];
        name = tmp.Slice(0, 4); // CS9080: Use of variable 'tmp' in this context
                                // may expose referenced variables outside of
                                // their declaration scope
    }
}

(no warning if unsafe is moved back to a method modifier; needs manual rework after the bulk pass)

Notes

  • The analyzer is intentionally idempotent: it suppresses the diagnostic when the fix would be a no-op, so repeated dotnet format runs don't loop.
  • The // SAFETY-TODO: comment wording is provisional — feedback on a better template is welcome.
  • Methods that already have unmanaged pointers in their signature keep the (now caller-unsafe-meaning) modifier, but the body still gets a global unsafe { ... } wrap.
  • This fixer is intended to run once per assembly during the unsafe-v2 migration; the smaller-scope minimization is then up to the developer.

Copilot AI review requested due to automatic review settings May 17, 2026 20:12
@dotnet-policy-service dotnet-policy-service Bot added the linkable-framework Issues associated with delivering a linker friendly framework label May 17, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new ILLink Roslyn analyzer + code fix (IL5005) intended to move the unsafe member modifier into a method-/accessor-scoped unsafe { ... } block, and wires an MSBuild property to enable the analyzer. It also applies the fixer output broadly across System.Text.Json and some shared library code.

Changes:

  • Add IL5005 (UnsafeModifierOnMethod) analyzer and code fix provider, plus associated resource strings and MSBuild property plumbed through ILLink analyzer infrastructure.
  • Update build logic (eng/liveILLink.targets) and System.Text.Json project settings to enable the analyzer.
  • Mechanical rewrites across many BCL files replacing unsafe modifiers with method-body unsafe { ... } blocks and inserting // SAFETY-TODO comments.

Reviewed changes

Copilot reviewed 58 out of 58 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/tools/illink/src/ILLink.Shared/SharedStrings.resx Adds IL5005 title/message strings.
src/tools/illink/src/ILLink.Shared/DiagnosticId.cs Introduces DiagnosticId.UnsafeModifierOnMethod = 5005 (DEBUG-only).
src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeModifierOnMethodAnalyzer.cs New analyzer that reports IL5005 on members using the unsafe modifier (when enabled).
src/tools/illink/src/ILLink.RoslynAnalyzer/MSBuildPropertyOptionNames.cs Adds MSBuild property name constant to enable the new analyzer (DEBUG-only).
src/tools/illink/src/ILLink.RoslynAnalyzer/build/Microsoft.NET.ILLink.Analyzers.props Makes the new MSBuild property compiler-visible.
src/tools/illink/src/ILLink.CodeFix/UnsafeModifierOnMethodCodeFixProvider.cs New code fix to move unsafe into the body and insert audit comments.
src/tools/illink/src/ILLink.CodeFix/Resources.resx Adds the code fix title resource.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.UnsignedNumber.cs Applies fixer output: wraps stackalloc usage in unsafe {} and adds SAFETY-TODO.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.StringSegment.cs Applies fixer output to multiple writer helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.String.cs Applies fixer output to string escaping helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.SignedNumber.cs Applies fixer output for numeric formatting helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Raw.cs Applies fixer output around transcoding + pooled buffer logic.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs Applies fixer output for float formatting paths.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs Applies fixer output for double formatting paths.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Decimal.cs Applies fixer output for decimal formatting paths.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs Applies fixer output in property-name numeric writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs Applies fixer output in property-name numeric writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs Applies fixer output in literal property-name writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs Applies fixer output in Guid property-name writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.FormattedNumber.cs Applies fixer output in formatted-number property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs Applies fixer output in float property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs Applies fixer output in double property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs Applies fixer output in decimal property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs Applies fixer output in DateTimeOffset property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs Applies fixer output in DateTime property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Bytes.cs Applies fixer output in base64 property writers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs Applies fixer output in writer start-property escaping helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.Date.cs Applies fixer output in Date/DateTimeOffset trim formatting helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Writer/JsonWriterHelper.cs Applies fixer output in string quoting/escaping helper.
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs Applies fixer output in stackalloc-based Truncate helper.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs Applies fixer output in UTF-16->UTF-8 transcoding read helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs Applies fixer output in converter read/write helpers using stackalloc / pools.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs Applies fixer output in TimeSpan converter stackalloc paths.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs Applies fixer output in TimeOnly converter stackalloc paths.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs Applies fixer output in converter read/write helpers using stackalloc / pools.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs Applies fixer output in converter read/write helpers and constant parsing.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs Applies fixer output around ValueStringBuilder(stackalloc) usage.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateOnlyConverter.cs Applies fixer output in DateOnly converter stackalloc formatting.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs Applies fixer output around stackalloc + CopyString.
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs Applies fixer output around escape handling / parsing helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs Applies fixer output in multi-segment literal validation helper.
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs Applies fixer output in ValueTextEquals transcoding helper.
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs Applies fixer output across unescaping and base64 helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.netstandard.cs Applies fixer output in netstandard span scanning helper (vectorized path).
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs Applies fixer output in escaped DateTime/Guid parsing helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs Applies fixer output to GetPath, introducing unsafe {} + SAFETY-TODO.
src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Escaping.cs Applies fixer output in escaping helpers using stackalloc/ArrayPool.
src/libraries/System.Text.Json/src/System/Text/Json/JsonEncodedText.cs Applies fixer output in TranscodeAndEncode stackalloc/ArrayPool path.
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.TryGetProperty.cs Applies fixer output in property lookup helpers.
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs Applies fixer output in TextEquals transcoding helper.
src/libraries/System.Text.Json/src/System.Text.Json.csproj Enables the new analyzer via project property.
src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs Applies fixer output to remove unsafe modifier and wrap bodies in unsafe {}.
src/libraries/Common/src/System/Text/AsciiPolyfills.cs Applies fixer output to move unsafe from signature into body.
src/libraries/Common/src/Polyfills/StringBuilderPolyfills.cs Applies fixer output to move unsafe from signature into body.
src/libraries/Common/src/Polyfills/SinglePolyfills.cs Applies fixer output to move unsafe from signature into body.
src/libraries/Common/src/Polyfills/EncodingPolyfills.cs Applies fixer output to move unsafe from signatures into bodies and wrap pointer helpers.
eng/liveILLink.targets Treats the new MSBuild property as requiring live ILLink wiring.

Comment thread src/tools/illink/src/ILLink.CodeFix/UnsafeModifierOnMethodCodeFixProvider.cs Outdated
Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeModifierOnMethodAnalyzer.cs Outdated
Comment thread src/libraries/System.Text.Json/src/System.Text.Json.csproj
Comment thread src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs Outdated
Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeModifierOnMethodAnalyzer.cs Outdated
Copilot AI review requested due to automatic review settings May 17, 2026 21:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 55 out of 55 changed files in this pull request and generated 5 comments.

Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeModifierOnMethodAnalyzer.cs Outdated
Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeModifierOnMethodAnalyzer.cs Outdated
Comment thread src/tools/illink/src/ILLink.CodeFix/UnsafeModifierOnMethodCodeFixProvider.cs Outdated
Comment thread src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/UnsafeModifierOnMethodTests.cs Outdated
Comment thread src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs Outdated
Copilot AI review requested due to automatic review settings May 17, 2026 22:31
@EgorBo EgorBo force-pushed the unsafe-to-scope branch from 6286403 to d9e4f0b Compare May 17, 2026 22:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 59 out of 59 changed files in this pull request and generated 4 comments.

Comment thread src/libraries/System.Text.Json/src/System.Text.Json.csproj
Comment thread src/tools/illink/src/ILLink.Shared/SharedStrings.resx Outdated
@EgorBo EgorBo force-pushed the unsafe-to-scope branch from d9e4f0b to 595e1b5 Compare May 19, 2026 20:10
@EgorBo EgorBo marked this pull request as ready for review May 19, 2026 21:46
@EgorBo EgorBo requested a review from sbomer as a code owner May 19, 2026 21:46
Copilot AI review requested due to automatic review settings May 19, 2026 21:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeV2MigrationAnalyzer.cs Outdated
…r's own output shape

Previously BlockNeedsWrap used body.DescendantNodes() to look for an existing
'unsafe' block anywhere in the body, which incorrectly suppressed wrapping when:
- the outer body contained other top-level unsafe operations alongside a manual
  'unsafe { }' block, or
- a nested local function / lambda happened to contain its own 'unsafe' block.

In either case, removing the method-level 'unsafe' modifier without wrapping the
outer body produces code that no longer compiles. Tighten the skip condition so
we only treat the body as already-wrapped when it's literally
'{ unsafe { ... } }' — the shape the fixer itself emits — preserving idempotence
without false positives.

The same descendant-scan was also in ExpressionBodyNeedsWrap, where it could
only ever match nested lambdas (since an unsafe statement can't be the body of
an arrow expression directly); simplified to always wrap when an expression body
is present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented May 19, 2026

PTAL @agocke @jkotas @jjonescz @tannergooding @333fred
I think this blocks us from adopting the new rules: we don't want to enable them when suddenly thousands of functions where unsafe was put on the modifier level for "enable global unsafe context" become caller-unsafe.

I tested it on STJ and it worked as I expected, it gave up on a few cases (see desc.) but I was able to easily fix those by hand.
If someone wants to make it "smallest possible scope" - feel free to take over, in my opinion it's very hard and adds a lot of mess:

  • we need wrap every pointer dereference and there was an interesting case in @richlander's example on what is the minimal scope for ptr[2] = 0, by definination it should be tmp = ptr + 2; unsafe { *tmp = 0; }
  • In many cases it requires splitting many expressions into separate variable declarations
  • One way or another, it will be audited by a human (or AI?) to fix the // SAFETY comment and adjust the scope

We might need unsafe-as-expression for it then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Text.Json 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