Rename REAMDE to README.md
[nit.git] / src / loader.nit
index 8106e20..0e6878b 100644 (file)
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Load nit source files and build the associated model
+# Loading of Nit source files
 module loader
 
 import modelbuilder_base
@@ -60,7 +60,7 @@ redef class ModelBuilder
        # The result is the corresponding model elements.
        # Errors and warnings are printed with the toolcontext.
        #
-       # Note: class and property model element are not analysed.
+       # Note: class and property model elements are not analysed.
        fun parse(modules: Sequence[String]): Array[MModule]
        do
                var time0 = get_time
@@ -70,7 +70,109 @@ redef class ModelBuilder
                for a in modules do
                        var nmodule = self.load_module(a)
                        if nmodule == null then continue # Skip error
-                       mmodules.add(nmodule.mmodule.as(not null))
+                       # Load imported module
+                       build_module_importation(nmodule)
+                       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)
+
+               self.toolcontext.check_errors
+
+               if toolcontext.opt_only_parse.value then
+                       self.toolcontext.info("*** ONLY PARSE...", 1)
+                       exit(0)
+               end
+
+               return mmodules.to_a
+       end
+
+       # Load recursively all modules of the group `mgroup`.
+       # See `parse` for details.
+       fun parse_group(mgroup: MGroup): Array[MModule]
+       do
+               var res = new Array[MModule]
+               visit_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)
+                               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 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
+               var time0 = get_time
+               # Parse and recursively load
+               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)
+                       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)
@@ -86,15 +188,16 @@ redef class ModelBuilder
        end
 
        # The list of directories to search for top level modules
-       # The list is initially set with :
+       # The list is initially set with:
+       #
        #   * the toolcontext --path option
        #   * the NIT_PATH environment variable
        #   * `toolcontext.nit_dir`
        # Path can be added (or removed) by the client
        var paths = new Array[String]
 
-       # Like (an used by) `get_mmodule_by_name` but just return the ModulePath
-       private fun search_mmodule_by_name(anode: nullable ANode, mgroup: nullable MGroup, name: String): nullable ModulePath
+       # Like (and used by) `get_mmodule_by_name` but just return the ModulePath
+       fun search_mmodule_by_name(anode: nullable ANode, mgroup: nullable MGroup, name: String): nullable ModulePath
        do
                # First, look in groups
                var c = mgroup
@@ -125,7 +228,7 @@ redef class ModelBuilder
                # Look at some known directories
                var lookpaths = self.paths
 
-               # Look in the directory of the group project also (even if not explicitely in the path)
+               # Look in the directory of the group project also (even if not explicitly in the path)
                if mgroup != null then
                        # path of the root group
                        var dirname = mgroup.mproject.root.filepath
@@ -142,9 +245,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
@@ -161,7 +264,9 @@ redef class ModelBuilder
                if path == null then return null # Forward error
                var res = self.load_module(path.filepath)
                if res == null then return null # Forward error
-               return res.mmodule.as(not null)
+               # Load imported module
+               build_module_importation(res)
+               return res.mmodule
        end
 
        # Search a module `name` from path `lookpaths`.
@@ -179,7 +284,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
@@ -192,7 +297,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
@@ -201,20 +306,28 @@ redef class ModelBuilder
                return identify_file(candidate)
        end
 
-       # cache for `identify_file` by realpath
-       private var identified_files = new HashMap[String, nullable ModulePath]
+       # Cache for `identify_file` by realpath
+       private var identified_files_by_path = new HashMap[String, nullable ModulePath]
+
+       # All the currently identified modules.
+       # See `identify_file`.
+       var identified_files = new Array[ModulePath]
 
        # Identify a source file
        # Load the associated project and groups if required
-       private fun identify_file(path: String): nullable ModulePath
+       #
+       # Silently return `null` if `path` is not a valid module path.
+       fun identify_file(path: String): nullable ModulePath
        do
                # special case for not a nit file
                if path.file_extension != "nit" then
-                       # search in known -I paths
-                       var res = search_module_in_paths(null, path, self.paths)
-                       if res != null then return res
+                       # search dirless files in known -I paths
+                       if path.dirname == "" then
+                               var res = search_module_in_paths(null, path, self.paths)
+                               if res != null then return res
+                       end
 
-                       # Found nothins? maybe it is a group...
+                       # Found nothing? maybe it is a group...
                        var candidate = null
                        if path.file_exists then
                                var mgroup = get_mgroup(path)
@@ -225,7 +338,6 @@ redef class ModelBuilder
                        end
 
                        if candidate == null then
-                               toolcontext.error(null, "Error: cannot find module `{path}`.")
                                return null
                        end
                        path = candidate
@@ -234,7 +346,7 @@ redef class ModelBuilder
                # Fast track, the path is already known
                var pn = path.basename(".nit")
                var rp = module_absolute_path(path)
-               if identified_files.has_key(rp) then return identified_files[rp]
+               if identified_files_by_path.has_key(rp) then return identified_files_by_path[rp]
 
                # Search for a group
                var mgrouppath = path.join_path("..").simplify_path
@@ -252,28 +364,45 @@ redef class ModelBuilder
                var res = new ModulePath(pn, path, mgroup)
                mgroup.module_paths.add(res)
 
-               identified_files[rp] = res
+               identified_files_by_path[rp] = res
+               identified_files.add(res)
                return res
        end
 
-       # groups by path
+       # Groups by path
        private var mgroups = new HashMap[String, nullable MGroup]
 
-       # return the mgroup associated to a directory path
-       # if the directory is not a group null is returned
-       private fun get_mgroup(dirpath: String): nullable MGroup
+       # Return the mgroup associated to a directory path.
+       # If the directory is not a group null is returned.
+       #
+       # Note: `paths` is also used to look for mgroups
+       fun get_mgroup(dirpath: String): nullable MGroup
        do
+               if not dirpath.file_exists then do
+                       for p in paths do
+                               var try = p / dirpath
+                               if try.file_exists then
+                                       dirpath = try
+                                       break label
+                               end
+                       end
+                       return null
+               end label
+
                var rdp = module_absolute_path(dirpath)
                if mgroups.has_key(rdp) then
                        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
@@ -281,12 +410,17 @@ redef class ModelBuilder
                                dirpath2 = rdp.dirname
                                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
@@ -300,22 +434,47 @@ 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
-                       var s = new IFStream.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
 
+       # Load a markdown file as a documentation object
+       fun load_markdown(filepath: String): MDoc
+       do
+               var mdoc = new MDoc(new Location(new SourceFile.from_string(filepath, ""),0,0,0,0))
+               var s = new FileReader.open(filepath)
+               while not s.eof do
+                       mdoc.content.add(s.read_line)
+               end
+               return mdoc
+       end
+
+       # Force the identification of all ModulePath of the group and sub-groups.
+       fun visit_group(mgroup: MGroup) do
+               var p = mgroup.filepath
+               for f in p.files do
+                       var fp = p/f
+                       var g = get_mgroup(fp)
+                       if g != null then visit_group(g)
+                       identify_file(fp)
+               end
+       end
+
        # Transform relative paths (starting with '../') into absolute paths
        private fun module_absolute_path(path: String): String do
                return getcwd.join_path(path).simplify_path
@@ -326,18 +485,18 @@ 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
 
                self.toolcontext.info("load module {filename}", 2)
 
                # Load the file
-               var file = new IFStream.open(filename)
+               var file = new FileReader.open(filename)
                var lexer = new Lexer(new SourceFile(filename, file))
                var parser = new Parser(lexer)
                var tree = parser.parse
@@ -355,14 +514,44 @@ redef class ModelBuilder
                return nmodule
        end
 
-       # Try to load a module and its imported modules using a path.
-       # Display an error if there is a problem (IO / lexer / parser / importation) and return null
+       # 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.
+       #
+       # The MModule is created however, the importation is not performed,
+       # therefore you should call `build_module_importation`.
        fun load_module(filename: String): nullable AModule
        do
                # Look for the module
                var file = identify_file(filename)
-               if file == null then return null # forward error
+               if file == null then
+                       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
 
                # Already known and loaded? then return it
                var mmodule = file.mmodule
@@ -382,14 +571,11 @@ redef class ModelBuilder
                # Update the file information
                file.mmodule = mmodule
 
-               # Load imported module
-               build_module_importation(nmodule)
-
                return nmodule
        end
 
        # Injection of a new module without source.
-       # Used by the interpreted
+       # Used by the interpreter.
        fun load_rt_module(parent: nullable MModule, nmodule: AModule, mod_name: String): nullable AModule
        do
                # Create the module
@@ -418,12 +604,23 @@ redef class ModelBuilder
        do
                # Check the module name
                var decl = nmodule.n_moduledecl
-               if decl == null then
-                       #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
-               else
+               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
+
+               # Check for conflicting module names in the project
+               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
+                                       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}.")
+                                       break
+                               end
                        end
                end
 
@@ -433,7 +630,14 @@ redef class ModelBuilder
                nmodules.add(nmodule)
                self.mmodule2nmodule[mmodule] = nmodule
 
+               var source = nmodule.location.file
+               if source != null then
+                       assert source.mmodule == null
+                       source.mmodule = mmodule
+               end
+
                if decl != null then
+                       # Extract documentation
                        var ndoc = decl.n_doc
                        if ndoc != null then
                                var mdoc = ndoc.to_mdoc
@@ -442,13 +646,17 @@ redef class ModelBuilder
                        else
                                advice(decl, "missing-doc", "Documentation warning: Undocumented module `{mmodule}`")
                        end
+                       # Is the module a test suite?
+                       mmodule.is_test_suite = not decl.get_annotations("test_suite").is_empty
                end
 
                return mmodule
        end
 
-       # Analysis the module importation and fill the module_importation_hierarchy
-       private fun build_module_importation(nmodule: AModule)
+       # 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.
+       fun build_module_importation(nmodule: AModule)
        do
                if nmodule.is_importation_done then return
                nmodule.is_importation_done = true
@@ -464,24 +672,33 @@ redef class ModelBuilder
                        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
+                               if path == null then
+                                       nmodule.mmodule = null # invalidate the module
+                                       return # Skip error
+                               end
                                mgroup = path.mgroup
                        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
+                       if sup == null then
+                               nmodule.mmodule = null # invalidate the module
+                               continue # Skip error
+                       end
                        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)
@@ -489,7 +706,9 @@ 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
@@ -521,12 +740,22 @@ redef class ModelBuilder
        var nmodules = new Array[AModule]
 
        # Register the nmodule associated to each mmodule
-       # FIXME: why not refine the `MModule` class with a nullable attribute?
-       var mmodule2nmodule = new HashMap[MModule, AModule]
+       #
+       # Public clients need to use `mmodule2node` to access stuff.
+       private var mmodule2nmodule = new HashMap[MModule, AModule]
+
+       # Retrieve the associated AST node of a mmodule.
+       # This method is used to associate model entity with syntactic entities.
+       #
+       # If the module is not associated with a node, returns null.
+       fun mmodule2node(mmodule: MModule): nullable AModule
+       do
+               return mmodule2nmodule.get_or_null(mmodule)
+       end
 end
 
-# placeholder to a module file identified but not always loaded in a project
-private class ModulePath
+# File-system location of a module (file) that is identified but not always loaded.
+class ModulePath
        # The name of the module
        # (it's the basename of the filepath)
        var name: String
@@ -544,8 +773,29 @@ private class ModulePath
 end
 
 redef class MGroup
-       # modules paths associated with the group
-       private var module_paths = new Array[ModulePath]
+       # 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
+       # always interesting.
+       #
+       # A interesting group has, at least, one of the following true:
+       #
+       # * it has 2 modules or more
+       # * it has a subgroup
+       # * 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
+       end
+
+end
+
+redef class SourceFile
+       # Associated mmodule, once created
+       var mmodule: nullable MModule = null
 end
 
 redef class AStdImport