Skip to content
Merged
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 keywords. Restricted type identifiers are also rejected to keep generated Java source compatible when .NET for Android moves to a newer Java source level.

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 @@ -244,6 +244,10 @@ internal static void ValidateJniName (string jniName)
segmentStart = i + 1;
}
}

if (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)) {
throw new ArgumentException ($"JNI name '{jniName}' contains reserved Java identifier '{invalidIdentifier}'.", nameof (jniName));
}
}

/// <summary>
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);
}
129 changes: 129 additions & 0 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/JavaNameValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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 - 1; i++) {
if (JavaKeywords.Contains (segments [i])) {
invalidSegment = segments [i];
return true;
}
}

string typeName = segments [segments.Length - 1];
if (IsInvalidIdentifier (typeName, isTypeName: true)) {
invalidSegment = typeName;
return true;
}

invalidSegment = "";
return false;
}

internal static bool TryGetInvalidJniSourceTypeSegment (string jniName, out string invalidSegment)
{
if (TryGetInvalidJniNameSegment (jniName, out invalidSegment)) {
return true;
}

// '$' becomes '.' when a JNI binary name is emitted as a Java source type reference.
string typeName = jniName.Substring (jniName.LastIndexOf ('/') + 1);
foreach (var segment in typeName.Split ('$')) {
if (IsInvalidIdentifier (segment, isTypeName: true)) {
invalidSegment = segment;
return true;
}
}

return false;
}

internal static bool TryGetInvalidJniTypeSegment (string jniType, out string typeName, out string invalidSegment)
{
int typeStart = 0;
while (typeStart < jniType.Length && jniType [typeStart] == '[') {
typeStart++;
}

if (typeStart < jniType.Length - 1 && jniType [typeStart] == 'L' && jniType [jniType.Length - 1] == ';') {
typeName = jniType.Substring (typeStart + 1, jniType.Length - typeStart - 2);
return TryGetInvalidJniSourceTypeSegment (typeName, out invalidSegment);
}

typeName = "";
invalidSegment = "";
return false;
}

internal static bool TryGetInvalidJavaSourceTypeSegment (string javaType, out string invalidSegment)
{
string typeName = javaType;
while (typeName.EndsWith ("[]", StringComparison.Ordinal)) {
typeName = typeName.Substring (0, typeName.Length - 2);
}
if (typeName is "boolean" or "byte" or "char" or "short" or "int" or "long" or "float" or "double" or "void") {
invalidSegment = "";
return false;
}

var segments = typeName.Split ('.');
for (int i = 0; i < segments.Length; i++) {
bool isTypeName = i == segments.Length - 1;
if (IsInvalidIdentifier (segments [i], isTypeName)) {
invalidSegment = segments [i];
return true;
}
}

invalidSegment = "";
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public TrimmableTypeMapResult Execute (
RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass);
PropagateDeferredRegistrationToBaseClasses (allPeers);
PropagateCannotRegisterToDescendants (allPeers);
if (!ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass)) {
return new TrimmableTypeMapResult ([], [], allPeers);
}

var generatedAssemblies = generateTypeMapAssemblies
? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse)
Expand All @@ -70,6 +73,90 @@ public TrimmableTypeMapResult Execute (
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes);
}

internal bool ValidateJavaNames (IReadOnlyList<JavaPeerInfo> peers, string? applicationJavaClass = null)
{
bool valid = true;
var reportedNames = new HashSet<string> (StringComparer.Ordinal);
if (applicationJavaClass is not null &&
JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (applicationJavaClass, out var invalidApplicationIdentifier)) {
ReportInvalidName (applicationJavaClass, invalidApplicationIdentifier);
}
foreach (var peer in peers) {
if (!ShouldGenerateJcw (peer)) {
continue;
}
ReportInvalidJniName (peer.JavaName);
if (peer.CannotRegisterInStaticConstructor &&
JavaNameValidator.TryGetInvalidJniSourceTypeSegment (peer.JavaName, out var invalidIdentifier)) {
ReportInvalidName (peer.JavaName, invalidIdentifier);
}
if (peer.BaseJavaName is not null) {
ReportInvalidJniSourceType (peer.BaseJavaName);
}
foreach (var interfaceName in peer.ImplementedInterfaceJavaNames) {
ReportInvalidJniSourceType (interfaceName);
}
foreach (var constructor in peer.JavaConstructors) {
ValidateJniSignature (constructor.JniSignature);
}
foreach (var method in peer.MarshalMethods) {
if (!method.IsConstructor) {
ValidateJniSignature (method.JniSignature);
}
if (method.ThrownNames is not null) {
foreach (var thrownName in method.ThrownNames) {
if (JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (thrownName, out invalidIdentifier)) {
ReportInvalidName (thrownName, invalidIdentifier);
}
}
}
}
foreach (var field in peer.JavaFields) {
if (JavaNameValidator.TryGetInvalidJavaSourceTypeSegment (field.JavaTypeName, out invalidIdentifier)) {
ReportInvalidName (field.JavaTypeName, invalidIdentifier);
}
}
}
return valid;

void ValidateJniSignature (string jniSignature)
{
foreach (var parameter in JniSignatureHelper.ParseParameters (jniSignature)) {
ReportInvalidJniType (parameter.JniType);
}
ReportInvalidJniType (JniSignatureHelper.ParseReturnTypeString (jniSignature));
}

void ReportInvalidJniName (string jniName)
{
if (JavaNameValidator.TryGetInvalidJniNameSegment (jniName, out var invalidIdentifier)) {
ReportInvalidName (jniName, invalidIdentifier);
}
}

void ReportInvalidJniSourceType (string jniName)
{
if (JavaNameValidator.TryGetInvalidJniSourceTypeSegment (jniName, out var invalidIdentifier)) {
ReportInvalidName (jniName, invalidIdentifier);
}
}

void ReportInvalidJniType (string jniType)
{
if (JavaNameValidator.TryGetInvalidJniTypeSegment (jniType, out var typeName, out var invalidIdentifier)) {
ReportInvalidName (typeName, invalidIdentifier);
}
}

void ReportInvalidName (string name, string invalidIdentifier)
{
if (reportedNames.Add (name)) {
logger.LogInvalidJavaNameError (name, invalidIdentifier);
valid = false;
}
}
}

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
Loading