diff --git a/spec/compiler/semantic/annotation_spec.cr b/spec/compiler/semantic/annotation_spec.cr index 89dd5e734243..94ea352e4177 100644 --- a/spec/compiler/semantic/annotation_spec.cr +++ b/spec/compiler/semantic/annotation_spec.cr @@ -104,368 +104,605 @@ describe "Semantic: annotation" do end describe "#annotations" do - it "returns an empty array if there are none defined" do - assert_type(%( - annotation Foo - end + describe "all types" 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.annotations(Foo).size == 0 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.empty? %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "finds annotations on a module" 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 + @[Foo] + @[Bar] + module Moo + end - {% if Moo.annotations(Foo).size == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "uses annotations value, positional" do - assert_type(%( - annotation Foo - end + it "finds annotations on a class" do + assert_type(%( + annotation Foo; end + annotation Bar; end - @[Foo(1)] - @[Foo(2)] - module Moo - end + @[Foo] + @[Bar] + class Moo + end - {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "uses annotations value, keyword" do - assert_type(%( - annotation Foo - end + it "finds annotations on a struct" do + assert_type(%( + annotation Foo; end + annotation Bar; end - @[Foo(x: 1)] - @[Foo(x: 2)] - module Moo - end + @[Foo] + @[Bar] + struct Moo + end - {% if Moo.annotations(Foo)[0][:x] == 1 && Moo.annotations(Foo)[1][:x] == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "finds annotations in class" do - assert_type(%( - annotation Foo - end + it "finds annotations on a enum" do + assert_type(%( + annotation Foo; end + annotation Bar; end - @[Foo] - @[Foo] - @[Foo] - class Moo - end + @[Foo] + @[Bar] + enum Moo + A = 1 + end - {% if Moo.annotations(Foo).size == 3 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "finds annotations in struct" do - assert_type(%( - annotation Foo - end + it "finds annotations on a lib" do + assert_type(%( + annotation Foo; end + annotation Bar; end - @[Foo] - @[Foo] - @[Foo] - @[Foo] - struct Moo - end + @[Foo] + @[Bar] + lib Moo + A = 1 + end - {% if Moo.annotations(Foo).size == 4 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - it "finds annotations in enum" do - assert_type(%( - annotation Foo - end + it "finds annotations in instance var (declaration)" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + class Moo + @[Foo] + @[Bar] + @x : Int32 = 1 + + def foo + {% if @type.instance_vars.first.annotations.size == 2 %} + 1 + {% else %} + 'a' + {% end %} + end + end - @[Foo] - enum Moo - A = 1 - end + Moo.new.foo + )) { int32 } + end - {% if Moo.annotations(Foo).size == 1 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + it "finds annotations in instance var (declaration, generic)" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + class Moo(T) + @[Foo] + @[Bar] + @x : T + + def initialize(@x : T) + end + + def foo + {% if @type.instance_vars.first.annotations.size == 2 %} + 1 + {% else %} + 'a' + {% end %} + end + end - it "finds annotations in lib" do - assert_type(%( - annotation Foo - end + Moo.new(1).foo + )) { int32 } + end - @[Foo] - @[Foo] - lib Moo - A = 1 - end + it "adds annotations on def" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + class Moo + @[Foo] + @[Bar] + def foo + end + end - {% if Moo.annotations(Foo).size == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } + {% if Moo.methods.first.annotations.size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "finds annotations in generic parent (#7885)" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + @[Foo(1)] + @[Bar(2)] + class Parent(T) + end + + class Child < Parent(Int32) + end + + {% if Child.superclass.annotations.map(&.[0]) == [1, 2] %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "find annotations on method parameters" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + class Moo + def foo(@[Foo] @[Bar] value) + end + end + + {% if Moo.methods.first.args.first.annotations.size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end end - it "can't find annotations in instance var" do - assert_type(%( - annotation Foo - end + describe "of a specific type" do + it "returns an empty array if there are none defined" do + assert_type(%( + annotation Foo + end + + module Moo + end + + {% if Moo.annotations(Foo).size == 0 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "finds annotations on a module" do + assert_type(%( + annotation Foo + end + + @[Foo] + @[Foo] + module Moo + end + + {% if Moo.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "uses annotations value, positional" do + assert_type(%( + annotation Foo + end + + @[Foo(1)] + @[Foo(2)] + 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 + + @[Foo] + @[Foo] + @[Foo] + class Moo + end + + {% if Moo.annotations(Foo).size == 3 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "finds annotations in struct" do + assert_type(%( + annotation Foo + end + + @[Foo] + @[Foo] + @[Foo] + @[Foo] + struct Moo + end - class Moo - @x : Int32 = 1 + {% if Moo.annotations(Foo).size == 4 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - def foo - {% unless @type.instance_vars.first.annotations(Foo).empty? %} - 1 - {% else %} - 'a' - {% end %} + it "finds annotations in enum" do + assert_type(%( + annotation Foo end - end - Moo.new.foo - )) { char } - end + @[Foo] + enum Moo + A = 1 + end - it "can't find annotations in instance var, when other annotations are present" do - assert_type(%( - annotation Foo - end + {% if Moo.annotations(Foo).size == 1 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - annotation Bar - end + it "finds annotations in lib" do + assert_type(%( + annotation Foo + end - class Moo - @[Bar] - @x : Int32 = 1 + @[Foo] + @[Foo] + lib Moo + A = 1 + end - def foo - {% unless @type.instance_vars.first.annotations(Foo).empty? %} - 1 - {% else %} - 'a' - {% 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 - end - Moo.new.foo - )) { char } - end + class Moo + @x : Int32 = 1 - it "finds annotations in instance var (declaration)" do - assert_type(%( - annotation Foo - end + def foo + {% unless @type.instance_vars.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + end + end - class Moo - @[Foo] - @[Foo] - @x : Int32 = 1 + Moo.new.foo + )) { char } + end - def foo - {% if @type.instance_vars.first.annotations(Foo).size == 2 %} - 1 - {% else %} - 'a' - {% end %} + it "can't find annotations in instance var, when other annotations are present" do + assert_type(%( + annotation Foo end - end - Moo.new.foo - )) { int32 } - end + annotation Bar + end - it "finds annotations in instance var (declaration, generic)" do - assert_type(%( - annotation Foo - end + class Moo + @[Bar] + @x : Int32 = 1 + + def foo + {% unless @type.instance_vars.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + end + end - class Moo(T) - @[Foo] - @x : T + Moo.new.foo + )) { char } + end - def initialize(@x : T) + it "finds annotations in instance var (declaration)" do + assert_type(%( + annotation Foo end - def foo - {% if @type.instance_vars.first.annotations(Foo).size == 1 %} - 1 - {% else %} - 'a' - {% 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 - end - Moo.new(1).foo - )) { int32 } - end + Moo.new.foo + )) { int32 } + end - it "collects annotations values in type" do - assert_type(%( - annotation Foo - end + it "finds annotations in instance var (declaration, generic)" do + assert_type(%( + annotation Foo + end - @[Foo(1)] - module Moo - end + class Moo(T) + @[Foo] + @x : T - @[Foo(2)] - module Moo - end + def initialize(@x : T) + end - {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + def foo + {% if @type.instance_vars.first.annotations(Foo).size == 1 %} + 1 + {% else %} + 'a' + {% end %} + end + end - it "overrides annotations value in type" do - assert_type(%( - annotation Foo - end + Moo.new(1).foo + )) { int32 } + end + + it "collects annotations values 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 - def foo - {% if @type.instance_vars.first.annotations(Foo).size == 1 && @type.instance_vars.first.annotations(Foo)[0][0] == 2 %} - 1 - {% else %} - 'a' - {% end %} + {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end + + it "overrides annotations value in type" do + assert_type(%( + annotation Foo end - end - Moo.new.foo - )) { int32 } - end + class Moo + @[Foo(1)] + @x : Int32 = 1 + end - it "adds annotations on def" do - assert_type(%( - annotation Foo - 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 - class Moo - @[Foo] - @[Foo] - def foo + Moo.new.foo + )) { int32 } + end + + it "adds annotations on def" do + assert_type(%( + annotation Foo end - end - {% if Moo.methods.first.annotations(Foo).size == 2 %} - 1 - {% else %} - 'a' - {% end %} - )) { int32 } - end + class Moo + @[Foo] + @[Foo] + def foo + end + end - it "can't find annotations on def" do - assert_type(%( - annotation Foo - end + {% if Moo.methods.first.annotations(Foo).size == 2 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end - class Moo - def foo + it "can't find annotations on def" do + assert_type(%( + annotation Foo end - end - {% unless Moo.methods.first.annotations(Foo).empty? %} - 1 - {% else %} - 'a' - {% end %} - )) { char } - end + class Moo + def foo + end + end - it "can't find annotations on def, when other annotations are present" do - assert_type(%( - annotation Foo - end + {% unless Moo.methods.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + )) { char } + end - annotation Bar - end + it "can't find annotations on def, when other annotations are present" do + assert_type(%( + annotation Foo + end - class Moo - @[Bar] - def foo + annotation Bar end - end - {% unless Moo.methods.first.annotations(Foo).empty? %} - 1 - {% else %} - 'a' - {% end %} - )) { char } - end + class Moo + @[Bar] + def foo + end + end - it "finds annotations in generic parent (#7885)" do - assert_type(%( - annotation Ann - end + {% unless Moo.methods.first.annotations(Foo).empty? %} + 1 + {% else %} + 'a' + {% end %} + )) { char } + end - @[Ann(1)] - class Parent(T) - end + it "finds annotations in generic parent (#7885)" do + assert_type(%( + annotation Ann + end - class Child < Parent(Int32) - end + @[Ann(1)] + class Parent(T) + end - {{ Child.superclass.annotations(Ann)[0][0] }} - )) { int32 } + class Child < Parent(Int32) + end + + {{ Child.superclass.annotations(Ann)[0][0] }} + )) { int32 } + end + + it "find annotations on method parameters" do + assert_type(%( + annotation Foo; end + annotation Bar; end + + class Moo + def foo(@[Foo] @[Bar] value) + end + end + + {% if Moo.methods.first.args.first.annotations(Foo).size == 1 %} + 1 + {% else %} + 'a' + {% end %} + )) { int32 } + end end end diff --git a/src/compiler/crystal/annotatable.cr b/src/compiler/crystal/annotatable.cr index f5adc5127684..ca12790335f9 100644 --- a/src/compiler/crystal/annotatable.cr +++ b/src/compiler/crystal/annotatable.cr @@ -19,5 +19,10 @@ module Crystal def annotations(annotation_type : AnnotationType) : Array(Annotation)? @annotations.try &.[annotation_type]? end + + # Returns all annotations on this type, if any, or `nil` otherwise + def all_annotations : Array(Annotation)? + @annotations.try &.values.flatten + end end end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 91ade699fa8b..9dadf48af06c 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -918,6 +918,11 @@ module Crystal::Macros # attached to this variable, or an empty `ArrayLiteral` if there are none. def annotations(type : TypeNode) : ArrayLiteral(Annotation) end + + # Returns an array of all annotations attached to this + # variable, or an empty `ArrayLiteral` if there are none. + def annotations : ArrayLiteral(Annotation) + end end # An annotation on top of a type or variable. @@ -1124,6 +1129,11 @@ module Crystal::Macros def annotations(type : TypeNode) : ArrayLiteral(Annotation) end + # Returns an array of all annotations attached to this + # arg, or an empty `ArrayLiteral` if there are none. + def annotations : ArrayLiteral(Annotation) + end + # Returns the external name of this argument. # # For example, for `def write(to file)` returns `to`. @@ -1219,14 +1229,19 @@ module Crystal::Macros end # Returns the last `Annotation` with the given `type` - # attached to this variable or `NilLiteral` if there are none. + # attached to this method 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. + # attached to this method, or an empty `ArrayLiteral` if there are none. def annotations(type : TypeNode) : ArrayLiteral(Annotation) end + + # Returns an array of all annotations attached to this + # method, or an empty `ArrayLiteral` if there are none. + def annotations : ArrayLiteral(Annotation) + end end # A macro definition. @@ -1974,15 +1989,20 @@ module Crystal::Macros end # Returns the last `Annotation` with the given `type` - # attached to this variable or `NilLiteral` if there are none. + # attached to this type 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. + # attached to this type, or an empty `ArrayLiteral` if there are none. def annotations(type : TypeNode) : ArrayLiteral(Annotation) end + # Returns an array of all annotations attached to this + # type, or an empty `ArrayLiteral` if there are none. + def annotations : ArrayLiteral(Annotation) + end + # Returns the number of elements in this tuple type or tuple metaclass type. # Gives a compile error if this is not one of those types. def size : NumberLiteral diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index 115b771a399a..de453f0ea4f5 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -1173,8 +1173,8 @@ module Crystal self.var.annotation(type) end when "annotations" - fetch_annotation(self, method, args, named_args, block) do |type| - annotations = self.var.annotations(type) + fetch_annotations(self, method, args, named_args, block) do |type| + annotations = type ? self.var.annotations(type) : self.var.all_annotations return ArrayLiteral.new if annotations.nil? ArrayLiteral.map(annotations, &.itself) end @@ -1347,8 +1347,8 @@ module Crystal self.annotation(type) end when "annotations" - fetch_annotation(self, method, args, named_args, block) do |type| - annotations = self.annotations(type) + fetch_annotations(self, method, args, named_args, block) do |type| + annotations = type ? self.annotations(type) : self.all_annotations return ArrayLiteral.new if annotations.nil? ArrayLiteral.map(annotations, &.itself) end @@ -1400,8 +1400,8 @@ module Crystal self.annotation(type) end when "annotations" - fetch_annotation(self, method, args, named_args, block) do |type| - annotations = self.annotations(type) + fetch_annotations(self, method, args, named_args, block) do |type| + annotations = type ? self.annotations(type) : self.all_annotations return ArrayLiteral.new if annotations.nil? ArrayLiteral.map(annotations, &.itself) end @@ -1658,8 +1658,8 @@ module Crystal self.type.annotation(type) end when "annotations" - fetch_annotation(self, method, args, named_args, block) do |type| - annotations = self.type.annotations(type) + fetch_annotations(self, method, args, named_args, block) do |type| + annotations = type ? self.type.annotations(type) : self.type.all_annotations return ArrayLiteral.new if annotations.nil? ArrayLiteral.map(annotations, &.itself) end @@ -2691,6 +2691,26 @@ private def fetch_annotation(node, method, args, named_args, block) end end +private def fetch_annotations(node, method, args, named_args, block) + interpret_check_args(node: node, min_count: 0) do |arg| + unless arg + return yield(nil) || Crystal::NilLiteral.new + end + + unless arg.is_a?(Crystal::TypeNode) + args[0].raise "argument to '#{node.class_desc}#annotation' must be a TypeNode, not #{arg.class_desc}" + end + + type = arg.type + unless type.is_a?(Crystal::AnnotationType) + args[0].raise "argument to '#{node.class_desc}#annotation' must be an annotation type, not #{type} (#{type.type_desc})" + end + + value = yield type + value || Crystal::NilLiteral.new + end +end + private def sort_by(object, klass, block, interpreter) block_arg = block.args.first? diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr index 8ff8df3c6f13..94ee4af77598 100644 --- a/src/compiler/crystal/types.cr +++ b/src/compiler/crystal/types.cr @@ -1968,7 +1968,7 @@ module Crystal getter generic_type : GenericType getter type_vars : Hash(String, ASTNode) - delegate :annotation, :annotations, to: generic_type + delegate :annotation, :annotations, :all_annotations, to: generic_type def initialize(program, @generic_type, @type_vars) super(program)