loader: use `mmodule_paths_by_name` to simplify `search_mmodule_by_name` and `search_...
[nit.git] / src / loader.nit
index 153bf9f..d3c9f7e 100644 (file)
@@ -96,7 +96,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 +203,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 +267,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 +293,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.
+       #
+       # * 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` 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
                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 +331,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)
@@ -505,13 +467,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
@@ -958,9 +934,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