model: add `MEntity::is_broken`
[nit.git] / src / loader.nit
index 0723cad..f5b7be1 100644 (file)
@@ -116,14 +116,14 @@ redef class ModelBuilder
        #
        # Each name can be:
        #
-       # * a path to a module, a group or a directory of projects.
+       # * a path to a module, a group or a directory of packages.
        # * a short name of a module or a group that are looked in the `paths` (-I)
        #
        # Then, for each entry, if it is:
        #
        # * a module, then is it parser and returned.
        # * a group then recursively all its modules are parsed.
-       # * a directory of projects then all the modules of all projects are parsed.
+       # * a directory of packages then all the modules of all packages are parsed.
        # * else an error is displayed.
        #
        # See `parse` for details.
@@ -205,7 +205,7 @@ redef class ModelBuilder
                # First, look in groups
                var c = mgroup
                if c != null then
-                       var r = c.mproject.root
+                       var r = c.mpackage.root
                        assert r != null
                        scan_group(r)
                        var res = r.mmodule_paths_by_name(name)
@@ -215,10 +215,10 @@ redef class ModelBuilder
                # Look at some known directories
                var lookpaths = self.paths
 
-               # Look in the directory of the group project also (even if not explicitly in the path)
+               # Look in the directory of the group package also (even if not explicitly in the path)
                if mgroup != null then
                        # path of the root group
-                       var dirname = mgroup.mproject.root.filepath
+                       var dirname = mgroup.mpackage.root.filepath
                        if dirname != null then
                                dirname = dirname.join_path("..").simplify_path
                                if not lookpaths.has(dirname) and dirname.file_exists then
@@ -287,6 +287,20 @@ redef class ModelBuilder
                return res.first
        end
 
+       # Search groups named `name` from paths `lookpaths`.
+       private fun search_group_in_paths(name: String, lookpaths: Collection[String]): ArraySet[MGroup]
+       do
+               var res = new ArraySet[MGroup]
+               for dirname in lookpaths do
+                       # try a single group directory
+                       var mg = get_mgroup(dirname/name)
+                       if mg != null then
+                               res.add mg
+                       end
+               end
+               return res
+       end
+
        # Cache for `identify_file` by realpath
        private var identified_files_by_path = new HashMap[String, nullable ModulePath]
 
@@ -294,7 +308,7 @@ redef class ModelBuilder
        # See `identify_file`.
        var identified_files = new Array[ModulePath]
 
-       # Identify a source file and load the associated project and groups if required.
+       # Identify a source file and load the associated package and groups if required.
        #
        # This method does what the user expects when giving an argument to a Nit tool.
        #
@@ -303,7 +317,7 @@ redef class ModelBuilder
        # * If `path` is a directory (with a `/`),
        #   then the ModulePath of its default module is returned (if any)
        # * If `path` is a simple identifier (eg. `digraph`),
-       #   then the main module of the project `digraph` is searched in `paths` and returned.
+       #   then the main module of the package `digraph` is searched in `paths` and returned.
        #
        # Silently return `null` if `path` does not exists or cannot be identified.
        fun identify_file(path: String): nullable ModulePath
@@ -347,12 +361,19 @@ redef class ModelBuilder
                var mgroup = get_mgroup(mgrouppath)
 
                if mgroup == null then
-                       # singleton project
-                       var mproject = new MProject(pn, model)
-                       mgroup = new MGroup(pn, mproject, null) # same name for the root group
+                       # singleton package
+                       var mpackage = new MPackage(pn, model)
+                       mgroup = new MGroup(pn, mpackage, null) # same name for the root group
                        mgroup.filepath = path
-                       mproject.root = mgroup
-                       toolcontext.info("found singleton project `{pn}` at {path}", 2)
+                       mpackage.root = mgroup
+                       toolcontext.info("found singleton package `{pn}` at {path}", 2)
+
+                       # Attach homonymous `ini` file to the package
+                       var inipath = path.dirname / "{pn}.ini"
+                       if inipath.file_exists then
+                               var ini = new ConfigTree(inipath)
+                               mpackage.ini = ini
+                       end
                end
 
                var res = new ModulePath(pn, path, mgroup)
@@ -395,13 +416,13 @@ redef class ModelBuilder
                        return null
                end
 
-               # By default, the name of the project or group is the base_name of the directory
+               # By default, the name of the package or group is the base_name of the directory
                var pn = rdp.basename(".nit")
 
-               # Check `project.ini` that indicate a project
+               # Check `package.ini` that indicate a package
                var ini = null
                var parent = null
-               var inipath = dirpath / "project.ini"
+               var inipath = dirpath / "package.ini"
                if inipath.file_exists then
                        ini = new ConfigTree(inipath)
                end
@@ -415,16 +436,16 @@ redef class ModelBuilder
                                return null
                        end
 
-                       # Special stopper `projects.ini`
-                       if (dirpath/"projects.ini").file_exists then
-                               # dirpath cannot be a project since it is a project directory
+                       # Special stopper `packages.ini`
+                       if (dirpath/"packages.ini").file_exists then
+                               # dirpath cannot be a package since it is a package directory
                                mgroups[rdp] = null
                                return null
                        end
 
                        # check the parent directory (if it does not contain the stopper file)
                        var parentpath = dirpath.join_path("..").simplify_path
-                       var stopper = parentpath / "projects.ini"
+                       var stopper = parentpath / "packages.ini"
                        if not stopper.file_exists then
                                # Recursively get the parent group
                                parent = get_mgroup(parentpath)
@@ -438,20 +459,20 @@ redef class ModelBuilder
 
                var mgroup
                if parent == null then
-                       # no parent, thus new project
-                       if ini != null and ini.has_key("name") then pn = ini["name"]
-                       var mproject = new MProject(pn, model)
-                       mgroup = new MGroup(pn, mproject, null) # same name for the root group
-                       mproject.root = mgroup
-                       toolcontext.info("found project `{mproject}` at {dirpath}", 2)
-                       mproject.ini = ini
+                       # no parent, thus new package
+                       if ini != null then pn = ini["package.name"] or else pn
+                       var mpackage = new MPackage(pn, model)
+                       mgroup = new MGroup(pn, mpackage, null) # same name for the root group
+                       mpackage.root = mgroup
+                       toolcontext.info("found package `{mpackage}` at {dirpath}", 2)
+                       mpackage.ini = ini
                else
-                       mgroup = new MGroup(pn, parent.mproject, parent)
+                       mgroup = new MGroup(pn, parent.mpackage, parent)
                        toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
                end
 
                # search documentation
-               # in src first so the documentation of the project code can be distinct for the documentation of the project usage
+               # in src first so the documentation of the package code can be distinct for the documentation of the package usage
                var readme = dirpath.join_path("README.md")
                if not readme.file_exists then readme = dirpath.join_path("README")
                if readme.file_exists then
@@ -503,8 +524,8 @@ redef class ModelBuilder
                for f in p.files do
                        var fp = p/f
                        var g = get_mgroup(fp)
-                       # Recursively scan for groups of the same project
-                       if g != null and g.mproject == mgroup.mproject then
+                       # Recursively scan for groups of the same package
+                       if g != null and g.mpackage == mgroup.mpackage then
                                scan_group(g)
                        end
                        identify_file(fp)
@@ -647,11 +668,11 @@ redef class ModelBuilder
                        end
                end
 
-               # Check for conflicting module names in the project
+               # Check for conflicting module names in the package
                if mgroup != null then
                        var others = model.get_mmodules_by_name(mod_name)
                        if others != null then for other in others do
-                               if other.mgroup!= null and other.mgroup.mproject == mgroup.mproject then
+                               if other.mgroup!= null and other.mgroup.mpackage == mgroup.mpackage then
                                        var node: ANode
                                        if decl == null then node = nmodule else node = decl.n_name
                                        error(node, "Error: a module named `{other.full_name}` already exists at {other.location}.")
@@ -694,14 +715,77 @@ redef class ModelBuilder
        # This method handles qualified names as used in `AModuleName`.
        fun seach_module_by_amodule_name(n_name: AModuleName, mgroup: nullable MGroup): nullable ModulePath
        do
-               if n_name.n_quad != null then mgroup = null # Start from top level
-               for grp in n_name.n_path do
-                       var path = search_mmodule_by_name(grp, mgroup, grp.text)
-                       if path == null then return null # Forward error
-                       mgroup = path.mgroup
-               end
                var mod_name = n_name.n_id.text
-               return search_mmodule_by_name(n_name, mgroup, mod_name)
+
+               # If a quad is given, we ignore the starting group (go from path)
+               if n_name.n_quad != null then mgroup = null
+
+               # If name not qualified, just search the name
+               if n_name.n_path.is_empty then
+                       # Fast search if no n_path
+                       return search_mmodule_by_name(n_name, mgroup, mod_name)
+               end
+
+               # If qualified and in a group
+               if mgroup != null then
+                       # First search in the package
+                       var r = mgroup.mpackage.root
+                       assert r != null
+                       scan_group(r)
+                       # Get all modules with the final name
+                       var res = r.mmodule_paths_by_name(mod_name)
+                       # Filter out the name that does not match the qualifiers
+                       res = [for x in res do if match_amodulename(n_name, x) then x]
+                       if res.not_empty then
+                               if res.length > 1 then
+                                       error(n_name, "Error: conflicting module files for `{mod_name}`: `{res.join(",")}`")
+                               end
+                               return res.first
+                       end
+               end
+
+               # If no module yet, then assume that the first element of the path
+               # Is to be searched in the path.
+               var root_name = n_name.n_path.first.text
+               var roots = search_group_in_paths(root_name, paths)
+               if roots.is_empty then
+                       error(n_name, "Error: cannot find `{root_name}`. Tried: {paths.join(", ")}.")
+                       return null
+               end
+
+               var res = new ArraySet[ModulePath]
+               for r in roots do
+                       # Then, for each root, collect modules that matches the qualifiers
+                       scan_group(r)
+                       var root_res = r.mmodule_paths_by_name(mod_name)
+                       for x in root_res do if match_amodulename(n_name, x) then res.add x
+               end
+               if res.not_empty then
+                       if res.length > 1 then
+                               error(n_name, "Error: conflicting module files for `{mod_name}`: `{res.join(",")}`")
+                       end
+                       return res.first
+               end
+               # If still nothing, just call a basic search that will fail and will produce an error message
+               error(n_name, "Error: cannot find module `{mod_name}` from `{root_name}`. Tried: {paths.join(", ")}.")
+               return null
+       end
+
+       # Is elements of `n_name` correspond to the group nesting of `m`?
+       #
+       # Basically it check that `bar::foo` matches `bar/foo.nit` and `bar/baz/foo.nit`
+       # but not `baz/foo.nit` nor `foo/bar.nit`
+       #
+       # Is used by `seach_module_by_amodule_name` to validate qualified names.
+       private fun match_amodulename(n_name: AModuleName, m: ModulePath): Bool
+       do
+               var g: nullable MGroup = m.mgroup
+               for grp in n_name.n_path.reverse_iterator do
+                       while g != null and grp.text != g.name do
+                               g = g.parent
+                       end
+               end
+               return g != null
        end
 
        # Analyze the module importation and fill the module_importation_hierarchy
@@ -727,11 +811,13 @@ redef class ModelBuilder
                        # Load the imported module
                        var suppath = seach_module_by_amodule_name(aimport.n_name, mmodule.mgroup)
                        if suppath == null then
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                                continue # Skip error
                        end
                        var sup = load_module_path(suppath)
                        if sup == null then
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                                continue # Skip error
                        end
@@ -740,25 +826,30 @@ redef class ModelBuilder
                        imported_modules.add(sup)
                        var mvisibility = aimport.n_visibility.mvisibility
                        if mvisibility == protected_visibility then
+                               mmodule.is_broken = true
                                error(aimport.n_visibility, "Error: only properties can be protected.")
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                                return
                        end
                        if sup == mmodule then
                                error(aimport.n_name, "Error: dependency loop in module {mmodule}.")
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                        end
                        if sup.in_importation < mmodule then
                                error(aimport.n_name, "Error: dependency loop between modules {mmodule} and {sup}.")
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                                return
                        end
                        mmodule.set_visibility_for(sup, mvisibility)
                end
                if stdimport then
-                       var mod_name = "standard"
+                       var mod_name = "core"
                        var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
                        if sup == null then
+                               mmodule.is_broken = true
                                nmodule.mmodule = null # invalidate the module
                        else # Skip error
                                imported_modules.add(sup)
@@ -815,9 +906,9 @@ redef class ModelBuilder
 
                self.toolcontext.info("{mmodule} imports {mmodule.in_importation.direct_greaters.join(", ")}", 3)
 
-               # Force standard to be public if imported
+               # Force `core` to be public if imported
                for sup in mmodule.in_importation.greaters do
-                       if sup.name == "standard" then
+                       if sup.name == "core" then
                                mmodule.set_visibility_for(sup, public_visibility)
                        end
                end
@@ -928,7 +1019,7 @@ class ModulePath
        # The human path of the module
        var filepath: String
 
-       # The group (and the project) of the possible module
+       # The group (and the package) of the possible module
        var mgroup: MGroup
 
        # The loaded module (if any)
@@ -937,12 +1028,12 @@ class ModulePath
        redef fun to_s do return filepath
 end
 
-redef class MProject
+redef class MPackage
        # The associated `.ini` file, if any
        #
        # The `ini` file is given as is and might contain invalid or missing information.
        #
-       # Some projects, like stand-alone projects or virtual projects have no `ini` file associated.
+       # Some packages, like stand-alone packages or virtual packages have no `ini` file associated.
        var ini: nullable ConfigTree = null
 end
 
@@ -952,7 +1043,7 @@ redef class MGroup
 
        # Is the group interesting for a final user?
        #
-       # Groups are mandatory in the model but for simple projects they are not
+       # Groups are mandatory in the model but for simple packages they are not
        # always interesting.
        #
        # A interesting group has, at least, one of the following true: