Skip to content

Controls: Remove Regex from ShadowTypeConverter - #38615

Merged
kubaflo merged 6 commits into
inflight/currentfrom
simonrozsival-remove-shadow-regex
Sep 18, 2026
Merged

kubaflo merged 6 commits into
inflight/currentfrom
simonrozsival-remove-shadow-regex

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Root Cause

ShadowTypeConverter constructs a runtime Regex whenever it parses a shadow string. That reference contributes to retaining System.Text.RegularExpressions and additional regex implementation code in trimmed MAUI Android apps.

Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked System.Text.RegularExpressions assembly from MAUI apps that do not use regular expressions in their own application logic. Removing the ShadowTypeConverter root is not sufficient by itself because other framework and dependency roots still retain regex code.

Related work toward that goal includes:

Additional roots may still need to be removed or made trimmable before System.Text.RegularExpressions disappears entirely from a given app. For example, the measured sample-content app also reaches regex through CommunityToolkit.Maui, System.Data.Common, and System.Private.Xml.

Description of Change

Replaces the runtime Regex with an allocation-conscious inline tokenizer/parser.

The parser intentionally preserves the existing unanchored Regex.Matches behavior, including supported color forms, invariant-culture decimal/scientific numbers, Unicode whitespace, ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated Span<Part>, and only matched tokens are materialized as strings for the existing color and numeric parsers.

The project now also bans the System.Text.RegularExpressions namespace through a Controls.Core-specific BannedSymbols.txt, preventing this dependency from being reintroduced.

Performance considerations

Vectorizing the character scans was considered but intentionally not implemented. Shadow strings are normally short (roughly tens of characters) and whitespace runs are generally zero to two characters, so SIMD setup and branching are unlikely to beat the straightforward scalar loops. A vectorized path would also require separate netstandard fallbacks and careful tables to preserve the old regex's Unicode \s and \d behavior, adding disproportionate complexity without demonstrated benefit.

Tests

  • Added focused coverage for every supported color syntax, offsets/radius/opacity, whitespace, malformed values, invariant culture, and legacy regex edge cases.
  • ShadowTests: 85 passed.
  • Controls.Core builds for netstandard2.0, netstandard2.1, and net10.0.
  • Verified an intentional System.Text.RegularExpressions.Regex usage in Controls.Core fails with RS0030.

Android size measurement

Published identical stock .NET 11 preview 7 dotnet new maui --sample-content apps in Release with TrimMode=partial, replacing only the Microsoft.Maui.Controls.dll input in the patched build.

Artifact Baseline Patched Delta
Signed APK 36,903,788 B 36,899,692 B -4,096 B
Unsigned APK 36,774,978 B 36,771,221 B -3,757 B
Linked Microsoft.Maui.Controls.dll (per ABI) 1,495,552 B 1,495,040 B -512 B
Linked System.Text.RegularExpressions.dll (per ABI) 334,336 B 330,752 B -3,584 B
arm64 assembly store 7,689,840 B 7,688,216 B -1,624 B
x64 assembly store 7,899,160 B 7,896,448 B -2,712 B

System.Text.RegularExpressions remains retained by other roots in this sample: CommunityToolkit.Maui, Microsoft.Maui (LocalhostRegex), System.Data.Common, and System.Private.Xml. This change removes the Microsoft.Maui.Controls reference and reduces the linked regex implementation, but does not eliminate the assembly from this app.

Issues Fixed

None.

Replace runtime Regex tokenization with an allocation-conscious inline parser while preserving the existing accepted grammar and legacy tokenization behavior. Add focused compatibility tests for colors, numbers, whitespace, malformed input, and invariant culture.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 07:18
@simonrozsival
simonrozsival deployed to copilot-pat-pool September 17, 2026 07:18 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 38615

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 38615"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@simonrozsival simonrozsival changed the title Remove Regex dependency from ShadowTypeConverter Controls: Remove Regex from ShadowTypeConverter Sep 17, 2026
@simonrozsival
simonrozsival deployed to copilot-pat-pool September 17, 2026 07:18 — with GitHub Actions Active
@simonrozsival
simonrozsival deployed to copilot-pat-pool September 17, 2026 07:19 — with GitHub Actions Active
simonrozsival and others added 4 commits September 17, 2026 09:20
Inline invariant-culture float parsing and remove the unnecessary helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use char.IsAsciiLetter on modern targets while retaining the equivalent netstandard fallback.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use char.IsAsciiHexDigit on modern targets while retaining the equivalent netstandard fallback.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace paired integer slots with a small stack-allocated Part value type for clearer token handling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

Copilot review overview

🟡 Changes recommended

Two moderate parser issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Replaces runtime regex parsing in ShadowTypeConverter with an inline tokenizer and expands shadow parsing tests.

Changes:

  • Adds manual color and numeric tokenization.
  • Expands coverage for valid, malformed, whitespace, and culture-specific inputs.
  • Reports reduced linked assembly size.
  • Numeric parsing still allocates substrings, and whitespace handling differs from the previous regex behavior.
File Summary
src/​Controls/​src/​Core/​ShadowTypeConverter.cs Replaces regex parsing; requires span-based numeric parsing and regex-compatible whitespace handling.
src/​Controls/​tests/​Core.UnitTests/​ShadowTests.cs Adds comprehensive parser and edge-case coverage.

Comment thread src/Controls/src/Core/ShadowTypeConverter.cs
Add a project-specific banned API rule to prevent System.Text.RegularExpressions from being reintroduced into Microsoft.Maui.Controls.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 17, 2026 07:28

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.

Copilot review overview

🔵 Needs a closer look

Whitespace handling does not fully preserve the previous regex behavior and requires correction with regression coverage.

Review effort: Lite
Findings: 1 Medium severity

Open (1)

@kubaflo

This comment has been minimized.

MauiBot

This comment was marked as outdated.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) and removed s/agent-review-in-progress AI review is currently running for this PR labels Sep 17, 2026
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Note

🔍 /review started

AzDO build 15379551 is running for android.

The s/agent-review-in-progress label stays on this PR while the run is active. The final recommendation and outcome labels are posted only after Gate, expert review, and Deep UI tests finish.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@simonrozsival — new AI review results are available based on commit 7834a02.

Gate Partial Confidence Low Platform Android


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: ANDROID · Base: main · Merge base: 49b62062

🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.

Test Without Fix (expect FAIL) With Fix (expect PASS)
🧪 ShadowTests ShadowTests ❌ PASS — 224s ✅ PASS — 124s
🔴 Without fix — 🧪 ShadowTests: PASS ❌ · 224s

(no coded error found; showing last 1200 chars)

s]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "rgba(100%, 0%, 0%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsla(120, 100%, 50%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "rgb(255, 0, 0) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "AliceBlue 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsva(120, 100%, 50%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsv(120, 100%, 50%) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "rgba(100%, 32%, 64%, 0.27) 4 4") [< 1 ms]
[xUnit.net 00:00:02.80]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "hsl(6, 20%, 45%)\n1\t5") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "rgb(\t6,\r\n201, 198) 4 4") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "hsva(6, 20%, 45%, 0.75)\r\n6 3") [< 1 ms]
  Passed ShadowInitializesCorrectly [7 ms]
Test Run Successful.
Total tests: 85
     Passed: 85
 Total time: 3.6758 Seconds
🟢 With fix — 🧪 ShadowTests: PASS ✅ · 124s

(no coded error found; showing last 1200 chars)

]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "rgba(100%, 0%, 0%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsla(120, 100%, 50%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "rgb(255, 0, 0) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "AliceBlue 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsva(120, 100%, 50%, 0.5) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsSupportedColorSyntax(value: "hsv(120, 100%, 50%) 1 2") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "rgba(100%, 32%, 64%, 0.27) 4 4") [< 1 ms]
[xUnit.net 00:00:02.64]   Finished:    Microsoft.Maui.Controls.Core.UnitTests
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "hsl(6, 20%, 45%)\n1\t5") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "rgb(\t6,\r\n201, 198) 4 4") [< 1 ms]
  Passed ShadowTypeConverter_AcceptsRegexWhitespace(value: "hsva(6, 20%, 45%, 0.75)\r\n6 3") [< 1 ms]
  Passed ShadowInitializesCorrectly [17 ms]
Test Run Successful.
Total tests: 85
     Passed: 85
 Total time: 3.3422 Seconds

⚠️ Failure Details

  • ❌ ShadowTests PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (2 files)
  • src/Controls/src/Core/Controls.Core.csproj
  • src/Controls/src/Core/ShadowTypeConverter.cs

New files (not reverted):

  • src/Controls/src/Core/BannedSymbols.txt

📋 Pre-Flight — Context & Validation

PR #38615 Pre-Flight

Context

  • Title: Controls: Remove Regex from ShadowTypeConverter
  • PR: #38615
  • Base / head: main / simonrozsival-remove-shadow-regex
  • Author: simonrozsival
  • Problem: ShadowTypeConverter constructs a runtime Regex for every conversion. That reference roots System.Text.RegularExpressions implementation code in trimmed MAUI Android applications. The goal is to remove the Controls dependency on regular expressions while preserving the converter's existing parsing behavior.
  • Linked issue: None.
  • Platform: Android.

Direct Diff Inspection

The PR changes four files:

  1. src/Controls/src/Core/ShadowTypeConverter.cs replaces the runtime, unanchored Regex.Matches implementation with an allocation-conscious tokenizer. It scans a ReadOnlySpan<char>, records at most five stack-allocated token boundaries, and has dedicated readers for numbers, hex colors, RGB/RGBA, HSL/HSLA, HSV/HSVA, and named colors.
  2. src/Controls/src/Core/Controls.Core.csproj registers a project-specific banned-symbols file.
  3. src/Controls/src/Core/BannedSymbols.txt is a new file banning System.Text.RegularExpressions.
  4. src/Controls/tests/Core.UnitTests/ShadowTests.cs expands the converter regression matrix for supported color forms, invariant numeric parsing, Unicode/regex whitespace, permissive punctuation, malformed input, and legacy unanchored/greedy regex edge cases.

The current approach is specifically a scalar, hand-written compatibility parser with stack-allocated token metadata. Alternative candidates must use a different mechanism/root-cause strategy rather than merely rearranging these helper methods.

Gate and Test Scope

  • The prior gate is authoritative and must not be rerun.
  • Gate result: FAILED because ShadowTests passed without the fix as well as with it. This is expected for a compatibility suite and does not directly measure the trimming/dependency objective.
  • Detected primary regression test: all ShadowTests in src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj.
  • Targeted command: dotnet test src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj --filter "FullyQualifiedName~ShadowTests"
  • No additional mandatory regression tests were supplied in the STEP 5a request; no full suite is permitted.

Try-Fix Boundary

The production target files are:

  • src/Controls/src/Core/ShadowTypeConverter.cs
  • src/Controls/src/Core/Controls.Core.csproj
  • src/Controls/src/Core/BannedSymbols.txt

BannedSymbols.txt is new in the PR. The try-fix restoration contract requires an attempt to report Blocked before editing whenever .github/.baseline-state.json contains any NewFiles; each child must enforce that boundary and still capture artifacts and run the exact restore script. The existing public API is unchanged.


🔬 Code Review — Deep Analysis

Code Review — PR #38615

Independent Assessment

What this changes: Replaces ShadowTypeConverter's per-conversion, unanchored Regex.Matches tokenizer with a bounded span-based scanner, wires a Controls.Core banned-symbol rule for System.Text.RegularExpressions, and expands compatibility tests.

Inferred motivation: Remove a regex reachability root and its linked-size cost from trimmed MAUI applications without changing the converter's permissive legacy token extraction.

Reconciliation with PR Narrative

Author claims: The PR removes the Controls regex root, preserves legacy parsing behavior, adds a project-specific dependency ban and compatibility tests, and reduces linked Android output size.

Agreement/disagreement: The submitted diff supports the implementation and compatibility claims. The externally measured Android size results were not independently reproduced.

Prior Review Reconciliation

No prior ❌ Error findings were found across top-level reviews, inline review comments, or PR issue comments. A prior inline suggestion about per-token string materialization is an accepted non-blocking tradeoff: the old implementation also materialized Match.Value, and removing regex reachability does not require cross-target span parsing.

Blast Radius Assessment

  • Runs for all instances: The converter implementation changes for every string conversion, but only when ShadowTypeConverter is invoked.
  • Startup impact: None; no startup registration or static initialization was added.
  • Static/shared state: None.
  • Null/default state: Null retains the existing ArgumentNullException path; incomplete token sets are rejected.

CI Status

  • Required-check result: Unavailable. gh pr checks 38615 --repo dotnet/maui --required returned no rows because GitHub CLI authentication is unavailable.
  • Trusted Gate result: FAILED because all 85 ShadowTests passed both without and with the production fix.
  • Classification: The tests establish parser compatibility but do not discriminate the regex-dependency objective; this is an evidence gap, not a with-fix parser regression.
  • Action taken: The Gate was not rerun, and confidence is capped at low.

Findings

No actionable file:line findings. The expert reviewer verified the submitted parser against the former alternative ordering, greedy hexadecimal matching, incomplete decimal/exponent rollback, Unicode whitespace and digit handling, ignored punctuation, exception normalization, analyzer wiring, and focused tests.

Failure-Mode Probing

  • Inputs without shadow syntax: The scanner either finds no supported token sequence or downstream color/number conversion rejects it with the existing InvalidOperationException contract.
  • More than five recognized tokens: Tokenize returns a count greater than five without indexing beyond the stack-allocated span, so conversion rejects the input.
  • Malformed token prefixes: Failed structured-color readers fall back to the same unanchored scanning behavior rather than consuming partial structured tokens as valid colors.
  • Null/default and lifecycle state: The converter is stateless apart from its existing color converter; this change adds no startup path, shared mutable state, subscriptions, handlers, or platform-view dependencies.

External Output Contract

Not applicable.

Verdict: NEEDS_DISCUSSION

Confidence: low

Summary: The raw submitted implementation is a sound, allocation-conscious replacement for the prior runtime regex, and the expert pass produced [] in inline-findings.json; no reviewer refinement is warranted from code findings. Approval is not supported by this run because required-check status is unavailable and the trusted Gate does not prove the dependency-removal objective.


🛠️ Try-Fix — Analysis & Comparison

Try-Fix Aggregate

Candidate 1 — Generated Regex Matcher

  • Model: gpt-5.3-codex
  • Approach: Replace the per-conversion new Regex(...) with a cached source-generated [GeneratedRegex] matcher. This retains regex-driven, unanchored Matches token discovery rather than duplicating regex behavior in the PR's scalar ReadOnlySpan<char> tokenizer.
  • Mechanism-level difference: The PR removes regex through a custom lexical parser. This candidate would preserve the regex engine's matching semantics but move pattern construction to compile time and cache the matcher.
  • Production diff: None.
  • Test result: Blocked before testing.
  • Failure analysis: EstablishBrokenBaseline.ps1 recorded the PR-added src/Controls/src/Core/BannedSymbols.txt in a non-empty NewFiles array. The try-fix restoration contract therefore prohibited all production edits. The targeted ShadowTests command was not run.
  • Self-review: 0 findings.
  • Restore: The exact script-only restore command completed.
  • Detailed narrative: ../try-fix-1/content.md

Candidate 1 is complete and must not be reopened or rerun. Candidate 2 must avoid both the PR's hand-written span tokenizer and this source-generated/cached-regex strategy.

Candidate 2 — Table-Driven DFA

  • Model: gpt-5.6-sol
  • Approach: Replace regex matching with a generic, table-driven deterministic finite automaton whose transitions and actions reproduce the ordered, greedy, unanchored token discovery rules.
  • Mechanism-level difference: Unlike the PR's syntax-specific span readers, this candidate would interpret generic transition/action tables. Unlike Candidate 1, it would not use or retain System.Text.RegularExpressions; matching would be performed entirely by the DFA runtime.
  • Production diff: None.
  • Test result: Blocked before testing.
  • Failure analysis: EstablishBrokenBaseline.ps1 again recorded the PR-added src/Controls/src/Core/BannedSymbols.txt in a non-empty NewFiles array. The try-fix restoration contract prohibited production edits, so the targeted ShadowTests command was not run and an empty test log was captured.
  • Self-review: 0 findings.
  • Restore: The exact script-only restore completed with Restored True; target files were reported clean.
  • Detailed narrative: ../try-fix-2/content.md

Aggregate Outcome

Two candidates were attempted in the required order and no more candidates were launched. Both are distinct from the PR and from each other, but neither could be implemented or empirically tested because the try-fix baseline safety contract cannot restore PR-added production files. Consequently, STEP 5a has two Blocked candidates, zero passing candidates, and no candidate production diff for STEP 5b to select.


🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr

The submitted PR is the single best candidate. It is the only candidate with a complete implementation, its 85 focused ShadowTests pass with the fix, the expert reviewer found no actionable correctness issue, and the regression cross-reference is clean. pr-plus-reviewer is byte-for-byte equivalent because the expert review produced no feedback to apply; selecting that no-op variant would misrepresent it as an improvement.

Approval is nevertheless blocked by the trusted Gate. The same 85 tests also pass without the production fix, so they validate compatibility but do not demonstrate the dependency-removal regression. A focused check that fails when Controls.Core references System.Text.RegularExpressions and passes when that root is removed is still needed before approval.

Comparative ranking

Rank Candidate Implementation Targeted regression evidence Expert assessment Result
1 pr Complete span-based tokenizer plus banned-symbol wiring 85/85 pass with fix, but also pass without fix; trusted Gate FAILED LGTM, medium confidence, 0 inline findings Winner, but not approvable under the Gate
2 pr-plus-reviewer Identical to pr; reviewer.patch is empty Not rerun because there was no reviewer change No actionable feedback to apply Sound but provides no advantage over pr
3 try-fix-1 Proposed cached/source-generated regex only; no production diff Blocked, command not run 0 findings over an absent candidate diff Unevaluated and retains the regex mechanism/root
4 try-fix-2 Proposed table-driven DFA only; no production diff Blocked, command not run 0 findings over an absent candidate diff Unevaluated and substantially more complex

No candidate failed the targeted tests with its fix applied. The two STEP 5a candidates rank below the implemented candidates because they were blocked before editing and have no regression result at all; the Gate failure on pr/pr-plus-reviewer is specifically a non-discriminating-test failure, not a with-fix test regression.

Why the raw PR wins

  • It directly removes System.Text.RegularExpressions from ShadowTypeConverter rather than merely caching a regex.
  • Its bounded stack allocation and span scans preserve the old unanchored token extraction without introducing shared state.
  • Its compatibility matrix exercises color alternatives, Unicode whitespace, invariant/scientific numbers, ignored punctuation, greedy hex matching, and incomplete-token rollback.
  • The expert pass found no concrete parser, exception, analyzer-wiring, performance, or test-matrix defect.
  • The DFA alternative is only a design sketch and would add disproportionate machinery for this small grammar.

Required change before approval

Add or identify a focused regression check for the actual objective—for example, a deterministic analyzer/build assertion that fails when Controls.Core references the banned regex namespace and passes at the submitted HEAD. Do not rely solely on behavioral compatibility tests, because preserving parser behavior necessarily lets those tests pass against the old regex implementation.


📱 UI Tests — Shadow

Detected UI test categories: Shadow

✅ Deep UI tests — 21 passed, 0 failed, 1 skipped across 1 category on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Shadow 21/22 (1 skipped) ✓ —
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Sep 18, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current September 18, 2026 22:34
@kubaflo
kubaflo merged commit 5f4ff5a into inflight/current Sep 18, 2026
34 checks passed
@kubaflo
kubaflo deleted the simonrozsival-remove-shadow-regex branch September 18, 2026 22:34
@github-actions github-actions Bot added this to the .NET 10 SR12 milestone Sep 18, 2026
kubaflo pushed a commit that referenced this pull request Sep 21, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Sep 22, 2026
## Summary

- create XML marshalling metadata references lazily in generated `[Export]` dispatch assemblies
- avoid adding `System.Xml.ReaderWriter` to a generated type-map assembly when exported signatures do not use XML parser mappings
- preserve the existing XML marshalling references when `XmlPullParser` or `XmlResourceParser` mappings are used

## APK size and combined MAUI validation

The PR is draft because the stock MAUI `sample-content` application does not currently demonstrate an APK-size or packaged-assembly reduction from this change.

The first stock .NET 11 Android CoreCLR Release comparison produced an identical signed APK before and after this change:

| Build | Signed APK size |
|---|---:|
| Baseline | 23,300,175 bytes |
| Patched | 23,300,175 bytes |
| Difference | **0 bytes** |

Three independent MAUI trimming fixes were then applied together:

- dotnet/maui#38613 trims unused runtime XAML loader/parser code
- dotnet/maui#38615 removes `ShadowTypeConverter`'s regex dependency
- dotnet/maui#38616 trims disabled MAUI Aspire support and its generated localhost regex

The combined MAUI changes reduced the sample-content signed APK by 81,920 bytes, removed `XamlLoader`, `XamlParser`, and `LocalhostRegex`, and reduced the retained XML/regex implementations. However, `System.Xml.ReaderWriter` remains rooted directly by source-generated XAML line-info calls in the application, and `System.Private.Xml` remains transitively and through additional framework/control references.

To exercise this PR's actual code path, the identical combined-MAUI input was then published twice with the trimmable Android type map explicitly enabled:

```sh
dotnet publish CombinedTrimApp.csproj \
  -c Release -f net11.0-android -r android-arm64 \
  -p:AndroidKeyStore=false \
  -p:PublishReadyToRun=false \
  -p:TrimMode=partial \
  -p:_AndroidTypeMapImplementation=trimmable \
  -p:AndroidPackageFormats=apk
```

| Output | Baseline generator | Patched generator | Difference |
|---|---:|---:|---:|
| Signed APK | 21,248,079 B | 21,248,079 B | **0 B** |
| Unsigned APK | 21,118,994 B | 21,119,021 B | +27 B packaging noise |
| Generated type-map DLLs | 11,133,952 B | 11,133,952 B | **0 B** |
| `System.Xml.ReaderWriter.dll` | 5,120 B | 5,120 B | **0 B** |
| `System.Private.Xml.dll` | 381,440 B | 381,440 B | **0 B** |

All 86 generated type-map DLLs were byte-identical, and none had a `System.Xml.ReaderWriter` assembly reference. The sample-content app and its dependencies do not contain a non-XML `[Export]` dispatcher that triggers the bug fixed here. Its remaining `System.Xml.ReaderWriter` reference comes from generated XAML `InitializeComponent` line-info services (`System.Xml.IXmlLineInfo`), outside the Android type-map generator.

Therefore this change is valid generator cleanup and is covered by focused tests, but it does **not** currently reduce the stock MAUI sample-content APK or remove its XML assemblies. The benefit applies only to an app/library that generates a non-XML `[Export]` dispatcher and has no independent XML roots.

## Tests

- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore --nologo -v:minimal`
- targeted `TypeMapAssemblyGeneratorTests.Generate_Export*` coverage
kubaflo pushed a commit that referenced this pull request Sep 27, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Sep 27, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Oct 3, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Oct 3, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Oct 6, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Oct 6, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Root Cause

`ShadowTypeConverter` constructs a runtime `Regex` whenever it parses a
shadow string. That reference contributes to retaining
`System.Text.RegularExpressions` and additional regex implementation
code in trimmed MAUI Android apps.

### Broader goal and related work

This PR is one step toward removing the roughly 300 KB linked
`System.Text.RegularExpressions` assembly from MAUI apps that do not use
regular expressions in their own application logic. Removing the
`ShadowTypeConverter` root is not sufficient by itself because other
framework and dependency roots still retain regex code.

Related work toward that goal includes:

- #38616 trims disabled MAUI Aspire support, including
`Microsoft.Maui`'s source-generated `LocalhostRegex`, when the Aspire
feature switch is off.
- dotnet/android#12813 avoids unnecessary XML marshalling references in
trimmable type-map dispatch assemblies. This helps remove XML assemblies
where they are not otherwise needed; `System.Private.Xml` contains a
generated `LanguageRegex` implementation that can independently retain
regex code.

Additional roots may still need to be removed or made trimmable before
`System.Text.RegularExpressions` disappears entirely from a given app.
For example, the measured sample-content app also reaches regex through
`CommunityToolkit.Maui`, `System.Data.Common`, and `System.Private.Xml`.

### Description of Change

Replaces the runtime `Regex` with an allocation-conscious inline
tokenizer/parser.

The parser intentionally preserves the existing unanchored
`Regex.Matches` behavior, including supported color forms,
invariant-culture decimal/scientific numbers, Unicode whitespace,
ignored punctuation, greedy 3-to-8 digit hex matching, and rollback for
incomplete decimals or exponents.

Token boundaries are stored in a stack-allocated `Span<Part>`, and only
matched tokens are materialized as strings for the existing color and
numeric parsers.

The project now also bans the `System.Text.RegularExpressions` namespace
through a `Controls.Core`-specific `BannedSymbols.txt`, preventing this
dependency from being reintroduced.

### Performance considerations

Vectorizing the character scans was considered but intentionally not
implemented. Shadow strings are normally short (roughly tens of
characters) and whitespace runs are generally zero to two characters, so
SIMD setup and branching are unlikely to beat the straightforward scalar
loops. A vectorized path would also require separate `netstandard`
fallbacks and careful tables to preserve the old regex's Unicode `\s`
and `\d` behavior, adding disproportionate complexity without
demonstrated benefit.

### Tests

- Added focused coverage for every supported color syntax,
offsets/radius/opacity, whitespace, malformed values, invariant culture,
and legacy regex edge cases.
- `ShadowTests`: 85 passed.
- `Controls.Core` builds for `netstandard2.0`, `netstandard2.1`, and
`net10.0`.
- Verified an intentional `System.Text.RegularExpressions.Regex` usage
in `Controls.Core` fails with RS0030.

### Android size measurement

Published identical stock `.NET 11 preview 7` `dotnet new maui
--sample-content` apps in Release with `TrimMode=partial`, replacing
only the `Microsoft.Maui.Controls.dll` input in the patched build.

| Artifact | Baseline | Patched | Delta |
|---|---:|---:|---:|
| Signed APK | 36,903,788 B | 36,899,692 B | **-4,096 B** |
| Unsigned APK | 36,774,978 B | 36,771,221 B | **-3,757 B** |
| Linked `Microsoft.Maui.Controls.dll` (per ABI) | 1,495,552 B |
1,495,040 B | **-512 B** |
| Linked `System.Text.RegularExpressions.dll` (per ABI) | 334,336 B |
330,752 B | **-3,584 B** |
| arm64 assembly store | 7,689,840 B | 7,688,216 B | **-1,624 B** |
| x64 assembly store | 7,899,160 B | 7,896,448 B | **-2,712 B** |

`System.Text.RegularExpressions` remains retained by other roots in this
sample: `CommunityToolkit.Maui`, `Microsoft.Maui` (`LocalhostRegex`),
`System.Data.Common`, and `System.Private.Xml`. This change removes the
`Microsoft.Maui.Controls` reference and reduces the linked regex
implementation, but does not eliminate the assembly from this app.

### Issues Fixed

None.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants