require "../../spec_helper"

describe "Semantic: annotation" do
  it "declares annotation" do
    result = semantic(<<-CRYSTAL)
      annotation Foo
      end
      CRYSTAL

    type = result.program.types["Foo"]
    type.should be_a(AnnotationType)
    type.name.should eq("Foo")
  end

  describe "arguments" do
    describe "#args" do
      it "returns an empty TupleLiteral if there are none defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          module Moo
          end

          {% if (pos_args = Moo.annotation(Foo).args) && pos_args.is_a? TupleLiteral && pos_args.empty? %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "returns a TupleLiteral if there are positional arguments defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo(1, "foo", true)]
            module Moo
          end

          {% if Moo.annotation(Foo).args == {1, "foo", true} %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end
    end

    describe "#named_args" do
      it "returns an empty NamedTupleLiteral if there are none defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          module Moo
          end

          {% if (args = Moo.annotation(Foo).named_args) && args.is_a? NamedTupleLiteral && args.empty? %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "returns a NamedTupleLiteral if there are named arguments defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo(extra: "three", "foo": 99)]
            module Moo
          end

          {% if Moo.annotation(Foo).named_args == {extra: "three", foo: 99} %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end
    end

    it "returns a correctly with named and positional args" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo(1, "foo", true, foo: "bar", "cat": 0..0)]
          module Moo
        end

        {% if Moo.annotation(Foo).args == {1, "foo", true} && Moo.annotation(Foo).named_args == {foo: "bar", cat: 0..0} %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end
  end

  describe "#annotations" do
    describe "all types" do
      it "returns an empty array if there are none defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end

          module Moo
          end

          {% if Moo.annotations.empty? %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a module" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          @[Foo]
          @[Bar]
          module Moo
          end

          {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a class" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          @[Foo]
          @[Bar]
          class Moo
          end

          {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a struct" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          @[Foo]
          @[Bar]
          struct Moo
          end

          {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a enum" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          @[Foo]
          @[Bar]
          enum Moo
            A = 1
          end

          {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a lib" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          @[Foo]
          @[Bar]
          lib Moo
            A = 1
          end

          {% if Moo.annotations.map(&.name.id) == [Foo.id, Bar.id] %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in instance var (declaration)" do
        assert_type(<<-CRYSTAL) { int32 }
          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

          Moo.new.foo
          CRYSTAL
      end

      it "finds annotations in instance var (declaration, generic)" do
        assert_type(<<-CRYSTAL) { int32 }
          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

          Moo.new(1).foo
          CRYSTAL
      end

      it "adds annotations on def" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo; end
          annotation Bar; end

          class Moo
            @[Foo]
            @[Bar]
            def foo
            end
          end

          {% if Moo.methods.first.annotations.size == 2 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in generic parent (#7885)" do
        assert_type(<<-CRYSTAL) { int32 }
          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 %}
          CRYSTAL
      end

      it "find annotations on method parameters" do
        assert_type(<<-CRYSTAL) { int32 }
          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 %}
          CRYSTAL
      end
    end

    describe "of a specific type" do
      it "returns an empty array if there are none defined" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          module Moo
          end

          {% if Moo.annotations(Foo).size == 0 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations on a module" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          @[Foo]
          module Moo
          end

          {% if Moo.annotations(Foo).size == 2 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "uses annotations value, positional" do
        assert_type(<<-CRYSTAL) { int32 }
          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 %}
          CRYSTAL
      end

      it "uses annotations value, keyword" do
        assert_type(<<-CRYSTAL) { int32 }
          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 %}
          CRYSTAL
      end

      it "finds annotations in class" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          @[Foo]
          @[Foo]
          class Moo
          end

          {% if Moo.annotations(Foo).size == 3 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in struct" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          @[Foo]
          @[Foo]
          @[Foo]
          struct Moo
          end

          {% if Moo.annotations(Foo).size == 4 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in enum" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          enum Moo
            A = 1
          end

          {% if Moo.annotations(Foo).size == 1 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in lib" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo]
          @[Foo]
          lib Moo
            A = 1
          end

          {% if Moo.annotations(Foo).size == 2 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "can't find annotations in instance var" do
        assert_type(<<-CRYSTAL) { char }
          annotation Foo
          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
          CRYSTAL
      end

      it "can't find annotations in instance var, when other annotations are present" do
        assert_type(<<-CRYSTAL) { char }
          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
          CRYSTAL
      end

      it "finds annotations in instance var (declaration)" do
        assert_type(<<-CRYSTAL) { int32 }
          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
          CRYSTAL
      end

      it "finds annotations in instance var (declaration, generic)" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          class Moo(T)
            @[Foo]
            @x : T

            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
          CRYSTAL
      end

      it "collects annotations values in type" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          @[Foo(1)]
          module Moo
          end

          @[Foo(2)]
          module Moo
          end

          {% if Moo.annotations(Foo)[0][0] == 1 && Moo.annotations(Foo)[1][0] == 2 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "overrides annotations value in type" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          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

          Moo.new.foo
          CRYSTAL
      end

      it "adds annotations on def" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Foo
          end

          class Moo
            @[Foo]
            @[Foo]
            def foo
            end
          end

          {% if Moo.methods.first.annotations(Foo).size == 2 %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "can't find annotations on def" do
        assert_type(<<-CRYSTAL) { char }
          annotation Foo
          end

          class Moo
            def foo
            end
          end

          {% unless Moo.methods.first.annotations(Foo).empty? %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "can't find annotations on def, when other annotations are present" do
        assert_type(<<-CRYSTAL) { char }
          annotation Foo
          end

          annotation Bar
          end

          class Moo
            @[Bar]
            def foo
            end
          end

          {% unless Moo.methods.first.annotations(Foo).empty? %}
            1
          {% else %}
            'a'
          {% end %}
          CRYSTAL
      end

      it "finds annotations in generic parent (#7885)" do
        assert_type(<<-CRYSTAL) { int32 }
          annotation Ann
          end

          @[Ann(1)]
          class Parent(T)
          end

          class Child < Parent(Int32)
          end

          {{ Child.superclass.annotations(Ann)[0][0] }}
          CRYSTAL
      end

      it "find annotations on method parameters" do
        assert_type(<<-CRYSTAL) { int32 }
          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 %}
          CRYSTAL
      end
    end
  end

  describe "#annotation" do
    it "can't find annotation in module" do
      assert_type(<<-CRYSTAL) { char }
        annotation Foo
        end

        module Moo
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "can't find annotation in module, when other annotations are present" do
      assert_type(<<-CRYSTAL) { char }
        annotation Foo
        end

        annotation Bar
        end

        @[Bar]
        module Moo
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation in module" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo]
        module Moo
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "uses annotation value, positional" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo(1)]
        module Moo
        end

        {% if Moo.annotation(Foo)[0] == 1 %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "uses annotation value, keyword" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo(x: 1)]
        module Moo
        end

        {% if Moo.annotation(Foo)[:x] == 1 %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation in class" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo]
        class Moo
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation in struct" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo]
        struct Moo
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation in enum" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo]
        enum Moo
          A = 1
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation in lib" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo]
        lib Moo
          A = 1
        end

        {% if Moo.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "can't find annotation in instance var" do
      assert_type(<<-CRYSTAL) { char }
        annotation Foo
        end

        class Moo
          @x : Int32 = 1

          def foo
            {% if @type.instance_vars.first.annotation(Foo) %}
              1
            {% else %}
              'a'
            {% end %}
          end
        end

        Moo.new.foo
        CRYSTAL
    end

    it "can't find annotation in instance var, when other annotations are present" do
      assert_type(<<-CRYSTAL) { char }
        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
        CRYSTAL
    end

    it "finds annotation in instance var (declaration)" do
      assert_type(<<-CRYSTAL) { int32 }
        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
        CRYSTAL
    end

    it "finds annotation in instance var (assignment)" do
      assert_type(<<-CRYSTAL) { int32 }
        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
        CRYSTAL
    end

    it "finds annotation in instance var (declaration, generic)" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        class Moo(T)
          @[Foo]
          @x : T

          def initialize(@x : T)
          end

          def foo
            {% if @type.instance_vars.first.annotation(Foo) %}
              1
            {% else %}
              'a'
            {% end %}
          end
        end

        Moo.new(1).foo
        CRYSTAL
    end

    it "overrides annotation value in type" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        @[Foo(1)]
        module Moo
        end

        @[Foo(2)]
        module Moo
        end

        {% if Moo.annotation(Foo)[0] == 2 %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "overrides annotation in instance var" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        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
        CRYSTAL
    end

    it "errors if annotation doesn't exist" do
      assert_error <<-CRYSTAL, "undefined constant DoesntExist"
        @[DoesntExist]
        class Moo
        end
        CRYSTAL
    end

    it "errors if annotation doesn't point to an annotation type" do
      assert_error <<-CRYSTAL, "Int32 is not an annotation, it's a struct"
        @[Int32]
        class Moo
        end
        CRYSTAL
    end

    it "errors if using annotation other than ThreadLocal for class vars" do
      assert_error <<-CRYSTAL, "class variables can only be annotated with ThreadLocal"
        annotation Foo
        end

        class Moo
          @[Foo]
          @@x = 0
        end
        CRYSTAL
    end

    it "adds annotation on def" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Foo
        end

        class Moo
          @[Foo]
          def foo
          end
        end

        {% if Moo.methods.first.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "can't find annotation on def" do
      assert_type(<<-CRYSTAL) { char }
        annotation Foo
        end

        class Moo
          def foo
          end
        end

        {% if Moo.methods.first.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "can't find annotation on def, when other annotations are present" do
      assert_type(<<-CRYSTAL) { char }
        annotation Foo
        end

        annotation Bar
        end

        class Moo
          @[Bar]
          def foo
          end
        end

        {% if Moo.methods.first.annotation(Foo) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "errors if using invalid annotation on fun" do
      assert_error <<-CRYSTAL, "funs can only be annotated with: NoInline, AlwaysInline, Naked, ReturnsTwice, Raises, CallConvention"
        annotation Foo
        end

        @[Foo]
        fun foo : Void
        end
        CRYSTAL
    end

    it "doesn't carry link annotation from lib to fun" do
      assert_no_errors <<-CRYSTAL
        @[Link("foo")]
        lib LibFoo
          fun foo
        end
        CRYSTAL
    end

    it "finds annotation in generic parent (#7885)" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Ann
        end

        @[Ann(1)]
        class Parent(T)
        end

        class Child < Parent(Int32)
        end

        {{ Child.superclass.annotation(Ann)[0] }}
        CRYSTAL
    end

    it "finds annotation on method arg" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Ann; end

        def foo(
          @[Ann] foo : Int32
        )
        end

        {% if @top_level.methods.find(&.name.==("foo")).args.first.annotation(Ann) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation on method splat arg" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Ann; end

        def foo(
          id : Int32,
          @[Ann] *nums : Int32
        )
        end

        {% if @top_level.methods.find(&.name.==("foo")).args[1].annotation(Ann) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation on method double splat arg" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Ann; end

        def foo(
          id : Int32,
          @[Ann] **nums
        )
        end

        {% if @top_level.methods.find(&.name.==("foo")).double_splat.annotation(Ann) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end

    it "finds annotation on an restricted method block arg" do
      assert_type(<<-CRYSTAL) { int32 }
        annotation Ann; end

        def foo(
          id : Int32,
          @[Ann] &block : Int32 ->
        )
          yield 10
        end

        {% if @top_level.methods.find(&.name.==("foo")).block_arg.annotation(Ann) %}
          1
        {% else %}
          'a'
        {% end %}
        CRYSTAL
    end
  end

  it "errors when annotate instance variable in subclass" do
    assert_error <<-CRYSTAL, "can't annotate @x in Child because it was first defined in Base"
      annotation Foo
      end

      class Base
        @x : Nil
      end

      class Child < Base
        @[Foo]
        @x : Nil
      end
      CRYSTAL
  end

  it "errors if wanting to add type inside annotation (1) (#8614)" do
    assert_error <<-CRYSTAL, "can't declare type inside annotation Ann"
      annotation Ann
      end

      class Ann::Foo
      end

      Ann::Foo.new
      CRYSTAL
  end

  it "errors if wanting to add type inside annotation (2) (#8614)" do
    assert_error <<-CRYSTAL, "can't declare type inside annotation Ann"
      annotation Ann
      end

      class Ann::Foo::Bar
      end

      Ann::Foo::Bar.new
      CRYSTAL
  end

  it "doesn't bleed annotation from class into class variable (#8314)" do
    assert_no_errors <<-CRYSTAL
      annotation Attr; end

      @[Attr]
      class Bar
        @@x = 0
      end
      CRYSTAL
  end
end
