diff --git a/spec/compiler/macro/macro_methods_spec.cr b/spec/compiler/macro/macro_methods_spec.cr index e132ff75acea..c1f0fb76194c 100644 --- a/spec/compiler/macro/macro_methods_spec.cr +++ b/spec/compiler/macro/macro_methods_spec.cr @@ -2822,6 +2822,63 @@ module Crystal assert_macro %({{compare_versions("1.10.3", "1.2.3")}}), %(1) end + describe "#parse_type" do + it "path" do + assert_type(%[class Bar; end; {{ parse_type("Bar").is_a?(Path) ? 1 : 'a'}}]) { int32 } + assert_type(%[class Bar; end; {{ parse_type(:Bar.id.stringify).is_a?(Path) ? 1 : 'a'}}]) { int32 } + end + + it "generic" do + assert_type(%[class Foo(A, B); end; {{ parse_type("Foo(Int32, String)").resolve.type_vars.size == 2 ? 1 : 'a' }}]) { int32 } + end + + it "union - |" do + assert_type(%[class Foo; end; class Bar; end; {{ parse_type("Foo|Bar").resolve.union_types.size == 2 ? 1 : 'a' }}]) { int32 } + end + + it "union - Union" do + assert_type(%[class Foo; end; class Bar; end; {{ parse_type("Union(Foo,Bar)").resolve.union_types.size == 2 ? 1 : 'a' }}]) { int32 } + end + + it "union - in generic" do + assert_type(%[{{ parse_type("Array(Int32 | String)").resolve.type_vars[0].union_types.size == 2 ? 1 : 'a' }}]) { int32 } + end + + it "proc" do + assert_type(%[{{ parse_type("String, Int32 -> Bool").inputs.size == 2 ? 1 : 'a' }}]) { int32 } + assert_type(%[{{ parse_type("String, Int32 -> Bool").output.resolve == Bool ? 1 : 'a' }}]) { int32 } + end + + it "metaclass" do + assert_type(%[{{ parse_type("Int32.class").resolve == Int32.class ? 1 : 'a' }}]) { int32 } + assert_type(%[{{ parse_type("Int32").resolve == Int32.instance ? 1 : 'a' }}]) { int32 } + end + + it "raises on empty string" do + expect_raises(Crystal::TypeException, "argument to parse_type cannot be an empty value") do + assert_macro %({{parse_type ""}}), %(nil) + end + end + + it "raises on extra unparsed tokens before the type" do + expect_raises(Crystal::TypeException, %(Invalid type name: "100Foo")) do + assert_macro %({{parse_type "100Foo" }}), %(nil) + end + end + + it "raises on extra unparsed tokens after the type" do + expect_raises(Crystal::TypeException, %(Invalid type name: "Foo(Int32)100")) do + assert_macro %({{parse_type "Foo(Int32)100" }}), %(nil) + end + end + + it "raises on non StringLiteral arguments" do + expect_raises(Crystal::TypeException, "argument to parse_type must be a StringLiteral, not SymbolLiteral") do + assert_macro %({{parse_type :Foo }}), %(nil) + end + end + end + describe "printing" do it "puts" do String.build do |io| diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 8985919b4e26..7ff383fd5e85 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -153,6 +153,35 @@ module Crystal::Macros def host_flag?(name) : BoolLiteral end + # Parses *type_name* into a `Path`, `Generic` (also used for unions), `ProcNotation`, or `Metaclass`. + # + # The `#resolve` method could then be used to resolve the value into a `TypeNode`, if the *type_name* represents a type, + # otherwise the value of the constant. + # + # A compile time error is raised if the type/constant does not actually exist, + # or if a required generic argument was not provided. + # + # ``` + # class Foo; end + # + # struct Some::Namespace::Foo; end + # + # module Bar(T); end + # + # MY_CONST = 1234 + # + # {{ parse_type("Foo").resolve.class? }} # => true + # {{ parse_type("Some::Namespace::Foo").resolve.struct? }} # => true + # {{ parse_type("Foo|Some::Namespace::Foo").resolve.union_types.size }} # => 2 + # {{ parse_type("Bar(Int32)|Foo").resolve.union_types[0].type_vars.size }} # => 1 + # {{ parse_type("MY_CONST").resolve }} # => 1234 + # + # {{ parse_type("MissingType").resolve }} # Error: undefined constant MissingType + # {{ parse_type("UNKNOWN_CONST").resolve }} # Error: undefined constant UNKNOWN_CONST + # ``` + def parse_type(type_name : StringLiteral) : Path | Generic | ProcNotation | Metaclass + end + # Prints AST nodes at compile-time. Useful for debugging macros. def puts(*expressions) : Nop end diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index 95c04ccbc513..262a72bef4f2 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -55,6 +55,8 @@ module Crystal interpret_env(node) when "flag?", "host_flag?" interpret_flag?(node) + when "parse_type" + interpret_parse_type(node) when "puts" interpret_puts(node) when "p", "pp" @@ -156,6 +158,29 @@ module Crystal end end + def interpret_parse_type(node) + interpret_check_args_toplevel do |arg| + arg.accept self + type_name = case last = @last + when StringLiteral then last.value + else + arg.raise "argument to parse_type must be a StringLiteral, not #{last.class_desc}" + end + + arg.raise "argument to parse_type cannot be an empty value" if type_name.blank? + + begin + parser = Crystal::Parser.new type_name + parser.next_token + type = parser.parse_bare_proc_type + parser.check :EOF + @last = type + rescue ex : Crystal::SyntaxException + arg.raise "Invalid type name: #{type_name.inspect}" + end + end + end + def interpret_puts(node) node.args.each do |arg| arg.accept self