[Xamarin.Android.Build.Tasks] XA4214, XA4215 in GenerateJavaStubs (#2349)
Fixes: https://github.com/xamarin/xamarin-android/issues/2266 Context: https://github.com/xamarin/xamarin-android/issues/1560 Java type names are not managed type names, and vice versa; there is a *mapping* between them, e.g. the Java class`java.lang.Object` is mapped (bound) as the managed type `Java.Lang.Object`. Ideally, there would be a 1:1 mapping between these names, e.g. `java.lang.Object` is *always* mapped to `Java.Lang.Object`, but *this is not required*, and occasionally there are "duplicate mappings"; for example, the Java class `java.util.HashSet` is mapped to `Java.Util.HashSet`, `Android.Runtime.JavaSet`, *and* `Android.Runtime.JavaSet<T>`: namespace Java.Util { [Register ("java/util/HashSet", DoNotGenerateAcw=true, ApiSince = 1)] public partial class HashSet {} } namespace Android.Runtime { [Register ("java/util/HashSet", DoNotGenerateAcw=true)] public partial class JavaSet {} [Register ("java/util/HashSet", DoNotGenerateAcw=true)] public partial class JavaSet<T> : JavaSet, ICollection<T> {} } Sometimes such 1:N mapping is acceptable, and sometimes it isn't. When is it acceptable? When Java types *are not generated*, as above. The above results in an *ambiguity* within `Java.Lang.Object.GetObject()` when given an instance of type `java.util.HashSet` -- which managed type should be used? -- but the ambiguity problems can be reduced by using `Object.GetObject<T>()` and various other means. When is it *not* acceptable? When two managed types would result in the same Java type name. For example: namespace Example { [Register ("type/Collision")] public partial class Bad1 : Java.Lang.Object {} [Register ("type/Collision")] public partial class Bad2 : Java.Lang.Object {} } Attempting to build an App project containing such a set of types would result in build errors: error : Duplicate Java type found! Mappings between managed types and Java types must be unique. First Type: 'Example.Bad1, Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'; Second Type: 'Example.Bad2, Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' (This doesn't happen with `Java.Util.HashSet` and `Android.Runtime.JavaSet` because they use `[Register(DoNotGenerateAcw=true)]`.) There are three issues that need to be addressed: 1. The error and warning messages do not contain codes or associated documentation. 2. The error and warning messages are not *actionable*. 3. The error checking is occasionally missed. (3) is a problem: Consider if we have two assemblies with the same Java name and same managed name: // This file present in two seprate assemblies namespace Example { [Register ("example.EmptyClass")] public class EmptyClass : Java.Lang.Object {} } Building such an app will result in an ambiguity warning: warning : Duplicate managed type found! Mappings between managed types and Java types must be unique. First Type: 'Example.EmptyClass, Assembly1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'; Second Type: 'Example.EmptyClass, Assembly2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. However, this should *also* elicit an *error* because of the Java- side type name collision, but the error isn't emitted! Fix these three issues: 1. Emit an XA4214 warning when identical managed types in different assemblies are found, such as `Example.EmptyClass`. It's only a warning because certain constructs such as `<fragment type="Example.EmptyClass, Assembly1">` support assembly-qualified names. 2. Emit an XA4215 error when identical Java types are encountered. 3. Improve the messages to be actionable. 4. Add documentation for XA4214 and XA4215. 5. Fix the error checking so that if an XA4214 warning is generated, the XA4215 error condition is *also* checked. The `Example.EmptyClass` scenario will result in warning XA4214: warning XA4214: The managed type `Example.EmptyClass` exists in multiple assemblies: Assembly1, Assembly2. Please refactor the managed type names in these assemblies so that they are not identical. warning XA4214: References to the type `Example.EmptyClass` will refer to `Example.EmptyClass, Assembly1`. The `Example.EmptyClass` scenario will also result in error XA4215: error XA4215: The Jvaa type `example.EmptyClass` is generated by more than one managed type. Please change the [Register] attribute so that the same Java type is not emitted. error XA4215: `example.EmptyClass` generated by: Example.EmptyClass, Assembly1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null error XA4215: `example.EmptyClass` generated by: Example.EmptyClass, Assembly2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null A possible future enhancement could be to move the warning into the `<ConvertCustomView/>` task so that it only appears when a managed type name from an Android resource file matches more than one line in the `acw-map`. That way, if a user wants to solve the issue by using `[Register]` attributes or assembly-qualified type names rather than renaming the managed types, the build process will automatically stop showing the warning.
This commit is contained in:
Родитель
f4f39906c8
Коммит
efbec22767
|
@ -60,6 +60,8 @@
|
|||
|
||||
### XA4xxx Code Generation
|
||||
|
||||
+ [XA4214](xa4214.md): The managed type \`Library1.Class1\` exists in multiple assemblies: Library1, Library2. Please refactor the managed type names in these assemblies so that they are not identical.
|
||||
+ [XA4215](xa4215.md): The Java type \`com.contoso.library1.Class1\` is generated by more than one managed type. Please change the \[Register\] attribute so that the same Java type is not emitted.
|
||||
+ [XA4301](xa4301.md): : Apk already contains the item `xxx`.
|
||||
|
||||
### XA5xxx GCC and toolchain
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Compiler Warning XA4214
|
||||
|
||||
If two or more C# types from different assemblies inherit from
|
||||
`Java.Lang.Object` and share the same fully qualified name, that name will
|
||||
always refer to just *one* of the types when used in Android resource files.
|
||||
|
||||
The typical way to resolve this warning is to rename the types so that each
|
||||
fully qualified name only exists in one assembly.
|
||||
|
||||
Another option is to add `[Register]` attributes on the conflicting managed
|
||||
types so that each one has a unique Java type name.
|
||||
|
||||
A third option is to qualify the type names with the assembly name in the
|
||||
Android resource files. For example, use the assembly-qualified name
|
||||
`Library1.Class1, Library` rather than just `Library1.Class1`. This only works
|
||||
in places where the XML schema allows a type name within an XML attribute. One
|
||||
example is the `class` attribute on `fragment` elements.
|
||||
|
||||
If you choose to use `[Register]` attributes or assembly-qualified names rather
|
||||
than renaming the managed types, then you can hide the warnings either by adding
|
||||
the `/nowarn:XA4214` switch to the MSBuild command line or by adding `XA4214` to
|
||||
the `$(NoWarn)` property in your .csproj file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);XA4214</NoWarn>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Example message:
|
||||
|
||||
* <code>warning XA4214: The managed type \`Library1.Class1\` exists in multiple assemblies: Library1, Library2. Please refactor the managed type names in these assemblies so that they are not identical.</code>
|
||||
<code>warning XA4214: References to the type \`Library1.Class1\` will refer to \`Library1.Class1, Library1\`.</code>
|
|
@ -0,0 +1,13 @@
|
|||
# Compiler Error XA4215
|
||||
|
||||
This error indicates that two or more C# types are emitting the same fully
|
||||
qualified Java type name.
|
||||
|
||||
To resolve this error, change the `[Register]` attribute on one of the C# types
|
||||
to a different Java type name.
|
||||
|
||||
Example message:
|
||||
|
||||
* <code>error XA4215: The Java type \`com.contoso.library1.Class1\` is generated by more than one managed type. Please change the \[Register\] attribute so that the same Java type is not emitted.</code>
|
||||
<code>error XA4215: \`com.contoso.library1.Class1\` generated by: Library1.Class1, Library1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</code>
|
||||
<code>error XA4215: \`com.contoso.library1.Class1\` generated by: Library1.Class1, Library2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</code>
|
|
@ -152,6 +152,10 @@ namespace Xamarin.Android.Tasks
|
|||
// We need to save a map of .NET type -> ACW type for resource file fixups
|
||||
var managed = new Dictionary<string, TypeDefinition> (java_types.Length, StringComparer.Ordinal);
|
||||
var java = new Dictionary<string, TypeDefinition> (java_types.Length, StringComparer.Ordinal);
|
||||
|
||||
var managedConflicts = new Dictionary<string, List<string>> (0, StringComparer.Ordinal);
|
||||
var javaConflicts = new Dictionary<string, List<string>> (0, StringComparer.Ordinal);
|
||||
|
||||
// Allocate a MemoryStream with a reasonable guess at its capacity
|
||||
using (var stream = new MemoryStream (java_types.Length * 32))
|
||||
using (var acw_map = new StreamWriter (stream)) {
|
||||
|
@ -165,45 +169,60 @@ namespace Xamarin.Android.Tasks
|
|||
acw_map.WriteLine ();
|
||||
|
||||
TypeDefinition conflict;
|
||||
bool hasConflict = false;
|
||||
if (managed.TryGetValue (managedKey, out conflict)) {
|
||||
Log.LogWarning (
|
||||
"Duplicate managed type found! Mappings between managed types and Java types must be unique. " +
|
||||
"First Type: '{0}'; Second Type: '{1}'.",
|
||||
conflict.GetAssemblyQualifiedName (),
|
||||
type.GetAssemblyQualifiedName ());
|
||||
Log.LogWarning (
|
||||
"References to the type '{0}' will refer to '{1}'.",
|
||||
managedKey, conflict.GetAssemblyQualifiedName ());
|
||||
continue;
|
||||
if (!managedConflicts.TryGetValue (managedKey, out var list))
|
||||
managedConflicts.Add (managedKey, list = new List<string> { conflict.GetPartialAssemblyName () });
|
||||
list.Add (type.GetPartialAssemblyName ());
|
||||
hasConflict = true;
|
||||
}
|
||||
if (java.TryGetValue (javaKey, out conflict)) {
|
||||
Log.LogError (
|
||||
"Duplicate Java type found! Mappings between managed types and Java types must be unique. " +
|
||||
"First Type: '{0}'; Second Type: '{1}'",
|
||||
conflict.GetAssemblyQualifiedName (),
|
||||
type.GetAssemblyQualifiedName ());
|
||||
if (!javaConflicts.TryGetValue (javaKey, out var list))
|
||||
javaConflicts.Add (javaKey, list = new List<string> { conflict.GetAssemblyQualifiedName () });
|
||||
list.Add (type.GetAssemblyQualifiedName ());
|
||||
success = false;
|
||||
continue;
|
||||
hasConflict = true;
|
||||
}
|
||||
if (!hasConflict) {
|
||||
managed.Add (managedKey, type);
|
||||
java.Add (javaKey, type);
|
||||
|
||||
managed.Add (managedKey, type);
|
||||
java.Add (javaKey, type);
|
||||
acw_map.Write (managedKey);
|
||||
acw_map.Write (';');
|
||||
acw_map.Write (javaKey);
|
||||
acw_map.WriteLine ();
|
||||
|
||||
acw_map.Write (managedKey);
|
||||
acw_map.Write (';');
|
||||
acw_map.Write (javaKey);
|
||||
acw_map.WriteLine ();
|
||||
|
||||
acw_map.Write (JavaNativeTypeManager.ToCompatJniName (type).Replace ('/', '.'));
|
||||
acw_map.Write (';');
|
||||
acw_map.Write (javaKey);
|
||||
acw_map.WriteLine ();
|
||||
acw_map.Write (JavaNativeTypeManager.ToCompatJniName (type).Replace ('/', '.'));
|
||||
acw_map.Write (';');
|
||||
acw_map.Write (javaKey);
|
||||
acw_map.WriteLine ();
|
||||
}
|
||||
}
|
||||
|
||||
acw_map.Flush ();
|
||||
MonoAndroidHelper.CopyIfStreamChanged (stream, AcwMapFile);
|
||||
}
|
||||
|
||||
foreach (var kvp in managedConflicts) {
|
||||
Log.LogCodedWarning (
|
||||
"XA4214",
|
||||
"The managed type `{0}` exists in multiple assemblies: {1}. " +
|
||||
"Please refactor the managed type names in these assemblies so that they are not identical.",
|
||||
kvp.Key,
|
||||
string.Join (", ", kvp.Value));
|
||||
Log.LogCodedWarning ("XA4214", "References to the type `{0}` will refer to `{0}, {1}`.", kvp.Key, kvp.Value [0]);
|
||||
}
|
||||
|
||||
foreach (var kvp in javaConflicts) {
|
||||
Log.LogCodedError (
|
||||
"XA4215",
|
||||
"The Java type `{0}` is generated by more than one managed type. " +
|
||||
"Please change the [Register] attribute so that the same Java type is not emitted.",
|
||||
kvp.Key);
|
||||
foreach (var typeName in kvp.Value)
|
||||
Log.LogCodedError ("XA4215", " `{0}` generated by: {1}", kvp.Key, typeName);
|
||||
}
|
||||
|
||||
// Step 3 - Merge [Activity] and friends into AndroidManifest.xml
|
||||
var manifest = new ManifestDocument (ManifestTemplate, this.Log);
|
||||
|
||||
|
|
|
@ -3364,6 +3364,94 @@ AAAAAAAAAAAAPQAAAE1FVEEtSU5GL01BTklGRVNULk1GUEsBAhQAFAAICAgAJZFnS7uHtAn+AQAA
|
|||
Assert.IsFalse (StringAssertEx.ContainsText (content, type), $"`{type}` should not exist in `AndroidManifest.xml`!");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateJCWNames ()
|
||||
{
|
||||
var source = @"[Android.Runtime.Register (""examplelib.EmptyClass"")] public class EmptyClass : Java.Lang.Object { }";
|
||||
var library1 = new XamarinAndroidLibraryProject () {
|
||||
ProjectName = "Library1",
|
||||
Sources = {
|
||||
new BuildItem.Source ("EmptyClass.cs") {
|
||||
TextContent = () => source
|
||||
}
|
||||
}
|
||||
};
|
||||
var library2 = new XamarinAndroidLibraryProject () {
|
||||
ProjectName = "Library2",
|
||||
Sources = {
|
||||
new BuildItem.Source ("EmptyClass.cs") {
|
||||
TextContent = () => source
|
||||
}
|
||||
}
|
||||
};
|
||||
var app = new XamarinAndroidApplicationProject {
|
||||
ProjectName = "App1",
|
||||
References = {
|
||||
new BuildItem ("ProjectReference", "..\\Library1\\Library1.csproj"),
|
||||
new BuildItem ("ProjectReference", "..\\Library2\\Library2.csproj")
|
||||
},
|
||||
};
|
||||
var projectPath = Path.Combine ("temp", TestName);
|
||||
using (var lib1b = CreateDllBuilder (Path.Combine (projectPath, library1.ProjectName), cleanupAfterSuccessfulBuild: false))
|
||||
using (var lib2b = CreateDllBuilder (Path.Combine (projectPath, library2.ProjectName), cleanupAfterSuccessfulBuild: false)) {
|
||||
Assert.IsTrue (lib1b.Build (library1), "Build of Library1 should have succeeded");
|
||||
Assert.IsTrue (lib2b.Build (library2), "Build of Library2 should have succeeded");
|
||||
using (var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName))) {
|
||||
appb.ThrowOnBuildFailure = false;
|
||||
Assert.IsFalse (appb.Build (app), "Build of App1 should have failed");
|
||||
IEnumerable<string> errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215"));
|
||||
Assert.NotNull (errors, "Error should be XA4215");
|
||||
StringAssertEx.Contains ("EmptyClass", errors, "Error should mention the conflicting type name");
|
||||
StringAssertEx.Contains ("Library1", errors, "Error should mention all of the assemblies with conflicts");
|
||||
StringAssertEx.Contains ("Library2", errors, "Error should mention all of the assemblies with conflicts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DuplicateManagedNames ()
|
||||
{
|
||||
var source = @"public class EmptyClass : Java.Lang.Object { }";
|
||||
var library1 = new XamarinAndroidLibraryProject () {
|
||||
ProjectName = "Library1",
|
||||
Sources = {
|
||||
new BuildItem.Source ("EmptyClass.cs") {
|
||||
TextContent = () => source
|
||||
}
|
||||
}
|
||||
};
|
||||
var library2 = new XamarinAndroidLibraryProject () {
|
||||
ProjectName = "Library2",
|
||||
Sources = {
|
||||
new BuildItem.Source ("EmptyClass.cs") {
|
||||
TextContent = () => source
|
||||
}
|
||||
}
|
||||
};
|
||||
var app = new XamarinAndroidApplicationProject {
|
||||
ProjectName = "App1",
|
||||
References = {
|
||||
new BuildItem ("ProjectReference", "..\\Library1\\Library1.csproj"),
|
||||
new BuildItem ("ProjectReference", "..\\Library2\\Library2.csproj")
|
||||
},
|
||||
};
|
||||
var projectPath = Path.Combine ("temp", TestName);
|
||||
using (var lib1b = CreateDllBuilder (Path.Combine (projectPath, library1.ProjectName), cleanupAfterSuccessfulBuild: false))
|
||||
using (var lib2b = CreateDllBuilder (Path.Combine (projectPath, library2.ProjectName), cleanupAfterSuccessfulBuild: false)) {
|
||||
Assert.IsTrue (lib1b.Build (library1), "Build of Library1 should have succeeded");
|
||||
Assert.IsTrue (lib2b.Build (library2), "Build of Library2 should have succeeded");
|
||||
using (var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName))) {
|
||||
appb.ThrowOnBuildFailure = false;
|
||||
Assert.IsTrue (appb.Build (app), "Build of App1 should have succeeded");
|
||||
IEnumerable<string> warnings = appb.LastBuildOutput.Where (x => x.Contains ("warning XA4214"));
|
||||
Assert.NotNull (warnings, "Warning should be XA4214");
|
||||
StringAssertEx.Contains ("EmptyClass", warnings, "Warning should mention the conflicting type name");
|
||||
StringAssertEx.Contains ("Library1", warnings, "Warning should mention all of the assemblies with conflicts");
|
||||
StringAssertEx.Contains ("Library2", warnings, "Warning should mention all of the assemblies with conflicts");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче