contrib/objcwrapper: adapt indent according to method scope
[nit.git] / contrib / objcwrapper / src / objc_generator.nit
index 673e306..3750dba 100644 (file)
 # Code generation
 module objc_generator
 
+import opts
+
 import objc_model
 
-class CodeGenerator
-       # Merge the calls to `alloc` and `init...` in a single constructor?
+redef class Sys
+
+       # Path to the output file
+       var opt_output = new OptionString("Output file", "-o")
+
+       # Shall `init` methods/constructors be wrapped as methods?
        #
-       # If `true`, also the default behavior, initializing an extern Objective-C object looks like:
+       # By default, these methods/constructors are wrapped as extern constructors.
+       # So initializing an extern Objective-C object looks like:
        # ~~~nitish
        # var o = new NSArray.init_with_array(some_other_array)
        # ~~~
        #
-       # If `false`, the object must first be allocated and then initialized.
+       # If this option is set, the object must first be allocated and then initialized.
        # This is closer to the Objective-C behavior:
        # ~~~nitish
        # var o = new NSArray
        # o.init_with_array(some_other_array)
        # ~~~
-       var init_with_alloc = true is writable
-
-       fun generator(classes: Array[nullable ObjcClass]) do
-               for classe in classes do
-                       var file = new FileWriter.open(classe.name + ".nit")
-                       nit_class_generator(classe, file, init_with_alloc)
-                       file.close
-               end
-       end
+       var opt_init_as_methods = new OptionBool(
+               "Wrap `init...` constructors as Nit methods instead of Nit constructors",
+               "--init-as-methods")
 
-       fun type_convertor(type_word: String): String do
+       private var objc_to_nit_types: Map[String, String] is lazy do
                var types = new HashMap[String, String]
                types["char"] = "Byte"
                types["short"] = "Int"
@@ -58,153 +59,278 @@ class CodeGenerator
                types["NSUInteger"] = "Int"
                types["BOOL"] = "Bool"
                types["id"] = "NSObject"
+               types["constid"] = "NSObject"
+               types["SEL"] = "NSObject"
+               types["void"] = "Pointer"
+
+               return types
+       end
+end
 
-               if types.has_key(type_word) then
-                       return types[type_word]
+redef class ObjcModel
+       redef fun knows_type(objc_type) do return super or
+               objc_to_nit_types.keys.has(objc_type)
+end
+
+# Wrapper generator
+class CodeGenerator
+
+       # `ObjcModel` to wrap
+       var model: ObjcModel
+
+       # Generate Nit code to wrap `classes`
+       fun generate
+       do
+               var classes = model.classes
+
+               # Open specified path or stdin
+               var file
+               var path = opt_output.value
+               if path != null then
+                       if path.file_extension != "nit" then
+                               print_error "Warning: output file path does not end with '.nit'"
+                       end
+
+                       file = new FileWriter.open(path)
                else
-                       return type_word
+                       file = stdout
                end
-       end
 
-       fun nit_class_generator(classe: nullable ObjcClass, file: FileWriter, init_with_alloc: Bool) do
-               var commented_methods = new Array[ObjcMethod]
+               # Generate code
                file.write "import cocoa::foundation\n\n"
-               file.write("extern class " + classe.name + """ in "ObjC" `{ """ + classe.name  + """ * `}\n""")
-               for super_name in classe.super_names do
-                       file.write("""  super """ + super_name + "\n")
+               for classe in classes do
+                       write_class(classe, file)
                end
-               if classe.super_names.is_empty then file.write("""      super NSObject\n""")
-               new_nit_generator(classe, file, init_with_alloc)
-               file.write("\n")
+
+               if path != null then file.close
+       end
+
+       private fun write_class(classe: ObjcClass, file: Writer)
+       do
+               # Class header
+               file.write """
+
+extern class {{{classe.name}}} in "ObjC" `{ {{{classe.name}}} * `}
+"""
+
+               # Supers
+               for super_name in classe.super_names do file.write """
+       super {{{super_name}}}
+"""
+               if classe.super_names.is_empty then file.write """
+       super NSObject
+"""
+
+               file.write "\n"
+
+               # Constructor or constructors
+               write_constructors(classe, file)
+
+               # Attributes
                for attribute in classe.attributes do
-                       nit_attribute_generator(attribute, file)
+                       write_attribute(attribute, file)
                end
+
+               # Instance methods '-'
                for method in classe.methods do
-                       if method.is_commented then
-                               commented_methods.add(method)
-                       else
-                               if init_with_alloc and method.params.first.name.has("init") then continue
-                               file.write("""  """)
-                               nit_method_generator(method, file, init_with_alloc)
-                               file.write(""" in "ObjC" `{\n           """)
-                               objc_method_generator(method, file)
-                               file.write("""  `}""")
-                               if method != classe.methods.last then file.write("\n\n")
-                       end
+                       if not model.knows_all_types(method) then method.is_commented = true
+
+                       if not opt_init_as_methods.value and method.is_init then continue
+                       if method.is_class_property then continue
+
+                       write_method_signature(method, file)
+                       write_objc_method_call(method, file)
                end
-               for commented_method in commented_methods do
-                       if commented_method == commented_methods.first then file.write("\n")
-                       file.write("""  #""")
-                       nit_method_generator(commented_method, file, init_with_alloc)
-                       if commented_method != commented_methods.last then file.write("\n")
+
+               file.write """
+end
+"""
+
+               # Class methods '+'
+               for method in classe.methods do
+                       if not method.is_class_property then continue
+
+                       write_method_signature(method, file)
+                       write_objc_method_call(method, file)
                end
-               file.write "\nend\n"
        end
 
-       fun new_nit_generator(classe: nullable ObjcClass, file: FileWriter, init_with_alloc: Bool) do
-               if init_with_alloc then
-                       for method in classe.methods do
-                               if method.params.first.name.has("init") and not method.is_commented then
-                                       file.write """\n        """
-                                       if method.params.first.name == "init" then
-                                               file.write "new"
-                                       else
-                                               nit_method_generator(method, file, init_with_alloc)
-                                       end
-                                       file.write """ in "ObjC" `{\n"""
-                                       new_alloc_init_objc_generator(classe.name, method, file)
-                                       file.write """  `}\n"""
-                               end
-                       end
-               else
-                       file.write """\n        new in "ObjC"`{\n"""
-                       new_objc_generator(classe, file)
-                       file.write """  `}\n"""
+       private fun write_constructors(classe: ObjcClass, file: Writer)
+       do
+               if opt_init_as_methods.value then
+                       # A single constructor for `alloc`
+                       file.write """
+       new in "ObjC" `{
+               return [{{{classe.name}}} alloc];
+       `}
+
+"""
+                       return
+               end
+
+               # A constructor per `init...` method
+               for method in classe.methods do
+                       if not method.is_init then continue
+
+                       if not model.knows_all_types(method) then method.is_commented = true
+
+                       write_method_signature(method, file)
+
+                               write_objc_init_call(classe.name, method, file)
                end
        end
 
-       fun nit_attribute_generator(attribute: ObjcAttribute, file: FileWriter) do
-               nit_attribute_setter_generator(attribute, file)
-               file.write "\n"
+       private fun write_attribute(attribute: ObjcAttribute, file: Writer)
+       do
+               if not model.knows_type(attribute.return_type) then attribute.is_commented = true
+
+               write_attribute_getter(attribute, file)
+               # TODO write_attribute_setter if there is no `readonly` annotation
        end
 
-       fun nit_attribute_setter_generator(attribute: ObjcAttribute, file: FileWriter) do
-               file.write("""  fun """ + attribute.name.to_snake_case + ": " + type_convertor(attribute.return_type))
-               file.write """ in "ObjC" `{\n"""
-               objc_attribute_setter_generator(attribute, file)
-               file.write """  `}\n"""
+       private fun write_attribute_getter(attribute: ObjcAttribute, file: Writer)
+       do
+               var nit_attr_name = attribute.name.to_snake_case
+               var nit_attr_type = attribute.return_type.objc_to_nit_type
+
+               var c = attribute.comment_str
+
+               file.write """
+{{{c}}}        fun {{{nit_attr_name}}}: {{{nit_attr_type}}} in "ObjC" `{
+{{{c}}}                return [self {{{attribute.name}}}];
+{{{c}}}        `}
+
+"""
        end
 
-       fun nit_attribute_getter_generator(attribute: ObjcAttribute, file: FileWriter) do
-               file.write("""  fun """ + attribute.name.to_snake_case + "=(value: " + type_convertor(attribute.return_type) + ")")
-               file.write " in \"ObjC\" `\{\n"
-               objc_attribute_getter_generator(attribute, file)
-               file.write """  `}\n"""
+       private fun write_attribute_setter(attribute: ObjcAttribute, file: Writer)
+       do
+               var nit_attr_name = attribute.name.to_snake_case
+               var nit_attr_type = attribute.return_type.objc_to_nit_type
+
+               var c = attribute.comment_str
+
+               file.write """
+{{{c}}}        fun {{{nit_attr_name}}}=(value: {{{nit_attr_type}}}) in "ObjC" `{
+{{{c}}}                return self.{{{attribute.name}}} = value;
+{{{c}}}        `}
+
+"""
        end
 
-       fun nit_method_generator(method: ObjcMethod, file: FileWriter, init_with_alloc: Bool) do
+       private fun write_method_signature(method: ObjcMethod, file: Writer)
+       do
+               var c = method.comment_str
+
+               # Build Nit method name
                var name = ""
                for param in method.params do
                        name += param.name[0].to_upper.to_s + param.name.substring_from(1)
-                       name = name.to_snake_case
                end
-               if name.has("init") and init_with_alloc then
-                       file.write "new "
-               else
-                       if not init_with_alloc and name == "init" then name = "init_0"
-                       file.write "fun "
+               name = name.to_snake_case
+
+               if name == "init" then name = ""
+
+               # If class method, prefix with class name
+               if method.is_class_property then name = "{method.objc_class.name.to_snake_case}_{name}"
+
+               # Kind of method
+               var fun_keyword = "fun"
+               if not opt_init_as_methods.value and method.is_init then
+                       fun_keyword = "new"
                end
-               file.write(name)
+
+               # Params
+               var params = new Array[String]
                for param in method.params do
-                       if param == method.params.first and not param.is_single then
-                               file.write("(" + param.variable_name + ": " + type_convertor(param.return_type))
-                       end
-                       if param != method.params.first and not param.is_single then
-                               file.write(", " + param.variable_name + ": " + type_convertor(param.return_type))
-                       end
-                       if param == method.params.last and not param.is_single then
-                               file.write ")"
-                       end
+                       if param.is_single then break
+                       params.add "{param.variable_name}: {param.return_type.objc_to_nit_type}"
+               end
+
+               var params_with_par = ""
+               if params.not_empty then params_with_par = "({params.join(", ")})"
+
+               # Return
+               var ret = ""
+               if method.return_type != "void" and fun_keyword != "new" then
+                       ret = ": {method.return_type.objc_to_nit_type}"
                end
-               if method.return_type != "void" and not method.params.first.name.has("init") then file.write(": " + type_convertor(method.return_type))
+
+               file.write """
+{{{c}}}{{{fun_keyword}}} {{{name}}}{{{params_with_par}}}{{{ret}}} in "ObjC" `{
+"""
        end
 
-       fun new_alloc_init_objc_generator(classe_name: String, method: ObjcMethod, file: FileWriter) do
-               file.write("""          return [[""" + classe_name + " alloc] ")
+       # Write a combined call to alloc and to a constructor/method
+       private fun write_objc_init_call(class_name: String, method: ObjcMethod, file: Writer)
+       do
+               # Method name and other params
+               var params = new Array[String]
                for param in method.params do
                        if not param.is_single then
-                               file.write(param.name + ":" + param.variable_name)
-                               if not param == method.params.last then file.write(" ")
-                       else
-                               file.write param.name
-                       end
+                               params.add "{param.name}: {param.variable_name}"
+                       else params.add param.name
                end
-               file.write "];\n"
-       end
 
-       fun new_objc_generator(classe: nullable ObjcClass, file: FileWriter) do
-               file.write("""          return [""" + classe.name + " alloc];\n")
-       end
+               var c = method.comment_str
 
-       fun objc_attribute_setter_generator(attribute: ObjcAttribute, file: FileWriter) do
-               file.write("""          return [self """ + attribute.name + "];\n")
-       end
+               file.write """
+{{{c}}}        return [[{{{class_name}}} alloc] {{{params.join(" ")}}}];
+{{{c}}}`}
 
-       fun objc_attribute_getter_generator(attribute: ObjcAttribute, file: FileWriter) do
-               file.write("""          self.""" + attribute.name + " = value;\n")
+"""
        end
 
-       fun objc_method_generator(method: ObjcMethod, file: FileWriter) do
-               if method.return_type != "void" then file.write("return ")
-               file.write "[self "
+       private fun write_objc_method_call(method: ObjcMethod, file: Writer)
+       do
+               # Is there a value to return?
+               var ret = ""
+               if method.return_type != "void" then ret = "return "
+
+               # Method name and other params
+               var params = new Array[String]
                for param in method.params do
                        if not param.is_single then
-                               file.write(param.name + ":" + param.variable_name)
-                               if not param == method.params.last then file.write " "
-                       else
-                               file.write(param.name)
-                       end
+                               params.add "{param.name}: {param.variable_name}"
+                       else params.add param.name
+               end
+
+               # Receiver, instance or class
+               var recv = "self"
+               if method.is_class_property then recv = method.objc_class.name
+
+               var c = method.comment_str
+
+               file.write """
+{{{c}}}        {{{ret}}}[{{{recv}}} {{{params.join(" ")}}}];
+{{{c}}}`}
+
+"""
+       end
+end
+
+redef class Text
+       # Nit equivalent to this type
+       private fun objc_to_nit_type: String
+       do
+               var types = sys.objc_to_nit_types
+
+               if types.has_key(self) then
+                       return types[self]
+               else
+                       return to_s
                end
-               file.write "];\n"
        end
 end
+
+redef class ObjcProperty
+       private fun comment_str: String do if is_commented then
+               return "#"
+       else return ""
+end
+
+redef class ObjcMethod
+       private fun indent: String do return if is_class_property then "" else "\t"
+
+       redef fun comment_str do return indent + super
+end