diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 4129d338fff..6a06ea74c05 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -330,6 +330,14 @@ href: messages/xa4249.md - name: XA4258 href: messages/xa4258.md + - name: XA4259 + href: messages/xa4259.md + - name: XA4260 + href: messages/xa4260.md + - name: XA4261 + href: messages/xa4261.md + - name: XA4262 + href: messages/xa4262.md - name: XA4263 href: messages/xa4263.md - name: XA4301 diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index f50638f2f54..ae126efcf04 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -228,6 +228,10 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [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. ++ [XA4259](xa4259.md): Type '{type}' has multiple managed constructors that map to the same Java Native Interface (JNI) signature '{signature}'. ++ [XA4260](xa4260.md): Type '{type}' has a constructor parameter type '{parameterType}' that cannot be represented in a Java constructor. ++ [XA4261](xa4261.md): Type '{type}' has Java constructor signature '{signature}', but its base type does not expose a compatible Java constructor. ++ [XA4262](xa4262.md): Type '{type}' has an invalid [Export] SuperArgumentsString value '{value}'. + [XA4263](xa4263.md): The exported member '{member}' has unsupported signature type '{type}'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. diff --git a/Documentation/docs-mobile/messages/xa4259.md b/Documentation/docs-mobile/messages/xa4259.md new file mode 100644 index 00000000000..8981f2b2faa --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4259.md @@ -0,0 +1,23 @@ +--- +title: .NET for Android error XA4259 +description: XA4259 error code +ms.date: 08/28/2026 +f1_keywords: + - "XA4259" +--- + +# .NET for Android error XA4259 + +## Example message + +```text +error XA4259: Type 'Example.MyActivity' has multiple managed constructors that map to the same Java Native Interface (JNI) signature '(I)V'. Change the constructor parameter types so each constructor has a unique JNI signature. +``` + +## Issue + +Two managed constructor parameter lists collapse to the same JNI signature. For example, `int` and `uint` both map to Java `int`, and two managed binding types can alias the same Java type. + +## Solution + +Remove one constructor or change its parameters so every Java-visible constructor has a unique JNI signature. diff --git a/Documentation/docs-mobile/messages/xa4260.md b/Documentation/docs-mobile/messages/xa4260.md new file mode 100644 index 00000000000..fb2be996c27 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4260.md @@ -0,0 +1,23 @@ +--- +title: .NET for Android error XA4260 +description: XA4260 error code +ms.date: 08/28/2026 +f1_keywords: + - "XA4260" +--- + +# .NET for Android error XA4260 + +## Example message + +```text +error XA4260: Type 'Example.MyActivity' has a constructor parameter type 'System.Int32&' that cannot be represented in a Java constructor. Remove the constructor or change the parameter to a supported Java type. +``` + +## Issue + +Java callable wrappers cannot represent generic parameters or instantiations, by-reference types, pointers, function pointers, or rectangular arrays in constructor signatures. + +## Solution + +Remove the Java-visible constructor or use primitive, string, bound Java object, or single-dimensional array parameters. diff --git a/Documentation/docs-mobile/messages/xa4261.md b/Documentation/docs-mobile/messages/xa4261.md new file mode 100644 index 00000000000..f6bc6cf821b --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4261.md @@ -0,0 +1,23 @@ +--- +title: .NET for Android error XA4261 +description: XA4261 error code +ms.date: 08/28/2026 +f1_keywords: + - "XA4261" +--- + +# .NET for Android error XA4261 + +## Example message + +```text +error XA4261: Type 'Example.MyActivity' has Java constructor signature '(Ljava/lang/String;)V', but its base type does not expose a compatible Java constructor. Add a compatible base constructor or remove the derived constructor. +``` + +## Issue + +The generated Java constructor must invoke a Java constructor on its base type. No compatible base constructor or parameterless fallback is available. + +## Solution + +Expose a compatible registered constructor on the base type, add a parameterless base constructor, or remove the derived constructor. diff --git a/Documentation/docs-mobile/messages/xa4262.md b/Documentation/docs-mobile/messages/xa4262.md new file mode 100644 index 00000000000..c08a4dd731e --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4262.md @@ -0,0 +1,23 @@ +--- +title: .NET for Android error XA4262 +description: XA4262 error code +ms.date: 08/28/2026 +f1_keywords: + - "XA4262" +--- + +# .NET for Android error XA4262 + +## Example message + +```text +error XA4262: Type 'Example.MyActivity' has an invalid [Export] SuperArgumentsString value 'p1'. The value references a constructor parameter that does not exist. +``` + +## Issue + +`SuperArgumentsString` references a generated Java constructor parameter outside the available `p0`, `p1`, and subsequent parameter range. + +## Solution + +Correct the parameter reference or remove `SuperArgumentsString` to forward all constructor parameters to the Java base constructor. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index edd36592f83..87dc265bcd1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -27,6 +27,10 @@ void LogUnresolvableJavaPeerSkippedWarning ( void LogExportFieldOnGenericTypeError (); void LogExportFieldReturnsVoidError (); void LogUnsupportedExportSignatureError (string memberName, string managedTypeName); + void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature); + void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType); + void LogMissingBaseConstructorError (string managedTypeName, string jniSignature); + void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 114856936f9..007f86559bd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -120,6 +120,12 @@ public sealed record JavaPeerInfo /// public IReadOnlyList JavaConstructors { get; init; } = []; + /// + /// Constructor shapes which cannot be emitted safely by the trimmable type map. + /// Generation reports these before producing any Java or type-map output. + /// + public IReadOnlyList ConstructorDiagnostics { get; init; } = []; + /// /// Java fields from [ExportField] attributes. /// Each field is initialized by calling the annotated method. @@ -384,6 +390,20 @@ public sealed record JavaConstructorInfo public IReadOnlyList Annotations { get; init; } = []; } +public sealed record ConstructorDiagnosticInfo +{ + public required ConstructorDiagnosticKind Kind { get; init; } + public required string Detail { get; init; } +} + +public enum ConstructorDiagnosticKind +{ + AmbiguousJniSignature, + UnsupportedParameterType, + MissingBaseConstructor, + InvalidSuperArgumentsString, +} + /// /// Describes a Java field from an [ExportField] attribute. /// The field is initialized by calling the annotated method. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index de15953a39d..a69e2839ba7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -395,8 +395,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // - Interface types don't implement other interfaces' methods in JCWs List? marshalMethods = null; List? exportFields = null; + var rejectedExportConstructors = new HashSet (); if (!doNotGenerateAcw || collectMarshalMethodsForNonAcw) { - (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); + (marshalMethods, exportFields, rejectedExportConstructors) = + CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); } // Resolve activation constructor @@ -433,6 +435,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, MarshalMethods = marshalMethods ?? [], JavaConstructors = marshalMethods is not null ? BuildJavaConstructors (marshalMethods, typeDef, index) : [], + ConstructorDiagnostics = doNotGenerateAcw || marshalMethods is null + ? [] + : AnalyzeConstructorDiagnostics (typeDef, index, marshalMethods, rejectedExportConstructors), JavaFields = exportFields ?? [], ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, @@ -683,11 +688,15 @@ static void SkipArrayShape (ref BlobReader reader) } } - (List, List) CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index, bool detectBaseOverrides) + (List, List, HashSet) CollectMarshalMethods ( + TypeDefinition typeDef, + AssemblyIndex index, + bool detectBaseOverrides) { var methods = new List (); var fields = new List (); HashSet? registeredMethodKeys = detectBaseOverrides ? new (StringComparer.Ordinal) : null; + var rejectedExportConstructors = new HashSet (); bool isGenericType = typeDef.GetGenericParameters ().Count > 0; // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them @@ -702,6 +711,9 @@ static void SkipArrayShape (ref BlobReader reader) continue; } if (!ValidateExportSignature (methodDef, index, isGenericType)) { + if (methodName == ".ctor") { + rejectedExportConstructors.Add (methodHandle); + } continue; } @@ -765,7 +777,7 @@ static void SkipArrayShape (ref BlobReader reader) CollectBaseConstructorChain (typeDef, index, methods); } - return (methods, fields); + return (methods, fields, rejectedExportConstructors); } static bool IsExportFieldAttribute (CustomAttribute attribute, AssemblyIndex index) @@ -845,14 +857,9 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b ? declaringTypeName + methodName : $"{declaringTypeName}.{methodName}"; - for (int i = 0; i < sig.ParameterTypes.Length; i++) { - if (isConstructor && IsOwnedByConstructorDiagnostics (sig.ParameterTypes [i])) { - continue; - } - if (!HasExportSignatureMapping (sig.ParameterTypes [i], parameterKinds [i])) { - logger?.LogUnsupportedExportSignatureError (memberName, sig.ParameterTypes [i].DisplayName); - return false; - } + if (TryGetUnsupportedExportParameterType (sig.ParameterTypes, parameterKinds, isConstructor, out var unsupportedType)) { + logger?.LogUnsupportedExportSignatureError (memberName, unsupportedType); + return false; } if (isConstructor) { return true; @@ -864,6 +871,26 @@ bool ValidateExportSignature (MethodDefinition methodDef, AssemblyIndex index, b return true; } + bool TryGetUnsupportedExportParameterType ( + IReadOnlyList parameterTypes, + IReadOnlyList parameterKinds, + bool isConstructor, + out string unsupportedType) + { + for (int i = 0; i < parameterTypes.Count; i++) { + if (isConstructor && IsOwnedByConstructorDiagnostics (parameterTypes [i])) { + continue; + } + if (!HasExportSignatureMapping (parameterTypes [i], parameterKinds [i])) { + unsupportedType = parameterTypes [i].DisplayName; + return true; + } + } + + unsupportedType = ""; + return false; + } + /// /// PR #12567 owns XA4260 for signature shapes that no Java constructor can represent. /// This layer only adds XA4263 for unresolved types and invalid [ExportParameter] pairs. @@ -1202,7 +1229,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // then delegates to nctor_N(...) which handles the args on the managed side. // This matches legacy CecilImporter behavior (CecilImporter.cs:394-397). if (hasParameterlessBaseCtor) { - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); var jniSignature = BuildJniCtorSignature (sig); if (jniSignature is not null && !alreadyRegisteredSignatures.Contains (jniSignature)) { methods.Add (new MarshalMethodInfo { @@ -1220,7 +1247,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, } } - string? BuildJniCtorSignature (MethodSignature sig) + string? BuildJniCtorSignature (MethodSignature sig) { var sb = new System.Text.StringBuilder (); sb.Append ('('); @@ -1229,7 +1256,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // System.Object, System.Action, etc.). ManagedTypeToJniDescriptor maps // these to "Ljava/lang/Object;" by default, but legacy would reject the // whole ctor. Use the nullable variant to match legacy behavior. - var jniType = ManagedTypeToJniDescriptorOrNull (param); + var jniType = TryGetConstructorJniDescriptor (param); if (jniType is null) { return null; } @@ -1881,39 +1908,40 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { + RegisterInfo? explicitRegisterInfo = null; + RegisterInfo? exportRegisterInfo = null; exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); var attrName = index.GetCustomAttributeName (ca); - if (attrName == "RegisterAttribute") { - registerInfo = index.ParseRegisterAttribute (ca); - return true; + if (attrName == "RegisterAttribute" && explicitRegisterInfo is null) { + explicitRegisterInfo = index.ParseRegisterAttribute (ca); + continue; } - if (IsExportAttribute (ca, index)) { - (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); - return true; + if (IsExportAttribute (ca, index) && exportInfo is null) { + (exportRegisterInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + continue; } - if (IsExportFieldAttribute (ca, index)) { - (registerInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); - return true; + if (IsExportFieldAttribute (ca, index) && exportInfo is null) { + (exportRegisterInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); + continue; } // JI-style constructor registration: [JniConstructorSignature("()V")] // Single arg = JNI signature; name is always ".ctor", connector is empty. - if (attrName == "JniConstructorSignatureAttribute") { + if (attrName == "JniConstructorSignatureAttribute" && explicitRegisterInfo is null) { var value = index.DecodeAttribute (ca); var jniSignature = value.FixedArguments.Length > 0 ? (string?)value.FixedArguments [0].Value : null; if (jniSignature is not null) { - registerInfo = new RegisterInfo { JniName = ".ctor", Signature = jniSignature, Connector = "", DoNotGenerateAcw = false }; - return true; + explicitRegisterInfo = new RegisterInfo { JniName = ".ctor", Signature = jniSignature, Connector = "", DoNotGenerateAcw = false }; } } } - registerInfo = null; - return false; + registerInfo = explicitRegisterInfo ?? exportRegisterInfo; + return registerInfo is not null; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) @@ -2139,7 +2167,7 @@ bool TryManagedTypeToJniDescriptor ( return false; } - var primitive = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); + var primitive = TryGetPrimitiveJniDescriptor (managedType); if (primitive is not null) { descriptor = primitive; return true; @@ -2245,6 +2273,21 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para }; } + string? TryGetPrimitiveJniDescriptor (TypeRefData managedType) + { + var descriptor = TryGetPrimitiveJniDescriptor (managedType.ManagedTypeName); + return descriptor is not null && + IsSpecialManagedType ( + managedType, + managedType.ManagedTypeName, + "System.Runtime", + "System.Private.CoreLib", + "mscorlib", + "netstandard") + ? descriptor + : null; + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index, TypeRefData? currentTypeRef = null) { var cacheKey = (currentTypeRef?.DisplayName ?? typeName, index.AssemblyName); @@ -2292,7 +2335,7 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para return null; } - static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) + ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) { foreach (var methodHandle in typeDef.GetMethods ()) { var method = index.Reader.GetMethodDefinition (methodHandle); @@ -2301,59 +2344,16 @@ bool IsSpecialManagedType (TypeRefData managedType, string managedTypeName, para continue; } - var signature = index.Reader.GetBlobReader (method.Signature); - var header = signature.ReadSignatureHeader (); - if (header.IsGeneric) { - signature.ReadCompressedInteger (); - } - if (signature.ReadCompressedInteger () != 2 || - (SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.Void) { - continue; - } - - // XI style: (IntPtr, JniHandleOwnership) - var firstParameter = signature; - if ((SignatureTypeCode) signature.ReadByte () == SignatureTypeCode.IntPtr && - IsSignatureType (ref signature, index, "Android.Runtime", "JniHandleOwnership")) { - return ActivationCtorStyle.XamarinAndroid; - } - - // JI style: (ref JniObjectReference, JniObjectReferenceOptions) - signature = firstParameter; - if ((SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.ByReference) { - signature = firstParameter; - } - if (IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReference") && - IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReferenceOptions")) { - return ActivationCtorStyle.JavaInterop; + var sig = method.DecodeSignature (index.TypeRefSignatureProvider, index); + var style = GetActivationConstructorStyle (sig.ParameterTypes); + if (style is not null) { + return style; } } return null; } - static bool IsSignatureType (ref BlobReader signature, AssemblyIndex index, string typeNamespace, string typeName) - { - var kind = (SignatureTypeKind) signature.ReadByte (); - if (kind is not (SignatureTypeKind.Class or SignatureTypeKind.ValueType)) { - return false; - } - - var handle = signature.ReadTypeHandle (); - switch (handle.Kind) { - case HandleKind.TypeReference: - var typeRef = index.Reader.GetTypeReference ((TypeReferenceHandle) handle); - return index.Reader.StringComparer.Equals (typeRef.Namespace, typeNamespace) && - index.Reader.StringComparer.Equals (typeRef.Name, typeName); - case HandleKind.TypeDefinition: - var typeDef = index.Reader.GetTypeDefinition ((TypeDefinitionHandle) handle); - return index.Reader.StringComparer.Equals (typeDef.Namespace, typeNamespace) && - index.Reader.StringComparer.Equals (typeDef.Name, typeName); - default: - return false; - } - } - /// /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying /// type's (fullName, assemblyName) by reading the raw signature blob. @@ -2832,6 +2832,271 @@ List GetSupportedPublicConstructors (TypeDefinition typeD return constructors; } + IReadOnlyList AnalyzeConstructorDiagnostics ( + TypeDefinition typeDef, + AssemblyIndex index, + IReadOnlyList marshalMethods, + HashSet rejectedExportConstructors) + { + var diagnostics = new List (); + var signatures = new Dictionary> (StringComparer.Ordinal); + var reportedAmbiguousSignatures = new HashSet (StringComparer.Ordinal); + var baseConstructors = CollectBaseRegisteredCtors (typeDef, index); + bool hasParameterlessBaseConstructor = baseConstructors.Any (c => c.RegisterInfo.Signature == "()V"); + + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if ((methodDef.Attributes & MethodAttributes.Static) != 0 || + index.Reader.GetString (methodDef.Name) != ".ctor") { + continue; + } + if (rejectedExportConstructors.Contains (methodHandle)) { + continue; + } + + bool hasExplicitRegistration = TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo); + if ((methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public && + !hasExplicitRegistration) { + continue; + } + + var signature = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + if (IsActivationConstructor (signature.ParameterTypes)) { + continue; + } + string jniSignature; + string unsupportedParameterType; + if (registerInfo?.Signature is string registeredSignature) { + if (exportInfo is not null && + !TryValidateConstructorParameterShapes (signature.ParameterTypes, out unsupportedParameterType)) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.UnsupportedParameterType, + Detail = unsupportedParameterType, + }); + continue; + } + jniSignature = registeredSignature; + } else if (!TryBuildConstructorJniSignature (signature.ParameterTypes, out jniSignature, out unsupportedParameterType)) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.UnsupportedParameterType, + Detail = unsupportedParameterType, + }); + continue; + } + + if (signatures.TryGetValue (jniSignature, out var existingManagedSignature) && + !existingManagedSignature.SequenceEqual (signature.ParameterTypes) && + reportedAmbiguousSignatures.Add (jniSignature)) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.AmbiguousJniSignature, + Detail = jniSignature, + }); + } else { + signatures [jniSignature] = signature.ParameterTypes; + } + + if (exportInfo?.SuperArgumentsString is string superArgumentsString && + superArgumentsString.Length > 0) { + continue; + } + + string forwardedBaseSignature = exportInfo?.SuperArgumentsString == "" ? "()V" : jniSignature; + bool hasCompatibleBaseConstructor = baseConstructors.Any (baseCtor => + baseCtor.RegisterInfo.Signature is string baseJniSignature + ? baseJniSignature == forwardedBaseSignature + : HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)); + bool canUseParameterlessBaseConstructor = !hasExplicitRegistration && hasParameterlessBaseConstructor; + if (baseConstructors.Count > 0 && !hasCompatibleBaseConstructor && !canUseParameterlessBaseConstructor) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.MissingBaseConstructor, + Detail = jniSignature, + }); + } + } + + foreach (var constructor in marshalMethods.Where (m => m.IsConstructor && m.SuperArgumentsString is not null)) { + if (!HasValidSuperArgumentReferences (constructor.SuperArgumentsString ?? "", JniSignatureHelper.ParseParameters (constructor.JniSignature).Count)) { + diagnostics.Add (new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.InvalidSuperArgumentsString, + Detail = constructor.SuperArgumentsString ?? "", + }); + } + } + + return diagnostics; + } + + bool TryBuildConstructorJniSignature ( + IReadOnlyList parameterTypes, + out string jniSignature, + out string unsupportedParameterType) + { + if (!TryValidateConstructorParameterShapes (parameterTypes, out unsupportedParameterType)) { + jniSignature = ""; + return false; + } + + var descriptors = new List (parameterTypes.Count); + foreach (var parameterType in parameterTypes) { + var descriptor = TryGetConstructorJniDescriptor (parameterType); + if (descriptor is null) { + jniSignature = ""; + unsupportedParameterType = parameterType.DisplayName; + return false; + } + descriptors.Add (descriptor); + } + + jniSignature = $"({string.Join ("", descriptors)})V"; + unsupportedParameterType = ""; + return true; + } + + static bool TryValidateConstructorParameterShapes ( + IReadOnlyList parameterTypes, + out string unsupportedParameterType) + { + foreach (var parameterType in parameterTypes) { + if (IsOwnedByConstructorDiagnostics (parameterType)) { + unsupportedParameterType = parameterType.DisplayName; + return false; + } + } + + unsupportedParameterType = ""; + return true; + } + + string? TryGetConstructorJniDescriptor (TypeRefData parameterType) + { + if (parameterType.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + var elementType = parameterType with { + ManagedTypeName = parameterType.ManagedTypeName.Substring (0, parameterType.ManagedTypeName.Length - 2), + }; + var elementDescriptor = TryGetConstructorJniDescriptor (elementType); + return elementDescriptor is null ? null : $"[{elementDescriptor}"; + } + + var primitiveDescriptor = TryGetPrimitiveJniDescriptor (parameterType); + if (primitiveDescriptor is not null) { + return primitiveDescriptor; + } + + var enumDescriptor = TryResolveEnumUnderlyingDescriptor (parameterType.ManagedTypeName, parameterType.AssemblyName); + if (enumDescriptor is not null) { + return enumDescriptor; + } + + return TryResolveJniObjectDescriptor (parameterType); + } + + bool IsActivationConstructor (IReadOnlyList parameterTypes) + => GetActivationConstructorStyle (parameterTypes) is not null; + + ActivationCtorStyle? GetActivationConstructorStyle (IReadOnlyList parameterTypes) + { + if (parameterTypes.Count != 2) { + return null; + } + + bool isJniObjectReference = parameterTypes [0].ManagedTypeName is + "Java.Interop.JniObjectReference" or "Java.Interop.JniObjectReference&"; + var jniObjectReference = parameterTypes [0] with { + ManagedTypeName = "Java.Interop.JniObjectReference", + }; + if (IsSpecialManagedType (parameterTypes [0], "System.IntPtr", "System.Runtime", "System.Private.CoreLib", "mscorlib") && + IsActivationFrameworkType (parameterTypes [1], "Android.Runtime.JniHandleOwnership", "Mono.Android")) { + return ActivationCtorStyle.XamarinAndroid; + } + if (isJniObjectReference && + IsActivationFrameworkType (jniObjectReference, "Java.Interop.JniObjectReference", "Java.Interop") && + IsActivationFrameworkType (parameterTypes [1], "Java.Interop.JniObjectReferenceOptions", "Java.Interop")) { + return ActivationCtorStyle.JavaInterop; + } + return null; + } + + bool IsActivationFrameworkType (TypeRefData type, string managedTypeName, string assemblyName) => + IsSpecialManagedType (type, managedTypeName, assemblyName) || + (string.Equals (type.ManagedTypeName, managedTypeName, StringComparison.Ordinal) && + frameworkAssemblyNames.Contains (type.AssemblyName)); + + static bool HasValidSuperArgumentReferences (string superArgumentsString, int parameterCount) + { + var tokens = TokenizeJavaExpression (superArgumentsString); + if (tokens.Any (token => token.Text is "->" or "::")) { + return true; + } + + for (int i = 0; i < tokens.Count; i++) { + var token = tokens [i]; + if (!token.IsIdentifier || token.Text.Length < 2 || token.Text [0] != 'p' || + token.Text.Skip (1).Any (c => !char.IsDigit (c))) { + continue; + } + if (i > 0 && tokens [i - 1].Text == ".") { + continue; + } + + var parameterToken = token.Text.Substring (1); + if ((parameterToken.Length > 1 && parameterToken [0] == '0') || + !int.TryParse (parameterToken, out int parameterIndex) || + parameterIndex >= parameterCount) { + return false; + } + } + return true; + } + + readonly record struct JavaLexicalToken (string Text, bool IsIdentifier); + + static List TokenizeJavaExpression (string value) + { + var tokens = new List (); + for (int i = 0; i < value.Length;) { + if (char.IsWhiteSpace (value [i])) { + i++; + continue; + } + if (i + 1 < value.Length && value [i] == '/' && (value [i + 1] == '/' || value [i + 1] == '*')) { + bool block = value [i + 1] == '*'; + i += 2; + while (i < value.Length && (block ? !(i + 1 < value.Length && value [i] == '*' && value [i + 1] == '/') : value [i] != '\r' && value [i] != '\n')) { + i++; + } + i = block && i < value.Length ? Math.Min (i + 2, value.Length) : i; + continue; + } + if (value [i] == '"' || value [i] == '\'') { + char quote = value [i++]; + while (i < value.Length) { + if (value [i] == '\\') { + i = Math.Min (i + 2, value.Length); + } else if (value [i++] == quote) { + break; + } + } + continue; + } + if (char.IsLetter (value [i]) || value [i] == '_' || value [i] == '$') { + int start = i++; + while (i < value.Length && IsJavaIdentifierCharacter (value [i])) { + i++; + } + tokens.Add (new JavaLexicalToken (value.Substring (start, i - start), IsIdentifier: true)); + continue; + } + int symbolLength = i + 1 < value.Length && + ((value [i] == '-' && value [i + 1] == '>') || (value [i] == ':' && value [i + 1] == ':')) ? 2 : 1; + tokens.Add (new JavaLexicalToken (value.Substring (i, symbolLength), IsIdentifier: false)); + i += symbolLength; + } + return tokens; + } + + static bool IsJavaIdentifierCharacter (char value) => + char.IsLetterOrDigit (value) || value == '_' || value == '$'; + /// /// Attempts to find a managed instance constructor in /// whose parameters match the supplied JNI signature, and returns its managed diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 176caf7e79d..7daf6188d73 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -56,11 +56,13 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); + bool validConstructors = ValidateConstructors (allPeers); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - if (!ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass)) { + bool validJavaNames = ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass); + if (!validConstructors || !validJavaNames) { return new TrimmableTypeMapResult ([], [], allPeers); } @@ -83,6 +85,36 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } + internal bool ValidateConstructors (IReadOnlyList peers) + { + bool valid = true; + foreach (var peer in peers) { + if (peer.IsFrameworkAssembly || !ShouldGenerateJcw (peer)) { + continue; + } + foreach (var diagnostic in peer.ConstructorDiagnostics) { + valid = false; + switch (diagnostic.Kind) { + case ConstructorDiagnosticKind.AmbiguousJniSignature: + logger.LogAmbiguousConstructorSignatureError (peer.ManagedTypeName, diagnostic.Detail); + break; + case ConstructorDiagnosticKind.UnsupportedParameterType: + logger.LogUnsupportedConstructorParameterTypeError (peer.ManagedTypeName, diagnostic.Detail); + break; + case ConstructorDiagnosticKind.MissingBaseConstructor: + logger.LogMissingBaseConstructorError (peer.ManagedTypeName, diagnostic.Detail); + break; + case ConstructorDiagnosticKind.InvalidSuperArgumentsString: + logger.LogInvalidSuperArgumentsStringError (peer.ManagedTypeName, diagnostic.Detail); + break; + default: + throw new InvalidOperationException ($"Unknown constructor diagnostic kind '{diagnostic.Kind}'."); + } + } + } + return valid; + } + internal bool ValidateJavaNames (IReadOnlyList peers, string? applicationJavaClass = null) { bool valid = true; diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 961f3191ed7..deb96daab5d 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1659,6 +1659,42 @@ public static string XA4258 { } } + /// + /// Looks up a localized string similar to Type '{0}' has multiple managed constructors that map to the same Java Native Interface (JNI) signature '{1}'. Change the constructor parameter types so each constructor has a unique JNI signature.. + /// + public static string XA4259 { + get { + return ResourceManager.GetString("XA4259", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' has a constructor parameter type '{1}' that cannot be represented in a Java constructor. Remove the constructor or change the parameter to a supported Java type.. + /// + public static string XA4260 { + get { + return ResourceManager.GetString("XA4260", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' has Java constructor signature '{1}', but its base type does not expose a compatible Java constructor. Add a compatible base constructor or remove the derived constructor.. + /// + public static string XA4261 { + get { + return ResourceManager.GetString("XA4261", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' has an invalid [Export] SuperArgumentsString value '{1}'. The value references a constructor parameter that does not exist.. + /// + public static string XA4262 { + get { + return ResourceManager.GetString("XA4262", resourceCulture); + } + } + /// /// Looks up a localized string similar to The exported member '{0}' has unsupported signature type '{1}'. /// Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter].. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index d85b166adea..0098a27fa4b 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1238,6 +1238,30 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS The following are literal names and should not be translated: Java. {0} - Java package or type name {1} - Java reserved keyword or restricted identifier + + + Type '{0}' has multiple managed constructors that map to the same Java Native Interface (JNI) signature '{1}'. Change the constructor parameter types so each constructor has a unique JNI signature. + The following terms should not be translated: Java Native Interface, JNI. +{0} - Fully-qualified managed type name +{1} - Colliding JNI constructor signature + + + Type '{0}' has a constructor parameter type '{1}' that cannot be represented in a Java constructor. Remove the constructor or change the parameter to a supported Java type. + The following term should not be translated: Java. +{0} - Fully-qualified managed type name +{1} - Unsupported managed constructor parameter type + + + Type '{0}' has Java constructor signature '{1}', but its base type does not expose a compatible Java constructor. Add a compatible base constructor or remove the derived constructor. + The following term should not be translated: Java. +{0} - Fully-qualified managed type name +{1} - JNI signature of the derived constructor + + + Type '{0}' has an invalid [Export] SuperArgumentsString value '{1}'. The value references a constructor parameter that does not exist. + The following terms should not be translated: Export, SuperArgumentsString. +{0} - Fully-qualified managed type name +{1} - Invalid SuperArgumentsString value The exported member '{0}' has unsupported signature type '{1}'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 702ec1d221a..9f94674db24 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -69,6 +69,14 @@ public void LogExportFieldReturnsVoidError () => log.LogCodedError ("XA4208", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4208); public void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => log.LogCodedError ("XA4263", Properties.Resources.XA4263, memberName, managedTypeName); + public void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature) => + log.LogCodedError ("XA4259", Properties.Resources.XA4259, managedTypeName, jniSignature); + public void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType) => + log.LogCodedError ("XA4260", Properties.Resources.XA4260, managedTypeName, parameterType); + public void LogMissingBaseConstructorError (string managedTypeName, string jniSignature) => + log.LogCodedError ("XA4261", Properties.Resources.XA4261, managedTypeName, jniSignature); + public void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString) => + log.LogCodedError ("XA4262", Properties.Resources.XA4262, managedTypeName, superArgumentsString); public void LogCustomJavaObjectError (string managedTypeName) => log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); public void LogCustomJavaObjectWarning (string managedTypeName) => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 1f081f4a397..f50f51edc38 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -574,6 +574,471 @@ static string [] FindExportOutputs (string typemapDirectory, string acwMapFile, return outputs.ToArray (); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "JAVAC0000", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4262", "XA4258")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4262", "XA4258")] + public void Build_IndependentConstructorAndJavaNameDiagnostics_AreBothReported ( + string typeMapImplementation, + AndroidRuntime runtime, + string constructorCode, + string javaNameCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("IndependentDiagnostics.cs") { + TextContent = () => """ + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/InvalidConstructor")] + public class InvalidConstructor : Java.Lang.Object + { + [Export (".ctor", SuperArgumentsString = "p1 +")] + public InvalidConstructor (string value) { } + } + + [Register ("my/app/for")] + public class ReservedName : Java.Lang.Object { } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should report both independent errors."); + StringAssertEx.Contains ($"error {constructorCode}", builder.LastBuildOutput); + StringAssertEx.Contains ($"error {javaNameCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + AssertNoExportOutputs (builder, "InvalidConstructor"); + } else { + StringAssertEx.Contains ("InvalidConstructor.java", builder.LastBuildOutput); + StringAssertEx.Contains ("for.java", builder.LastBuildOutput); + } + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XALNS7003")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "success")] + public void Build_ExplicitExportConstructorAttributeOrders_MatchLegacyPipeline ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ExplicitExportConstructors.cs") { + TextContent = () => """ + using Android.App; + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/RegisterFirst")] + public class RegisterFirst : Activity { + [Register (".ctor", "(I)V", "")] + [Export (".ctor", SuperArgumentsString = "")] + public RegisterFirst (uint value) { } + } + + [Register ("my/app/ExportFirst")] + public class ExportFirst : Activity { + [Export (".ctor", SuperArgumentsString = "")] + [JniConstructorSignature ("(I)V")] + public ExportFirst (uint value) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + var succeeded = builder.Build (proj); + if (expectedCode == "success") { + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should preserve explicit constructor metadata."); + } else { + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should retain measured legacy validation."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + } + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void Build_ImplicitConstructorUsesCompatibleBaseJniSignature (string typeMapImplementation, AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("CompatibleBaseConstructor.cs") { + TextContent = () => """ + using Android.Runtime; + + namespace UnnamedProject; + + [Register ("my/app/IntBase", DoNotGenerateAcw = true)] + public class IntBase : Java.Lang.Object { + [Register (".ctor", "(I)V", "")] + public IntBase (int value) { } + } + + public class UIntDerived : IntBase { + public UIntDerived (uint value) : base ((int)value) { } + } + """, + }); + proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("my\\app\\IntBase.java") { + Encoding = Encoding.ASCII, + TextContent = () => """ + package my.app; + + public class IntBase { + public IntBase (int value) {} + } + """, + }); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy JNI base compatibility."); + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parenthesized-lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "typed-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "typed-lambda", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "typed-lambda", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "literal-comma", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "literal-comma", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "literal-comma", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "method-reference", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-method-reference", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-construction", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-construction", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-construction", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "nested-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "nested-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "nested-generic", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "instanceof-generic", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "comparison", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "comparison", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "comparison", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "shift", "success")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "shift", "success")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "shift", "success")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-bare", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-bare", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-bare", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-call", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-call", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-call", "XA4262")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "JAVAC0000")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "XA4262")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-arithmetic", "XA4262")] + public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( + string typeMapImplementation, + AndroidRuntime runtime, + string shape, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var superArguments = shape switch { + "lambda" => "p1 -> p1", + "parenthesized-lambda" => "(p1) -> p1", + "typed-lambda" => "(String p1, String p2) -> p1 + p2", + "literal-comma" => "p1 -> \\\"a,b\\\"", + "method-reference" => "Helper::p1", + "generic-method-reference" => "Helper::p1", + "generic-construction" => "(p1) -> new SimpleEntry(p1, p1)", + "nested-generic" => "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\\\"a,b\\\", p1))", + "instanceof-generic" => "(p1) -> p1 instanceof java.util.Map ? p1 : p1", + "comparison" => "(int p1) -> p1 < 2 ? p1 : 2", + "shift" => "(int p1) -> p1 >> 1", + "ordinary-bare" => "p1", + "ordinary-call" => "p1.hashCode()", + "ordinary-arithmetic" => "p1 + 1", + _ => throw new InvalidOperationException ($"Unknown super argument shape '{shape}'."), + }; + var functionalType = shape switch { + "typed-lambda" => "java.util.function.BiFunction", + "comparison" or "shift" => "java.util.function.IntUnaryOperator", + "instanceof-generic" => "java.util.function.Function", + "method-reference" or "generic-method-reference" => "java.util.function.Supplier", + _ => "java.util.function.Function", + }; + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("SuperArgumentsPeer.cs") { + TextContent = () => $$""" + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + [Register ("my/app/SuperArgumentsBase", DoNotGenerateAcw = true)] + public class SuperArgumentsBase : Java.Lang.Object + { + [Register (".ctor", "(Ljava/util/function/Function;Ljava/lang/Object;)V", "")] + public SuperArgumentsBase () { } + } + + [Register ("my/app/SuperArgumentsPeer")] + public class SuperArgumentsPeer : SuperArgumentsBase + { + [Export (".ctor", SuperArgumentsString = "{{superArguments}}")] + public SuperArgumentsPeer () { } + } + """, + }); + proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("my\\app\\SuperArgumentsBase.java") { + Encoding = Encoding.ASCII, + TextContent = () => $$""" + package my.app; + + public class SuperArgumentsBase { + public SuperArgumentsBase ( + {{functionalType}} function) {} + public SuperArgumentsBase ( + java.util.function.Function function, + Object value) {} + + public static class Helper { + public static T p1 () { return null; } + } + + public static class SimpleEntry extends java.util.AbstractMap.SimpleEntry { + public SimpleEntry (K key, V value) { super (key, value); } + } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + var succeeded = builder.Build (proj); + if (expectedCode == "success") { + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should compile {shape} super arguments."); + } else { + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject the bare p1 reference."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + AssertNoExportOutputs (builder, "SuperArgumentsPeer"); + } + } + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7004")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4263")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4263")] + public void Build_UnsupportedExportConstructorOverloads_ReportOnlyExportDiagnostics ( + string typeMapImplementation, + AndroidRuntime runtime, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("UnsupportedExportConstructors.cs") { + TextContent = () => """ + using Android.Runtime; + using Java.Interop; + + namespace UnnamedProject; + + public sealed class UnsupportedOne { } + public sealed class UnsupportedTwo { } + + [Register ("my/app/NoDefaultBase", DoNotGenerateAcw = true)] + public class NoDefaultBase : Java.Lang.Object + { + [Register (".ctor", "(I)V", "")] + public NoDefaultBase (int value) { } + } + + [Register ("my/app/UnsupportedExportConstructorOverloads")] + public class UnsupportedExportConstructorOverloads : NoDefaultBase + { + [Export (".ctor")] + public UnsupportedExportConstructorOverloads ( + [ExportParameter (ExportParameterKind.InputStream)] UnsupportedOne value) : base (0) { } + + [Export (".ctor")] + public UnsupportedExportConstructorOverloads ( + [ExportParameter (ExportParameterKind.InputStream)] UnsupportedTwo value) : base (0) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject unsupported exported constructors."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); + if (typeMapImplementation == "trimmable") { + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedOne'", builder.LastBuildOutput); + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedTwo'", builder.LastBuildOutput); + Assert.IsFalse (builder.LastBuildOutput.Any (line => + line.Contains ("Type 'UnnamedProject.UnsupportedExportConstructorOverloads'", StringComparison.Ordinal) && + (line.Contains ("error XA4259", StringComparison.Ordinal) || + line.Contains ("error XA4260", StringComparison.Ordinal) || + line.Contains ("error XA4261", StringComparison.Ordinal)))); + } + AssertNoExportOutputs (builder, "UnsupportedExportConstructorOverloads"); + } + + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true)] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, false)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, false)] + public void Build_WithCollidingConstructorSignatures_MatchesLegacyCount ( + string typeMapImplementation, + AndroidRuntime runtime, + bool shouldSucceed) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ConstructorCollision.cs") { + TextContent = () => """ + using Android.App; + + namespace UnnamedProject; + + public class ConstructorCollision : Activity + { + public enum Kind { None } + public ConstructorCollision (int value) { } + public ConstructorCollision (uint value) { } + public ConstructorCollision (Kind value) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.AreEqual (shouldSucceed, builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy collision behavior."); + if (shouldSucceed) { + Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("error XA4259", StringComparison.Ordinal))); + return; + } + StringAssertEx.Contains ("error XA4259", builder.LastBuildOutput); + + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + Assert.IsFalse (Directory.Exists (typemapDirectory) && Directory.EnumerateFiles (typemapDirectory, "*.java", SearchOption.AllDirectories).Any (), + "Constructor diagnostics must be reported before partial Java output is written."); + } + + [TestCase ("rectangular-in-sz-array", false)] + [TestCase ("pointer-array", true)] + [TestCase ("function-pointer-array", true)] + public void Build_WithUnsupportedNestedConstructorParameter_FailsBeforeWritingTrimmableOutputs (string shape, bool isUnsafe) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var parameterType = shape switch { + "rectangular-in-sz-array" => "string[][,]", + "pointer-array" => "int*[]", + "function-pointer-array" => "delegate* unmanaged[]", + _ => throw new InvalidOperationException ($"Unknown nested constructor shape '{shape}'."), + }; + var proj = new XamarinAndroidApplicationProject { + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + if (isUnsafe) { + proj.SetProperty ("AllowUnsafeBlocks", "true"); + } + proj.Sources.Add (new BuildItem.Source ("NestedConstructorShape.cs") { + TextContent = () => $$""" + using Android.App; + using Java.Interop; + + namespace UnnamedProject; + + public {{(isUnsafe ? "unsafe " : "")}}class NestedConstructorShape : Activity + { + [Export (".ctor", SuperArgumentsString = "")] + public NestedConstructorShape ({{parameterType}} value) { } + } + """, + }); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"Build should fail for nested constructor parameter '{parameterType}'."); + StringAssertEx.Contains ("error XA4260", builder.LastBuildOutput); + + var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + Assert.IsFalse (Directory.Exists (typemapDirectory) && Directory.EnumerateFiles (typemapDirectory, "*.java", SearchOption.AllDirectories).Any (), + "Constructor diagnostics must be reported before partial Java output is written."); + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs new file mode 100644 index 00000000000..c36340da8e9 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ConstructorParityTests.cs @@ -0,0 +1,78 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public partial class ScannerComparisonTests +{ + [Theory] + [InlineData ("UserApp.SignedUnsignedConstructorCollision", "(I)V")] + [InlineData ("UserApp.AliasedTypeConstructorCollision", "(Lcom/example/userapp/Alias;)V")] + public void LegacyConstructorSignatureCollision_KeepsFirstSignature (string managedTypeName, string signature) + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName); + Assert.Single (constructors, c => c.JniSignature == signature); + } + + [Theory] + [InlineData ("UserApp.GenericParameterConstructor`1")] + [InlineData ("UserApp.GenericInstantiationConstructor")] + [InlineData ("UserApp.FunctionPointerConstructor")] + [InlineData ("UserApp.FunctionPointerArrayConstructor")] + public void LegacyUnrepresentableConstructor_IsSkipped (string managedTypeName) + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName); + var constructor = Assert.Single (constructors); + Assert.Equal ("()V", constructor.JniSignature); + } + + [Theory] + [InlineData ("UserApp.ByRefConstructor")] + [InlineData ("UserApp.PointerConstructor")] + public void LegacyElementMappedConstructor_UsesMeasuredJniSignatures (string managedTypeName) + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var signatures = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName) + .Select (constructor => constructor.JniSignature) + .ToList (); + Assert.Equal (["()V", "(I)V"], signatures); + } + + [Theory] + [InlineData ("UserApp.RectangularArrayConstructor", "([Ljava/lang/String;)V")] + [InlineData ("UserApp.NestedRectangularArrayConstructor", "([Ljava/lang/String;)V")] + [InlineData ("UserApp.PointerArrayConstructor", "([I)V")] + [InlineData ("UserApp.JaggedArrayConstructor", "([Ljava/lang/String;)V")] + public void LegacyArrayConstructor_UsesMeasuredJniSignature (string managedTypeName, string signature) + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, managedTypeName); + Assert.Single (constructors, c => c.JniSignature == signature); + } + + [Fact] + public void LegacyMissingBaseConstructor_IsSkipped () + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var constructors = ScannerRunner.RunLegacyConstructors (fixturePath, "UserApp.MissingBaseConstructor"); + Assert.DoesNotContain (constructors, c => c.JniSignature == "(Ljava/lang/String;)V"); + } + + [Fact] + public void LegacyInvalidSuperArgumentsString_IsEmittedVerbatim () + { + var fixturePath = UserTypesFixturePath; + Assert.NotNull (fixturePath); + var constructor = Assert.Single ( + ScannerRunner.RunLegacyConstructors (fixturePath, "UserApp.InvalidSuperArgumentsConstructor"), + c => c.JniSignature == "(Ljava/lang/String;)V"); + Assert.Equal ("p1", constructor.SuperCall); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 2db61b9ca8d..2b6f66a43ea 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.Metadata; @@ -135,9 +137,39 @@ public void ExactMarshalMethods_UserTypesFixture () .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); var newNormalized = newMethods .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => NormalizeMethodGroups (kvp.Value)); + ExcludeConstructorDiagnosticFixtures (legacyNormalized); + ExcludeConstructorDiagnosticFixtures (newNormalized); var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized); AssertNoDiffs ("MISSING from new scanner", result.Missing); AssertNoDiffs ("METHOD MISMATCHES", result.MethodMismatches); } + + static void ExcludeConstructorDiagnosticFixtures (Dictionary> methods) + { + var fixtureTypePrefix = "UserApp."; + var fixtureTypeNames = new HashSet (StringComparer.Ordinal) { + $"{fixtureTypePrefix}SignedUnsignedConstructorCollision, UserTypesFixture", + $"{fixtureTypePrefix}AliasedTypeConstructorCollision, UserTypesFixture", + $"{fixtureTypePrefix}GenericParameterConstructor`1, UserTypesFixture", + $"{fixtureTypePrefix}GenericInstantiationConstructor, UserTypesFixture", + $"{fixtureTypePrefix}ByRefConstructor, UserTypesFixture", + $"{fixtureTypePrefix}PointerConstructor, UserTypesFixture", + $"{fixtureTypePrefix}FunctionPointerConstructor, UserTypesFixture", + $"{fixtureTypePrefix}RectangularArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}NestedRectangularArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}PointerArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}FunctionPointerArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}JaggedArrayConstructor, UserTypesFixture", + $"{fixtureTypePrefix}MissingBaseConstructor, UserTypesFixture", + $"{fixtureTypePrefix}InvalidSuperArgumentsConstructor, UserTypesFixture", + }; + + foreach (var javaName in methods.Keys.ToList ()) { + methods [javaName].RemoveAll (group => fixtureTypeNames.Contains (group.ManagedName)); + if (methods [javaName].Count == 0) { + methods.Remove (javaName); + } + } + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index fcd8a0a477e..87e2f747157 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -18,6 +18,8 @@ record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManag record MethodEntry (string JniName, string JniSignature, string? Connector); +record ConstructorEntry (string JniSignature, string? SuperCall); + record TypeMethodGroup (string ManagedName, List Methods); static class ScannerRunner @@ -79,6 +81,34 @@ public static (List entries, Dictionary RunLegacyConstructors (string assemblyPath, string managedTypeName) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + var assemblyDirectory = Path.GetDirectoryName (assemblyPath); + if (assemblyDirectory is null) { + throw new InvalidOperationException ($"Could not determine the assembly directory for '{assemblyPath}'."); + } + resolver.AddSearchDirectory (assemblyDirectory); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir is not null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = CecilAssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + var type = assembly.MainModule.Types.FirstOrDefault (t => t.FullName.Replace ('/', '+') == managedTypeName); + if (type is null) { + throw new InvalidOperationException ($"Could not find managed type '{managedTypeName}' in '{assemblyPath}'."); + } + + var wrapper = CecilImporter.CreateType (type, cache); + return wrapper.Constructors + .Select (c => new ConstructorEntry (c.JniSignature, c.SuperCall)) + .ToList (); + } + public static (List entries, Dictionary> methodsByJavaName) RunNew (string[] assemblyPaths) { using var scanner = new JavaPeerScanner (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 70aa95a84f0..c100186667d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -156,6 +156,100 @@ public void DoWork () } } + public class SignedUnsignedConstructorCollision : Activity + { + public SignedUnsignedConstructorCollision (int value) { } + public SignedUnsignedConstructorCollision (uint value) { } + } + + [Register ("com/example/userapp/Alias")] + public class ConstructorAliasOne : Java.Lang.Object + { + protected ConstructorAliasOne (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("com/example/userapp/Alias")] + public class ConstructorAliasTwo : Java.Lang.Object + { + protected ConstructorAliasTwo (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + public class AliasedTypeConstructorCollision : Activity + { + public AliasedTypeConstructorCollision (ConstructorAliasOne value) { } + public AliasedTypeConstructorCollision (ConstructorAliasTwo value) { } + } + + public class GenericParameterConstructor : Activity + { + public GenericParameterConstructor (T value) { } + } + + public class GenericInstantiationConstructor : Activity + { + public GenericInstantiationConstructor (System.Collections.Generic.List value) { } + } + + public class ByRefConstructor : Activity + { + public ByRefConstructor (ref int value) { } + } + + public unsafe class PointerConstructor : Activity + { + public PointerConstructor (int* value) { } + } + + public unsafe class FunctionPointerConstructor : Activity + { + public FunctionPointerConstructor (delegate* unmanaged value) { } + } + + public class RectangularArrayConstructor : Activity + { + public RectangularArrayConstructor (string[,] value) { } + } + + public class NestedRectangularArrayConstructor : Activity + { + public NestedRectangularArrayConstructor (string[][,] value) { } + } + + public unsafe class PointerArrayConstructor : Activity + { + public PointerArrayConstructor (int*[] value) { } + } + + public unsafe class FunctionPointerArrayConstructor : Activity + { + public FunctionPointerArrayConstructor (delegate* unmanaged[] value) { } + } + + public class JaggedArrayConstructor : Activity + { + public JaggedArrayConstructor (string[][] value) { } + } + + [Register ("com/example/userapp/NoDefaultBase")] + public class NoDefaultConstructorBase : Java.Lang.Object + { + [Register (".ctor", "(I)V", "")] + public NoDefaultConstructorBase (int value) { } + + protected NoDefaultConstructorBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + public class MissingBaseConstructor : NoDefaultConstructorBase + { + public MissingBaseConstructor (string value) : base (0) { } + } + + public class InvalidSuperArgumentsConstructor : Activity + { + [Export (".ctor", SuperArgumentsString = "p1")] + public InvalidSuperArgumentsConstructor (string value) { } + } + // [Export] shapes that the legacy JCW emitter (CecilImporter.GetJniSignature) // cannot encode but that the trimmable scanner is expected to handle. These // types are excluded from legacy↔new comparison in ScannerComparisonTests diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj index 496ae910f0a..43276120d58 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj @@ -7,6 +7,7 @@ $(DotNetTargetFramework) latest enable + true false Library true diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj new file mode 100644 index 00000000000..63d9e010ce6 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/ActivationStubs.Java.Interop.csproj @@ -0,0 +1,9 @@ + + + + $(DotNetStableTargetFramework) + Java.Interop + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs new file mode 100644 index 00000000000..da787fca570 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Java.Interop/JniObjectReference.cs @@ -0,0 +1,15 @@ +using System; + +namespace Java.Interop; + +public struct JniObjectReference +{ + public IntPtr Handle; +} + +public enum JniObjectReferenceOptions +{ + None, + Copy, + CopyAndDispose, +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj new file mode 100644 index 00000000000..fb574c524e6 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/ActivationStubs.Mono.Android.csproj @@ -0,0 +1,9 @@ + + + + $(DotNetStableTargetFramework) + Mono.Android + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs new file mode 100644 index 00000000000..115d615d2c4 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ActivationStubs.Mono.Android/JniHandleOwnership.cs @@ -0,0 +1,8 @@ +namespace Android.Runtime; + +public enum JniHandleOwnership +{ + DoNotTransfer, + TransferLocalRef, + TransferGlobalRef, +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index dd12967da2f..5ac0e10eb09 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -34,9 +34,9 @@ static string TestAttributeFixtureAssemblyPath { } static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { - using var scanner = new JavaPeerScanner (); using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath)); + using var scanner = new JavaPeerScanner (); var assemblies = new [] { GetAssemblyInput (peReader), GetAssemblyInput (attributePeReader), diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 65e9e8dc358..a351425ebc3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -63,6 +63,14 @@ public void LogExportFieldOnGenericTypeError () => logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); public void LogUnsupportedExportSignatureError (string memberName, string managedTypeName) => logMessages.Add ($"XA4263: The exported member '{memberName}' has unsupported signature type '{managedTypeName}'."); + public void LogAmbiguousConstructorSignatureError (string managedTypeName, string jniSignature) => + logMessages.Add ($"XA4259: Type '{managedTypeName}' has multiple managed constructors that map to JNI signature '{jniSignature}'."); + public void LogUnsupportedConstructorParameterTypeError (string managedTypeName, string parameterType) => + logMessages.Add ($"XA4260: Type '{managedTypeName}' has a constructor parameter type '{parameterType}' that cannot be represented in Java."); + public void LogMissingBaseConstructorError (string managedTypeName, string jniSignature) => + logMessages.Add ($"XA4261: Type '{managedTypeName}' has constructor '{jniSignature}' with no callable Java base constructor."); + public void LogInvalidSuperArgumentsStringError (string managedTypeName, string superArgumentsString) => + logMessages.Add ($"XA4262: Type '{managedTypeName}' has invalid SuperArgumentsString '{superArgumentsString}'."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => @@ -90,6 +98,60 @@ public void ValidateJavaNames_ReservedIdentifier_LogsError (string javaName, str Assert.Contains (logMessages, message => message.Contains ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'.")); } + [Theory] + [InlineData (ConstructorDiagnosticKind.AmbiguousJniSignature, "(I)V", "XA4259")] + [InlineData (ConstructorDiagnosticKind.UnsupportedParameterType, "System.Int32&", "XA4260")] + [InlineData (ConstructorDiagnosticKind.MissingBaseConstructor, "(Ljava/lang/String;)V", "XA4261")] + [InlineData (ConstructorDiagnosticKind.InvalidSuperArgumentsString, "p1", "XA4262")] + public void ValidateConstructors_LogsCodedError ( + ConstructorDiagnosticKind kind, + string detail, + string errorCode) + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/Type", + CompatJniName = "com/example/Type", + ManagedTypeName = "Example.Type", + ManagedTypeNamespace = "Example", + ManagedTypeShortName = "Type", + AssemblyName = "Example", + ConstructorDiagnostics = [ + new ConstructorDiagnosticInfo { + Kind = kind, + Detail = detail, + }, + ], + }, + }; + + Assert.False (CreateGenerator ().ValidateConstructors (peers)); + Assert.Contains (logMessages, message => message.StartsWith ($"{errorCode}:", StringComparison.Ordinal)); + } + + [Fact] + public void ValidateConstructors_IgnoresFrameworkPeers () + { + var peer = new JavaPeerInfo { + JavaName = "android/example/FrameworkType", + CompatJniName = "android/example/FrameworkType", + ManagedTypeName = "Android.Example.FrameworkType", + ManagedTypeNamespace = "Android.Example", + ManagedTypeShortName = "FrameworkType", + AssemblyName = "Mono.Android", + IsFrameworkAssembly = true, + ConstructorDiagnostics = [ + new ConstructorDiagnosticInfo { + Kind = ConstructorDiagnosticKind.UnsupportedParameterType, + Detail = "System.Object", + }, + ], + }; + + Assert.True (CreateGenerator ().ValidateConstructors ([peer])); + Assert.Empty (logMessages); + } + [Fact] public void ValidateJavaNames_ContextualKeywordInPackage_IsValid () { @@ -386,6 +448,57 @@ public void Execute_UnsupportedExportSignatures_ReportCodedDiagnosticsWithoutPar } } + [Fact] + public void Execute_WithConstructorDiagnostics_ReturnsNoPartialOutputs () + { + using var fixtureReader = CreateTestFixturePEReader (); + using var invalidReader = CreateFixturePEReader ("InvalidConstructorFixtures.dll"); + var result = CreateGenerator ().Execute ( + [ + Input ("TestFixtures", fixtureReader), + Input ("InvalidConstructorFixtures", invalidReader), + ], + new Version (11, 0), + new HashSet ()); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaSources); + Assert.Contains (logMessages, message => message.StartsWith ("XA4259:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4260:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4261:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4262:", StringComparison.Ordinal)); + Assert.Contains (logMessages, message => message.StartsWith ("XA4258:", StringComparison.Ordinal)); + Assert.Single (logMessages, message => + message.StartsWith ("XA4259:", StringComparison.Ordinal) && + message.Contains ("MyApp.SignedUnsignedCollisionActivity", StringComparison.Ordinal)); + + foreach (var typeName in new [] { + "UnsupportedExportConstructorOverloadsActivity", + "RegisterBeforeExportActivity", + "ExportBeforeRegisterActivity", + }) { + var messages = logMessages.Where (message => message.Contains ($"MyApp.{typeName}", StringComparison.Ordinal)).ToList (); + Assert.Equal (2, messages.Count); + Assert.All (messages, message => Assert.StartsWith ("XA4263:", message)); + } + + foreach (var typeName in new [] { + "GenericParameterCtorActivity", + "GenericInstantiationCtorActivity", + "ByRefCtorActivity", + "PointerCtorActivity", + "FunctionPointerCtorActivity", + "RectangularArrayCtorActivity", + "NestedRectangularArrayCtorActivity", + "PointerArrayCtorActivity", + "FunctionPointerArrayCtorActivity", + }) { + var messages = logMessages.Where (message => message.Contains ($"MyApp.{typeName}", StringComparison.Ordinal)).ToList (); + var message = Assert.Single (messages); + Assert.StartsWith ("XA4260:", message); + } + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { @@ -1229,9 +1342,12 @@ public void RootManifestReferencedTypes_MatchesNestedTypes () static PEReader CreateTestFixturePEReader () + => CreateFixturePEReader ("TestFixtures.dll"); + + static PEReader CreateFixturePEReader (string fileName) { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); - return new PEReader (File.OpenRead (Path.Combine (dir, "TestFixtures.dll"))); + return new PEReader (File.OpenRead (Path.Combine (dir, fileName))); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 5a5a0c2f4f8..55fe60952c8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -187,6 +187,25 @@ public void Generate_InheritedCtor_CreateInstanceDoesNotActivate () AssertCreateInstanceReturnsNull (pe, reader, "MyApp_SimpleActivity_Proxy"); } + [Fact] + public void Generate_NoActivationCtor_CreateInstanceDoesNotReferenceLookalikeSignature () + { + var peer = MakeMcwPeer ("test/Lookalike", "Test.Lookalike", "TestAsm") with { + DoNotGenerateAcw = true, + }; + using var stream = GenerateAssembly ([peer], "LookalikeCreateInstanceTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + AssertCreateInstanceReturnsNull (pe, reader, "Test_Lookalike_Proxy"); + Assert.Empty (FindCtorMemberRefs ( + reader, + "Test", + "Lookalike", + "System.IntPtr", + "Android.Runtime.JniHandleOwnership")); + } + [Fact] public void Generate_InheritedJavaInteropCtor_CreateInstanceDoesNotActivate () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj new file mode 100644 index 00000000000..90c17710b7b --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructorFixtures.csproj @@ -0,0 +1,21 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + true + $(NoWarn);CS0436 + false + + + + + global,TestFixtures + + + Lookalikes + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs new file mode 100644 index 00000000000..9ca4c8a580f --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/InvalidConstructorFixtures/InvalidConstructors.cs @@ -0,0 +1,491 @@ +extern alias TestFixtures; +extern alias Lookalikes; + +using System; +using Android.Runtime; +using ExternalCollisionParameter = TestFixtures::MyApp.CrossAssemblyCollisionParameter; +using LookalikeInt32 = Lookalikes::System.Int32; +using LookalikeIntPtr = Lookalikes::System.IntPtr; +using LookalikeJniObjectReference = Lookalikes::Java.Interop.JniObjectReference; +using LookalikeJniObjectReferenceOptions = Lookalikes::Java.Interop.JniObjectReferenceOptions; +using LookalikeOwnership = Lookalikes::Android.Runtime.JniHandleOwnership; + +namespace MyApp; + +public enum ConstructorKind +{ + None, +} + +[Register ("my/app/for")] +public class ReservedJavaNameActivity : Android.App.Activity +{ +} + +[Register ("my/app/EnumCtorActivity")] +public class EnumCtorActivity : Android.App.Activity +{ + public EnumCtorActivity (ConstructorKind value) { } +} + +[Register ("my/app/IntSignatureBase", DoNotGenerateAcw = true)] +public class IntSignatureBase : Java.Lang.Object +{ + [Register (".ctor", "(I)V", "")] + public IntSignatureBase (int value) { } +} + +[Register ("my/app/LongSignatureBase", DoNotGenerateAcw = true)] +public class LongSignatureBase : Java.Lang.Object +{ + [Register (".ctor", "(J)V", "")] + public LongSignatureBase (long value) { } +} + +[Register ("my/app/ExplicitPointerCtorActivity")] +public unsafe class ExplicitPointerCtorActivity : LongSignatureBase +{ + [Register (".ctor", "(J)V", "")] + public ExplicitPointerCtorActivity (int* value) : base ((long)value) { } +} + +[Register ("my/app/ExplicitRegisterCollisionActivity")] +public class ExplicitRegisterCollisionActivity : IntSignatureBase +{ + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCollisionActivity (int value) : base (value) { } + + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCollisionActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/JniSignatureCollisionActivity")] +public class JniSignatureCollisionActivity : IntSignatureBase +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCollisionActivity (int value) : base (value) { } + + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCollisionActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ExplicitRegisterCompatibleBaseActivity")] +public class ExplicitRegisterCompatibleBaseActivity : IntSignatureBase +{ + [Register (".ctor", "(I)V", "")] + public ExplicitRegisterCompatibleBaseActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/JniSignatureCompatibleBaseActivity")] +public class JniSignatureCompatibleBaseActivity : IntSignatureBase +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + public JniSignatureCompatibleBaseActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ImplicitJniCompatibleBaseActivity")] +public class ImplicitJniCompatibleBaseActivity : IntSignatureBase +{ + public ImplicitJniCompatibleBaseActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/RegisterBeforeExportValidActivity")] +public class RegisterBeforeExportValidActivity : IntSignatureBase +{ + [Register (".ctor", "(I)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportValidActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/ExportBeforeRegisterValidActivity")] +public class ExportBeforeRegisterValidActivity : IntSignatureBase +{ + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(I)V", "")] + public ExportBeforeRegisterValidActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/JniBeforeExportValidActivity")] +public class JniBeforeExportValidActivity : Android.App.Activity +{ + [Java.Interop.JniConstructorSignature ("(I)V")] + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public JniBeforeExportValidActivity (uint value) { } +} + +[Register ("my/app/ExportBeforeJniValidActivity")] +public class ExportBeforeJniValidActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + [Java.Interop.JniConstructorSignature ("(I)V")] + public ExportBeforeJniValidActivity (uint value) { } +} + +[Register ("my/app/BindingPointerCtor", DoNotGenerateAcw = true)] +public unsafe class BindingPointerCtor : Java.Lang.Object +{ + [Register (".ctor", "(J)V", "")] + public BindingPointerCtor (int* value) { } +} + +[Register ("my/app/SignedUnsignedCollisionActivity")] +public class SignedUnsignedCollisionActivity : Android.App.Activity +{ + public SignedUnsignedCollisionActivity (int value) { } + public SignedUnsignedCollisionActivity (uint value) { } + public SignedUnsignedCollisionActivity (ConstructorKind value) { } +} + +[Register ("my/app/AliasOne", DoNotGenerateAcw = true)] +public class AliasOne : Java.Lang.Object +{ + protected AliasOne (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } +} + +[Register ("my/app/AliasOne", DoNotGenerateAcw = true)] +public class AliasTwo : Java.Lang.Object +{ + protected AliasTwo (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } +} + +[Register ("my/app/AliasedTypeCollisionActivity")] +public class AliasedTypeCollisionActivity : Android.App.Activity +{ + public AliasedTypeCollisionActivity (AliasOne value) { } + public AliasedTypeCollisionActivity (AliasTwo value) { } +} + +[Register ("my/app/CrossAssemblyParameter", DoNotGenerateAcw = true)] +public class CrossAssemblyCollisionParameter : Java.Lang.Object +{ +} + +public sealed class CrossAssemblyBorrowedParameter +{ +} + +[Register ("my/app/CrossAssemblyCollisionActivity")] +public class CrossAssemblyCollisionActivity : Android.App.Activity +{ + public CrossAssemblyCollisionActivity (CrossAssemblyCollisionParameter value) { } + public CrossAssemblyCollisionActivity (ExternalCollisionParameter value) { } +} + +[Register ("my/app/CrossAssemblyBorrowActivity")] +public class CrossAssemblyBorrowActivity : Android.App.Activity +{ + public CrossAssemblyBorrowActivity (CrossAssemblyBorrowedParameter value) { } +} + +[Register ("my/app/PrimitiveLookalikeActivity")] +public class PrimitiveLookalikeActivity +{ + public PrimitiveLookalikeActivity (LookalikeInt32 value) { } +} + +[Register ("my/app/XamarinActivationLookalikeActivity")] +public class XamarinActivationLookalikeActivity +{ + public XamarinActivationLookalikeActivity (LookalikeIntPtr handle, LookalikeOwnership ownership) { } +} + +[Register ("my/app/JniActivationLookalikeActivity")] +public class JniActivationLookalikeActivity +{ + public JniActivationLookalikeActivity (LookalikeJniObjectReference reference, LookalikeJniObjectReferenceOptions options) { } +} + +[Register ("my/app/GenericParameterCtorActivity")] +public class GenericParameterCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public GenericParameterCtorActivity (T value) { } +} + +[Register ("my/app/GenericInstantiationCtorActivity")] +public class GenericInstantiationCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public GenericInstantiationCtorActivity (System.Collections.Generic.List value) { } +} + +[Register ("my/app/ByRefCtorActivity")] +public class ByRefCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ByRefCtorActivity (ref int value) { } +} + +[Register ("my/app/PointerCtorActivity")] +public unsafe class PointerCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public PointerCtorActivity (int* value) { } +} + +[Register ("my/app/FunctionPointerCtorActivity")] +public unsafe class FunctionPointerCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public FunctionPointerCtorActivity (delegate* unmanaged value) { } +} + +[Register ("my/app/RectangularArrayCtorActivity")] +public class RectangularArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public RectangularArrayCtorActivity (string[,] value) { } +} + +[Register ("my/app/NestedRectangularArrayCtorActivity")] +public class NestedRectangularArrayCtorActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public NestedRectangularArrayCtorActivity (string[][,] value) : base (0) { } +} + +[Register ("my/app/PointerArrayCtorActivity")] +public unsafe class PointerArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public PointerArrayCtorActivity (int*[] value) { } +} + +[Register ("my/app/FunctionPointerArrayCtorActivity")] +public unsafe class FunctionPointerArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public FunctionPointerArrayCtorActivity (delegate* unmanaged[] value) { } +} + +[Register ("my/app/JaggedArrayCtorActivity")] +public class JaggedArrayCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public JaggedArrayCtorActivity (string[][] value) { } +} + +[Register ("my/app/NoDefaultBase", DoNotGenerateAcw = true)] +public class NoDefaultBase : Java.Lang.Object +{ + [Register (".ctor", "(I)V", "")] + public NoDefaultBase (int value) { } +} + +public sealed class UnsupportedConstructorValueOne +{ +} + +public sealed class UnsupportedConstructorValueTwo +{ +} + +[Register ("my/app/UnsupportedExportConstructorOverloadsActivity")] +public class UnsupportedExportConstructorOverloadsActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + public UnsupportedExportConstructorOverloadsActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Java.Interop.Export (".ctor")] + public UnsupportedExportConstructorOverloadsActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + +[Register ("my/app/RegisterBeforeExportActivity")] +public class RegisterBeforeExportActivity : NoDefaultBase +{ + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + [Java.Interop.Export (".ctor")] + public RegisterBeforeExportActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + +[Register ("my/app/ExportBeforeRegisterActivity")] +public class ExportBeforeRegisterActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + public ExportBeforeRegisterActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueOne value) : base (0) { } + + [Java.Interop.Export (".ctor")] + [Register (".ctor", "(Ljava/lang/Object;)V", "")] + public ExportBeforeRegisterActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] UnsupportedConstructorValueTwo value) : base (0) { } +} + +[Register ("my/app/MissingBaseCtorActivity")] +public class MissingBaseCtorActivity : NoDefaultBase +{ + public MissingBaseCtorActivity (string value) : base (0) { } +} + +[Register ("my/app/InvalidSuperArgumentsActivity")] +public class InvalidSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1")] + public InvalidSuperArgumentsActivity (string value) { } +} + +[Register ("my/app/NonCanonicalSuperArgumentZeroActivity")] +public class NonCanonicalSuperArgumentZeroActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p00")] + public NonCanonicalSuperArgumentZeroActivity (string value) { } +} + +[Register ("my/app/NonCanonicalSuperArgumentOneActivity")] +public class NonCanonicalSuperArgumentOneActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p01")] + public NonCanonicalSuperArgumentOneActivity (string first, string second) { } +} + +[Register ("my/app/ValidSuperExpressionActivity")] +public class ValidSuperExpressionActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p0.hashCode()")] + public ValidSuperExpressionActivity (string value) { } +} + +[Register ("my/app/LexicalSuperArgumentsActivity")] +public class LexicalSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Constants.p1, \"p2\", /* p3 */ p0")] + public LexicalSuperArgumentsActivity (string value) { } +} + +[Register ("my/app/LambdaSuperArgumentsActivity")] +public class LambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> p1")] + public LambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/ParenthesizedLambdaSuperArgumentsActivity")] +public class ParenthesizedLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1")] + public ParenthesizedLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/TypedLambdaSuperArgumentsActivity")] +public class TypedLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(String p1, String p2) -> p1 + p2")] + public TypedLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/LiteralCommaLambdaSuperArgumentsActivity")] +public class LiteralCommaLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "p1 -> \"a,b\"")] + public LiteralCommaLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/GenericConstructionLambdaSuperArgumentsActivity")] +public class GenericConstructionLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry(p1, p1)")] + public GenericConstructionLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/NestedGenericLambdaSuperArgumentsActivity")] +public class NestedGenericLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> new SimpleEntry>(p1, java.util.Arrays.asList(\"a,b\", p1))")] + public NestedGenericLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/InstanceOfGenericLambdaSuperArgumentsActivity")] +public class InstanceOfGenericLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(p1) -> p1 instanceof java.util.Map ? p1 : p1")] + public InstanceOfGenericLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/ComparisonLambdaSuperArgumentsActivity")] +public class ComparisonLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 < 2 ? p1 : 2")] + public ComparisonLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/ShiftLambdaSuperArgumentsActivity")] +public class ShiftLambdaSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "(int p1) -> p1 >> 1")] + public ShiftLambdaSuperArgumentsActivity () { } +} + +[Register ("my/app/MethodReferenceSuperArgumentsActivity")] +public class MethodReferenceSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1")] + public MethodReferenceSuperArgumentsActivity () { } +} + +[Register ("my/app/GenericMethodReferenceSuperArgumentsActivity")] +public class GenericMethodReferenceSuperArgumentsActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "Helper::p1")] + public GenericMethodReferenceSuperArgumentsActivity () { } +} + +[Register ("my/app/ExportMappedCtorActivity")] +public class ExportMappedCtorActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportMappedCtorActivity ( + [Java.Interop.ExportParameter (Java.Interop.ExportParameterKind.InputStream)] System.IO.Stream value) { } + + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportMappedCtorActivity (Java.Lang.ICharSequence value) { } + + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportMappedCtorActivity (System.Collections.IList value) { } +} + +[Register ("my/app/ExportMissingBaseCtorActivity")] +public class ExportMissingBaseCtorActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + public ExportMissingBaseCtorActivity (string value) : base (0) { } +} + +[Register ("my/app/ExportEmptySuperMissingBaseActivity")] +public class ExportEmptySuperMissingBaseActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + public ExportEmptySuperMissingBaseActivity (string value) : base (0) { } +} + +[Register ("my/app/NonPublicExportCollisionActivity")] +public class NonPublicExportCollisionActivity : IntSignatureBase +{ + [Java.Interop.Export (".ctor")] + protected NonPublicExportCollisionActivity (int value) : base (value) { } + + [Java.Interop.Export (".ctor")] + protected NonPublicExportCollisionActivity (uint value) : base ((int)value) { } +} + +[Register ("my/app/NonPublicExportUnsupportedActivity")] +public class NonPublicExportUnsupportedActivity : Android.App.Activity +{ + [Java.Interop.Export (".ctor", SuperArgumentsString = "")] + protected NonPublicExportUnsupportedActivity (T value) { } +} + +[Register ("my/app/NonPublicExportMissingBaseActivity")] +public class NonPublicExportMissingBaseActivity : NoDefaultBase +{ + [Java.Interop.Export (".ctor")] + protected NonPublicExportMissingBaseActivity (string value) : base (0) { } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj new file mode 100644 index 00000000000..0b655968f54 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeConstructorTypes.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + enable + false + + + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs new file mode 100644 index 00000000000..9a0b4966699 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/LookalikeConstructorTypes/LookalikeTypes.cs @@ -0,0 +1,33 @@ +namespace System +{ + [Android.Runtime.Register ("my/app/LookalikeInt32", DoNotGenerateAcw = true)] + public class Int32 : Java.Lang.Object + { + } + + [Android.Runtime.Register ("my/app/LookalikeIntPtr", DoNotGenerateAcw = true)] + public class IntPtr : Java.Lang.Object + { + } +} + +namespace Android.Runtime +{ + [Register ("my/app/LookalikeOwnership", DoNotGenerateAcw = true)] + public class JniHandleOwnership : Java.Lang.Object + { + } +} + +namespace Java.Interop +{ + [Android.Runtime.Register ("my/app/LookalikeReference", DoNotGenerateAcw = true)] + public class JniObjectReference : Java.Lang.Object + { + } + + [Android.Runtime.Register ("my/app/LookalikeOptions", DoNotGenerateAcw = true)] + public class JniObjectReferenceOptions : Java.Lang.Object + { + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index f42e38eb363..ff9cadf61f2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -14,6 +14,10 @@ + + + + @@ -31,6 +35,12 @@ false + + false + + + false + @@ -38,6 +48,10 @@ <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" /> + <_TestFixtureFiles Include="InvalidConstructorFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\InvalidConstructorFixtures.dll" /> + <_TestFixtureFiles Include="LookalikeConstructorTypes\bin\$(Configuration)\$(DotNetStableTargetFramework)\LookalikeConstructorTypes.dll" /> + <_TestFixtureFiles Include="ActivationStubs.Java.Interop\bin\$(Configuration)\$(DotNetStableTargetFramework)\Java.Interop.dll" /> + <_TestFixtureFiles Include="ActivationStubs.Mono.Android\bin\$(Configuration)\$(DotNetStableTargetFramework)\Mono.Android.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs index 3e051ecffc7..46b3e5c992e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ConstructorDetectionTests.cs @@ -1,4 +1,8 @@ +using System; +using System.IO; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -155,6 +159,184 @@ public void UnsignedPrimitiveCtor_MapsCorrectly () Assert.Contains ("(SIJ)V", ctorSigs); } + [Theory] + [InlineData ("MyApp.GenericParameterCtorActivity`1", "!0")] + [InlineData ("MyApp.GenericInstantiationCtorActivity", "System.Collections.Generic.List`1")] + [InlineData ("MyApp.ByRefCtorActivity", "System.Int32&")] + [InlineData ("MyApp.PointerCtorActivity", "System.Int32*")] + [InlineData ("MyApp.FunctionPointerCtorActivity", "delegate*")] + [InlineData ("MyApp.RectangularArrayCtorActivity", "System.String[,]")] + [InlineData ("MyApp.NestedRectangularArrayCtorActivity", "System.String[,][]")] + [InlineData ("MyApp.PointerArrayCtorActivity", "System.Int32*[]")] + [InlineData ("MyApp.FunctionPointerArrayCtorActivity", "delegate*[]")] + [InlineData ("MyApp.NonPublicExportUnsupportedActivity`1", "!0")] + public void UnsupportedConstructorParameter_IsDiagnosed (string managedTypeName, string parameterType) + { + var diagnostics = ScanConstructorDiagnostics (managedTypeName); + var diagnostic = Assert.Single (diagnostics); + Assert.Equal (ConstructorDiagnosticKind.UnsupportedParameterType, diagnostic.Kind); + Assert.Equal (parameterType, diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.SignedUnsignedCollisionActivity", "(I)V")] + [InlineData ("MyApp.AliasedTypeCollisionActivity", "(Lmy/app/AliasOne;)V")] + [InlineData ("MyApp.ExplicitRegisterCollisionActivity", "(I)V")] + [InlineData ("MyApp.JniSignatureCollisionActivity", "(I)V")] + [InlineData ("MyApp.NonPublicExportCollisionActivity", "(I)V")] + [InlineData ("MyApp.CrossAssemblyCollisionActivity", "(Lmy/app/CrossAssemblyParameter;)V")] + public void CollapsedConstructorSignatures_AreDiagnosed (string managedTypeName, string jniSignature) + { + var diagnostics = ScanConstructorDiagnostics (managedTypeName); + var diagnostic = Assert.Single (diagnostics); + Assert.Equal (ConstructorDiagnosticKind.AmbiguousJniSignature, diagnostic.Kind); + Assert.Equal (jniSignature, diagnostic.Detail); + } + + [Fact] + public void ConstructorDescriptor_DoesNotBorrowSameNamedTypeFromAnotherAssembly () + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.CrossAssemblyBorrowActivity")); + Assert.Equal (ConstructorDiagnosticKind.UnsupportedParameterType, diagnostic.Kind); + Assert.Equal ("MyApp.CrossAssemblyBorrowedParameter", diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.PrimitiveLookalikeActivity")] + [InlineData ("MyApp.XamarinActivationLookalikeActivity")] + [InlineData ("MyApp.JniActivationLookalikeActivity")] + public void PrimitiveAndActivationLookalikes_UseRegisteredJavaDescriptors (string managedTypeName) + { + var peer = ScanPeer (managedTypeName); + Assert.Empty (peer.ConstructorDiagnostics); + Assert.Null (peer.ActivationCtor); + } + + [Theory] + [InlineData ("my/app/GlobalType", ActivationCtorStyle.XamarinAndroid)] + [InlineData ("my/app/JiStylePeer", ActivationCtorStyle.JavaInterop)] + public void CanonicalActivationConstructors_AreRecognized (string javaName, ActivationCtorStyle style) + { + var peer = FindFixtureByJavaName (javaName); + Assert.NotNull (peer.ActivationCtor); + Assert.Equal (style, peer.ActivationCtor.Style); + Assert.Equal (peer.ManagedTypeName, peer.ActivationCtor.DeclaringTypeName); + } + + [Fact] + public void MissingBaseConstructor_IsDiagnosed () + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics ("MyApp.MissingBaseCtorActivity")); + Assert.Equal (ConstructorDiagnosticKind.MissingBaseConstructor, diagnostic.Kind); + Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.ExportMissingBaseCtorActivity")] + [InlineData ("MyApp.ExportEmptySuperMissingBaseActivity")] + [InlineData ("MyApp.NonPublicExportMissingBaseActivity")] + public void ExportWithoutSuperArgumentsString_MissingBaseConstructor_IsDiagnosed (string managedTypeName) + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); + Assert.Equal (ConstructorDiagnosticKind.MissingBaseConstructor, diagnostic.Kind); + Assert.Equal ("(Ljava/lang/String;)V", diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.InvalidSuperArgumentsActivity", "p1")] + [InlineData ("MyApp.NonCanonicalSuperArgumentZeroActivity", "p00")] + [InlineData ("MyApp.NonCanonicalSuperArgumentOneActivity", "p01")] + public void InvalidSuperArgumentsString_IsDiagnosed (string managedTypeName, string superArgumentsString) + { + var diagnostic = Assert.Single (ScanConstructorDiagnostics (managedTypeName)); + Assert.Equal (ConstructorDiagnosticKind.InvalidSuperArgumentsString, diagnostic.Kind); + Assert.Equal (superArgumentsString, diagnostic.Detail); + } + + [Theory] + [InlineData ("MyApp.EnumCtorActivity")] + [InlineData ("MyApp.ExplicitPointerCtorActivity")] + [InlineData ("MyApp.ExplicitRegisterCompatibleBaseActivity")] + [InlineData ("MyApp.JniSignatureCompatibleBaseActivity")] + [InlineData ("MyApp.ImplicitJniCompatibleBaseActivity")] + [InlineData ("MyApp.RegisterBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeRegisterValidActivity")] + [InlineData ("MyApp.JniBeforeExportValidActivity")] + [InlineData ("MyApp.ExportBeforeJniValidActivity")] + [InlineData ("MyApp.BindingPointerCtor")] + [InlineData ("MyApp.JaggedArrayCtorActivity")] + [InlineData ("MyApp.ValidSuperExpressionActivity")] + [InlineData ("MyApp.LexicalSuperArgumentsActivity")] + [InlineData ("MyApp.LambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ParenthesizedLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.TypedLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.LiteralCommaLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.GenericConstructionLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.NestedGenericLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.InstanceOfGenericLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ComparisonLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.ShiftLambdaSuperArgumentsActivity")] + [InlineData ("MyApp.MethodReferenceSuperArgumentsActivity")] + [InlineData ("MyApp.GenericMethodReferenceSuperArgumentsActivity")] + public void RepresentableOrExplicitConstructor_HasNoDiagnostic (string managedTypeName) + { + Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); + } + + [Theory] + [InlineData ("MyApp.RegisterBeforeExportValidActivity", null)] + [InlineData ("MyApp.ExportBeforeRegisterValidActivity", null)] + [InlineData ("MyApp.JniBeforeExportValidActivity", "")] + [InlineData ("MyApp.ExportBeforeJniValidActivity", "")] + public void ExplicitExportConstructor_PreservesRegistrationAndBaseForwarding (string managedTypeName, string? superArgumentsString) + { + var constructor = Assert.Single (ScanPeer (managedTypeName).JavaConstructors, c => c.JniSignature == "(I)V"); + Assert.Equal (superArgumentsString, constructor.SuperArgumentsString); + } + + [Fact] + public void ExportMappedConstructors_UseEffectiveJniSignaturesWithoutDiagnostics () + { + var peer = ScanPeer ("MyApp.ExportMappedCtorActivity"); + Assert.Empty (peer.ConstructorDiagnostics); + var signatures = peer.JavaConstructors.Select (c => c.JniSignature).ToList (); + Assert.Contains ("(Ljava/io/InputStream;)V", signatures); + Assert.Contains ("(Ljava/lang/CharSequence;)V", signatures); + Assert.Contains ("(Ljava/util/List;)V", signatures); + } + + [Theory] + [InlineData ("MyApp.UnsupportedExportConstructorOverloadsActivity")] + [InlineData ("MyApp.RegisterBeforeExportActivity")] + [InlineData ("MyApp.ExportBeforeRegisterActivity")] + public void UnsupportedExportConstructors_HaveNoSecondaryConstructorDiagnostics (string managedTypeName) + { + Assert.Empty (ScanConstructorDiagnostics (managedTypeName)); + } + + static System.Collections.Generic.IReadOnlyList ScanConstructorDiagnostics (string managedTypeName) => + ScanPeer (managedTypeName).ConstructorDiagnostics; + + static JavaPeerInfo ScanPeer (string managedTypeName) + { + using var scanner = new JavaPeerScanner (); + var testAssemblyDir = Path.GetDirectoryName (typeof (ConstructorDetectionTests).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory."); + using var fixtureReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var invalidFixturePath = Path.Combine (testAssemblyDir, "InvalidConstructorFixtures.dll"); + var lookalikeFixturePath = Path.Combine (testAssemblyDir, "LookalikeConstructorTypes.dll"); + using var invalidFixtureReader = new PEReader (File.OpenRead (invalidFixturePath)); + using var lookalikeFixtureReader = new PEReader (File.OpenRead (lookalikeFixturePath)); + var fixtureMetadata = fixtureReader.GetMetadataReader (); + var invalidFixtureMetadata = invalidFixtureReader.GetMetadataReader (); + var assemblies = new [] { + (fixtureMetadata.GetString (fixtureMetadata.GetAssemblyDefinition ().Name), fixtureReader), + (invalidFixtureMetadata.GetString (invalidFixtureMetadata.GetAssemblyDefinition ().Name), invalidFixtureReader), + ("LookalikeConstructorTypes", lookalikeFixtureReader), + }; + return Assert.Single (scanner.Scan (assemblies), p => p.ManagedTypeName == managedTypeName); + } + // --- Regression: HasMatchingManagedCtor semantics --- // These guard the safety net introduced for Java.Lang.Thread+RunnableImplementor: // when a Java ctor (e.g. ()V seeded from a [Register]'d base) has no matching diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 2baf133e290..c8ae803f862 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -1,15 +1,5 @@ using System; -namespace Android.Runtime -{ - public enum JniHandleOwnership - { - DoNotTransfer = 0, - TransferLocalRef = 1, - TransferGlobalRef = 2, - } -} - namespace Lookalike { [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] @@ -33,17 +23,6 @@ public ExportFieldAttribute (string name) { } namespace Java.Interop { - public struct JniObjectReference - { - public IntPtr Handle; - } - - public enum JniObjectReferenceOptions - { - None = 0, - Copy = 1, - CopyAndDispose = 2, - } } namespace Android.App diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj index c5f61f8238c..7b81de1019f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -12,6 +12,8 @@ + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a27e7464572..0703e88fd2f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1368,6 +1368,16 @@ protected UnsignedParamActivity (IntPtr handle, JniHandleOwnership transfer) : b public UnsignedParamActivity (ushort a, uint b, ulong c) { } } + + [Register ("my/app/CrossAssemblyParameter", DoNotGenerateAcw = true)] + public class CrossAssemblyCollisionParameter : Java.Lang.Object + { + } + + [Register ("my/app/BorrowedParameter", DoNotGenerateAcw = true)] + public class CrossAssemblyBorrowedParameter : Java.Lang.Object + { + } } namespace MyApp.Generic