From 8fdd018d393a65ce677a5270248b040f5353655d Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sat, 23 Feb 2019 09:22:37 -0500 Subject: [PATCH] Implement #annotations (#7326) --- spec/compiler/semantic/annotation_spec.cr | 988 +++++++++++++++------- src/compiler/crystal/macros.cr | 33 +- src/compiler/crystal/macros/methods.cr | 18 + src/compiler/crystal/semantic/ast.cr | 28 +- src/compiler/crystal/types.cr | 12 +- 5 files changed, 741 insertions(+), 338 deletions(-) diff --git a/spec/compiler/semantic/annotation_spec.cr b/spec/compiler/semantic/annotation_spec.cr index 79bb4eda6680..e8a67fdf5f96 100644 --- a/spec/compiler/semantic/annotation_spec.cr +++ b/spec/compiler/semantic/annotation_spec.cr @@ -12,434 +12,786 @@ describe "Semantic: annotation" do type.name.should eq("Foo") end - it "can't find annotation in module" do - assert_type(%( - annotation Foo - end + describe "#annotations" do + it "returns an empty array if there are none defined" do + assert_type(%( + annotation Foo + end - module Moo - end + module Moo + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} - )) { char } - end + {% if Moo.annotations(Foo).size == 0 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "can't find annotation in module, when other annotations are present" do - assert_type(%( - annotation Foo - end + it "finds annotations on a module" do + assert_type(%( + annotation Foo + end - annotation Bar - end + @[Foo] + @[Foo] + module Moo + end - @[Bar] - module Moo - end + {% if Moo.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} - )) { char } - end + it "uses annotations value, positional" do + assert_type(%( + annotation Foo + end - it "finds annotation in module" do - assert_type(%( - annotation Foo - end + @[Foo(1)] + @[Foo(2)] + module Moo + end - @[Foo] - module Moo - end + {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "uses annotations value, keyword" do + assert_type(%( + annotation Foo + end + + @[Foo(x: 1)] + @[Foo(x: 2)] + module Moo + end + + {% if Moo.annotations(Foo)[0][:x] == 1 && Moo.annotations(Foo)[1][:x] == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "finds annotations in class" do + assert_type(%( + annotation Foo + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + @[Foo] + @[Foo] + @[Foo] + class Moo + end + + {% if Moo.annotations(Foo).size == 3 %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "uses annotation value, positional" do - assert_type(%( - annotation Foo - end + it "finds annotations in struct" do + assert_type(%( + annotation Foo + end - @[Foo(1)] - module Moo - end + @[Foo] + @[Foo] + @[Foo] + @[Foo] + struct Moo + end - {% if Moo.annotation(Foo)[0] == 1 %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.annotations(Foo).size == 4 %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "uses annotation value, keyword" do - assert_type(%( - annotation Foo - end + it "finds annotations in enum" do + assert_type(%( + annotation Foo + end - @[Foo(x: 1)] - module Moo - end + @[Foo] + enum Moo + A = 1 + end - {% if Moo.annotation(Foo)[:x] == 1 %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.annotations(Foo).size == 1 %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "finds annotation in class" do - assert_type(%( - annotation Foo - end + it "finds annotations in lib" do + assert_type(%( + annotation Foo + end - @[Foo] - class Moo - end + @[Foo] + @[Foo] + lib Moo + A = 1 + end + + {% if Moo.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "can't find annotations in instance var" do + assert_type(%( + annotation Foo + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + class Moo + @x : Int32 = 1 + + def foo + {% unless @type.instance_vars.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo + )) { char } + end + + it "can't find annotations in instance var, when other annotations are present" do + assert_type(%( + annotation Foo + end + + annotation Bar + end + + class Moo + @[Bar] + @x : Int32 = 1 + + def foo + {% unless @type.instance_vars.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo + )) { char } + end + + it "finds annotations in instance var (declaration)" do + assert_type(%( + annotation Foo + end + + class Moo + @[Foo] + @[Foo] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo )) { int32 } - end + end - it "finds annotation in struct" do - assert_type(%( - annotation Foo - end + it "finds annotations in instance var (declaration, generic)" do + assert_type(%( + annotation Foo + end - @[Foo] - struct Moo - end + class Moo(T) + @[Foo] + @x : T - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + def initialize(@x : T) + end + + def foo + {% if @type.instance_vars.first.annotations(Foo).size == 1 %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new(1).foo )) { int32 } - end + end - it "finds annotation in enum" do - assert_type(%( - annotation Foo - end + it "collects annotations values in type" do + assert_type(%( + annotation Foo + end - @[Foo] - enum Moo - A = 1 - end + @[Foo(1)] + module Moo + end + + @[Foo(2)] + module Moo + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "finds annotation in lib" do - assert_type(%( - annotation Foo - end + it "overrides annotations value in type" do + assert_type(%( + annotation Foo + end - @[Foo] - lib Moo - A = 1 - end + class Moo + @[Foo(1)] + @x : Int32 = 1 + end + + class Moo + @[Foo(2)] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotations(Foo).size == 1 && @type.instance_vars.first.annotations(Foo)[0][0] == 2 %} + 1 + {% else %} + 'a' + {% end %} + end + end - {% if Moo.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + Moo.new.foo )) { int32 } - end + end - it "can't find annotation in instance var" do - assert_type(%( - annotation Foo - end + it "adds annotations on def" do + assert_type(%( + annotation Foo + end - class Moo - @x : Int32 = 1 + class Moo + @[Foo] + @[Foo] + def foo + end + end + + {% if Moo.methods.first.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - def foo - {% if @type.instance_vars.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + it "can't find annotations on def" do + assert_type(%( + annotation Foo end - end - Moo.new.foo - )) { char } + class Moo + def foo + end + end + + {% unless Moo.methods.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + )) { char } + end + + it "can't find annotations on def, when other annotations are present" do + assert_type(%( + annotation Foo + end + + annotation Bar + end + + class Moo + @[Bar] + def foo + end + end + + {% unless Moo.methods.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + )) { char } + end end - it "can't find annotation in instance var, when other annotations are present" do - assert_type(%( - annotation Foo - end + describe "#annotation" do + it "can't find annotation in module" do + assert_type(%( + annotation Foo + end - annotation Bar - end + module Moo + end - class Moo - @[Bar] - @x : Int32 = 1 + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + )) { char } + end - def foo - {% if @type.instance_vars.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + it "can't find annotation in module, when other annotations are present" do + assert_type(%( + annotation Foo + end + + annotation Bar end - end - Moo.new.foo + @[Bar] + module Moo + end + + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { char } - end + end - it "finds annotation in instance var (declaration)" do - assert_type(%( - annotation Foo - end + it "finds annotation in module" do + assert_type(%( + annotation Foo + end - class Moo @[Foo] - @x : Int32 = 1 + module Moo + end - def foo - {% if @type.instance_vars.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "uses annotation value, positional" do + assert_type(%( + annotation Foo end - end - Moo.new.foo + @[Foo(1)] + module Moo + end + + {% if Moo.annotation(Foo)[0] == 1 %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "finds annotation in instance var (assignment)" do - assert_type(%( - annotation Foo - end + it "uses annotation value, keyword" do + assert_type(%( + annotation Foo + end + + @[Foo(x: 1)] + module Moo + end + + {% if Moo.annotation(Foo)[:x] == 1 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "finds annotation in class" do + assert_type(%( + annotation Foo + end - class Moo @[Foo] - @x = 1 + class Moo + end + + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - def foo - {% if @type.instance_vars.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + it "finds annotation in struct" do + assert_type(%( + annotation Foo end - end - Moo.new.foo + @[Foo] + struct Moo + end + + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "finds annotation in instance var (declaration, generic)" do - assert_type(%( - annotation Foo - end + it "finds annotation in enum" do + assert_type(%( + annotation Foo + end - class Moo(T) @[Foo] - @x : T + enum Moo + A = 1 + end + + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - def initialize(@x : T) + it "finds annotation in lib" do + assert_type(%( + annotation Foo end - def foo - {% if @type.instance_vars.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + @[Foo] + lib Moo + A = 1 end - end - Moo.new(1).foo + {% if Moo.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "overrides annotation value in type" do - assert_type(%( - annotation Foo - end + it "can't find annotation in instance var" do + assert_type(%( + annotation Foo + end - @[Foo(1)] - module Moo - end + class Moo + @x : Int32 = 1 - @[Foo(2)] - module Moo - end + def foo + {% if @type.instance_vars.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + end + end - {% if Moo.annotation(Foo)[0] == 2 %} - 1 - {% else %} - 'a' - {% end %} + Moo.new.foo + )) { char } + end + + it "can't find annotation in instance var, when other annotations are present" do + assert_type(%( + annotation Foo + end + + annotation Bar + end + + class Moo + @[Bar] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo + )) { char } + end + + it "finds annotation in instance var (declaration)" do + assert_type(%( + annotation Foo + end + + class Moo + @[Foo] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo )) { int32 } - end + end - it "overrides annotation in instance var" do - assert_type(%( - annotation Foo - end + it "finds annotation in instance var (assignment)" do + assert_type(%( + annotation Foo + end + + class Moo + @[Foo] + @x = 1 + + def foo + {% if @type.instance_vars.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo + )) { int32 } + end + + it "finds annotation in instance var (declaration, generic)" do + assert_type(%( + annotation Foo + end + + class Moo(T) + @[Foo] + @x : T + + def initialize(@x : T) + end + + def foo + {% if @type.instance_vars.first.annotations(Foo) %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new(1).foo + )) { int32 } + end + + it "overrides annotation value in type" do + assert_type(%( + annotation Foo + end - class Moo @[Foo(1)] - @x : Int32 = 1 - end + module Moo + end - class Moo @[Foo(2)] - @x : Int32 = 1 + module Moo + end + + {% if Moo.annotation(Foo)[0] == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - def foo - {% if @type.instance_vars.first.annotation(Foo)[0] == 2 %} - 1 - {% else %} - 'a' - {% end %} + it "overrides annotation in instance var" do + assert_type(%( + annotation Foo end - end - Moo.new.foo + class Moo + @[Foo(1)] + @x : Int32 = 1 + end + + class Moo + @[Foo(2)] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotation(Foo)[0] == 2 %} + 1 + {% else %} + 'a' + {% end %} + end + end + + Moo.new.foo )) { int32 } - end + end - it "errors if annotation doesn't exist" do - assert_error %( - @[DoesntExist] - class Moo - end + it "errors if annotation doesn't exist" do + assert_error %( + @[DoesntExist] + class Moo + end ), - "undefined constant DoesntExist" - end + "undefined constant DoesntExist" + end - it "errors if annotation doesn't point to an annotation type" do - assert_error %( - @[Int32] - class Moo - end + it "errors if annotation doesn't point to an annotation type" do + assert_error %( + @[Int32] + class Moo + end ), - "Int32 is not an annotation, it's a struct" - end + "Int32 is not an annotation, it's a struct" + end - it "errors if using annotation other than ThreadLocal for class vars" do - assert_error %( - annotation Foo - end + it "errors if using annotation other than ThreadLocal for class vars" do + assert_error %( + annotation Foo + end - class Moo - @[Foo] - @@x = 0 - end + class Moo + @[Foo] + @@x = 0 + end ), - "class variables can only be annotated with ThreadLocal" - end + "class variables can only be annotated with ThreadLocal" + end - it "adds annotation on def" do - assert_type(%( - annotation Foo - end + it "adds annotation on def" do + assert_type(%( + annotation Foo + end - class Moo - @[Foo] - def foo + class Moo + @[Foo] + def foo + end end - end - {% if Moo.methods.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.methods.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { int32 } - end + end - it "can't find annotation on def" do - assert_type(%( - annotation Foo - end + it "can't find annotation on def" do + assert_type(%( + annotation Foo + end - class Moo - def foo + class Moo + def foo + end end - end - {% if Moo.methods.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.methods.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { char } - end + end - it "can't find annotation on def, when other annotations are present" do - assert_type(%( - annotation Foo - end + it "can't find annotation on def, when other annotations are present" do + assert_type(%( + annotation Foo + end - annotation Bar - end + annotation Bar + end - class Moo - @[Bar] - def foo + class Moo + @[Bar] + def foo + end end - end - {% if Moo.methods.first.annotation(Foo) %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.methods.first.annotation(Foo) %} + 1 + {% else %} + 'a' + {% end %} )) { char } - end + end - it "errors if using invalid annotation on fun" do - assert_error %( - annotation Foo - end + it "errors if using invalid annotation on fun" do + assert_error %( + annotation Foo + end - @[Foo] - fun foo : Void - end + @[Foo] + fun foo : Void + end ), - "funs can only be annotated with: NoInline, AlwaysInline, Naked, ReturnsTwice, Raises, CallConvention" - end - - it "doesn't carry link attribute from lib to fun" do - semantic(%( - @[Link("foo")] - lib LibFoo - fun foo - end + "funs can only be annotated with: NoInline, AlwaysInline, Naked, ReturnsTwice, Raises, CallConvention" + end + + it "doesn't carry link attribute from lib to fun" do + semantic(%( + @[Link("foo")] + lib LibFoo + fun foo + end )) + end end end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 8a99b62484b9..c2a797f0265e 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -852,9 +852,14 @@ module Crystal::Macros def has_default_value? : BoolLiteral end - # Returns any `Annotation` with the given `type` - # attached to this variable. - def annotation(type : TypeNode) : Annotation + # Returns the last `Annotation` with the given `type` + # attached to this variable or `NilLiteral` if there are none. + def annotation(type : TypeNode) : Annotation | NilLiteral + end + + # Returns an array of annotations with the given `type` + # attached to this variable, or an empty `ArrayLiteral` if there are none. + def annotations(type : TypeNode) : ArrayLiteral(Annotation) end end @@ -1111,9 +1116,14 @@ module Crystal::Macros def visibility : SymbolLiteral end - # Returns any `Annotation` with the given `type` - # attached to this method. - def annotation(type : TypeNode) : Annotation + # Returns the last `Annotation` with the given `type` + # attached to this variable or `NilLiteral` if there are none. + def annotation(type : TypeNode) : Annotation | NilLiteral + end + + # Returns an array of annotations with the given `type` + # attached to this variable, or an empty `ArrayLiteral` if there are none. + def annotations(type : TypeNode) : ArrayLiteral(Annotation) end end @@ -1722,9 +1732,14 @@ module Crystal::Macros def has_attribute?(name : StringLiteral | SymbolLiteral) : BoolLiteral end - # Returns any `Annotation` with the given `type` - # attached to this type. - def annotation(type : TypeNode) : Annotation + # Returns the last `Annotation` with the given `type` + # attached to this variable or `NilLiteral` if there are none. + def annotation(type : TypeNode) : Annotation | NilLiteral + end + + # Returns an array of annotations with the given `type` + # attached to this variable, or an empty `ArrayLiteral` if there are none. + def annotations(type : TypeNode) : ArrayLiteral(Annotation) end # Returns the number of elements in this tuple type or tuple metaclass type. diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index f240a97c3d45..72aa0445fd72 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -1110,6 +1110,12 @@ module Crystal fetch_annotation(self, method, args) do |type| self.var.annotation(type) end + when "annotations" + fetch_annotation(self, method, args) do |type| + annotations = self.var.annotations(type) + return ArrayLiteral.new if annotations.nil? + ArrayLiteral.map(annotations, &.itself) + end else super end @@ -1295,6 +1301,12 @@ module Crystal fetch_annotation(self, method, args) do |type| self.annotation(type) end + when "annotations" + fetch_annotation(self, method, args) do |type| + annotations = self.annotations(type) + return ArrayLiteral.new if annotations.nil? + ArrayLiteral.map(annotations, &.itself) + end else super end @@ -1500,6 +1512,12 @@ module Crystal fetch_annotation(self, method, args) do |type| self.type.annotation(type) end + when "annotations" + fetch_annotation(self, method, args) do |type| + annotations = self.type.annotations(type) + return ArrayLiteral.new if annotations.nil? + ArrayLiteral.map(annotations, &.itself) + end when "size" interpret_argless_method(method, args) do type = self.type.instance_type diff --git a/src/compiler/crystal/semantic/ast.cr b/src/compiler/crystal/semantic/ast.cr index f13eefa15254..e9dd605c20c2 100644 --- a/src/compiler/crystal/semantic/ast.cr +++ b/src/compiler/crystal/semantic/ast.cr @@ -139,7 +139,7 @@ module Crystal property? new = false # Annotations on this def - property annotations : Hash(AnnotationType, Annotation)? + property annotations : Hash(AnnotationType, Array(Annotation))? @macro_owner : Type? @@ -172,12 +172,18 @@ module Crystal # Adds an annotation with the given type and value def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Annotation - annotations[annotation_type] = value + annotations = @annotations ||= {} of AnnotationType => Array(Annotation) + annotations[annotation_type] ||= [] of Annotation + annotations[annotation_type] << value end - # Returns the annotation with the given type, if any, or nil otherwise + # Returns the last defined annotation with the given type, if any, or `nil` otherwise def annotation(annotation_type) : Annotation? + @annotations.try &.[annotation_type]?.try &.last? + end + + # Returns all annotations with the given type, if any, or `nil` otherwise + def annotations(annotation_type) : Array(Annotation)? @annotations.try &.[annotation_type]? end @@ -495,7 +501,7 @@ module Crystal property? uninitialized = false # Annotations of this instance var - property annotations : Hash(AnnotationType, Annotation)? + property annotations : Hash(AnnotationType, Array(Annotation))? def kind case name[0] @@ -516,12 +522,18 @@ module Crystal # Adds an annotation with the given type and value def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Annotation - annotations[annotation_type] = value + annotations = @annotations ||= {} of AnnotationType => Array(Annotation) + annotations[annotation_type] ||= [] of Annotation + annotations[annotation_type] << value end - # Returns the annotation with the given type, if any, or nil otherwise + # Returns the last defined annotation with the given type, if any, or `nil` otherwise def annotation(annotation_type) : Annotation? + @annotations.try &.[annotation_type]?.try &.last? + end + + # Returns all annotations with the given type, if any, or `nil` otherwise + def annotations(annotation_type) : Array(Annotation)? @annotations.try &.[annotation_type]? end end diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr index cf5ec4da7294..579b29e770f9 100644 --- a/src/compiler/crystal/types.cr +++ b/src/compiler/crystal/types.cr @@ -658,12 +658,18 @@ module Crystal # Adds an annotation with the given type and value def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Annotation - annotations[annotation_type] = value + annotations = @annotations ||= {} of AnnotationType => Array(Annotation) + annotations[annotation_type] ||= [] of Annotation + annotations[annotation_type] << value end - # Returns the annotation with the given type, if any, or nil otherwise + # Returns the last defined annotation with the given type, if any, or `nil` otherwise def annotation(annotation_type) : Annotation? + @annotations.try &.[annotation_type]?.try &.last? + end + + # Returns all annotations with the given type, if any, or `nil` otherwise + def annotations(annotation_type) : Array(Annotation)? @annotations.try &.[annotation_type]? end