[Microsoft.Android.Build.BaseTasks] Guard LogCodedError overloads#12093
Conversation
PR #11994 fixed the XA0145 emulator boot-timeout message rendering literal `{0}`/`{1}` placeholders instead of substituting the values. The root cause was overload resolution: a call like LogCodedError ("XA0145", Resources.XA0145, Device, BootTimeoutSeconds); binds to `LogCodedError (code, message, string? file, int lineNumber)` rather than the `params object[]` formatting overload, because a non-`params` method that matches the trailing `(string, int)` exactly is always preferred. The message was logged verbatim and `Device` / `BootTimeoutSeconds` were silently reinterpreted as a source file and line number. This makes the whole class of bug a compile error instead of a silent runtime failure: * Extract the real logging body into private `LogCodedErrorCore` / `LogCodedWarningCore` sinks; every public overload now delegates to them, so nothing routes through the trap overload internally. * Mark the `(code, message, string? file, int lineNumber)` overloads on both `LogCodedError` and `LogCodedWarning` `[Obsolete (error: true)]`, with a message pointing callers at the formatting overload or the file-first overload. Any accidental `(string, int)` tail now fails to build with CS0619. * Add a `FormatMessage` helper that returns the message verbatim when there are no arguments, so pre-formatted messages containing literal `{`/`}` are never passed through `string.Format`. * Migrate the two intentional `Aapt2` callers to the file-first overload `LogCodedError (code, file, lineNumber, message)`. The `Log`/`log` extension-method versions in `MSBuildExtensions` were never affected: that overload set has no ambiguous trailing overload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f03fd34e-2c48-48fe-a2af-b42ed2b4905c
There was a problem hiding this comment.
Pull request overview
This PR hardens Microsoft.Android.Build.BaseTasks.AsyncTask logging APIs against accidental overload resolution that can silently treat (string, int) formatting arguments as (file, lineNumber), preventing message placeholders from being substituted and misattributing source locations.
Changes:
- Refactors
AsyncTaskto route allLogCodedError/LogCodedWarningoverloads through private core sinks and marks the ambiguous(code, message, file, lineNumber)overloads as[Obsolete(error: true)]to force compile-time failures for the trap pattern. - Adds a
FormatMessagehelper to avoidstring.Formatwhen no args are provided, preventingFormatExceptionfor pre-formatted messages containing literal{/}. - Updates
Aapt2to use the file-first overload for the intentional file/line diagnostics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Xamarin.Android.Build.Tasks/Tasks/Aapt2.cs | Migrates coded-error logging to the file-first overload to avoid the ambiguous (message, file, line) shape. |
| external/xamarin-android-tools/src/Microsoft.Android.Build.BaseTasks/AsyncTask.cs | Introduces core logging sinks, compile-time guards for trap overloads, and safe message formatting behavior. |
What about the
|
Why this is necessary
#11994 fixed the
XA0145emulator boot-timeout error rendering literal{0}/{1}placeholders instead of the AVD name and timeout. The underlying cause was overload resolution, and it can bite any coded-error call whose trailing arguments happen to be(string, int):This binds to
LogCodedError (code, message, string? file, int lineNumber)instead of theparams object[]formatting overload, because a non-paramsmethod that matches the(string, int)tail exactly is always preferred. The result: the message is logged verbatim (placeholders never substituted) andDevice/BootTimeoutSecondsare silently reinterpreted as a source file and line number. #11994 patched the one call site; this PR turns the whole class of bug into a build break.Approach
LogCodedErrorCore/LogCodedWarningCore. Every public overload now delegates to them, so no overload routes through the trap internally.LogCodedError (code, message, string? file, int lineNumber)and itsLogCodedWarningtwin are now[Obsolete (error: true)]with a message steering callers to either the formatting overload (code, message, params object[]) or the file-first overload (code, file, lineNumber, message, ...). Any accidental(string, int)tail now fails to compile withCS0619.FormatMessagehelper returns the message verbatim when there are no args, so pre-formatted messages containing literal{/}(e.g. rawaapt2output) are never run throughstring.Format.Aapt2's two file+line calls now use the file-first overload.Notes for reviewers
Log/logextension-method versions inMSBuildExtensionswere never affected; that overload set has no ambiguous trailing overload, so nothing there changed.AsyncTaskcaller in the repo: onlyBootAndroidEmulator(already fixed in Fix XA0145 emulator boot-timeout message rendering literal placeholders #11994) andAapt2used the trap shape. Everything else passes a trailingstring/object or has theintin a non-trap position.Microsoft.Android.Build.BaseTasksclean and compiling a probe against the built assembly: the(string, int)tail now errorsCS0619, whilestring.Format, file-first, and literal-brace forms all compile.Unit tests
No new tests. The behavior is enforced at compile time (
CS0619), and the runtime substitution is already covered by theBootAndroidEmulatorTestsadded in #11994.