Skip to content

Commit 18c29b7

Browse files
jpobstjonpryor
authored andcommitted
[Xamarin.Android.Tools.Bytecode] Add @not-null support (#526)
Context: #468 Add support for reading `@NotNull`/`@NonNull` annotations from Java bytecode and inserting them into `api.xml` as `//*/@not-null` and `//*/@return-not-null` attribute values: `@not-null` is for parameters and fields, while `@return-not-null` is emitted for method return values. Support for `generator` to consume the annotations will be done later. For example, given the Java code: // Java: public class Example { @nonnull public static final Creator<DirectAction> CREATOR = /* ... */ ; public void foo(@nonnull android.os.Handler handler) { /* ... */ } @nonnull public Context getContext() { /* ... */ } } then `Example.CREATOR` would contain: <field deprecated="not deprecated" final="true" name="CREATOR" jni-signature="Landroid/os/Parcelable$Creator;" not-null="true" static="true" transient="false" type="android.os.Parcelable.Creator" type-generic-aware="android.os.Parcelable.Creator&lt;android.app.DirectAction&gt;" visibility="public" volatile="false" /> The `handler` parameter for `Example.foo()` would contain: <parameter name="handler" type="android.os.Handler" jni-type="Landroid/os/Handler;" not-null="true" /> And the `Example.getContext()` method would contain: <method abstract="false" deprecated="not deprecated" final="true" name="getContext" jni-signature="()Landroid/content/Context;" bridge="false" native="false" return="android.content.Context" jni-return="Landroid/content/Context;" static="false" synchronized="false" synthetic="false" visibility="public" return-not-null="true" /> We look for the following annotations: * Android: * `android/annotation/NonNull` * `androidx/annotation/RecentlyNonNull` * Java: * `android/support/annotation/NonNull` * `edu/umd/cs/findbugs/annotations/NonNull` * `javax/annotation/Nonnull` * `javax/validation/constraints/NotNull` * `lombok/NonNull` * `org/eclipse/jdt/annotation/NonNull` * `org/jetbrains/annotations/NotNull` List of annotation types partially taken from: https://stackoverflow.com/questions/4963300/which-notnull-java-annotation-should-i-use
1 parent 1315bfe commit 18c29b7

File tree

11 files changed

+276
-17
lines changed

11 files changed

+276
-17
lines changed

src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApi.XmlModel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ public JavaField (JavaType parent)
176176
: base (parent)
177177
{
178178
}
179-
179+
180+
public bool NotNull { get; set; }
180181
public bool Transient { get; set; }
181182
public string Type { get; set; }
182183
public string TypeGeneric { get; set; }
@@ -248,6 +249,7 @@ public JavaMethod (JavaType parent)
248249
public bool Abstract { get; set; }
249250
public bool Native { get; set; }
250251
public string Return { get; set; }
252+
public bool ReturnNotNull { get; set; }
251253
public bool Synchronized { get; set; }
252254

253255
// Content of this value is not stable.
@@ -268,6 +270,7 @@ public JavaParameter (JavaMethodBase parent)
268270
public string Name { get; set; }
269271
public string Type { get; set; }
270272
public string JniType { get; set; }
273+
public bool NotNull { get; set; }
271274

272275
// Content of this value is not stable.
273276
public override string ToString ()

src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlGeneratorExtensions.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,13 @@ static void Save (this JavaField field, XmlWriter writer)
160160
null,
161161
null,
162162
null,
163-
null);
163+
null,
164+
field.NotNull);
164165
}
165166

166167
static void Save (this JavaConstructor ctor, XmlWriter writer)
167168
{
168-
SaveCommon (ctor, writer, "constructor", null, null, null, null, null, ctor.Type ?? ctor.Parent.FullName, null, null, null, ctor.TypeParameters, ctor.Parameters, ctor.Exceptions, ctor.ExtendedBridge, ctor.ExtendedJniReturn, ctor.ExtendedSynthetic);
169+
SaveCommon (ctor, writer, "constructor", null, null, null, null, null, ctor.Type ?? ctor.Parent.FullName, null, null, null, ctor.TypeParameters, ctor.Parameters, ctor.Exceptions, ctor.ExtendedBridge, ctor.ExtendedJniReturn, ctor.ExtendedSynthetic, null);
169170
}
170171

171172
static void Save (this JavaMethod method, XmlWriter writer)
@@ -217,7 +218,8 @@ static void Save (this JavaMethod method, XmlWriter writer)
217218
method.Exceptions,
218219
method.ExtendedBridge,
219220
method.ExtendedJniReturn,
220-
method.ExtendedSynthetic);
221+
method.ExtendedSynthetic,
222+
method.ReturnNotNull);
221223
}
222224

223225
static void SaveCommon (this JavaMember m, XmlWriter writer, string elementName,
@@ -227,7 +229,7 @@ static void SaveCommon (this JavaMember m, XmlWriter writer, string elementName,
227229
JavaTypeParameters typeParameters,
228230
IEnumerable<JavaParameter> parameters,
229231
IEnumerable<JavaException> exceptions,
230-
bool? extBridge, string jniReturn, bool? extSynthetic)
232+
bool? extBridge, string jniReturn, bool? extSynthetic, bool? notNull)
231233
{
232234
// If any of the parameters contain reference to non-public type, it cannot be generated.
233235
if (parameters != null && parameters.Any (p => p.ResolvedType.ReferencedType != null && string.IsNullOrEmpty (p.ResolvedType.ReferencedType.Visibility)))
@@ -248,6 +250,8 @@ static void SaveCommon (this JavaMember m, XmlWriter writer, string elementName,
248250
writer.WriteAttributeString ("return", ret);
249251
if (jniReturn != null)
250252
writer.WriteAttributeString ("jni-return", jniReturn);
253+
if (notNull.GetValueOrDefault ())
254+
writer.WriteAttributeString (m is JavaField ? "not-null" : "return-not-null", "true");
251255
writer.WriteAttributeString ("static", XmlConvert.ToString (m.Static));
252256
if (sync != null)
253257
writer.WriteAttributeString ("synchronized", sync);
@@ -276,6 +280,9 @@ static void SaveCommon (this JavaMember m, XmlWriter writer, string elementName,
276280
if (!string.IsNullOrEmpty (p.JniType)) {
277281
writer.WriteAttributeString ("jni-type", p.JniType);
278282
}
283+
if (p.NotNull == true) {
284+
writer.WriteAttributeString ("not-null", "true");
285+
}
279286
writer.WriteString ("\n ");
280287
writer.WriteFullEndElement ();
281288
}

src/Xamarin.Android.Tools.ApiXmlAdjuster/JavaApiXmlLoaderExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,11 @@ public static void Load (this JavaField field, XmlReader reader)
215215
{
216216
field.LoadMemberAttributes (reader);
217217
field.Transient = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "transient"));
218-
field.Volatile = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "transient"));
218+
field.Volatile = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "volatile"));
219219
field.Type = XmlUtil.GetRequiredAttribute (reader, "type");
220220
field.TypeGeneric = XmlUtil.GetRequiredAttribute (reader, "type-generic-aware");
221221
field.Value = reader.GetAttribute ("value");
222+
field.NotNull = reader.GetAttribute ("not-null") == "true";
222223

223224
reader.Skip ();
224225
}
@@ -277,9 +278,10 @@ public static void Load (this JavaMethod method, XmlReader reader)
277278
method.Abstract = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "abstract"));
278279
method.Native = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "native"));
279280
method.Return = XmlUtil.GetRequiredAttribute (reader, "return");
281+
method.ReturnNotNull = reader.GetAttribute ("return-not-null") == "true";
280282
method.Synchronized = XmlConvert.ToBoolean (XmlUtil.GetRequiredAttribute (reader, "synchronized"));
281283
XmlUtil.CheckExtraneousAttributes ("method", reader, "deprecated", "final", "name", "static", "visibility", "jni-signature", "jni-return", "synthetic", "bridge",
282-
"abstract", "native", "return", "synchronized");
284+
"abstract", "native", "return", "synchronized", "return-not-null");
283285
method.LoadMethodBase ("method", reader);
284286
}
285287

@@ -288,6 +290,7 @@ internal static void Load (this JavaParameter p, XmlReader reader)
288290
p.Name = XmlUtil.GetRequiredAttribute (reader, "name");
289291
p.Type = XmlUtil.GetRequiredAttribute (reader, "type");
290292
p.JniType = reader.GetAttribute ("jni-type");
293+
p.NotNull = reader.GetAttribute ("not-null") == "true";
291294
reader.Skip ();
292295
}
293296

src/Xamarin.Android.Tools.Bytecode/Annotation.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
using System.Linq;
34
using System.Text;
45

56
namespace Xamarin.Android.Tools.Bytecode
@@ -48,4 +49,31 @@ void Append (KeyValuePair<string, AnnotationElementValue> value)
4849
}
4950
}
5051
}
52+
53+
public sealed class ParameterAnnotation
54+
{
55+
public int ParameterIndex { get; }
56+
public IList<Annotation> Annotations { get; } = new List<Annotation> ();
57+
public ConstantPool ConstantPool { get; }
58+
59+
public ParameterAnnotation (ConstantPool constantPool, Stream stream, int index)
60+
{
61+
ConstantPool = constantPool;
62+
63+
ParameterIndex = index;
64+
65+
var ann_count = stream.ReadNetworkUInt16 ();
66+
67+
for (var i = 0; i < ann_count; ++i) {
68+
var a = new Annotation (constantPool, stream);
69+
Annotations.Add (a);
70+
}
71+
}
72+
73+
public override string ToString ()
74+
{
75+
var annotations = string.Join (", ", Annotations.Select (v => v.ToString ()));
76+
return $"Parameter{ParameterIndex}({annotations})";
77+
}
78+
}
5179
}

src/Xamarin.Android.Tools.Bytecode/AttributeInfo.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class AttributeInfo {
4949
public const string StackMapTable = "StackMapTable";
5050
public const string RuntimeVisibleAnnotations = "RuntimeVisibleAnnotations";
5151
public const string RuntimeInvisibleAnnotations = "RuntimeInvisibleAnnotations";
52+
public const string RuntimeInvisibleParameterAnnotations = "RuntimeInvisibleParameterAnnotations";
5253

5354
ushort nameIndex;
5455

@@ -115,6 +116,7 @@ static AttributeInfo CreateAttribute (string name, ConstantPool constantPool, us
115116
case MethodParameters: return new MethodParametersAttribute (constantPool, nameIndex, stream);
116117
case RuntimeVisibleAnnotations: return new RuntimeVisibleAnnotationsAttribute (constantPool, nameIndex, stream);
117118
case RuntimeInvisibleAnnotations: return new RuntimeInvisibleAnnotationsAttribute (constantPool, nameIndex, stream);
119+
case RuntimeInvisibleParameterAnnotations: return new RuntimeInvisibleParameterAnnotationsAttribute (constantPool, nameIndex, stream);
118120
case Signature: return new SignatureAttribute (constantPool, nameIndex, stream);
119121
case SourceFile: return new SourceFileAttribute (constantPool, nameIndex, stream);
120122
case StackMapTable: return new StackMapTableAttribute (constantPool, nameIndex, stream);
@@ -543,7 +545,32 @@ public RuntimeInvisibleAnnotationsAttribute (ConstantPool constantPool, ushort n
543545
public override string ToString ()
544546
{
545547
var annotations = string.Join (", ", Annotations.Select (v => v.ToString ()));
546-
return $"RuntimeVisibleAnnotations({annotations})";
548+
return $"RuntimeInvisibleAnnotations({annotations})";
549+
}
550+
}
551+
552+
553+
// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.19
554+
public sealed class RuntimeInvisibleParameterAnnotationsAttribute : AttributeInfo
555+
{
556+
public IList<ParameterAnnotation> Annotations { get; } = new List<ParameterAnnotation> ();
557+
558+
public RuntimeInvisibleParameterAnnotationsAttribute (ConstantPool constantPool, ushort nameIndex, Stream stream)
559+
: base (constantPool, nameIndex, stream)
560+
{
561+
var length = stream.ReadNetworkUInt32 ();
562+
var param_count = stream.ReadNetworkByte ();
563+
564+
for (var i = 0; i < param_count; ++i) {
565+
var a = new ParameterAnnotation (constantPool, stream, i);
566+
Annotations.Add (a);
567+
}
568+
}
569+
570+
public override string ToString ()
571+
{
572+
var annotations = string.Join (", ", Annotations.Select (v => v.ToString ()));
573+
return $"RuntimeInvisibleParameterAnnotationsAttribute({annotations})";
547574
}
548575
}
549576

src/Xamarin.Android.Tools.Bytecode/XmlClassDeclarationBuilder.cs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ XElement GetMethod (string element, string name, MethodInfo method, string retur
350350
new XAttribute ("bridge", (method.AccessFlags & MethodAccessFlags.Bridge) != 0),
351351
new XAttribute ("synthetic", (method.AccessFlags & MethodAccessFlags.Synthetic) != 0),
352352
new XAttribute ("jni-signature", method.Descriptor),
353+
GetNotNull (method),
353354
GetTypeParmeters (msig == null ? null : msig.TypeParameters),
354355
GetMethodParameters (method),
355356
GetExceptions (method));
@@ -382,6 +383,7 @@ static string GetVisibility (MethodAccessFlags accessFlags)
382383

383384
IEnumerable<XElement> GetMethodParameters (MethodInfo method)
384385
{
386+
var annotations = method.Attributes?.OfType<RuntimeInvisibleParameterAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
385387
var varargs = (method.AccessFlags & MethodAccessFlags.Varargs) != 0;
386388
var parameters = method.GetParameters ();
387389
for (int i = 0; i < parameters.Length; ++i) {
@@ -405,7 +407,8 @@ IEnumerable<XElement> GetMethodParameters (MethodInfo method)
405407
yield return new XElement ("parameter",
406408
new XAttribute ("name", p.Name),
407409
new XAttribute ("type", genericType),
408-
new XAttribute ("jni-type", p.Type.TypeSignature));
410+
new XAttribute ("jni-type", p.Type.TypeSignature),
411+
GetNotNull (annotations, i));
409412
}
410413
}
411414

@@ -421,6 +424,56 @@ IEnumerable<XElement> GetExceptions (MethodInfo method)
421424
}
422425
}
423426

427+
static XAttribute GetNotNull (MethodInfo method)
428+
{
429+
var annotations = method.Attributes?.OfType<RuntimeInvisibleAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
430+
431+
if (annotations?.Any (a => IsNotNullAnnotation (a)) == true)
432+
return new XAttribute ("return-not-null", "true");
433+
434+
return null;
435+
}
436+
437+
static XAttribute GetNotNull (IList<ParameterAnnotation> annotations, int parameterIndex)
438+
{
439+
var ann = annotations?.FirstOrDefault (a => a.ParameterIndex == parameterIndex)?.Annotations;
440+
441+
if (ann?.Any (a => IsNotNullAnnotation (a)) == true)
442+
return new XAttribute ("not-null", "true");
443+
444+
return null;
445+
}
446+
447+
static XAttribute GetNotNull (FieldInfo field)
448+
{
449+
var annotations = field.Attributes?.OfType<RuntimeInvisibleAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
450+
451+
if (annotations?.Any (a => IsNotNullAnnotation (a)) == true)
452+
return new XAttribute ("not-null", "true");
453+
454+
return null;
455+
}
456+
457+
static bool IsNotNullAnnotation (Annotation annotation)
458+
{
459+
// Android ones plus the list from here:
460+
// https://stackoverflow.com/questions/4963300/which-notnull-java-annotation-should-i-use
461+
switch (annotation.Type) {
462+
case "Landroid/annotation/NonNull;":
463+
case "Landroidx/annotation/RecentlyNonNull;":
464+
case "Ljavax/validation/constraints/NotNull;":
465+
case "Ledu/umd/cs/findbugs/annotations/NonNull;":
466+
case "Ljavax/annotation/Nonnull;":
467+
case "Lorg/jetbrains/annotations/NotNull;":
468+
case "Llombok/NonNull;":
469+
case "Landroid/support/annotation/NonNull;":
470+
case "Lorg/eclipse/jdt/annotation/NonNull;":
471+
return true;
472+
}
473+
474+
return false;
475+
}
476+
424477
IEnumerable<XElement> GetFields ()
425478
{
426479
foreach (var field in classFile.Fields.OrderBy (n => n.Name, StringComparer.OrdinalIgnoreCase)) {
@@ -437,6 +490,7 @@ IEnumerable<XElement> GetFields ()
437490
new XAttribute ("type", SignatureToJavaTypeName (field.Descriptor)),
438491
new XAttribute ("type-generic-aware", GetGenericType (field)),
439492
new XAttribute ("jni-signature", field.Descriptor),
493+
GetNotNull (field),
440494
GetValue (field),
441495
new XAttribute ("visibility", visibility),
442496
new XAttribute ("volatile", (field.AccessFlags & FieldAccessFlags.Volatile) != 0));
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
3+
using Xamarin.Android.Tools.Bytecode;
4+
5+
using NUnit.Framework;
6+
using System.Linq;
7+
8+
namespace Xamarin.Android.Tools.BytecodeTests
9+
{
10+
[TestFixture]
11+
public class NullableAnnotationTests : ClassFileFixture
12+
{
13+
[Test]
14+
public void RuntimeInvisibleAnnotations ()
15+
{
16+
var c = LoadClassFile ("NotNullClass.class");
17+
18+
// Method with no annotations
19+
var null_method = c.Methods.First (m => m.Name == "nullFunc");
20+
21+
Assert.AreEqual (0, null_method.Attributes.OfType<RuntimeInvisibleAnnotationsAttribute> ().Count ());
22+
Assert.AreEqual (0, null_method.Attributes.OfType<RuntimeInvisibleParameterAnnotationsAttribute> ().Count ());
23+
24+
// Method with not-null parameter and return value annotations
25+
var notnull_method = c.Methods.First (m => m.Name == "notNullFunc");
26+
var return_ann = notnull_method.Attributes.OfType<RuntimeInvisibleAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
27+
var param_ann = notnull_method.Attributes.OfType<RuntimeInvisibleParameterAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
28+
29+
Assert.NotNull (return_ann);
30+
Assert.IsTrue (return_ann.Any (a => a.Type == "Landroid/annotation/NonNull;"));
31+
32+
Assert.NotNull (param_ann);
33+
Assert.IsTrue (param_ann.Any (a => a.ParameterIndex == 0 && a.Annotations[0].Type == "Landroid/annotation/NonNull;"));
34+
35+
// Field with no annotations
36+
var null_field = c.Fields.First (f => f.Name == "nullField");
37+
38+
Assert.AreEqual (0, null_field.Attributes.OfType<RuntimeInvisibleAnnotationsAttribute> ().Count ());
39+
Assert.AreEqual (0, null_field.Attributes.OfType<RuntimeInvisibleParameterAnnotationsAttribute> ().Count ());
40+
41+
// Field with not-null annotation
42+
var notnull_field = c.Fields.First (f => f.Name == "notNullField");
43+
44+
var field_ann = notnull_method.Attributes.OfType<RuntimeInvisibleAnnotationsAttribute> ().FirstOrDefault ()?.Annotations;
45+
46+
Assert.NotNull (field_ann);
47+
Assert.IsTrue (field_ann.Any (a => a.Type == "Landroid/annotation/NonNull;"));
48+
}
49+
50+
[Test]
51+
public void NullableAnnotationOutput ()
52+
{
53+
var c = LoadClassFile ("NotNullClass.class");
54+
var builder = new XmlClassDeclarationBuilder (c);
55+
var xml = builder.ToXElement ();
56+
57+
var method = xml.Elements ("method").First (m => m.Attribute ("name").Value == "notNullFunc");
58+
Assert.AreEqual ("true", method.Attribute ("return-not-null").Value);
59+
60+
var parameter = method.Element ("parameter");
61+
Assert.AreEqual ("true", parameter.Attribute ("not-null").Value);
62+
63+
var field = xml.Elements ("field").First (f => f.Attribute ("name").Value == "notNullField");
64+
Assert.AreEqual ("true", field.Attribute ("not-null").Value);
65+
}
66+
}
67+
}
68+

tests/Xamarin.Android.Tools.Bytecode-Tests/Xamarin.Android.Tools.Bytecode-Tests.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<EmbeddedResource Include="Resources\*" />
3232
<EmbeddedResource Include="kotlin\**\*.class" />
3333

34+
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\NotNullClass.class" />
3435
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\IJavaInterface.class" />
3536
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\IParameterInterface.class" />
3637
<EmbeddedResource Include="$(IntermediateOutputPath)classes\com\xamarin\JavaAnnotation.class" />
@@ -51,14 +52,14 @@
5152
</ItemGroup>
5253

5354
<ItemGroup>
54-
<TestJar Include="java\**\*.java" Exclude="java\java\util\Collection.java" />
55+
<TestJar Include="java\**\*.java" Exclude="java\java\util\Collection.java,java\android\annotation\NonNull.java" />
5556
<TestJarNoParameters Include="java\java\util\Collection.java" />
5657
<TestKotlinJar Include="kotlin\**\*.kt" />
5758
</ItemGroup>
5859

5960
<Target Name="BuildClasses" BeforeTargets="BeforeBuild" Inputs="@(TestJar)" Outputs="@(TestJar->'$(IntermediateOutputPath)classes\%(RecursiveDir)%(Filename).class')">
6061
<MakeDir Directories="$(IntermediateOutputPath)classes" />
61-
<Exec Command="&quot;$(JavaCPath)&quot; -parameters $(_JavacSourceOptions) -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJar->'%(Identity)', ' ')" />
62+
<Exec Command="&quot;$(JavaCPath)&quot; -parameters $(_JavacSourceOptions) -g -d &quot;$(IntermediateOutputPath)classes&quot; java\android\annotation\NonNull.java @(TestJar->'%(Identity)', ' ')" />
6263
<Exec Command="&quot;$(JavaCPath)&quot; $(_JavacSourceOptions) -g -d &quot;$(IntermediateOutputPath)classes&quot; @(TestJarNoParameters->'%(Identity)', ' ')" />
6364
</Target>
6465

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package android.annotation;
2+
3+
import java.lang.annotation.*;
4+
import java.lang.reflect.*;
5+
6+
public @interface NonNull {
7+
}

0 commit comments

Comments
 (0)