modelbuilder_base: Define ANode as nullable
[nit.git] / src / modelbuilder_base.nit
index 18c8669..0d75563 100644 (file)
@@ -60,7 +60,7 @@ class ModelBuilder
        # If no such a class exists, then null is returned.
        # If more than one class exists, then an error on `anode` is displayed and null is returned.
        # FIXME: add a way to handle class name conflict
-       fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
+       fun try_get_mclass_by_name(anode: nullable ANode, mmodule: MModule, name: String): nullable MClass
        do
                var classes = model.get_mclasses_by_name(name)
                if classes == null then
@@ -74,20 +74,60 @@ class ModelBuilder
                        if res == null then
                                res = mclass
                        else
-                               error(anode, "Ambigous class name '{name}'; conflict between {mclass.full_name} and {res.full_name}")
+                               error(anode, "Error: ambiguous class name `{name}`; conflict between `{mclass.full_name}` and `{res.full_name}`.")
                                return null
                        end
                end
                return res
        end
 
+       # Return a class identified by `qid` visible by the module `mmodule`.
+       # Visibility in modules and qualified names are correctly handled.
+       #
+       # If more than one class exists, then null is silently returned.
+       # It is up to the caller to post-analysis the result and display a correct error message.
+       # The method `class_not_found` can be used to display such a message.
+       fun try_get_mclass_by_qid(qid: AQclassid, mmodule: MModule): nullable MClass
+       do
+               var name = qid.n_id.text
+
+               var classes = model.get_mclasses_by_name(name)
+               if classes == null then
+                       return null
+               end
+
+               var res: nullable MClass = null
+               for mclass in classes do
+                       if not mmodule.in_importation <= mclass.intro_mmodule then continue
+                       if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
+                       if not qid.accept(mclass) then continue
+                       if res == null then
+                               res = mclass
+                       else
+                               return null
+                       end
+               end
+
+               return res
+       end
+
+       # Like `try_get_mclass_by_name` but display an error message when the class is not found
+       fun get_mclass_by_name(node: nullable ANode, mmodule: MModule, name: String): nullable MClass
+       do
+               var mclass = try_get_mclass_by_name(node, mmodule, name)
+               if mclass == null then
+                       error(node, "Type Error: missing primitive class `{name}'.")
+               end
+               return mclass
+       end
+
        # Return a property named `name` on the type `mtype` visible in the module `mmodule`.
        # Visibility in modules is correctly handled.
        # Protected properties are returned (it is up to the caller to check and reject protected properties).
        # If no such a property exists, then null is returned.
        # If more than one property exists, then an error on `anode` is displayed and null is returned.
        # FIXME: add a way to handle property name conflict
-       fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
+       fun try_get_mproperty_by_name2(anode: nullable ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
        do
                var props = self.model.get_mproperties_by_name(name)
                if props == null then
@@ -158,7 +198,7 @@ class ModelBuilder
                        assert ress.length > 1
                        var s = new Array[String]
                        for mprop in ress do s.add mprop.full_name
-                       self.error(anode, "Ambigous property name '{name}' for {mtype}; conflict between {s.join(" and ")}")
+                       self.error(anode, "Error: ambiguous property name `{name}` for `{mtype}`; conflict between {s.join(" and ")}.")
                end
 
                self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
@@ -169,30 +209,41 @@ class ModelBuilder
 
 
        # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
-       fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
+       fun try_get_mproperty_by_name(anode: nullable ANode, mclassdef: MClassDef, name: String): nullable MProperty
        do
                return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
        end
 
        # Helper function to display an error on a node.
        # Alias for `self.toolcontext.error(n.hot_location, text)`
-       fun error(n: ANode, text: String)
+       #
+       # This automatically sets `n.is_broken` to true.
+       fun error(n: nullable ANode, text: String)
        do
-               self.toolcontext.error(n.hot_location, text)
+               var l = null
+               if n != null then
+                       l = n.hot_location
+                       n.is_broken = true
+               end
+               self.toolcontext.error(l, text)
        end
 
        # Helper function to display a warning on a node.
        # Alias for: `self.toolcontext.warning(n.hot_location, text)`
-       fun warning(n: ANode, tag, text: String)
+       fun warning(n: nullable ANode, tag, text: String)
        do
-               self.toolcontext.warning(n.hot_location, tag, text)
+               var l = null
+               if n != null then l = n.hot_location
+               self.toolcontext.warning(l, tag, text)
        end
 
        # Helper function to display an advice on a node.
        # Alias for: `self.toolcontext.advice(n.hot_location, text)`
-       fun advice(n: ANode, tag, text: String)
+       fun advice(n: nullable ANode, tag, text: String)
        do
-               self.toolcontext.advice(n.hot_location, tag, text)
+               var l = null
+               if n != null then l = n.hot_location
+               self.toolcontext.advice(l, tag, text)
        end
 
        # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
@@ -202,11 +253,320 @@ class ModelBuilder
                if res == null then
                        var l = null
                        if n != null then l = n.hot_location
-                       self.toolcontext.fatal_error(l, "Fatal Error: {recv} must have a property named {name}.")
+                       self.toolcontext.fatal_error(l, "Fatal Error: `{recv}` must have a property named `{name}`.")
                        abort
                end
                return res
        end
+
+       # Return the static type associated to the node `ntype`.
+       #
+       # `mclassdef` is the context where the call is made (used to understand
+       # formal types).
+       # In case of problem, an error is displayed on `ntype` and null is returned.
+       #
+       # Same as `resolve_mtype_unchecked3`, but get the context (module, class and
+       # anchor) from `mclassdef`.
+       #
+       # SEE: `resolve_mtype`
+       # SEE: `resolve_mtype3_unchecked`
+       #
+       # FIXME: Find a better name for this method.
+       fun resolve_mtype_unchecked(mclassdef: MClassDef, ntype: AType, with_virtual: Bool): nullable MType
+       do
+               return resolve_mtype3_unchecked(
+                       mclassdef.mmodule,
+                       mclassdef.mclass,
+                       mclassdef.bound_mtype,
+                       ntype,
+                       with_virtual
+               )
+       end
+
+       # Return the static type associated to the node `ntype`.
+       #
+       # `mmodule`, `mclass` and `anchor` compose the context where the call is
+       # made (used to understand formal types).
+       # In case of problem, an error is displayed on `ntype` and null is returned.
+       #
+       # Note: The “3” is for 3 contextual parameters.
+       #
+       # SEE: `resolve_mtype`
+       # SEE: `resolve_mtype_unchecked`
+       #
+       # FIXME: Find a better name for this method.
+       fun resolve_mtype3_unchecked(mmodule: MModule, mclass: nullable MClass, anchor: nullable MClassType, ntype: AType, with_virtual: Bool): nullable MType
+       do
+               var qid = ntype.n_qid
+               var name = qid.n_id.text
+               var res: MType
+
+               # Check virtual type
+               if anchor != null and with_virtual then
+                       var prop = try_get_mproperty_by_name2(ntype, mmodule, anchor, name).as(nullable MVirtualTypeProp)
+                       if prop != null then
+                               if not ntype.n_types.is_empty then
+                                       error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
+                               end
+                               res = prop.mvirtualtype
+                               if ntype.n_kwnullable != null then res = res.as_nullable
+                               ntype.mtype = res
+                               return res
+                       end
+               end
+
+               # Check parameter type
+               if mclass != null then
+                       for p in mclass.mparameters do
+                               if p.name != name then continue
+
+                               if not ntype.n_types.is_empty then
+                                       error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
+                               end
+
+                               res = p
+                               if ntype.n_kwnullable != null then res = res.as_nullable
+                               ntype.mtype = res
+                               return res
+                       end
+               end
+
+               # Check class
+               var found_class = try_get_mclass_by_qid(qid, mmodule)
+               if found_class != null then
+                       var arity = ntype.n_types.length
+                       if arity != found_class.arity then
+                               if arity == 0 then
+                                       error(ntype, "Type Error: `{found_class.signature_to_s}` is a generic class.")
+                               else if found_class.arity == 0 then
+                                       error(ntype, "Type Error: `{name}` is not a generic class.")
+                               else
+                                       error(ntype, "Type Error: expected {found_class.arity} formal argument(s) for `{found_class.signature_to_s}`; got {arity}.")
+                               end
+                               return null
+                       end
+                       if arity == 0 then
+                               res = found_class.mclass_type
+                               if ntype.n_kwnullable != null then res = res.as_nullable
+                               ntype.mtype = res
+                               return res
+                       else
+                               var mtypes = new Array[MType]
+                               for nt in ntype.n_types do
+                                       var mt = resolve_mtype3_unchecked(mmodule, mclass, anchor, nt, with_virtual)
+                                       if mt == null then return null # Forward error
+                                       mtypes.add(mt)
+                               end
+                               res = found_class.get_mtype(mtypes)
+                               if ntype.n_kwnullable != null then res = res.as_nullable
+                               ntype.mtype = res
+                               return res
+                       end
+               end
+
+               # If everything fail, then give up with class by proposing things.
+               #
+               # TODO Give hints on formal types (param and virtual)
+               class_not_found(qid, mmodule)
+               ntype.is_broken = true
+               return null
+       end
+
+       # Print an error and suggest hints when the class identified by `qid` in `mmodule` is not found.
+       #
+       # This just print error messages.
+       fun class_not_found(qid: AQclassid, mmodule: MModule)
+       do
+               var name = qid.n_id.text
+               var qname = qid.full_name
+
+               if bad_class_names[mmodule].has(qname) then
+                       error(qid, "Error: class `{qname}` not found in module `{mmodule}`.")
+                       return
+               end
+               bad_class_names[mmodule].add(qname)
+
+               var all_classes = model.get_mclasses_by_name(name)
+               var hints = new Array[String]
+
+               # Look for conflicting classes.
+               if all_classes != null then for c in all_classes do
+                       if not mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
+                       if not qid.accept(c) then continue
+                       hints.add "`{c.full_name}`"
+               end
+               if hints.length > 1 then
+                       error(qid, "Error: ambiguous class name `{qname}` in module `{mmodule}`. Conflicts are between {hints.join(",", " and ")}.")
+                       return
+               end
+               hints.clear
+
+               # Look for imported but invisible classes.
+               if all_classes != null then for c in all_classes do
+                       if not mmodule.in_importation <= c.intro_mmodule then continue
+                       if mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
+                       if not qid.accept(c) then continue
+                       error(qid, "Error: class `{c.full_name}` not visible in module `{mmodule}`.")
+                       return
+               end
+
+               # Look for not imported but known classes from importable modules
+               if all_classes != null then for c in all_classes do
+                       if mmodule.in_importation <= c.intro_mmodule then continue
+                       if c.intro_mmodule.in_importation <= mmodule then continue
+                       if c.visibility <= private_visibility then continue
+                       if not qid.accept(c) then continue
+                       hints.add "`{c.intro_mmodule.full_name}`"
+               end
+               if hints.not_empty then
+                       error(qid, "Error: class `{qname}` not found in module `{mmodule}`. Maybe import {hints.join(",", " or ")}?")
+                       return
+               end
+
+               # Look for classes with an approximative name.
+               var bests = new BestDistance[MClass](qname.length - name.length / 2) # limit up to 50% name change
+               for c in model.mclasses do
+                       if not mmodule.in_importation <= c.intro_mmodule then continue
+                       if not mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
+                       var d = qname.levenshtein_distance(c.name)
+                       bests.update(d, c)
+                       d = qname.levenshtein_distance(c.full_name)
+                       bests.update(d, c)
+               end
+               if bests.best_items.not_empty then
+                       for c in bests.best_items do hints.add "`{c.full_name}`"
+                       error(qid, "Error: class `{qname}` not found in module `{mmodule}`. Did you mean {hints.join(",", " or ")}?")
+                       return
+               end
+
+               error(qid, "Error: class `{qname}` not found in module `{mmodule}`.")
+       end
+
+       # List of already reported bad class names.
+       # Used to not perform and repeat hints again and again.
+       private var bad_class_names = new MultiHashMap[MModule, String]
+
+       # Return the static type associated to the node `ntype`.
+       #
+       # `mclassdef` is the context where the call is made (used to understand
+       # formal types).
+       # In case of problem, an error is displayed on `ntype` and null is returned.
+       #
+       # Same as `resolve_mtype3`, but get the context (module, class and ) from
+       # `mclassdef`.
+       #
+       # SEE: `resolve_mtype3`
+       # SEE: `resolve_mtype_unchecked`
+       #
+       # FIXME: Find a better name for this method.
+       fun resolve_mtype(mclassdef: MClassDef, ntype: AType): nullable MType
+       do
+               return resolve_mtype3(
+                       mclassdef.mmodule,
+                       mclassdef.mclass,
+                       mclassdef.bound_mtype,
+                       ntype
+               )
+       end
+
+       # Return the static type associated to the node `ntype`.
+       #
+       # `mmodule`, `mclass` and `anchor` compose the context where the call is
+       # made (used to understand formal types).
+       # In case of problem, an error is displayed on `ntype` and null is returned.
+       #
+       # Note: The “3” is for 3 contextual parameters.
+       #
+       # SEE: `resolve_mtype`
+       # SEE: `resolve_mtype_unchecked`
+       #
+       # FIXME: Find a better name for this method.
+       fun resolve_mtype3(mmodule: MModule, mclass: nullable MClass, anchor: nullable MClassType, ntype: AType): nullable MType
+       do
+               var mtype = ntype.mtype
+               if mtype == null then mtype = resolve_mtype3_unchecked(mmodule, mclass, anchor, ntype, true)
+               if mtype == null then return null # Forward error
+
+               if ntype.checked_mtype then return mtype
+               if mtype isa MGenericType then
+                       var found_class = mtype.mclass
+                       for i in [0..found_class.arity[ do
+                               var intro = found_class.try_intro
+                               if intro == null then return null # skip error
+                               var bound = intro.bound_mtype.arguments[i]
+                               var nt = ntype.n_types[i]
+                               var mt = resolve_mtype3(mmodule, mclass, anchor, nt)
+                               if mt == null then return null # forward error
+                               if not check_subtype(nt, mmodule, anchor, mt, bound) then
+                                       error(nt, "Type Error: expected `{bound}`, got `{mt}`.")
+                                       return null
+                               end
+                       end
+               end
+               ntype.checked_mtype = true
+               return mtype
+       end
+
+       # Check that `sub` is a subtype of `sup`.
+       # Do not display an error message.
+       #
+       # This method is used a an entry point for the modelize phase to test static subtypes.
+       # Some refinements could redefine it to collect statictics.
+       fun check_subtype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
+       do
+               return sub.is_subtype(mmodule, anchor, sup)
+       end
+
+       # Check that `sub` and `sup` are equvalent types.
+       # Do not display an error message.
+       #
+       # This method is used a an entry point for the modelize phase to test static equivalent types.
+       # Some refinements could redefine it to collect statictics.
+       fun check_sametype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
+       do
+               return sub.is_subtype(mmodule, anchor, sup) and sup.is_subtype(mmodule, anchor, sub)
+       end
+end
+
+redef class ANode
+       # The indication that the node did not pass some semantic verifications.
+       #
+       # This simple flag is set by a given analysis to say that the node is broken and unusable in
+       # an execution.
+       # When a node status is set to broken, it is usually associated with a error message.
+       #
+       # If it is safe to do so, clients of the AST SHOULD just skip broken nodes in their processing.
+       # Clients that do not care about the executability (e.g. metrics) MAY still process the node or
+       # perform specific checks to determinate the validity of the node.
+       #
+       # Note that the broken status is not propagated to parent or children nodes.
+       # e.g. a broken expression used as argument does not make the whole call broken.
+       var is_broken = false is writable
+
+       redef fun dump_info(v) do
+               var res = super
+               if is_broken then
+                       res += v.red("*broken*")
+               end
+               return res
+       end
+end
+
+redef class AType
+       # The mtype associated to the node
+       var mtype: nullable MType = null
+
+       # Is the mtype a valid one?
+       var checked_mtype: Bool = false
+
+       redef fun dump_info(v) do
+               var res = super
+               var mtype = self.mtype
+               if mtype != null then
+                       res += v.yellow(":{mtype}")
+               end
+               return res
+       end
 end
 
 redef class AVisibility
@@ -228,11 +588,13 @@ end
 
 redef class ADoc
        private var mdoc_cache: nullable MDoc
+
+       # Convert `self` to a `MDoc`
        fun to_mdoc: MDoc
        do
                var res = mdoc_cache
                if res != null then return res
-               res = new MDoc
+               res = new MDoc(location)
                for c in n_comment do
                        var text = c.text
                        if text.length < 2 then
@@ -252,3 +614,53 @@ redef class ADoc
                return res
        end
 end
+
+redef class AQclassid
+       # The name of the package part, if any
+       fun mpackname: nullable String do
+               var nqualified = n_qualified
+               if nqualified == null then return null
+               var nids = nqualified.n_id
+               if nids.length <= 0 then return null
+               return nids[0].text
+       end
+
+       # The name of the module part, if any
+       fun mmodname: nullable String do
+               var nqualified = n_qualified
+               if nqualified == null then return null
+               var nids = nqualified.n_id
+               if nids.length <= 1 then return null
+               return nids[1].text
+       end
+
+       # Does `mclass` match the full qualified name?
+       fun accept(mclass: MClass): Bool
+       do
+               if mclass.name != n_id.text then return false
+               var mpackname = self.mpackname
+               if mpackname != null then
+                       var mpackage = mclass.intro_mmodule.mpackage
+                       if mpackage == null then return false
+                       if mpackage.name != mpackname then return false
+                       var mmodname = self.mmodname
+                       if mmodname != null and mclass.intro_mmodule.name != mmodname then return false
+               end
+               return true
+       end
+
+       # The pretty name represented by self.
+       fun full_name: String
+       do
+               var res = n_id.text
+               var nqualified = n_qualified
+               if nqualified == null then return res
+               var ncid = nqualified.n_classid
+               if ncid != null then res = ncid.text + "::" + res
+               var nids = nqualified.n_id
+               if nids.not_empty then for n in nids.reverse_iterator do
+                       res = n.text + "::" + res
+               end
+               return res
+       end
+end