loader: get_mgroup check and assign the `project.ini` file to projects
[nit.git] / src / loader.nit
index 153bf9f..90c439f 100644 (file)
@@ -18,6 +18,7 @@
 module loader
 
 import modelbuilder_base
+import ini
 
 redef class ToolContext
        # Option --path
@@ -96,7 +97,7 @@ redef class ModelBuilder
        fun parse_group(mgroup: MGroup): Array[MModule]
        do
                var res = new Array[MModule]
-               visit_group(mgroup)
+               scan_group(mgroup)
                for mg in mgroup.in_nesting.smallers do
                        for mp in mg.module_paths do
                                var nmodule = self.load_module(mp.filepath)
@@ -203,36 +204,12 @@ 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.mproject.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
@@ -291,50 +268,23 @@ 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
-                       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
+                       # 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
                end
-               if candidate == null then return null
-               return identify_file(candidate)
+               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
 
        # Cache for `identify_file` by realpath
@@ -344,16 +294,24 @@ 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 project and groups if required.
+       #
+       # This method does what the user expects when giving an argument to a Nit tool.
        #
-       # Silently return `null` if `path` is not a valid module path.
+       # * 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 project `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
        do
                # special case for not a nit file
                if path.file_extension != "nit" then
                        # search dirless files in known -I paths
-                       if path.dirname == "" then
+                       if not path.chars.has('/') then
                                var res = search_module_in_paths(null, path, self.paths)
                                if res != null then return res
                        end
@@ -374,6 +332,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)
@@ -389,7 +352,7 @@ redef class ModelBuilder
                        mgroup = new MGroup(pn, mproject, null) # same name for the root group
                        mgroup.filepath = path
                        mproject.root = mgroup
-                       toolcontext.info("found project `{pn}` at {path}", 2)
+                       toolcontext.info("found singleton project `{pn}` at {path}", 2)
                end
 
                var res = new ModulePath(pn, path, mgroup)
@@ -425,6 +388,13 @@ redef class ModelBuilder
                        return mgroups[rdp]
                end
 
+               # Filter out non-directories
+               var stat = dirpath.file_stat
+               if stat == null or not stat.is_dir then
+                       mgroups[rdp] = null
+                       return null
+               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`
@@ -432,6 +402,14 @@ redef class ModelBuilder
                var pn = rdp.basename(".nit")
                var mp = dirpath.join_path(pn + ".nit").simplify_path
 
+               # Check `project.ini` that indicate a project
+               var ini = null
+               var parent = null
+               var inipath = dirpath / "project.ini"
+               if inipath.file_exists then
+                       ini = new ConfigTree(inipath)
+               end
+
                # dirpath2 is the root directory
                # dirpath is the src subdirectory directory, if any, else it is the same that dirpath2
                var dirpath2 = dirpath
@@ -457,10 +435,12 @@ 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
                else
                        mgroup = new MGroup(pn, parent.mproject, parent)
                        toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
@@ -505,13 +485,27 @@ redef class ModelBuilder
                return mdoc
        end
 
-       # Force the identification of all ModulePath of the group and sub-groups.
-       fun visit_group(mgroup: MGroup) do
+       # Force the identification of all ModulePath of the group and sub-groups in the file system.
+       #
+       # When a group is scanned, its sub-groups hierarchy is filled (see `MGroup::in_nesting`)
+       # 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 visit_group(g)
+                       # Recursively scan for groups of the same project
+                       if g != null and g.mproject == mgroup.mproject then
+                               scan_group(g)
+                       end
                        identify_file(fp)
                end
        end
@@ -942,6 +936,15 @@ class ModulePath
        redef fun to_s do return filepath
 end
 
+redef class MProject
+       # 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.
+       var ini: nullable ConfigTree = null
+end
+
 redef class MGroup
        # Modules paths associated with the group
        var module_paths = new Array[ModulePath]
@@ -958,9 +961,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