Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ff7e772
Use resource designer Type in ResourceIdManager
simonrozsival May 21, 2026
4430cbf
Keep resource designer string attribute compatible
simonrozsival May 21, 2026
55ca233
Move resource designer type lookup into attribute
simonrozsival May 21, 2026
b2fed7e
Return null for resource designer assembly mismatch
simonrozsival May 21, 2026
d026b03
Use RUC for legacy resource designer attribute
simonrozsival May 21, 2026
0d4d5b9
Continue scanning resource designer attributes
simonrozsival May 21, 2026
a12930a
Handle Type resource designer import arguments
simonrozsival May 21, 2026
65be7bc
Keep dummy attribute provider unchanged
simonrozsival May 21, 2026
75e8ee9
Use provider strategy for cleaner separation of concerns
simonrozsival May 21, 2026
c0c8fe1
Decode resource designer attribute argument directly
simonrozsival May 21, 2026
8ed1be6
Explain resource designer attribute decoding
simonrozsival May 21, 2026
cb89b78
Test typeof resource designer imports
simonrozsival May 21, 2026
6a6b180
fixup! Test typeof resource designer imports
simonrozsival May 21, 2026
d3d74d1
Decode resource designer named arguments directly
simonrozsival May 21, 2026
d3b944d
Address resource designer review feedback
simonrozsival May 21, 2026
99877b0
Simplify resource designer attribute decoding
simonrozsival May 22, 2026
c42f89d
Clarify System.Type sentinel in attribute decoder
simonrozsival May 22, 2026
a9ecd4c
Revert "Clarify System.Type sentinel in attribute decoder"
simonrozsival May 22, 2026
f761140
Revert "Simplify resource designer attribute decoding"
simonrozsival May 22, 2026
3b800f3
Expect legacy resource designer warnings in NativeAOT test
simonrozsival May 22, 2026
6776665
Drop the Type constructor in favor of AQN type name constructor
simonrozsival May 22, 2026
7c62539
Use proper AQN and drop Type ctor from ResourceDesignerAttribute
simonrozsival May 22, 2026
0b73eb5
Simplify
simonrozsival May 22, 2026
50316cd
Refactor FullName property and update resource type retrieval
simonrozsival May 22, 2026
d70293f
Merge branch 'main' into copilot/resource-designer-typeof-resource
simonrozsival May 22, 2026
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
30 changes: 30 additions & 0 deletions src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Android.Runtime
{
Expand All @@ -17,5 +18,34 @@ public ResourceDesignerAttribute (
public string FullName { get; set; }

public bool IsApplication { get; set; }

[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
internal Type? GetResourceTypeFromAssembly (Assembly assembly)
{
// Primary scenario: FullName is an assembly-qualified name
if (FullName.IndexOf (',') > 0) {
var resourceType = Type.GetType (FullName);
if (resourceType is not null) {
if (resourceType.Assembly == assembly) {
return resourceType;
} else {
return null; // no need to fallback to the assembly lookup if the type is found but in a different assembly
}
}
}

// Fallback for when the type name is not an assembly-qualified name. If a non-AQN is passed to the constructor,
// the trimmer will report the following warning:
//
// warning IL2122: Type 'XYZ' is not assembly qualified. Type name strings used for dynamically accessing a type should be assembly qualified
//
// Since there is already a build warning, we can suppress the fallback warning.
[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")]
[UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")]
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
static Type? FallbackAssemblyGetType (Assembly a, string name) => a.GetType (name);

return FallbackAssemblyGetType (assembly, FullName);
}
}
}
10 changes: 1 addition & 9 deletions src/Mono.Android/Android.Runtime/ResourceIdManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,9 @@ public static void UpdateIdValues ()
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
static Type? GetResourceTypeFromAssembly (Assembly assembly)
{
const string rootAssembly = "Resources.UpdateIdValues() methods are trimmed away by the LinkResourceDesigner trimmer step. This codepath is not called unless $(AndroidUseDesignerAssembly) is disabled.";

[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = rootAssembly)]
[UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = rootAssembly)]
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
static Type AssemblyGetType (Assembly a, string name) => a.GetType (name);

foreach (var customAttribute in assembly.GetCustomAttributes (typeof (ResourceDesignerAttribute), true)) {
if (customAttribute is ResourceDesignerAttribute resourceDesignerAttribute && resourceDesignerAttribute.IsApplication) {
var type = AssemblyGetType (assembly, resourceDesignerAttribute.FullName);
var type = resourceDesignerAttribute.GetResourceTypeFromAssembly (assembly);
if (type != null)
return type;
}
Comment thread
simonrozsival marked this conversation as resolved.
Expand All @@ -52,4 +45,3 @@ public static void UpdateIdValues ()
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli
{
if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value))
{
designerFullName = attribute.ConstructorArguments[0].Value.ToString ();
designerFullName = ResourceDesignerImportGenerator.GetTypeFullNameFromAssemblyQualifiedName (attribute.ConstructorArguments[0].Value.ToString ());
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class GenerateResourceDesigner : AndroidTask

public string? Namespace { get; set; }

[Required]
public string AssemblyName { get; set; } = "";

[Required]
public string ProjectDir { get; set; } = "";

Expand Down Expand Up @@ -201,7 +204,9 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu
unit.Namespaces.Add (ns);

var resgenatt = new CodeAttributeDeclaration (new CodeTypeReference ("Android.Runtime.ResourceDesignerAttribute", CodeTypeReferenceOptions.GlobalReference));
resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource")));
var resourceTypeName = namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource";
var resourceAssemblyQualifiedName = $"{resourceTypeName}, {AssemblyName}";
resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (resourceAssemblyQualifiedName)));
resgenatt.Arguments.Add (new CodeAttributeArgument ("IsApplication", new CodePrimitiveExpression (IsApplication)));
unit.AssemblyCustomAttributes.Add (resgenatt);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ string[] GetBuildProperties (LocalBuilder builder, AndroidRuntime runtime, bool
Console.WriteLine ("CodeBehindTests: using NativeAOT and running on CI, disabling warnings.");
noWarn.Add ("IL2091");
noWarn.Add ("IL2104");
// Transitive AndroidX bindings can still reference ResourceDesignerAttribute(string)
// with non-AQN type names; this is expected until those libraries rebuild.
noWarn.Add ("IL2122");
noWarn.Add ("IL3053");
noWarn.Add ("XA1040");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)]
[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)]

namespace Foo.Foo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)]
[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)]

namespace Foo.Foo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)]
[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)]

namespace Foo.Foo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ GenerateResourceDesigner CreateTask (string path)
task.UseManagedResourceGenerator = true;
task.DesignTimeBuild = true;
task.Namespace = "Foo.Foo";
task.AssemblyName = "Foo";
task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs");
task.DesignTimeOutputFile = Path.Combine (Root, path, "designtime", "Resource.designer.cs");
task.ProjectDir = Path.Combine (Root, path);
Expand Down Expand Up @@ -460,6 +461,34 @@ public void GenerateDesignerFileFromRtxt ([Values] bool withLibraryReference, [V
Directory.Delete (Path.Combine (Root, path), recursive: true);
}

[Test]
public void ResourceDesignerImportGeneratorHandlesAssemblyQualifiedResourceDesignerAttribute ()
{
var path = Path.Combine ("temp", TestName + " Some Space");
CreateResourceDirectory (path);
var mapTask = CreateCaseMapTask (path);
Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully.");

var libraryPath = Path.Combine (path, "Library");
// BuildLibraryWithResources() generates a library Resource.designer.cs with:
// [assembly: ResourceDesignerAttribute("Library.Resource, Library", IsApplication=false)]
BuildLibraryWithResources (libraryPath, AndroidRuntime.MonoVM);

var task = CreateTask (path);
task.RTxtFile = Path.Combine (Root, path, "R.txt");
File.WriteAllText (task.RTxtFile, Rtxt);
task.References = new TaskItem [] {
new TaskItem (Path.Combine (Root, libraryPath, "bin", "Debug", "Library.dll"))
};

Assert.IsTrue (task.Execute (), "Task should have executed successfully.");
var designer = File.ReadAllText (task.NetResgenOutputFile);
// The import generator can only emit this assignment if it decoded the
// assembly-qualified attribute argument back to "Library.Resource".
StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer);
Directory.Delete (Path.Combine (Root, path), recursive: true);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Testing — Good targeted test for the AQN round-trip! Consider also adding a negative/edge-case assertion that a library with the old non-AQN format (e.g., "Library.Resource" without assembly name) still works, to confirm backward compatibility in the same test or a sibling test. The existing GenerateDesignerFileFromRtxt test may already cover this, but an explicit assertion would make the compat guarantee visible.

Rule: Test edge cases

[Test]
public void GenerateDesignerFileFromEmptyRtxt ()
{
Expand Down Expand Up @@ -489,6 +518,7 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt
task.UseManagedResourceGenerator = true;
task.DesignTimeBuild = true;
task.Namespace = "Foo.Foo";
task.AssemblyName = "Foo";
task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs");
task.ProjectDir = Path.Combine (Root, path);
task.ResourceDirectory = Path.Combine (Root, path, "res") + Path.DirectorySeparatorChar;
Expand Down Expand Up @@ -607,6 +637,7 @@ public void CompareAapt2AndManagedParserOutput ()
task.UseManagedResourceGenerator = true;
task.DesignTimeBuild = false;
task.Namespace = "MonoAndroidApplication4.MonoAndroidApplication4";
task.AssemblyName = "MonoAndroidApplication4";
task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.aapt2.cs");
task.ProjectDir = Path.Combine (Root, path);
task.CaseMapFile = Path.Combine (Root, path, "case_map.txt");
Expand Down Expand Up @@ -726,6 +757,7 @@ int styleable ElevenAttributes_attr09 9
task.UseManagedResourceGenerator = true;
task.DesignTimeBuild = true;
task.Namespace = "Foo.Foo";
task.AssemblyName = "Foo";
task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs");
task.ProjectDir = Path.Combine (Root, path);
task.CaseMapFile = Path.Combine (Root, path, "case_map.txt");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ public void CreateImportMethods (IEnumerable<ITaskItem> libraries)

string? GetResourceDesignerClass (MetadataReader reader)
{
// Looking for:
// [assembly: Android.Runtime.ResourceDesignerAttribute("MyApp.Resource", IsApplication=true)]
// Looking for library assemblies:
// [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource, MyLibrary", IsApplication=false)]
// [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource", IsApplication=false)]

var assembly = reader.GetAssemblyDefinition ();
foreach (var handle in assembly.GetCustomAttributes ()) {
Expand All @@ -98,12 +99,25 @@ public void CreateImportMethods (IEnumerable<ITaskItem> libraries)
return null;
}
}
return (string?) values.FixedArguments.First ().Value;
var typeName = (string?) values.FixedArguments.First ().Value;
return GetTypeFullNameFromAssemblyQualifiedName (typeName);
}
}
return null;
}

internal static string? GetTypeFullNameFromAssemblyQualifiedName (string? typeName)
{
if (typeName is null)
return typeName;

int assemblySeparator = typeName.IndexOf (',');
if (assemblySeparator < 0)
return typeName;

return typeName.Substring (0, assemblySeparator).Trim ();
}

void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias)
{
var typeName = reader.GetString (type.Name);
Expand All @@ -127,4 +141,3 @@ void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMem
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ because xbuild doesn't support framework reference assemblies.
JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)"
RTxtFile="$(IntermediateOutputPath)R.txt"
Namespace="$(AndroidResgenNamespace)"
AssemblyName="$(AssemblyName)"
ProjectDir="$(ProjectDir)"
Resources="@(AndroidResource);@(AndroidBoundLayout)"
ResourceDirectory="$(MonoAndroidResourcePrefix)"
Expand Down Expand Up @@ -1301,6 +1302,7 @@ because xbuild doesn't support framework reference assemblies.
JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)"
RTxtFile="$(IntermediateOutputPath)R.txt"
Namespace="$(AndroidResgenNamespace)"
AssemblyName="$(AssemblyName)"
ProjectDir="$(ProjectDir)"
Resources="@(_AndroidResourceDest)"
ResourceDirectory="$(MonoAndroidResDirIntermediate)"
Expand Down