Rename REAMDE to README.md
[nit.git] / src / toolcontext.nit
index dd5014f..8196aeb 100644 (file)
@@ -24,6 +24,7 @@ import opts
 import location
 import version
 import template
+import more_collections
 
 # A warning or an error
 class Message
@@ -40,9 +41,21 @@ class Message
 
        # The human-readable description of the message.
        #
-       # It should be short and fit on a single line.
-       # It should also have meaningful information first in case
-       # on truncation by an IDE for instance.
+       # eg. "Error: cannot find method `foo`."
+       #
+       # A good message should:
+       #
+       # * start with a message type like "Error:", "Syntax Error:", "Warning:".
+       #   The type is capitalized and terminated by a column.
+       #   The rest on the message starts with a lowercase letter and is terminated with a dot.
+       #
+       # * be short and fit on a single line.
+       #
+       # * have meaningful information first.
+       #   This helps the reader and remain usable
+       #   when truncated, by an IDE for instance.
+       #
+       # * enclose identifiers, keywords and pieces of code with back-quotes.
        var text: String
 
        # Comparisons are made on message locations.
@@ -53,6 +66,11 @@ class Message
                return location.as(not null) < other.location.as(not null)
        end
 
+       redef fun ==(other): Bool do
+               if not other isa Message then return false
+               return location == other.location and tag == other.tag and text == other.text
+       end
+
        redef fun to_s: String
        do
                var l = location
@@ -90,6 +108,24 @@ class Message
        end
 end
 
+redef class Location
+       # Errors and warnings associated to this location.
+       var messages: nullable Array[Message]
+
+       # Add a message to `self`
+       #
+       # See `messages`
+       private fun add_message(m: Message)
+       do
+               var ms = messages
+               if ms == null then
+                       ms = new Array[Message]
+                       messages = ms
+               end
+               ms.add m
+       end
+end
+
 # Global context for tools
 class ToolContext
        # Number of errors
@@ -101,13 +137,46 @@ class ToolContext
        # Directory where to generate log files
        var log_directory: String = "logs"
 
+       # Stream in `log_directory` where all info messages are written
+       var log_info: nullable Writer = null
+
        # Messages
        private var messages = new Array[Message]
        private var message_sorter: Comparator = default_comparator
 
-       # Output all current stacked messages.
-       # If some errors occurred, exits the program.
-       fun check_errors
+       # Does an error prevent the program to stop at `check_errors`?
+       #
+       # Default to false.
+       # Set this value to `true` if you need to keep the program going in case of error.
+       var keep_going = false is writable
+
+       # List of tags per source-file whose warnings are not displayed.
+       #
+       # Initially empty, it is up to the toll to fill it.
+       # The tag "all" means all warnings and advices.
+       var warning_blacklist = new MultiHashMap[SourceFile, String]
+
+       # Is the source-file of `l` associated with `tag` in `warning_blacklist`?
+       #
+       # currently returns `false` if `l` is null or does not have a source-file.
+       fun is_warning_blacklisted(l: nullable Location, tag: String): Bool
+       do
+               if l == null then return false
+               var f = l.file
+               if f == null then return false
+               var tags = warning_blacklist.get_or_null(f)
+               if tags == null then return false
+               return tags.has("all") or tags.has(tag)
+       end
+
+       # Output all current stacked messages
+       #
+       # Return true if no errors occurred.
+       #
+       # If some errors occurred, the behavior depends on the value of `keep_going`.
+       # If `keep_going` is false, then the total error informations is displayed and the program exits.
+       # Else, false is returned.
+       fun check_errors: Bool
        do
                if messages.length > 0 then
                        message_sorter.sort(messages)
@@ -124,30 +193,40 @@ class ToolContext
                end
 
                if error_count > 0 then
-                       errors_info
-                       exit(1)
+                       if not keep_going then
+                               errors_info
+                               exit(1)
+                       end
+                       return false
                end
+               return true
        end
 
-       # Display (and reset) total error informations
+       # Display total error informations
        fun errors_info
        do
                if error_count == 0 and warning_count == 0 then return
                if opt_no_color.value then return
                sys.stderr.write "Errors: {error_count}. Warnings: {warning_count}.\n"
-               error_count = 0
-               warning_count = 0
        end
 
        # Display an error
-       fun error(l: nullable Location, s: String)
+       #
+       # Return the message (to add information)
+       fun error(l: nullable Location, s: String): Message
        do
-               messages.add(new Message(l,null,s))
+               var m = new Message(l,null,s)
+               if messages.has(m) then return m
+               if l != null then l.add_message m
+               messages.add m
                error_count = error_count + 1
                if opt_stop_on_first_error.value then check_errors
+               return m
        end
 
        # Add an error, show errors and quit
+       #
+       # Because the program will quit, nothing is returned.
        fun fatal_error(l: nullable Location, s: String)
        do
                error(l,s)
@@ -164,13 +243,20 @@ class ToolContext
        # * They always are real issues (no false positive)
        #
        # First-level warnings are displayed by default (except if option `-q` is given).
-       fun warning(l: nullable Location, tag: String, text: String)
+       #
+       # Return the message (to add information) or null if the warning is disabled
+       fun warning(l: nullable Location, tag: String, text: String): nullable Message
        do
-               if opt_warning.value.has("no-{tag}") then return
-               if not opt_warning.value.has(tag) and opt_warn.value == 0 then return
-               messages.add(new Message(l, tag, text))
+               if opt_warning.value.has("no-{tag}") then return null
+               if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
+               if is_warning_blacklisted(l, tag) then return null
+               var m = new Message(l, tag, text)
+               if messages.has(m) then return null
+               if l != null then l.add_message m
+               messages.add m
                warning_count = warning_count + 1
                if opt_stop_on_first_error.value then check_errors
+               return m
        end
 
        # Display a second-level warning.
@@ -186,13 +272,20 @@ class ToolContext
        #
        # In order to prevent warning inflation à la Java, second-level warnings are not displayed by
        # default and require an additional option `-W`.
-       fun advice(l: nullable Location, tag: String, text: String)
+       #
+       # Return the message (to add information) or null if the warning is disabled
+       fun advice(l: nullable Location, tag: String, text: String): nullable Message
        do
-               if opt_warning.value.has("no-{tag}") then return
-               if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return
-               messages.add(new Message(l, tag, text))
+               if opt_warning.value.has("no-{tag}") then return null
+               if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
+               if is_warning_blacklisted(l, tag) then return null
+               var m = new Message(l, tag, text)
+               if messages.has(m) then return null
+               if l != null then l.add_message m
+               messages.add m
                warning_count = warning_count + 1
                if opt_stop_on_first_error.value then check_errors
+               return m
        end
 
        # Display an info
@@ -201,34 +294,40 @@ class ToolContext
                if level <= verbose_level then
                        print "{s}"
                end
+               if log_info != null then
+                       log_info.write s
+                       log_info.write "\n"
+               end
        end
 
        # Executes a program while checking if it's available and if the execution ended correctly
        #
        # Stops execution and prints errors if the program isn't available or didn't end correctly
        fun exec_and_check(args: Array[String], error: String)
-        do
-                var prog = args.first
-                args.remove_at 0
-
-                # Is the wanted program available?
-                var proc_which = new IProcess.from_a("which", [prog])
-                proc_which.wait
-                var res = proc_which.status
-                if res != 0 then
-                        print "{error}: executable \"{prog}\" not found"
-                        exit 1
-                end
-
-                # Execute the wanted program
-                var proc = new Process.from_a(prog, args)
-                proc.wait
-                res = proc.status
-                if res != 0 then
-                        print "{error}: execution of \"{prog} {args.join(" ")}\" failed"
-                        exit 1
-                end
-        end
+       do
+               info("+ {args.join(" ")}", 2)
+
+               var prog = args.first
+               args.remove_at 0
+
+               # Is the wanted program available?
+               var proc_which = new ProcessReader.from_a("which", [prog])
+               proc_which.wait
+               var res = proc_which.status
+               if res != 0 then
+                       print "{error}: executable \"{prog}\" not found"
+                       exit 1
+               end
+
+               # Execute the wanted program
+               var proc = new Process.from_a(prog, args)
+               proc.wait
+               res = proc.status
+               if res != 0 then
+                       print "{error}: execution of \"{prog} {args.join(" ")}\" failed"
+                       exit 1
+               end
+       end
 
        # Global OptionContext
        var option_context = new OptionContext
@@ -266,6 +365,9 @@ class ToolContext
        # Option --stop-on-first-error
        var opt_stop_on_first_error = new OptionBool("Stop on first error", "--stop-on-first-error")
 
+       # Option --keep-going
+       var opt_keep_going = new OptionBool("Continue after errors, whatever the consequences", "--keep-going")
+
        # Option --no-color
        var opt_no_color = new OptionBool("Do not use color to display errors and warnings", "--no-color")
 
@@ -280,7 +382,7 @@ class ToolContext
 
        init
        do
-               option_context.add_option(opt_warn, opt_warning, opt_quiet, opt_stop_on_first_error, opt_no_color, opt_log, opt_log_dir, opt_nit_dir, opt_help, opt_version, opt_set_dummy_tool, opt_verbose, opt_bash_completion, opt_stub_man)
+               option_context.add_option(opt_warn, opt_warning, opt_quiet, opt_stop_on_first_error, opt_keep_going, opt_no_color, opt_log, opt_log_dir, opt_nit_dir, opt_help, opt_version, opt_set_dummy_tool, opt_verbose, opt_bash_completion, opt_stub_man)
 
                # Hide some internal options
                opt_stub_man.hidden = true
@@ -386,14 +488,18 @@ The Nit language documentation and the source code of its tools and libraries ma
                # Set verbose level
                verbose_level = opt_verbose.value
 
+               if opt_keep_going.value then keep_going = true
+
                if self.opt_quiet.value then self.opt_warn.value = 0
 
                if opt_log_dir.value != null then log_directory = opt_log_dir.value.as(not null)
                if opt_log.value then
                        # Make sure the output directory exists
                        log_directory.mkdir
-               end
 
+                       # Redirect the verbose messages
+                       log_info = (log_directory/"info.txt").to_path.open_wo
+               end
        end
 
        # Get the current `nit_version` or "DUMMY_VERSION" if `--set-dummy-tool` is set.
@@ -421,7 +527,7 @@ The Nit language documentation and the source code of its tools and libraries ma
                var res = opt_nit_dir.value
                if res != null then
                        if not check_nit_dir(res) then
-                               fatal_error(null, "Fatal Error: the value of --nit-dir does not seem to be a valid base Nit directory: {res}")
+                               fatal_error(null, "Fatal Error: the value of --nit-dir does not seem to be a valid base Nit directory: {res}.")
                        end
                        return res
                end
@@ -430,7 +536,7 @@ The Nit language documentation and the source code of its tools and libraries ma
                res = "NIT_DIR".environ
                if not res.is_empty then
                        if not check_nit_dir(res) then
-                               fatal_error(null, "Fatal Error: the value of NIT_DIR does not seem to be a valid base Nit directory: {res}")
+                               fatal_error(null, "Fatal Error: the value of NIT_DIR does not seem to be a valid base Nit directory: {res}.")
                        end
                        return res
                end
@@ -454,7 +560,7 @@ The Nit language documentation and the source code of its tools and libraries ma
                        if check_nit_dir(res) then return res.simplify_path
                end
 
-               fatal_error(null, "Fatal Error: Cannot locate a valid base nit directory. It is quite unexpected. Try to set the environment variable `NIT_DIR` or to use the `--nit-dir` option.")
+               fatal_error(null, "Fatal Error: cannot locate a valid base Nit directory. It is quite unexpected. Try to set the environment variable `NIT_DIR` or to use the `--nit-dir` option.")
                abort
        end