loader: look for packages installed via picnit
[nit.git] / src / loader.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2012 Jean Privat <jean@pryen.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Loading of Nit source files
18 #
19 # The loader takes care of looking for module and projects in the file system, and the associated case of errors.
20 # The loading requires several steps:
21 #
22 # Identify: create an empty model entity associated to a name or a file path.
23 # Identification is used for instance when names are given in the command line.
24 # See `identify_module` and `identify_group`.
25 #
26 # Scan: visit directories and identify their contents.
27 # Scanning is done to enable the searching of modules in projects.
28 # See `scan_group` and `scan_full`.
29 #
30 # Parse: load the AST and associate it with the model entity.
31 # See `MModule::parse`.
32 #
33 # Import: means recursively load modules imported by a module.
34 # See `build_module_importation`.
35 #
36 # Load: means doing the full sequence: identify, parse and import.
37 # See `ModelBuilder::parse`, `ModelBuilder::parse_full`, `MModule::load` `ModelBuilder::load_module.
38 module loader
39
40 import modelbuilder_base
41 import ini
42 import picnit_shared
43
44 redef class ToolContext
45 # Option --path
46 var opt_path = new OptionArray("Add an additional include path (may be used more than once)", "-I", "--path")
47
48 # Option --only-metamodel
49 var opt_only_metamodel = new OptionBool("Stop after meta-model processing", "--only-metamodel")
50
51 # Option --only-parse
52 var opt_only_parse = new OptionBool("Only proceed to parse files", "--only-parse")
53
54 redef init
55 do
56 super
57 option_context.add_option(opt_path, opt_only_parse, opt_only_metamodel)
58 end
59 end
60
61 redef class ModelBuilder
62 redef init
63 do
64 super
65
66 # Setup the paths value
67 paths.append(toolcontext.opt_path.value)
68
69 # Packages managed by picnit
70 paths.add picnit_lib_dir
71
72 var path_env = "NIT_PATH".environ
73 if not path_env.is_empty then
74 paths.append(path_env.split_with(':'))
75 end
76
77 var nit_dir = toolcontext.nit_dir
78 if nit_dir != null then
79 var libname = nit_dir/"lib"
80 if libname.file_exists then paths.add(libname)
81 libname = nit_dir/"contrib"
82 if libname.file_exists then paths.add(libname)
83 end
84 end
85
86 # Load a bunch of modules.
87 # `modules` can contains filenames or module names.
88 # Imported modules are automatically loaded and modelized.
89 # The result is the corresponding model elements.
90 # Errors and warnings are printed with the toolcontext.
91 #
92 # Note: class and property model elements are not analysed.
93 fun parse(modules: Sequence[String]): Array[MModule]
94 do
95 var time0 = get_time
96 # Parse and recursively load
97 self.toolcontext.info("*** PARSE ***", 1)
98 var mmodules = new ArraySet[MModule]
99 for a in modules do
100 var nmodule = self.load_module(a)
101 if nmodule == null then continue # Skip error
102 var mmodule = nmodule.mmodule
103 if mmodule == null then continue # skip error
104 mmodules.add mmodule
105 end
106 var time1 = get_time
107 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
108
109 self.toolcontext.check_errors
110
111 if toolcontext.opt_only_parse.value then
112 self.toolcontext.info("*** ONLY PARSE...", 1)
113 self.toolcontext.quit
114 end
115
116 return mmodules.to_a
117 end
118
119 # Identify a bunch of modules and groups.
120 #
121 # This does the same as `parse_full` but does only the identification (cf. `identify_module`)
122 fun scan_full(names: Sequence[String]): Array[MModule]
123 do
124 var mmodules = new Array[MModule]
125 for a in names do
126 # Case of a group (root or sub-directory)
127 var mgroup = self.identify_group(a)
128 if mgroup != null then
129 scan_group(mgroup)
130 for mg in mgroup.in_nesting.smallers do mmodules.add_all mg.mmodules
131 continue
132 end
133
134 # Case of a directory that is not a group
135 var stat = a.to_path.stat
136 if stat != null and stat.is_dir then
137 self.toolcontext.info("look in directory {a}", 2)
138 var fs = a.files
139 alpha_comparator.sort(fs)
140 # Try each entry as a group or a module
141 for f in fs do
142 if f.first == '.' then continue
143 var af = a/f
144 mgroup = identify_group(af)
145 if mgroup != null then
146 scan_group(mgroup)
147 for mg in mgroup.in_nesting.smallers do mmodules.add_all mg.mmodules
148 continue
149 end
150 var mmodule = identify_module(af)
151 if mmodule != null then
152 mmodules.add mmodule
153 else
154 self.toolcontext.info("ignore file {af}", 2)
155 end
156 end
157 continue
158 end
159
160 var mmodule = identify_module(a)
161 if mmodule == null then
162 var le = last_loader_error
163 if le != null then
164 toolcontext.error(null, le)
165 else if a.file_exists then
166 toolcontext.error(null, "Error: `{a}` is not a Nit source file.")
167 else
168 toolcontext.error(null, "Error: cannot find module `{a}`.")
169 end
170 continue
171 end
172
173 mmodules.add mmodule
174 end
175 return mmodules
176 end
177
178 # Load a bunch of modules and groups.
179 #
180 # Each name can be:
181 #
182 # * a path to a module, a group or a directory of packages.
183 # * a short name of a module or a group that are looked in the `paths` (-I)
184 #
185 # Then, for each entry, if it is:
186 #
187 # * a module, then is it parsed and returned.
188 # * a group then recursively all its modules are parsed.
189 # * a directory of packages then all the modules of all packages are parsed.
190 # * else an error is displayed.
191 #
192 # See `parse` for details.
193 fun parse_full(names: Sequence[String]): Array[MModule]
194 do
195 var time0 = get_time
196 # Parse and recursively load
197 self.toolcontext.info("*** PARSE ***", 1)
198 var mmodules = new ArraySet[MModule]
199 var scans = scan_full(names)
200 for mmodule in scans do
201 var ast = mmodule.load(self)
202 if ast == null then continue # Skip error
203 mmodules.add mmodule
204 end
205 var time1 = get_time
206 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
207
208 self.toolcontext.check_errors
209
210 if toolcontext.opt_only_parse.value then
211 self.toolcontext.info("*** ONLY PARSE...", 1)
212 self.toolcontext.quit
213 end
214
215 return mmodules.to_a
216 end
217
218 # The list of directories to search for top level modules
219 # The list is initially set with:
220 #
221 # * the toolcontext --path option
222 # * the NIT_PATH environment variable
223 # * `toolcontext.nit_dir`
224 # Path can be added (or removed) by the client
225 var paths = new Array[String]
226
227 # Like (and used by) `get_mmodule_by_name` but does not force the parsing of the MModule (cf. `identify_module`)
228 fun search_mmodule_by_name(anode: nullable ANode, mgroup: nullable MGroup, name: String): nullable MModule
229 do
230 # First, look in groups
231 var c = mgroup
232 if c != null then
233 var r = c.mpackage.root
234 assert r != null
235 scan_group(r)
236 var res = r.mmodules_by_name(name)
237 if res.not_empty then return res.first
238 end
239
240 # Look at some known directories
241 var lookpaths = self.paths
242
243 # Look in the directory of the group package also (even if not explicitly in the path)
244 if mgroup != null then
245 # path of the root group
246 var dirname = mgroup.mpackage.root.filepath
247 if dirname != null then
248 dirname = dirname.join_path("..").simplify_path
249 if not lookpaths.has(dirname) and dirname.file_exists then
250 lookpaths = lookpaths.to_a
251 lookpaths.add(dirname)
252 end
253 end
254 end
255
256 var loc = null
257 if anode != null then loc = anode.hot_location
258 var candidate = search_module_in_paths(loc, name, lookpaths)
259
260 if candidate == null then
261 if mgroup != null then
262 error(anode, "Error: cannot find module `{name}` from `{mgroup.name}`. Tried: {lookpaths.join(", ")}.")
263 else
264 error(anode, "Error: cannot find module `{name}`. Tried: {lookpaths.join(", ")}.")
265 end
266 return null
267 end
268 return candidate
269 end
270
271 # Get a module by its short name; if required, the module is loaded, parsed and its hierarchies computed.
272 # If `mgroup` is set, then the module search starts from it up to the top level (see `paths`);
273 # if `mgroup` is null then the module is searched in the top level only.
274 # If no module exists or there is a name conflict, then an error on `anode` is displayed and null is returned.
275 fun get_mmodule_by_name(anode: nullable ANode, mgroup: nullable MGroup, name: String): nullable MModule
276 do
277 var mmodule = search_mmodule_by_name(anode, mgroup, name)
278 if mmodule == null then return null # Forward error
279 var ast = mmodule.load(self)
280 if ast == null then return null # Forward error
281 return mmodule
282 end
283
284 # Search a module `name` from path `lookpaths`.
285 # If found, the module is returned.
286 private fun search_module_in_paths(location: nullable Location, name: String, lookpaths: Collection[String]): nullable MModule
287 do
288 var res = new ArraySet[MModule]
289 for dirname in lookpaths do
290 # Try a single module file
291 var mp = identify_module((dirname/"{name}.nit").simplify_path)
292 if mp != null then res.add mp
293 # Try the default module of a group
294 var g = identify_group((dirname/name).simplify_path)
295 if g != null then
296 scan_group(g)
297 res.add_all g.mmodules_by_name(name)
298 end
299 end
300 if res.is_empty then return null
301 if res.length > 1 then
302 toolcontext.error(location, "Error: conflicting module files for `{name}`: `{[for x in res do x.filepath or else x.full_name].join("`, `")}`")
303 end
304 return res.first
305 end
306
307 # Search groups named `name` from paths `lookpaths`.
308 private fun search_group_in_paths(name: String, lookpaths: Collection[String]): ArraySet[MGroup]
309 do
310 var res = new ArraySet[MGroup]
311 for dirname in lookpaths do
312 # try a single group directory
313 var mg = identify_group(dirname/name)
314 if mg != null then
315 res.add mg
316 end
317 end
318 return res
319 end
320
321 # Cache for `identify_module` by relative and real paths
322 private var identified_modules_by_path = new HashMap[String, nullable MModule]
323
324 # All the currently identified modules.
325 # See `identify_module`.
326 #
327 # An identified module exists in the model but might be not yet parsed (no AST), or not yet analysed (no importation).
328 var identified_modules = new Array[MModule]
329
330 # All the currently parsed modules.
331 #
332 # A parsed module exists in the model but might be not yet analysed (no importation).
333 var parsed_modules = new Array[MModule]
334
335 # Some `loader` services are silent and return `null` on error.
336 #
337 # Those services can set `last_loader_error` to precise an specific error message.
338 # if `last_loader_error == null` then a generic error message can be used.
339 #
340 # See `identified_modules` and `identify_group` for details.
341 var last_loader_error: nullable String = null
342
343 # Identify a source file and load the associated package and groups if required.
344 #
345 # This method does what the user expects when giving an argument to a Nit tool.
346 #
347 # * If `path` is an existing Nit source file (with the `.nit` extension),
348 # then the associated MModule is returned
349 # * If `path` is a directory (with a `/`),
350 # then the MModule of its default module is returned (if any)
351 # * If `path` is a simple identifier (eg. `digraph`),
352 # then the main module of the package `digraph` is searched in `paths` and returned.
353 #
354 # Silently return `null` if `path` does not exists or cannot be identified.
355 # If `null` is returned, `last_loader_error` can be set to a specific error message.
356 #
357 # On success, it returns a module that is possibly not yet parsed (no AST), or not yet analysed (no importation).
358 # If the module was already identified, or loaded, it is returned.
359 fun identify_module(path: String): nullable MModule
360 do
361 last_loader_error = null
362
363 # special case for not a nit file
364 if not path.has_suffix(".nit") then do
365 # search dirless files in known -I paths
366 if not path.chars.has('/') then
367 var res = search_module_in_paths(null, path, self.paths)
368 if res != null then return res
369 end
370
371 # Found nothing? maybe it is a group...
372 if path.file_exists then
373 var mgroup = identify_group(path)
374 if mgroup != null then
375 var owner_path = mgroup.filepath.join_path(mgroup.name + ".nit")
376 if owner_path.file_exists then
377 path = owner_path
378 break
379 end
380 end
381 end
382
383 # Found nothing? maybe it is a qualified name
384 if path.chars.has(':') then
385 var ids = path.split("::")
386 var g = identify_group(ids.first)
387 if g != null then
388 scan_group(g)
389 var ms = g.mmodules_by_name(ids.last)
390
391 # Return exact match
392 for m in ms do
393 if m.full_name == path then
394 return m
395 end
396 end
397
398 # Where there is only one or two names `foo::bar`
399 # then accept module that matches `foo::*::bar`
400 if ids.length <= 2 then
401 if ms.length == 1 then return ms.first
402 if ms.length > 1 then
403 var l = new Array[String]
404 for m in ms do
405 var fp = m.filepath
406 if fp != null then fp = " ({fp})" else fp = ""
407 l.add "`{m.full_name}`{fp}"
408 end
409 last_loader_error = "Error: conflicting module for `{path}`: {l.join(", ")} "
410 return null
411 end
412 end
413
414 var bests = new BestDistance[String](path.length / 2)
415 # We found nothing. But propose something in the package?
416 for sg in g.mpackage.mgroups do
417 for m in sg.mmodules do
418 var d = path.levenshtein_distance(m.full_name)
419 bests.update(d, m.full_name)
420 end
421 end
422 var last_loader_error = "Error: cannot find module `{path}`."
423 if bests.best_items.not_empty then
424 last_loader_error += " Did you mean " + bests.best_items.join(", ", " or ") + "?"
425 end
426 self.last_loader_error = last_loader_error
427 return null
428 end
429 end
430
431 return null
432 end
433
434 # Does the file exists?
435 if not path.file_exists then
436 return null
437 end
438
439 # Fast track, the path is already known
440 if identified_modules_by_path.has_key(path) then return identified_modules_by_path[path]
441 var rp = module_absolute_path(path)
442 if identified_modules_by_path.has_key(rp) then return identified_modules_by_path[rp]
443
444 var pn = path.basename(".nit")
445
446 # Search for a group
447 var mgrouppath = path.join_path("..").simplify_path
448 var mgroup = identify_group(mgrouppath)
449
450 if mgroup != null then
451 var mpackage = mgroup.mpackage
452 if not mpackage.accept(path) then
453 mgroup = null
454 toolcontext.info("module `{path}` excluded from package `{mpackage}`", 2)
455 end
456 end
457 if mgroup == null then
458 # singleton package
459 var loc = new Location.opaque_file(path)
460 var mpackage = new MPackage(pn, model, loc)
461 mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group
462 mpackage.root = mgroup
463 toolcontext.info("found singleton package `{pn}` at {path}", 2)
464
465 # Attach homonymous `ini` file to the package
466 var inipath = path.dirname / "{pn}.ini"
467 if inipath.file_exists then
468 var ini = new ConfigTree(inipath)
469 mpackage.ini = ini
470 end
471 end
472
473 var loc = new Location.opaque_file(path)
474 var res = new MModule(model, mgroup, pn, loc)
475
476 identified_modules_by_path[rp] = res
477 identified_modules_by_path[path] = res
478 identified_modules.add(res)
479 return res
480 end
481
482 # Groups by path
483 private var mgroups = new HashMap[String, nullable MGroup]
484
485 # Return the mgroup associated to a directory path.
486 # If the directory is not a group null is returned.
487 #
488 # Silently return `null` if `dirpath` does not exists, is not a directory,
489 # cannot be identified or cannot be attached to a mpackage.
490 # If `null` is returned, `last_loader_error` can be set to a specific error message.
491 #
492 # Note: `paths` is also used to look for mgroups
493 fun identify_group(dirpath: String): nullable MGroup
494 do
495 # Reset error
496 last_loader_error = null
497
498 var stat = dirpath.file_stat
499
500 if stat == null or not stat.is_dir then do
501 # search dirless directories in known -I paths
502 if dirpath.chars.has('/') then return null
503 for p in paths do
504 var try = p / dirpath
505 stat = try.file_stat
506 if stat != null then
507 dirpath = try
508 break label
509 end
510 end
511 return null
512 end label
513
514 # Filter out non-directories
515 if not stat.is_dir then
516 last_loader_error = "Error: `{dirpath}` is not a directory."
517 return null
518 end
519
520 # Fast track, the path is already known
521 var rdp = module_absolute_path(dirpath)
522 if mgroups.has_key(rdp) then
523 return mgroups[rdp]
524 end
525
526 # By default, the name of the package or group is the base_name of the directory
527 var pn = rdp.basename
528
529 # Check `package.ini` that indicate a package
530 var ini = null
531 var parent = null
532 var inipath = dirpath / "package.ini"
533 if inipath.file_exists then
534 ini = new ConfigTree(inipath)
535 end
536
537 if ini == null then
538 # No ini, multiple course of action
539
540 # The root of the directory hierarchy in the file system.
541 if rdp == "/" then
542 mgroups[rdp] = null
543 last_loader_error = "Error: `{dirpath}` is not a Nit package."
544 return null
545 end
546
547 # Special stopper `packages.ini`
548 if (dirpath/"packages.ini").file_exists then
549 # dirpath cannot be a package since it is a package directory
550 mgroups[rdp] = null
551 last_loader_error = "Error: `{dirpath}` is not a Nit package."
552 return null
553 end
554
555 # check the parent directory (if it does not contain the stopper file)
556 var parentpath = dirpath.join_path("..").simplify_path
557 var stopper = parentpath / "packages.ini"
558 if not stopper.file_exists then
559 # Recursively get the parent group
560 parent = identify_group(parentpath)
561 if parent != null then do
562 var mpackage = parent.mpackage
563 if not mpackage.accept(dirpath) then
564 toolcontext.info("directory `{dirpath}` excluded from package `{mpackage}`", 2)
565 parent = null
566 end
567 end
568 if parent == null then
569 # Parent is not a group, thus we are not a group either
570 mgroups[rdp] = null
571 last_loader_error = "Error: `{dirpath}` is not a Nit package."
572 return null
573 end
574 end
575 end
576
577 var loc = new Location.opaque_file(dirpath)
578 var mgroup
579 if parent == null then
580 # no parent, thus new package
581 if ini != null then pn = ini["package.name"] or else pn
582 var mpackage = new MPackage(pn, model, loc)
583 mgroup = new MGroup(pn, loc, mpackage, null) # same name for the root group
584 mpackage.root = mgroup
585 toolcontext.info("found package `{mpackage}` at {dirpath}", 2)
586 mpackage.ini = ini
587 else
588 mgroup = new MGroup(pn, loc, parent.mpackage, parent)
589 toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
590 end
591
592 # search documentation
593 # in src first so the documentation of the package code can be distinct for the documentation of the package usage
594 var readme = dirpath.join_path("README.md")
595 if not readme.file_exists then readme = dirpath.join_path("README")
596 if readme.file_exists then
597 var mdoc = load_markdown(readme)
598 mgroup.mdoc = mdoc
599 mdoc.original_mentity = mgroup
600 end
601
602 mgroups[rdp] = mgroup
603 return mgroup
604 end
605
606 # Load a markdown file as a documentation object
607 fun load_markdown(filepath: String): MDoc
608 do
609 var s = new FileReader.open(filepath)
610 var lines = new Array[String]
611 var line_starts = new Array[Int]
612 var len = 1
613 while not s.eof do
614 var line = s.read_line
615 lines.add(line)
616 line_starts.add(len)
617 len += line.length + 1
618 end
619 s.close
620 var source = new SourceFile.from_string(filepath, lines.join("\n"))
621 source.line_starts.add_all line_starts
622 var mdoc = new MDoc(new Location(source, 1, lines.length, 0, 0))
623 mdoc.content.add_all(lines)
624 return mdoc
625 end
626
627 # Force the identification of all MModule of the group and sub-groups in the file system.
628 #
629 # When a group is scanned, its sub-groups hierarchy is filled (see `MGroup::in_nesting`)
630 # and the potential modules (and nested modules) are identified (see `MGroup::modules`).
631 #
632 # Basically, this recursively call `identify_group` and `identify_module` on each directory entry.
633 #
634 # No-op if the group was already scanned (see `MGroup::scanned`).
635 fun scan_group(mgroup: MGroup) do
636 if mgroup.scanned then return
637 mgroup.scanned = true
638 var p = mgroup.filepath
639 # a virtual group has nothing to scan
640 if p == null then return
641 var files = p.files
642 alpha_comparator.sort(files)
643 for f in files do
644 if f.first == '.' then continue
645 var fp = p/f
646 var g = identify_group(fp)
647 # Recursively scan for groups of the same package
648 if g == null then
649 identify_module(fp)
650 else if g.mpackage == mgroup.mpackage then
651 scan_group(g)
652 end
653 end
654 end
655
656 # Transform relative paths (starting with '../') into absolute paths
657 private fun module_absolute_path(path: String): String do
658 return path.realpath
659 end
660
661 # Try to load a module AST using a path.
662 # Display an error if there is a problem (IO / lexer / parser) and return null
663 #
664 # The AST is loaded as is total independence of the model and its entities.
665 #
666 # AST are not cached or reused thus a new AST is returned on success.
667 fun load_module_ast(filename: String): nullable AModule
668 do
669 if not filename.has_suffix(".nit") then
670 self.toolcontext.error(null, "Error: file `{filename}` is not a valid nit module.")
671 return null
672 end
673 if not filename.file_exists then
674 self.toolcontext.error(null, "Error: file `{filename}` not found.")
675 return null
676 end
677
678 self.toolcontext.info("load module {filename}", 2)
679
680 # Load the file
681 var file = new FileReader.open(filename)
682 var lexer = new Lexer(new SourceFile(filename, file))
683 var parser = new Parser(lexer)
684 var tree = parser.parse
685 file.close
686
687 # Handle lexer and parser error
688 var nmodule = tree.n_base
689 if nmodule == null then
690 var neof = tree.n_eof
691 assert neof isa AError
692 error(neof, neof.message)
693 return null
694 end
695
696 return nmodule
697 end
698
699 # Remove Nit source files from a list of arguments.
700 #
701 # Items of `args` that can be loaded as a nit file will be removed from `args` and returned.
702 fun filter_nit_source(args: Array[String]): Array[String]
703 do
704 var keep = new Array[String]
705 var res = new Array[String]
706 for a in args do
707 var stat = a.to_path.stat
708 if stat != null and stat.is_dir then
709 res.add a
710 continue
711 end
712 var l = identify_module(a)
713 if l == null then
714 keep.add a
715 else
716 res.add a
717 end
718 end
719 args.clear
720 args.add_all(keep)
721 return res
722 end
723
724 # Try to load a module using a path.
725 # Display an error if there is a problem (IO / lexer / parser) and return null.
726 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
727 #
728 # The MModule is located, created, parsed and the importation is performed.
729 fun load_module(filename: String): nullable AModule
730 do
731 # Look for the module
732 var mmodule = identify_module(filename)
733 if mmodule == null then
734 var le = last_loader_error
735 if le != null then
736 toolcontext.error(null, le)
737 else if filename.file_exists then
738 toolcontext.error(null, "Error: `{filename}` is not a Nit source file.")
739 else
740 toolcontext.error(null, "Error: cannot find module `{filename}`.")
741 end
742 return null
743 end
744
745 # Load it
746 return mmodule.load(self)
747 end
748
749 # Injection of a new module without source.
750 # Used by the interpreter.
751 fun load_rt_module(parent: nullable MModule, nmodule: AModule, mod_name: String): nullable MModule
752 do
753 # Create the module
754
755 var mgroup = null
756 if parent != null then mgroup = parent.mgroup
757 var mmodule = new MModule(model, mgroup, mod_name, nmodule.location)
758 nmodule.mmodule = mmodule
759 nmodules.add(nmodule)
760 parsed_modules.add mmodule
761 self.mmodule2nmodule[mmodule] = nmodule
762
763 if parent!= null then
764 var imported_modules = new Array[MModule]
765 imported_modules.add(parent)
766 mmodule.set_visibility_for(parent, intrude_visibility)
767 mmodule.set_imported_mmodules(imported_modules)
768 end
769 build_module_importation(nmodule)
770
771 return mmodule
772 end
773
774 # Visit the AST and create the `MModule` object
775 private fun build_a_mmodule(mgroup: nullable MGroup, nmodule: AModule)
776 do
777 var mmodule = nmodule.mmodule
778 assert mmodule != null
779
780 # Check the module name
781 var decl = nmodule.n_moduledecl
782 if decl != null then
783 var decl_name = decl.n_name.n_id.text
784 if decl_name != mmodule.name then
785 warning(decl.n_name, "module-name-mismatch", "Error: module name mismatch; declared {decl_name} file named {mmodule.name}.")
786 end
787 end
788
789 # Check for conflicting module names in the package
790 if mgroup != null then
791 var others = model.get_mmodules_by_name(mmodule.name)
792 if others != null then for other in others do
793 if other != mmodule and mmodule2nmodule.has_key(mmodule) and other.mgroup!= null and other.mgroup.mpackage == mgroup.mpackage then
794 var node: ANode
795 if decl == null then node = nmodule else node = decl.n_name
796 error(node, "Error: a module named `{other.full_name}` already exists at {other.location}.")
797 break
798 end
799 end
800 end
801
802 nmodules.add(nmodule)
803 self.mmodule2nmodule[mmodule] = nmodule
804
805 var source = nmodule.location.file
806 if source != null then
807 assert source.mmodule == null
808 source.mmodule = mmodule
809 end
810
811 if decl != null then
812 # Extract documentation
813 var ndoc = decl.n_doc
814 if ndoc != null then
815 var mdoc = ndoc.to_mdoc
816 mmodule.mdoc = mdoc
817 mdoc.original_mentity = mmodule
818 end
819 # Is the module generated?
820 mmodule.is_generated = not decl.get_annotations("generated").is_empty
821 end
822 end
823
824 # Resolve the module identification for a given `AModuleName`.
825 #
826 # This method handles qualified names as used in `AModuleName`.
827 fun seach_module_by_amodule_name(n_name: AModuleName, mgroup: nullable MGroup): nullable MModule
828 do
829 var mod_name = n_name.n_id.text
830
831 # If a quad is given, we ignore the starting group (go from path)
832 if n_name.n_quad != null then mgroup = null
833
834 # If name not qualified, just search the name
835 if n_name.n_path.is_empty then
836 # Fast search if no n_path
837 return search_mmodule_by_name(n_name, mgroup, mod_name)
838 end
839
840 # If qualified and in a group
841 if mgroup != null then
842 # First search in the package
843 var r = mgroup.mpackage.root
844 assert r != null
845 scan_group(r)
846 # Get all modules with the final name
847 var res = r.mmodules_by_name(mod_name)
848 # Filter out the name that does not match the qualifiers
849 res = [for x in res do if match_amodulename(n_name, x) then x]
850 if res.not_empty then
851 if res.length > 1 then
852 error(n_name, "Error: conflicting module files for `{mod_name}`: `{[for x in res do x.filepath or else x.full_name].join("`, `")}`")
853 end
854 return res.first
855 end
856 end
857
858 # If no module yet, then assume that the first element of the path
859 # Is to be searched in the path.
860 var root_name = n_name.n_path.first.text
861 var roots = search_group_in_paths(root_name, paths)
862 if roots.is_empty then
863 error(n_name, "Error: cannot find `{root_name}`. Tried: {paths.join(", ")}.")
864 return null
865 end
866
867 var res = new ArraySet[MModule]
868 for r in roots do
869 # Then, for each root, collect modules that matches the qualifiers
870 scan_group(r)
871 var root_res = r.mmodules_by_name(mod_name)
872 for x in root_res do if match_amodulename(n_name, x) then res.add x
873 end
874 if res.not_empty then
875 if res.length > 1 then
876 error(n_name, "Error: conflicting module files for `{mod_name}`: `{[for x in res do x.filepath or else x.full_name].join("`, `")}`")
877 end
878 return res.first
879 end
880 # If still nothing, just call a basic search that will fail and will produce an error message
881 error(n_name, "Error: cannot find module `{mod_name}` from `{root_name}`. Tried: {paths.join(", ")}.")
882 return null
883 end
884
885 # Is elements of `n_name` correspond to the group nesting of `m`?
886 #
887 # Basically it check that `bar::foo` matches `bar/foo.nit` and `bar/baz/foo.nit`
888 # but not `baz/foo.nit` nor `foo/bar.nit`
889 #
890 # Is used by `seach_module_by_amodule_name` to validate qualified names.
891 private fun match_amodulename(n_name: AModuleName, m: MModule): Bool
892 do
893 var g: nullable MGroup = m.mgroup
894 for grp in n_name.n_path.reverse_iterator do
895 while g != null and grp.text != g.name do
896 g = g.parent
897 end
898 end
899 return g != null
900 end
901
902 # Analyze the module importation and fill the module_importation_hierarchy
903 #
904 # If the importation was already done (`nmodule.is_importation_done`), this method does a no-op.
905 #
906 # REQUIRE `nmodule.mmodule != null`
907 # ENSURE `nmodule.is_importation_done`
908 fun build_module_importation(nmodule: AModule)
909 do
910 if nmodule.is_importation_done then return
911 nmodule.is_importation_done = true
912 var mmodule = nmodule.mmodule.as(not null)
913 var stdimport = true
914 var imported_modules = new Array[MModule]
915 for aimport in nmodule.n_imports do
916 # Do not imports conditional
917 var atconditionals = aimport.get_annotations("conditional")
918 if atconditionals.not_empty then continue
919
920 stdimport = false
921 if not aimport isa AStdImport then
922 continue
923 end
924
925 # Load the imported module
926 var sup = seach_module_by_amodule_name(aimport.n_name, mmodule.mgroup)
927 if sup == null then
928 mmodule.is_broken = true
929 nmodule.mmodule = null # invalidate the module
930 continue # Skip error
931 end
932 var ast = sup.load(self)
933 if ast == null then
934 mmodule.is_broken = true
935 nmodule.mmodule = null # invalidate the module
936 continue # Skip error
937 end
938
939 aimport.mmodule = sup
940 imported_modules.add(sup)
941 var mvisibility = aimport.n_visibility.mvisibility
942 if mvisibility == protected_visibility then
943 mmodule.is_broken = true
944 error(aimport.n_visibility, "Error: only properties can be protected.")
945 mmodule.is_broken = true
946 nmodule.mmodule = null # invalidate the module
947 return
948 end
949 if sup == mmodule then
950 error(aimport.n_name, "Error: dependency loop in module {mmodule}.")
951 mmodule.is_broken = true
952 nmodule.mmodule = null # invalidate the module
953 end
954 if sup.in_importation < mmodule then
955 error(aimport.n_name, "Error: dependency loop between modules {mmodule} and {sup}.")
956 mmodule.is_broken = true
957 nmodule.mmodule = null # invalidate the module
958 return
959 end
960 mmodule.set_visibility_for(sup, mvisibility)
961 end
962 if stdimport then
963 var mod_name = "core"
964 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
965 if sup == null then
966 mmodule.is_broken = true
967 nmodule.mmodule = null # invalidate the module
968 else # Skip error
969 imported_modules.add(sup)
970 mmodule.set_visibility_for(sup, public_visibility)
971 end
972 end
973
974 # Declare conditional importation
975 for aimport in nmodule.n_imports do
976 if not aimport isa AStdImport then continue
977 var atconditionals = aimport.get_annotations("conditional")
978 if atconditionals.is_empty then continue
979
980 var suppath = seach_module_by_amodule_name(aimport.n_name, mmodule.mgroup)
981 if suppath == null then continue # skip error
982
983 for atconditional in atconditionals do
984 var nargs = atconditional.n_args
985 if nargs.is_empty then
986 error(atconditional, "Syntax Error: `conditional` expects module identifiers as arguments.")
987 continue
988 end
989
990 # The rule
991 var rule = new Array[MModule]
992
993 # First element is the goal, thus
994 rule.add suppath
995
996 # Second element is the first condition, that is to be a client of the current module
997 rule.add mmodule
998
999 # Other condition are to be also a client of each modules indicated as arguments of the annotation
1000 for narg in nargs do
1001 var id = narg.as_id
1002 if id == null then
1003 error(narg, "Syntax Error: `conditional` expects module identifier as arguments.")
1004 continue
1005 end
1006
1007 var mp = search_mmodule_by_name(narg, mmodule.mgroup, id)
1008 if mp == null then continue
1009
1010 rule.add mp
1011 end
1012
1013 conditional_importations.add rule
1014 end
1015 end
1016
1017 mmodule.set_imported_mmodules(imported_modules)
1018
1019 apply_conditional_importations(mmodule)
1020
1021 self.toolcontext.info("{mmodule} imports {mmodule.in_importation.direct_greaters.join(", ")}", 3)
1022
1023 # Force `core` to be public if imported
1024 for sup in mmodule.in_importation.greaters do
1025 if sup.name == "core" then
1026 mmodule.set_visibility_for(sup, public_visibility)
1027 end
1028 end
1029
1030 # TODO: Correctly check for useless importation
1031 # It is even doable?
1032 var directs = mmodule.in_importation.direct_greaters
1033 for nim in nmodule.n_imports do
1034 if not nim isa AStdImport then continue
1035 var im = nim.mmodule
1036 if im == null then continue
1037 if directs.has(im) then continue
1038 # This generates so much noise that it is simpler to just comment it
1039 #warning(nim, "Warning: possible useless importation of {im}")
1040 end
1041 end
1042
1043 # Global list of conditional importation rules.
1044 #
1045 # Each rule is a "Horn clause"-like sequence of modules.
1046 # It means that the first module is the module to automatically import.
1047 # The remaining modules are the conditions of the rule.
1048 #
1049 # Rules are declared by `build_module_importation` and are applied by `apply_conditional_importations`
1050 # (and `build_module_importation` that calls it).
1051 #
1052 # TODO (when the loader will be rewritten): use a better representation and move up rules in the model.
1053 var conditional_importations = new Array[SequenceRead[MModule]]
1054
1055 # Extends the current importations according to imported rules about conditional importation
1056 fun apply_conditional_importations(mmodule: MModule)
1057 do
1058 # Because a conditional importation may cause additional conditional importation, use a fixed point
1059 # The rules are checked naively because we assume that it does not worth to be optimized
1060 var check_conditional_importations = true
1061 while check_conditional_importations do
1062 check_conditional_importations = false
1063
1064 for ci in conditional_importations do
1065 # Check conditions
1066 for i in [1..ci.length[ do
1067 var m = ci[i]
1068 # Is imported?
1069 if mmodule == m or not mmodule.in_importation.greaters.has(m) then continue label
1070 end
1071 # Still here? It means that all conditions modules are loaded and imported
1072
1073 # Identify the module to automatically import
1074 var sup = ci.first
1075 var ast = sup.load(self)
1076 if ast == null then continue
1077
1078 # Do nothing if already imported
1079 if mmodule.in_importation.greaters.has(sup) then continue label
1080
1081 # Import it
1082 self.toolcontext.info("{mmodule} conditionally imports {sup}", 3)
1083 # TODO visibility rules (currently always public)
1084 mmodule.set_visibility_for(sup, public_visibility)
1085 # TODO linearization rules (currently added at the end in the order of the rules)
1086 mmodule.set_imported_mmodules([sup])
1087
1088 # Prepare to reapply the rules
1089 check_conditional_importations = true
1090 end label
1091 end
1092 end
1093
1094 # All the loaded modules
1095 var nmodules = new Array[AModule]
1096
1097 # Register the nmodule associated to each mmodule
1098 #
1099 # Public clients need to use `mmodule2node` to access stuff.
1100 private var mmodule2nmodule = new HashMap[MModule, AModule]
1101
1102 # Retrieve the associated AST node of a mmodule.
1103 # This method is used to associate model entity with syntactic entities.
1104 #
1105 # If the module is not associated with a node, returns null.
1106 fun mmodule2node(mmodule: MModule): nullable AModule
1107 do
1108 return mmodule2nmodule.get_or_null(mmodule)
1109 end
1110 end
1111
1112 redef class MModule
1113 # Force the parsing of the module using `modelbuilder`.
1114 #
1115 # If the module was already parsed, the existing ASI is returned.
1116 # Else the source file is loaded, and parsed and some
1117 #
1118 # The importation is not done by this
1119 #
1120 # REQUIRE: `filepath != null`
1121 # ENSURE: `modelbuilder.parsed_modules.has(self)`
1122 fun parse(modelbuilder: ModelBuilder): nullable AModule
1123 do
1124 # Already known and loaded? then return it
1125 var nmodule = modelbuilder.mmodule2nmodule.get_or_null(self)
1126 if nmodule != null then return nmodule
1127
1128 var filepath = self.filepath
1129 assert filepath != null
1130 # Load it manually
1131 nmodule = modelbuilder.load_module_ast(filepath)
1132 if nmodule == null then return null # forward error
1133
1134 # build the mmodule
1135 nmodule.mmodule = self
1136 self.location = nmodule.location
1137 modelbuilder.build_a_mmodule(mgroup, nmodule)
1138
1139 modelbuilder.parsed_modules.add self
1140 return nmodule
1141 end
1142
1143 # Parse and process importation of a given MModule.
1144 #
1145 # Basically chains `parse` and `build_module_importation`.
1146 fun load(modelbuilder: ModelBuilder): nullable AModule
1147 do
1148 var nmodule = parse(modelbuilder)
1149 if nmodule == null then return null
1150
1151 modelbuilder.build_module_importation(nmodule)
1152 return nmodule
1153 end
1154 end
1155
1156 redef class MPackage
1157 # The associated `.ini` file, if any
1158 #
1159 # The `ini` file is given as is and might contain invalid or missing information.
1160 #
1161 # Some packages, like stand-alone packages or virtual packages have no `ini` file associated.
1162 var ini: nullable ConfigTree = null
1163
1164 # Array of relative source paths excluded according to the `source.exclude` key of the `ini`
1165 var excludes: nullable Array[String] is lazy do
1166 var ini = self.ini
1167 if ini == null then return null
1168 var exclude = ini["source.exclude"]
1169 if exclude == null then return null
1170 var excludes = exclude.split(":")
1171 return excludes
1172 end
1173
1174 # Does the source inclusion/inclusion rules of the package `ini` accept such path?
1175 fun accept(filepath: String): Bool
1176 do
1177 var excludes = self.excludes
1178 if excludes != null then
1179 var relpath = root.filepath.relpath(filepath)
1180 if excludes.has(relpath) then return false
1181 end
1182 return true
1183 end
1184 end
1185
1186 redef class MGroup
1187 # Is the group interesting for a final user?
1188 #
1189 # Groups are mandatory in the model but for simple packages they are not
1190 # always interesting.
1191 #
1192 # A interesting group has, at least, one of the following true:
1193 #
1194 # * it has 2 modules or more
1195 # * it has a subgroup
1196 # * it has a documentation
1197 fun is_interesting: Bool
1198 do
1199 return mmodules.length > 1 or
1200 not in_nesting.direct_smallers.is_empty or
1201 mdoc != null or
1202 (mmodules.length == 1 and default_mmodule == null)
1203 end
1204
1205 # Are files and directories in self scanned?
1206 #
1207 # See `ModelBuilder::scan_group`.
1208 var scanned = false
1209
1210 # Return the modules in self and subgroups named `name`.
1211 #
1212 # If `self` is not scanned (see `ModelBuilder::scan_group`) the
1213 # results might be partial.
1214 fun mmodules_by_name(name: String): Array[MModule]
1215 do
1216 var res = new Array[MModule]
1217 for g in in_nesting.smallers do
1218 for mp in g.mmodules do
1219 if mp.name == name then
1220 res.add mp
1221 end
1222 end
1223 end
1224 return res
1225 end
1226 end
1227
1228 redef class SourceFile
1229 # Associated mmodule, once created
1230 var mmodule: nullable MModule = null
1231 end
1232
1233 redef class AStdImport
1234 # The imported module once determined
1235 var mmodule: nullable MModule = null
1236 end
1237
1238 redef class AModule
1239 # The associated MModule once build by a `ModelBuilder`
1240 var mmodule: nullable MModule = null
1241
1242 # Flag that indicate if the importation is already completed
1243 var is_importation_done: Bool = false
1244 end