Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Documentation/docs-mobile/TOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@
href: messages/xa4248.md
- name: XA4249
href: messages/xa4249.md
- name: XA4258
href: messages/xa4258.md
- name: XA4301
href: messages/xa4301.md
- name: XA4302
Expand Down
1 change: 1 addition & 0 deletions Documentation/docs-mobile/messages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
+ [XA4254](xa4254.md): Trimmable type map Java source input directory '{input}' and output directory '{output}' must be different.
+ [XA4255](xa4255.md): Generated trimmable type map Java source '{path}' was not found.
+ [XA4256](xa4256.md): Skipping Java peer type '{type}' from assembly '{assembly}' because referenced type '{referencedType}' from assembly '{referencedAssembly}' could not be resolved in '{path}'. This type will not be included in the trimmable type map.
+ [XA4258](xa4258.md): Java name '{name}' contains reserved Java identifier '{identifier}'. Change the package or type name.
+ XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI.
+ [XA4301](xa4301.md): Apk already contains the item `xxx`.
+ [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex}
Expand Down
25 changes: 25 additions & 0 deletions Documentation/docs-mobile/messages/xa4258.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: .NET for Android error XA4258
description: XA4258 error code
ms.date: 08/07/2026
f1_keywords:
- "XA4258"
---

# .NET for Android error XA4258

## Example message

```text
error XA4258: Java name 'com.example.for' contains reserved Java identifier 'for'. Change the package or type name.
```

## Issue

A Java package or type name contains a Java keyword or restricted type identifier. Java does not provide a way to escape these identifiers, so the generated Java source cannot be compiled.

This can originate from the `$(ApplicationId)` MSBuild property, the `package` attribute in `AndroidManifest.xml`, a managed type name, or an explicit Java name supplied by an attribute such as `[Register]` or `[JniTypeSignature]`.

## Solution

Change the package or type name so no segment is a Java keyword. For a type name, also avoid restricted type identifiers such as `record`.
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ internal static void ValidateJniName (string jniName)
}
}

string segment = jniName.Substring (segmentStart, i - segmentStart);
bool isTypeName = i == jniName.Length;
if (JavaNameValidator.IsInvalidIdentifier (segment, isTypeName)) {
throw new ArgumentException ($"JNI name '{jniName}' contains reserved Java identifier '{segment}'.", nameof (jniName));
}

segmentStart = i + 1;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ void LogUnresolvableJavaPeerSkippedWarning (
string unresolvedAssemblyName,
string unresolvedAssemblyPath);
void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName);
void LogInvalidJavaNameError (string javaName, string invalidIdentifier);
void LogCustomJavaObjectError (string managedTypeName);
void LogCustomJavaObjectWarning (string managedTypeName);
}
65 changes: 65 additions & 0 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

internal static class JavaNameValidator
{
// Java SE 21 reserved keywords and literals:
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-3.9
static readonly HashSet<string> JavaKeywords = new (StringComparer.Ordinal) {
"_",
"abstract", "assert", "boolean", "break", "byte",
"case", "catch", "char", "class", "const", "continue",
"default", "do", "double",
"else", "enum", "extends",
"false", "final", "finally", "float", "for",
"goto",
"if", "implements", "import", "instanceof", "int", "interface",
"long",
"native", "new", "null",
"package", "private", "protected", "public",
"return",
"short", "static", "strictfp", "super", "switch", "synchronized",
"this", "throw", "throws", "transient", "true", "try",
"void", "volatile",
"while",
};

// TypeIdentifier additionally excludes these contextual keywords:
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-TypeIdentifier
static readonly HashSet<string> RestrictedTypeIdentifiers = new (StringComparer.Ordinal) {
"permits", "record", "sealed", "var", "yield",
};

internal static bool IsInvalidIdentifier (string identifier, bool isTypeName) =>
JavaKeywords.Contains (identifier) || isTypeName && RestrictedTypeIdentifiers.Contains (identifier);

internal static bool TryGetInvalidPackageSegment (string packageName, char separator, out string invalidSegment)
{
foreach (var segment in packageName.Split (separator)) {
if (JavaKeywords.Contains (segment)) {
invalidSegment = segment;
return true;
}
}

invalidSegment = "";
return false;
}

internal static bool TryGetInvalidJniNameSegment (string jniName, out string invalidSegment)
{
var segments = jniName.Split ('/');
for (int i = 0; i < segments.Length; i++) {
string segment = segments [i];
if (IsInvalidIdentifier (segment, i == segments.Length - 1)) {
invalidSegment = segment;
return true;
}
}

invalidSegment = "";
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public TrimmableTypeMapResult Execute (
logger.LogNoJavaPeerTypesFound ();
return new TrimmableTypeMapResult ([], [], allPeers);
}
if (!ValidateJavaNames (allPeers)) {
return new TrimmableTypeMapResult ([], [], allPeers);
}
MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames);

RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass);
Expand All @@ -70,6 +73,19 @@ public TrimmableTypeMapResult Execute (
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes);
}

internal bool ValidateJavaNames (IReadOnlyList<JavaPeerInfo> peers)
{
bool valid = true;
var reportedNames = new HashSet<string> (StringComparer.Ordinal);
foreach (var peer in peers) {
if (JavaNameValidator.TryGetInvalidJniNameSegment (peer.JavaName, out var invalidIdentifier) && reportedNames.Add (peer.JavaName)) {
logger.LogInvalidJavaNameError (peer.JavaName, invalidIdentifier);
valid = false;
}
}
return valid;
}

internal static List<string> CollectApplicationRegistrationTypes (List<JavaPeerInfo> allPeers)
{
var appRegTypes = new List<string> ();
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,12 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS
{2} - Fully-qualified managed referenced type name that could not be resolved
{3} - Assembly expected to contain the unresolved type
{4} - Full path to the resolved assembly file that was expected to contain the unresolved type</comment>
</data>
<data name="XA4258" xml:space="preserve">
<value>Java name '{0}' contains reserved Java identifier '{1}'. Change the package or type name.</value>
<comment>The following are literal names and should not be translated: Java.
{0} - Java package or type name
{1} - Java reserved keyword or restricted identifier</comment>
</data>
<data name="XA0142" xml:space="preserve">
<value>Command '{0}' failed.\n{1}</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public void LogUnresolvableJavaPeerSkippedWarning (
log.LogCodedWarning ("XA4257", Properties.Resources.XA4257, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath);
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName);
public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) =>
log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier);
public void LogCustomJavaObjectError (string managedTypeName) =>
log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}");
public void LogCustomJavaObjectWarning (string managedTypeName) =>
Expand Down Expand Up @@ -220,6 +222,9 @@ public override bool RunTask ()
packageNamingPolicy: PackageNamingPolicy,
generateTypeMapAssemblies: GenerateTypeMapAssemblies,
errorOnCustomJavaObject: ErrorOnCustomJavaObject);
if (Log.HasLoggedErrors) {
return false;
}

if (GenerateTypeMapAssemblies) {
GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using System.Linq;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Android.Sdk.TrimmableTypeMap;
using Xamarin.Android.Tools;
using Microsoft.Android.Build.Tasks;

Expand Down Expand Up @@ -70,6 +71,10 @@ public override bool RunTask ()
PackageName = AndroidAppManifest.CanonicalizePackageName (AssemblyName);
}

if (JavaNameValidator.TryGetInvalidPackageSegment (PackageName, '.', out var invalidIdentifier)) {
Log.LogCodedError ("XA4258", Properties.Resources.XA4258, PackageName, invalidIdentifier);
}

Log.LogDebugMessage ($" PackageName: {PackageName}");

return !Log.HasLoggedErrors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using Microsoft.Build.Framework;
using NUnit.Framework;
using Xamarin.Android.Tasks;

namespace Xamarin.Android.Build.Tests {

[TestFixture]
[Parallelizable (ParallelScope.Children)]
public class GetAndroidPackageNameTests {

[TestCase ("com.example.for", "for")]
[TestCase ("com.class.example", "class")]
[TestCase ("com.example.true", "true")]
public void ReservedJavaIdentifier_FailsWithXA4258 (string packageName, string invalidIdentifier)
{
var errors = new List<BuildErrorEventArgs> ();
var task = new GetAndroidPackageName {
BuildEngine = new MockBuildEngine (TestContext.Out, errors),
AssemblyName = "Example",
PackageName = packageName,
};

Assert.IsFalse (task.Execute ());
Assert.That (errors, Has.Exactly (1).Matches<BuildErrorEventArgs> (error =>
error.Code == "XA4258" &&
error.Message.Contains (packageName) &&
error.Message.Contains (invalidIdentifier)));
}

[Test]
public void ContextualKeywordInPackage_Succeeds ()
{
var errors = new List<BuildErrorEventArgs> ();
var task = new GetAndroidPackageName {
BuildEngine = new MockBuildEngine (TestContext.Out, errors),
AssemblyName = "Example",
PackageName = "com.example.record",
};

Assert.IsTrue (task.Execute ());
Assert.IsEmpty (errors);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
<Compile Include="..\..\external\Java.Interop\src\utils\NullableAttributes.cs">
<Link>Utilities\NullableAttributes.cs</Link>
</Compile>
<Compile Include="..\Microsoft.Android.Sdk.TrimmableTypeMap\JavaNameValidator.cs">
<Link>Utilities\JavaNameValidator.cs</Link>
</Compile>
<Compile Include="..\Mono.Android\\Android.App\UsesLibraryAttribute.cs">
<Link>Mono.Android\UsesLibraryAttribute.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using Xunit;

namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;

public class JavaNameValidatorTests
{
public static IEnumerable<object []> ReservedIdentifiers {
get {
string [] identifiers = [
"_",
"abstract", "assert", "boolean", "break", "byte",
"case", "catch", "char", "class", "const", "continue",
"default", "do", "double",
"else", "enum", "extends",
"false", "final", "finally", "float", "for",
"goto",
"if", "implements", "import", "instanceof", "int", "interface",
"long",
"native", "new", "null",
"package", "private", "protected", "public",
"return",
"short", "static", "strictfp", "super", "switch", "synchronized",
"this", "throw", "throws", "transient", "true", "try",
"void", "volatile",
"while",
];

foreach (var identifier in identifiers) {
yield return [identifier];
}
}
}

public static IEnumerable<object []> RestrictedTypeIdentifiers {
get {
yield return ["permits"];
yield return ["record"];
yield return ["sealed"];
yield return ["var"];
yield return ["yield"];
}
}

[Theory]
[MemberData (nameof (ReservedIdentifiers))]
public void TryGetInvalidPackageSegment_ReservedIdentifier_ReturnsTrue (string identifier)
{
Assert.True (JavaNameValidator.TryGetInvalidPackageSegment ($"com.{identifier}.example", '.', out var actual));
Assert.Equal (identifier, actual);
}

[Theory]
[MemberData (nameof (ReservedIdentifiers))]
public void TryGetInvalidJniNameSegment_ReservedPackageIdentifier_ReturnsTrue (string identifier)
{
Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/{identifier}/Example", out var actual));
Assert.Equal (identifier, actual);
}

[Theory]
[MemberData (nameof (ReservedIdentifiers))]
public void TryGetInvalidJniNameSegment_ReservedTypeIdentifier_ReturnsTrue (string identifier)
{
Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/example/{identifier}", out var actual));
Assert.Equal (identifier, actual);
}

[Theory]
[MemberData (nameof (RestrictedTypeIdentifiers))]
public void RestrictedTypeIdentifier_IsValidInPackageButInvalidAsType (string identifier)
{
Assert.False (JavaNameValidator.TryGetInvalidPackageSegment ($"com.example.{identifier}", '.', out var packageIdentifier));
Assert.Equal ("", packageIdentifier);
Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/{identifier}/Example", out var jniPackageIdentifier));
Assert.Equal ("", jniPackageIdentifier);
Assert.True (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/example/{identifier}", out var typeIdentifier));
Assert.Equal (identifier, typeIdentifier);
}

[Theory]
[InlineData ("module")]
[InlineData ("open")]
[InlineData ("requires")]
[InlineData ("exports")]
[InlineData ("opens")]
[InlineData ("to")]
[InlineData ("uses")]
[InlineData ("provides")]
[InlineData ("with")]
[InlineData ("transitive")]
public void ModuleContextualKeyword_IsValidIdentifier (string identifier)
{
Assert.False (JavaNameValidator.TryGetInvalidPackageSegment ($"com.example.{identifier}", '.', out _));
Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/{identifier}/Example", out _));
Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment ($"com/example/{identifier}", out _));
}

[Fact]
public void ValidNames_ReturnFalseAndEmptyIdentifier ()
{
Assert.False (JavaNameValidator.TryGetInvalidPackageSegment ("com.example.app", '.', out var packageIdentifier));
Assert.Equal ("", packageIdentifier);
Assert.False (JavaNameValidator.TryGetInvalidJniNameSegment ("com/example/MainActivity", out var jniIdentifier));
Assert.Equal ("", jniIdentifier);
}
}
Loading
Loading