model: add `MEntity::is_broken`
[nit.git] / src / loader.nit
index f17f9c2..f5b7be1 100644 (file)
@@ -18,6 +18,7 @@
 module loader
 
 import modelbuilder_base
+import ini
 
 redef class ToolContext
        # Option --path
@@ -115,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.
@@ -203,45 +204,21 @@ redef class ModelBuilder
        do
                # First, look in groups
                var c = mgroup
-               while c != null do
-                       var dirname = c.filepath
-                       if dirname == null then break # virtual group
-                       if dirname.has_suffix(".nit") then break # singleton project
-
-                       # Second, try the directory to find a file
-                       var try_file = dirname + "/" + name + ".nit"
-                       if try_file.file_exists then
-                               var res = self.identify_file(try_file.simplify_path)
-                               assert res != null
-                               return res
-                       end
-
-                       # Third, try if the requested module is itself a group
-                       try_file = dirname + "/" + name + "/" + name + ".nit"
-                       if try_file.file_exists then
-                               var res = self.identify_file(try_file.simplify_path)
-                               assert res != null
-                               return res
-                       end
-
-                       # Fourth, try if the requested module is itself a group with a src
-                       try_file = dirname + "/" + name + "/src/" + name + ".nit"
-                       if try_file.file_exists then
-                               var res = self.identify_file(try_file.simplify_path)
-                               assert res != null
-                               return res
-                       end
-
-                       c = c.parent
+               if c != null then
+                       var r = c.mpackage.root
+                       assert r != null
+                       scan_group(r)
+                       var res = r.mmodule_paths_by_name(name)
+                       if res.not_empty then return res.first
                end
 
                # 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
@@ -291,50 +268,37 @@ redef class ModelBuilder
        # If found, the path of the file is returned
        private fun search_module_in_paths(location: nullable Location, name: String, lookpaths: Collection[String]): nullable ModulePath
        do
-               var candidate: nullable String = null
+               var res = new ArraySet[ModulePath]
                for dirname in lookpaths do
-                       var try_file = (dirname + "/" + name + ".nit").simplify_path
-                       if try_file.file_exists then
-                               if candidate == null then
-                                       candidate = try_file
-                               else if candidate != try_file then
-                                       # try to disambiguate conflicting modules
-                                       var abs_candidate = module_absolute_path(candidate)
-                                       var abs_try_file = module_absolute_path(try_file)
-                                       if abs_candidate != abs_try_file then
-                                               toolcontext.error(location, "Error: conflicting module file for `{name}`: `{candidate}` `{try_file}`")
-                                       end
-                               end
-                       end
-                       try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
-                       if try_file.file_exists then
-                               if candidate == null then
-                                       candidate = try_file
-                               else if candidate != try_file then
-                                       # try to disambiguate conflicting modules
-                                       var abs_candidate = module_absolute_path(candidate)
-                                       var abs_try_file = module_absolute_path(try_file)
-                                       if abs_candidate != abs_try_file then
-                                               toolcontext.error(location, "Error: conflicting module file for `{name}`: `{candidate}` `{try_file}`")
-                                       end
-                               end
+                       # Try a single module file
+                       var mp = identify_file((dirname/"{name}.nit").simplify_path)
+                       if mp != null then res.add mp
+                       # Try the default module of a group
+                       var g = get_mgroup((dirname/name).simplify_path)
+                       if g != null then
+                               scan_group(g)
+                               res.add_all g.mmodule_paths_by_name(name)
                        end
-                       try_file = (dirname + "/" + name + "/src/" + name + ".nit").simplify_path
-                       if try_file.file_exists then
-                               if candidate == null then
-                                       candidate = try_file
-                               else if candidate != try_file then
-                                       # try to disambiguate conflicting modules
-                                       var abs_candidate = module_absolute_path(candidate)
-                                       var abs_try_file = module_absolute_path(try_file)
-                                       if abs_candidate != abs_try_file then
-                                               toolcontext.error(location, "Error: conflicting module file for `{name}`: `{candidate}` `{try_file}`")
-                                       end
-                               end
+               end
+               if res.is_empty then return null
+               if res.length > 1 then
+                       toolcontext.error(location, "Error: conflicting module files for `{name}`: `{res.join(",")}`")
+               end
+               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
-               if candidate == null then return null
-               return identify_file(candidate)
+               return res
        end
 
        # Cache for `identify_file` by realpath
@@ -344,10 +308,18 @@ redef class ModelBuilder
        # See `identify_file`.
        var identified_files = new Array[ModulePath]
 
-       # Identify a source file
-       # 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.
+       #
+       # * If `path` is an existing Nit source file (with the `.nit` extension),
+       #   then the associated ModulePath is returned
+       # * 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 package `digraph` is searched in `paths` and returned.
        #
-       # Silently return `null` if `path` is not a valid module path.
+       # Silently return `null` if `path` does not exists or cannot be identified.
        fun identify_file(path: String): nullable ModulePath
        do
                # special case for not a nit file
@@ -374,6 +346,11 @@ redef class ModelBuilder
                        path = candidate
                end
 
+               # Does the file exists?
+               if not path.file_exists then
+                       return null
+               end
+
                # Fast track, the path is already known
                var pn = path.basename(".nit")
                var rp = module_absolute_path(path)
@@ -384,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 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)
@@ -425,53 +409,72 @@ redef class ModelBuilder
                        return mgroups[rdp]
                end
 
-               # Hack, a group is determined by one of the following:
-               # * the presence of a honomymous nit file
-               # * the fact that the directory is named `src`
-               # * the fact that there is a sub-directory named `src`
+               # Filter out non-directories
+               var stat = dirpath.file_stat
+               if stat == null or not stat.is_dir then
+                       mgroups[rdp] = null
+                       return null
+               end
+
+               # By default, the name of the package or group is the base_name of the directory
                var pn = rdp.basename(".nit")
-               var mp = dirpath.join_path(pn + ".nit").simplify_path
-
-               # dirpath2 is the root directory
-               # dirpath is the src subdirectory directory, if any, else it is the same that dirpath2
-               var dirpath2 = dirpath
-               if not mp.file_exists then
-                       if pn == "src" then
-                               # With a src directory, the group name is the name of the parent directory
-                               dirpath2 = rdp.dirname
-                               pn = dirpath2.basename
-                       else
-                               # Check a `src` subdirectory
-                               dirpath = dirpath2 / "src"
-                               if not dirpath.file_exists then
-                                       # All rules failed, so return null
+
+               # Check `package.ini` that indicate a package
+               var ini = null
+               var parent = null
+               var inipath = dirpath / "package.ini"
+               if inipath.file_exists then
+                       ini = new ConfigTree(inipath)
+               end
+
+               if ini == null then
+                       # No ini, multiple course of action
+
+                       # The root of the directory hierarchy in the file system.
+                       if rdp == "/" then
+                               mgroups[rdp] = null
+                               return null
+                       end
+
+                       # 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 / "packages.ini"
+                       if not stopper.file_exists then
+                               # Recursively get the parent group
+                               parent = get_mgroup(parentpath)
+                               if parent == null then
+                                       # Parent is not a group, thus we are not a group either
+                                       mgroups[rdp] = null
                                        return null
                                end
                        end
                end
 
-               # check parent directory
-               var parentpath = dirpath2.join_path("..").simplify_path
-               var parent = get_mgroup(parentpath)
-
                var mgroup
                if parent == null then
-                       # no parent, thus new project
-                       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)
+                       # 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 not readme.file_exists then readme = dirpath2.join_path("README.md")
-               if not readme.file_exists then readme = dirpath2.join_path("README")
                if readme.file_exists then
                        var mdoc = load_markdown(readme)
                        mgroup.mdoc = mdoc
@@ -479,8 +482,7 @@ redef class ModelBuilder
                end
 
                mgroup.filepath = dirpath
-               mgroups[module_absolute_path(dirpath)] = mgroup
-               mgroups[module_absolute_path(dirpath2)] = mgroup
+               mgroups[rdp] = mgroup
                return mgroup
        end
 
@@ -511,12 +513,21 @@ redef class ModelBuilder
        # and the potential modules (and nested modules) are identified (see `MGroup::module_paths`).
        #
        # Basically, this recursively call `get_mgroup` and `identify_file` on each directory entry.
+       #
+       # No-op if the group was already scanned (see `MGroup::scanned`).
        fun scan_group(mgroup: MGroup) do
+               if mgroup.scanned then return
+               mgroup.scanned = true
                var p = mgroup.filepath
+               # a virtual group has nothing to scan
+               if p == null then return
                for f in p.files do
                        var fp = p/f
                        var g = get_mgroup(fp)
-                       if g != null then scan_group(g)
+                       # Recursively scan for groups of the same package
+                       if g != null and g.mpackage == mgroup.mpackage then
+                               scan_group(g)
+                       end
                        identify_file(fp)
                end
        end
@@ -657,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}.")
@@ -704,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
@@ -737,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
@@ -750,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)
@@ -825,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
@@ -938,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)
@@ -947,13 +1028,22 @@ class ModulePath
        redef fun to_s do return filepath
 end
 
+redef class MPackage
+       # The associated `.ini` file, if any
+       #
+       # The `ini` file is given as is and might contain invalid or missing information.
+       #
+       # Some packages, like stand-alone packages or virtual packages have no `ini` file associated.
+       var ini: nullable ConfigTree = null
+end
+
 redef class MGroup
        # Modules paths associated with the group
        var module_paths = new Array[ModulePath]
 
        # 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:
@@ -963,9 +1053,34 @@ redef class MGroup
        # * it has a documentation
        fun is_interesting: Bool
        do
-               return module_paths.length > 1 or mmodules.length > 1 or not in_nesting.direct_smallers.is_empty or mdoc != null
+               return module_paths.length > 1 or
+                       mmodules.length > 1 or
+                       not in_nesting.direct_smallers.is_empty or
+                       mdoc != null or
+                       (mmodules.length == 1 and default_mmodule == null)
        end
 
+       # Are files and directories in self scanned?
+       #
+       # See `ModelBuilder::scan_group`.
+       var scanned = false
+
+       # Return the modules in self and subgroups named `name`.
+       #
+       # If `self` is not scanned (see `ModelBuilder::scan_group`) the
+       # results might be partial.
+       fun mmodule_paths_by_name(name: String): Array[ModulePath]
+       do
+               var res = new Array[ModulePath]
+               for g in in_nesting.smallers do
+                       for mp in g.module_paths do
+                               if mp.name == name then
+                                       res.add mp
+                               end
+                       end
+               end
+               return res
+       end
 end
 
 redef class SourceFile