model: intro `MModule::first_real_mmodule` to get the first non-fictive module
[nit.git] / src / loader.nit
index 5edd243..492584b 100644 (file)
@@ -88,6 +88,61 @@ redef class ModelBuilder
                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)
+
+                               res.add(nmodule.mmodule.as(not null))
+                       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.
+       # 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
+                       var mgroup = self.get_mgroup(a)
+                       if mgroup != null then
+                               mmodules.add_all parse_group(mgroup)
+                               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))
+               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
+
        # The list of directories to search for top level modules
        # The list is initially set with:
        #
@@ -208,7 +263,11 @@ redef class ModelBuilder
        end
 
        # Cache for `identify_file` by realpath
-       private var identified_files = new HashMap[String, nullable ModulePath]
+       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
@@ -243,7 +302,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
@@ -261,7 +320,8 @@ 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
 
@@ -270,8 +330,21 @@ redef class ModelBuilder
 
        # 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]
@@ -312,11 +385,7 @@ redef class ModelBuilder
                var 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
@@ -325,6 +394,17 @@ redef class ModelBuilder
                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
@@ -357,7 +437,7 @@ redef class ModelBuilder
                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
@@ -375,6 +455,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.
@@ -386,7 +486,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
 
@@ -441,22 +545,40 @@ 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}")
                        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
+
                # Create the module
                var mmodule = new MModule(model, mgroup, mod_name, nmodule.location)
                nmodule.mmodule = mmodule
                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
@@ -465,6 +587,8 @@ 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
@@ -546,8 +670,18 @@ 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
 
 # File-system location of a module (file) that is identified but not always loaded.
@@ -589,6 +723,11 @@ redef class MGroup
 
 end
 
+redef class SourceFile
+       # Associated mmodule, once created
+       var mmodule: nullable MModule = null
+end
+
 redef class AStdImport
        # The imported module once determined
        var mmodule: nullable MModule = null