check_contrib: additional rules are given by arguments.
[nit.git] / src / loader.nit
index 3bd8404..7d1c564 100644 (file)
@@ -50,7 +50,9 @@ redef class ModelBuilder
                end
 
                var nit_dir = toolcontext.nit_dir
-               var libname = "{nit_dir}/lib"
+               var libname = nit_dir/"lib"
+               if libname.file_exists then paths.add(libname)
+               libname = nit_dir/"contrib"
                if libname.file_exists then paths.add(libname)
        end
 
@@ -72,8 +74,9 @@ redef class ModelBuilder
                        if nmodule == null then continue # Skip error
                        # Load imported module
                        build_module_importation(nmodule)
-
-                       mmodules.add(nmodule.mmodule.as(not null))
+                       var mmodule = nmodule.mmodule
+                       if mmodule == null then continue # skip error
+                       mmodules.add mmodule
                end
                var time1 = get_time
                self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
@@ -93,23 +96,35 @@ 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)
                                if nmodule == null then continue # Skip error
                                # Load imported module
                                build_module_importation(nmodule)
-
-                               res.add(nmodule.mmodule.as(not null))
+                               var mmodule = nmodule.mmodule
+                               if mmodule == null then continue # Skip error
+                               res.add mmodule
                        end
                end
                return res
        end
 
        # Load a bunch of modules and groups.
-       # Each name can be a module or a group.
-       # If it is a group then recursively all its modules are parsed.
+       #
+       # Each name can be:
+       #
+       # * a path to a module, a group or a directory of projects.
+       # * 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.
+       # * else an error is displayed.
+       #
        # See `parse` for details.
        fun parse_full(names: Sequence[String]): Array[MModule]
        do
@@ -118,17 +133,48 @@ redef class ModelBuilder
                self.toolcontext.info("*** PARSE ***", 1)
                var mmodules = new ArraySet[MModule]
                for a in names do
+                       # Case of a group
                        var mgroup = self.get_mgroup(a)
                        if mgroup != null then
                                mmodules.add_all parse_group(mgroup)
                                continue
                        end
+
+                       # Case of a directory that is not a group
+                       var stat = a.to_path.stat
+                       if stat != null and stat.is_dir then
+                               self.toolcontext.info("look in directory {a}", 2)
+                               var fs = a.files
+                               # Try each entry as a group or a module
+                               for f in fs do
+                                       var af = a/f
+                                       mgroup = get_mgroup(af)
+                                       if mgroup != null then
+                                               mmodules.add_all parse_group(mgroup)
+                                               continue
+                                       end
+                                       var mp = identify_file(af)
+                                       if mp != null then
+                                               var nmodule = self.load_module(af)
+                                               if nmodule == null then continue # Skip error
+                                               build_module_importation(nmodule)
+                                               var mmodule = nmodule.mmodule
+                                               if mmodule == null then continue # Skip error
+                                               mmodules.add mmodule
+                                       else
+                                               self.toolcontext.info("ignore file {af}", 2)
+                                       end
+                               end
+                               continue
+                       end
+
                        var nmodule = self.load_module(a)
                        if nmodule == null then continue # Skip error
                        # Load imported module
                        build_module_importation(nmodule)
-
-                       mmodules.add(nmodule.mmodule.as(not null))
+                       var mmodule = nmodule.mmodule
+                       if mmodule == null then continue # Skip error
+                       mmodules.add mmodule
                end
                var time1 = get_time
                self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
@@ -178,6 +224,14 @@ redef class ModelBuilder
                                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
                end
 
@@ -201,9 +255,9 @@ redef class ModelBuilder
 
                if candidate == null then
                        if mgroup != null then
-                               error(anode, "Error: cannot find module {name} from {mgroup.name}. tried {lookpaths.join(", ")}")
+                               error(anode, "Error: cannot find module `{name}` from `{mgroup.name}`. Tried: {lookpaths.join(", ")}.")
                        else
-                               error(anode, "Error: cannot find module {name}. tried {lookpaths.join(", ")}")
+                               error(anode, "Error: cannot find module `{name}`. Tried: {lookpaths.join(", ")}.")
                        end
                        return null
                end
@@ -218,11 +272,19 @@ redef class ModelBuilder
        do
                var path = search_mmodule_by_name(anode, mgroup, name)
                if path == null then return null # Forward error
+               return load_module_path(path)
+       end
+
+       # Load and process importation of a given ModulePath.
+       #
+       # Basically chains `load_module` and `build_module_importation`.
+       fun load_module_path(path: ModulePath): nullable MModule
+       do
                var res = self.load_module(path.filepath)
                if res == null then return null # Forward error
                # Load imported module
                build_module_importation(res)
-               return res.mmodule.as(not null)
+               return res.mmodule
        end
 
        # Search a module `name` from path `lookpaths`.
@@ -240,7 +302,7 @@ redef class ModelBuilder
                                        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}")
+                                               toolcontext.error(location, "Error: conflicting module file for `{name}`: `{candidate}` `{try_file}`")
                                        end
                                end
                        end
@@ -253,7 +315,20 @@ redef class ModelBuilder
                                        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}")
+                                               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
                        end
@@ -269,16 +344,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
@@ -299,6 +382,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)
@@ -350,25 +438,33 @@ redef class ModelBuilder
                        return mgroups[rdp]
                end
 
-               # Hack, a group is determined by:
+               # 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`
                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("")
+                               pn = dirpath2.basename
                        else
-                               return null
+                               # Check a `src` subdirectory
+                               dirpath = dirpath2 / "src"
+                               if not dirpath.file_exists then
+                                       # All rules failed, so return null
+                                       return null
+                               end
                        end
                end
 
                # check parent directory
-               var parentpath = dirpath.join_path("..").simplify_path
+               var parentpath = dirpath2.join_path("..").simplify_path
                var parent = get_mgroup(parentpath)
 
                var mgroup
@@ -382,29 +478,64 @@ redef class ModelBuilder
                        mgroup = new MGroup(pn, parent.mproject, parent)
                        toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
                end
-               var readme = dirpath2.join_path("README.md")
+
+               # search documentation
+               # in src first so the documentation of the project code can be distinct for the documentation of the project 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 = new MDoc(new Location(new SourceFile.from_string(readme, ""),0,0,0,0))
-                       var s = new FileReader.open(readme)
-                       while not s.eof do
-                               mdoc.content.add(s.read_line)
-                       end
+                       var mdoc = load_markdown(readme)
                        mgroup.mdoc = mdoc
                        mdoc.original_mentity = mgroup
                end
+
                mgroup.filepath = dirpath
-               mgroups[rdp] = mgroup
+               mgroups[module_absolute_path(dirpath)] = mgroup
+               mgroups[module_absolute_path(dirpath2)] = mgroup
                return mgroup
        end
 
-       # Force the identification of all ModulePath of the group and sub-groups.
-       fun visit_group(mgroup: MGroup) do
+       # Load a markdown file as a documentation object
+       fun load_markdown(filepath: String): MDoc
+       do
+               var s = new FileReader.open(filepath)
+               var lines = new Array[String]
+               var line_starts = new Array[Int]
+               var len = 1
+               while not s.eof do
+                       var line = s.read_line
+                       lines.add(line)
+                       line_starts.add(len)
+                       len += line.length + 1
+               end
+               s.close
+               var source = new SourceFile.from_string(filepath, lines.join("\n"))
+               source.line_starts.add_all line_starts
+               var mdoc = new MDoc(new Location(source, 1, lines.length, 0, 0))
+               mdoc.content.add_all(lines)
+               return mdoc
+       end
+
+       # 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)
+                       if g != null then scan_group(g)
                        identify_file(fp)
                end
        end
@@ -419,11 +550,11 @@ redef class ModelBuilder
        fun load_module_ast(filename: String): nullable AModule
        do
                if filename.file_extension != "nit" then
-                       self.toolcontext.error(null, "Error: file {filename} is not a valid nit module.")
+                       self.toolcontext.error(null, "Error: file `{filename}` is not a valid nit module.")
                        return null
                end
                if not filename.file_exists then
-                       self.toolcontext.error(null, "Error: file {filename} not found.")
+                       self.toolcontext.error(null, "Error: file `{filename}` not found.")
                        return null
                end
 
@@ -448,6 +579,26 @@ redef class ModelBuilder
                return nmodule
        end
 
+       # Remove Nit source files from a list of arguments.
+       #
+       # Items of `args` that can be loaded as a nit file will be removed from `args` and returned.
+       fun filter_nit_source(args: Array[String]): Array[String]
+       do
+               var keep = new Array[String]
+               var res = new Array[String]
+               for a in args do
+                       var l = identify_file(a)
+                       if l == null then
+                               keep.add a
+                       else
+                               res.add a
+                       end
+               end
+               args.clear
+               args.add_all(keep)
+               return res
+       end
+
        # Try to load a module using a path.
        # Display an error if there is a problem (IO / lexer / parser) and return null.
        # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
@@ -459,7 +610,11 @@ redef class ModelBuilder
                # Look for the module
                var file = identify_file(filename)
                if file == null then
-                       toolcontext.error(null, "Error: cannot find module `{filename}`.")
+                       if filename.file_exists then
+                               toolcontext.error(null, "Error: `{filename}` is not a Nit source file.")
+                       else
+                               toolcontext.error(null, "Error: cannot find module `{filename}`.")
+                       end
                        return null
                end
 
@@ -517,7 +672,7 @@ redef class ModelBuilder
                if decl != null then
                        var decl_name = decl.n_name.n_id.text
                        if decl_name != mod_name then
-                               error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
+                               error(decl.n_name, "Error: module name mismatch; declared {decl_name} file named {mod_name}.")
                        end
                end
 
@@ -528,7 +683,7 @@ redef class ModelBuilder
                                if other.mgroup!= null and other.mgroup.mproject == mgroup.mproject 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}")
+                                       error(node, "Error: a module named `{other.full_name}` already exists at {other.location}.")
                                        break
                                end
                        end
@@ -563,6 +718,21 @@ redef class ModelBuilder
                return mmodule
        end
 
+       # Resolve the module identification for a given `AModuleName`.
+       #
+       # 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)
+       end
+
        # Analyze the module importation and fill the module_importation_hierarchy
        #
        # Unless you used `load_module`, the importation is already done and this method does a no-op.
@@ -574,32 +744,42 @@ redef class ModelBuilder
                var stdimport = true
                var imported_modules = new Array[MModule]
                for aimport in nmodule.n_imports do
+                       # Do not imports conditional
+                       var atconditionals = aimport.get_annotations("conditional")
+                       if atconditionals.not_empty then continue
+
                        stdimport = false
                        if not aimport isa AStdImport then
                                continue
                        end
-                       var mgroup = mmodule.mgroup
-                       if aimport.n_name.n_quad != null then mgroup = null # Start from top level
-                       for grp in aimport.n_name.n_path do
-                               var path = search_mmodule_by_name(grp, mgroup, grp.text)
-                               if path == null then return # Skip error
-                               mgroup = path.mgroup
+
+                       # Load the imported module
+                       var suppath = seach_module_by_amodule_name(aimport.n_name, mmodule.mgroup)
+                       if suppath == null then
+                               nmodule.mmodule = null # invalidate the module
+                               continue # Skip error
+                       end
+                       var sup = load_module_path(suppath)
+                       if sup == null then
+                               nmodule.mmodule = null # invalidate the module
+                               continue # Skip error
                        end
-                       var mod_name = aimport.n_name.n_id.text
-                       var sup = self.get_mmodule_by_name(aimport.n_name, mgroup, mod_name)
-                       if sup == null then continue # Skip error
+
                        aimport.mmodule = sup
                        imported_modules.add(sup)
                        var mvisibility = aimport.n_visibility.mvisibility
                        if mvisibility == protected_visibility then
                                error(aimport.n_visibility, "Error: only properties can be protected.")
+                               nmodule.mmodule = null # invalidate the module
                                return
                        end
                        if sup == mmodule then
-                               error(aimport.n_name, "Error: Dependency loop in module {mmodule}.")
+                               error(aimport.n_name, "Error: dependency loop in module {mmodule}.")
+                               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}.")
+                               error(aimport.n_name, "Error: dependency loop between modules {mmodule} and {sup}.")
+                               nmodule.mmodule = null # invalidate the module
                                return
                        end
                        mmodule.set_visibility_for(sup, mvisibility)
@@ -607,14 +787,63 @@ redef class ModelBuilder
                if stdimport then
                        var mod_name = "standard"
                        var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
-                       if sup != null then # Skip error
+                       if sup == null then
+                               nmodule.mmodule = null # invalidate the module
+                       else # Skip error
                                imported_modules.add(sup)
                                mmodule.set_visibility_for(sup, public_visibility)
                        end
                end
-               self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
+
+               # Declare conditional importation
+               for aimport in nmodule.n_imports do
+                       if not aimport isa AStdImport then continue
+                       var atconditionals = aimport.get_annotations("conditional")
+                       if atconditionals.is_empty then continue
+
+                       var suppath = seach_module_by_amodule_name(aimport.n_name, mmodule.mgroup)
+                       if suppath == null then continue # skip error
+
+                       for atconditional in atconditionals do
+                               var nargs = atconditional.n_args
+                               if nargs.is_empty then
+                                       error(atconditional, "Syntax Error: `conditional` expects module identifiers as arguments.")
+                                       continue
+                               end
+
+                               # The rule
+                               var rule = new Array[Object]
+
+                               # First element is the goal, thus
+                               rule.add suppath
+
+                               # Second element is the first condition, that is to be a client of the current module
+                               rule.add mmodule
+
+                               # Other condition are to be also a client of each modules indicated as arguments of the annotation
+                               for narg in nargs do
+                                       var id = narg.as_id
+                                       if id == null then
+                                               error(narg, "Syntax Error: `conditional` expects module identifier as arguments.")
+                                               continue
+                                       end
+
+                                       var mp = search_mmodule_by_name(narg, mmodule.mgroup, id)
+                                       if mp == null then continue
+
+                                       rule.add mp
+                               end
+
+                               conditional_importations.add rule
+                       end
+               end
+
                mmodule.set_imported_mmodules(imported_modules)
 
+               apply_conditional_importations(mmodule)
+
+               self.toolcontext.info("{mmodule} imports {mmodule.in_importation.direct_greaters.join(", ")}", 3)
+
                # Force standard to be public if imported
                for sup in mmodule.in_importation.greaters do
                        if sup.name == "standard" then
@@ -635,6 +864,72 @@ redef class ModelBuilder
                end
        end
 
+       # Global list of conditional importation rules.
+       #
+       # Each rule is a "Horn clause"-like sequence of modules.
+       # It means that the first module is the module to automatically import.
+       # The remaining modules are the conditions of the rule.
+       #
+       # Each module is either represented by a MModule (if the module is already loaded)
+       # or by a ModulePath (if the module is not yet loaded).
+       #
+       # Rules are declared by `build_module_importation` and are applied by `apply_conditional_importations`
+       # (and `build_module_importation` that calls it).
+       #
+       # TODO (when the loader will be rewritten): use a better representation and move up rules in the model.
+       private var conditional_importations = new Array[SequenceRead[Object]]
+
+       # Extends the current importations according to imported rules about conditional importation
+       fun apply_conditional_importations(mmodule: MModule)
+       do
+               # Because a conditional importation may cause additional conditional importation, use a fixed point
+               # The rules are checked naively because we assume that it does not worth to be optimized
+               var check_conditional_importations = true
+               while check_conditional_importations do
+                       check_conditional_importations = false
+
+                       for ci in conditional_importations do
+                               # Check conditions
+                               for i in [1..ci.length[ do
+                                       var rule_element = ci[i]
+                                       # An element of a rule is either a MModule or a ModulePath
+                                       # We need the mmodule to resonate on the importation
+                                       var m
+                                       if rule_element isa MModule then
+                                               m = rule_element
+                                       else if rule_element isa ModulePath then
+                                               m = rule_element.mmodule
+                                               # Is loaded?
+                                               if m == null then continue label
+                                       else
+                                               abort
+                                       end
+                                       # Is imported?
+                                       if not mmodule.in_importation.greaters.has(m) then continue label
+                               end
+                               # Still here? It means that all conditions modules are loaded and imported
+
+                               # Identify the module to automatically import
+                               var suppath = ci.first.as(ModulePath)
+                               var sup = load_module_path(suppath)
+                               if sup == null then continue
+
+                               # Do nothing if already imported
+                               if mmodule.in_importation.greaters.has(sup) then continue label
+
+                               # Import it
+                               self.toolcontext.info("{mmodule} conditionally imports {sup}", 3)
+                               # TODO visibility rules (currently always public)
+                               mmodule.set_visibility_for(sup, public_visibility)
+                               # TODO linearization rules (currently added at the end in the order of the rules)
+                               mmodule.set_imported_mmodules([sup])
+
+                               # Prepare to reapply the rules
+                               check_conditional_importations = true
+                       end label
+               end
+       end
+
        # All the loaded modules
        var nmodules = new Array[AModule]
 
@@ -687,9 +982,17 @@ 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
 end
 
 redef class SourceFile