X-Git-Url: http://nitlanguage.org diff --git a/src/toolcontext.nit b/src/toolcontext.nit index dfb6f2d..043e9ee 100644 --- a/src/toolcontext.nit +++ b/src/toolcontext.nit @@ -24,6 +24,7 @@ import opts import location import version import template +import more_collections # A warning or an error class Message @@ -33,13 +34,37 @@ class Message # The origin of the message in the source code, if any. var location: nullable Location + # The category of the message. + # + # Used by quality-control tool for statistics or to enable/disable things individually. + var tag: nullable String + # 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 + # The severity level + # + # * 0 is advices (see `ToolContext::advice`) + # * 1 is warnings (see `ToolContext::warning`) + # * 2 is errors (see `ToolContext::error`) + var level: Int + # Comparisons are made on message locations. redef fun <(other: OTHER): Bool do if location == null then return true @@ -48,6 +73,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 @@ -58,30 +88,62 @@ class Message end end + # A colored version of the message including the original source line fun to_color_string: String do - var esc = 27.ascii - var red = "{esc}[0;31m" - var bred = "{esc}[1;31m" - var green = "{esc}[0;32m" + var esc = 27.code_point + #var red = "{esc}[0;31m" + #var bred = "{esc}[1;31m" + #var green = "{esc}[0;32m" var yellow = "{esc}[0;33m" var def = "{esc}[0m" + var tag = tag + if tag != null then + tag = " ({tag})" + else + tag = "" + end var l = location if l == null then - return text + return "{text}{tag}" else if l.file == null then - return "{yellow}{l}{def}: {text}" + return "{yellow}{l}{def}: {text}{tag}" else - return "{yellow}{l}{def}: {text}\n{l.colored_line("1;31")}" + return "{yellow}{l}{def}: {text}{tag}\n{l.colored_line("1;31")}" end 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 + var s = file + if s != null then s.messages.add m + end +end + +redef class SourceFile + # Errors and warnings associated to the whole source. + var messages = new Array[Message] +end + # Global context for tools class ToolContext # Number of errors - var error_count: Int = 0 + var error_count: Int = 0 is writable # Number of warnings var warning_count: Int = 0 @@ -89,11 +151,56 @@ 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: Array[Message] = new Array[Message] - private var message_sorter: ComparableSorter[Message] = new ComparableSorter[Message] + private var messages = new Array[Message] + private var message_sorter: Comparator = default_comparator + + # 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] - fun check_errors + # 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, total and exit the program + # + # If there is no error, exit with 0, else exit with 1. + fun quit + do + check_errors + errors_info + if error_count > 0 then exit(1) else exit(0) + 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) @@ -109,31 +216,101 @@ class ToolContext messages.clear end - if error_count > 0 then exit(1) + if error_count > 0 then + if not keep_going then + errors_info + exit(1) + end + return false + end + return true + end + + # 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" 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,s)) + var m = new Message(l, null, s, 2) + if messages.has(m) then return m + if l != null then l.add_message m + if opt_warn.value <= -1 then return 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) check_errors end - # Display a warning - fun warning(l: nullable Location, s: String) + # Display a first-level warning. + # + # First-level warnings are warnings that SHOULD be corrected, + # and COULD usually be immediately corrected. + # + # * There is a simple correction + # * There is no reason to let the code this way (no reasonable @supresswarning-like annotation) + # * They always are real issues (no false positive) + # + # First-level warnings are displayed by default (except if option `-q` is given). + # + # 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 is_warning_blacklisted(l, tag) then return null + var m = new Message(l, tag, text, 1) + if messages.has(m) then return null + if l != null then l.add_message m + 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 + 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. + # + # Second-level warnings are warnings that should require investigation, + # but cannot always be immediately corrected. + # + # * The correction could be complex. e.g. require a refactorisation or an API change. + # * The correction cannot be done. e.g. Code that use a deprecated API for some compatibility reason. + # * There is not a real issue (false positive). Note that this should be unlikely. + # * Transitional: While a real warning, it fires a lot in current code, so a transition is needed + # in order to fix them before promoting the advice to a warning. + # + # In order to prevent warning inflation à la Java, second-level warnings are not displayed by + # default and require an additional option `-W`. + # + # 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_warn.value == 0 then return - messages.add(new Message(l,s)) + if is_warning_blacklisted(l, tag) then return null + var m = new Message(l, tag, text, 0) + if messages.has(m) then return null + if l != null then l.add_message m + 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 + messages.add m warning_count = warning_count + 1 if opt_stop_on_first_error.value then check_errors + return m end # Display an info @@ -142,77 +319,109 @@ 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 "{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 "{error}: execution of \"{prog} {args.join(" ")}\" failed" + exit 1 + end + end # Global OptionContext - var option_context: OptionContext = new OptionContext + var option_context = new OptionContext # Option --warn - var opt_warn: OptionCount = new OptionCount("Show warnings", "-W", "--warn") + var opt_warn = new OptionCount("Show additional warnings (advices)", "-W", "--warn") + + # Option --warning + var opt_warning = new OptionArray("Show/hide a specific warning", "-w", "--warning") # Option --quiet - var opt_quiet: OptionBool = new OptionBool("Do not show warnings", "-q", "--quiet") + var opt_quiet = new OptionBool("Do not show warnings", "-q", "--quiet") # Option --log - var opt_log: OptionBool = new OptionBool("Generate various log files", "--log") + var opt_log = new OptionBool("Generate various log files", "--log") # Option --log-dir - var opt_log_dir: OptionString = new OptionString("Directory where to generate log files", "--log-dir") + var opt_log_dir = new OptionString("Directory where to generate log files", "--log-dir") + + # Option --nit-dir + var opt_nit_dir = new OptionString("Base directory of the Nit installation", "--nit-dir") + + # Option --share-dir + var opt_share_dir = new OptionString("Directory containing tools assets", "--share-dir") # Option --help - var opt_help: OptionBool = new OptionBool("Show Help (This screen)", "-h", "-?", "--help") + var opt_help = new OptionBool("Show Help (This screen)", "-h", "-?", "--help") # Option --version - var opt_version: OptionBool = new OptionBool("Show version and exit", "--version") + var opt_version = new OptionBool("Show version and exit", "--version") # Option --set-dummy-tool - var opt_set_dummy_tool: OptionBool = new OptionBool("Set toolname and version to DUMMY. Useful for testing", "--set-dummy-tool") + var opt_set_dummy_tool = new OptionBool("Set toolname and version to DUMMY. Useful for testing", "--set-dummy-tool") # Option --verbose - var opt_verbose: OptionCount = new OptionCount("Verbose", "-v", "--verbose") + var opt_verbose = new OptionCount("Additional messages from the tool", "-v", "--verbose") # Option --stop-on-first-error - var opt_stop_on_first_error: OptionBool = new OptionBool("Stop on first error", "--stop-on-first-error") + var opt_stop_on_first_error = new OptionBool("Just display the first encountered error then stop", "--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: OptionBool = new OptionBool("Do not use color to display errors and warnings", "--no-color") + var opt_no_color = new OptionBool("Do not use color to display errors and warnings", "--no-color") # Option --bash-completion - var opt_bash_completion: OptionBool = new OptionBool("Generate bash_completion file for this program", "--bash-completion") + var opt_bash_completion = new OptionBool("Generate bash_completion file for this program", "--bash-completion") + + # Option --stub-man + var opt_stub_man = new OptionBool("Generate a stub manpage in pandoc markdown format", "--stub-man") + + # Option --no-contract + var opt_no_contract = new OptionBool("Disable the contracts usage", "--no-contract") + + # Option --full-contract + var opt_full_contract = new OptionBool("Enable all contracts usage", "--full-contract") # Verbose level var verbose_level: Int = 0 init do - option_context.add_option(opt_warn, opt_quiet, opt_stop_on_first_error, opt_no_color, opt_log, opt_log_dir, opt_help, opt_version, opt_set_dummy_tool, opt_verbose, opt_bash_completion) + 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, opt_no_contract, opt_full_contract) + + # Hide some internal options + opt_stub_man.hidden = true + opt_bash_completion.hidden = true + opt_set_dummy_tool.hidden = true end # Name, usage and synopsis of the tool. @@ -263,7 +472,35 @@ class ToolContext exit 0 end - var errors = option_context.get_errors + if opt_stub_man.value then + print """ +# NAME + +{{{tooldescription.split("\n")[1]}}} + +# SYNOPSYS + +# OPTIONS +""" + for o in option_context.options do + var first = true + printn "### " + for n in o.names do + if first then first = false else printn ", " + printn "`{n}`" + end + print "" + print "{o.helptext}." + print "" + end + print """ +# SEE ALSO + +The Nit language documentation and the source code of its tools and libraries may be downloaded from """ + exit 0 + end + + var errors = option_context.errors if not errors.is_empty then for e in errors do print "Error: {e}" print tooldescription @@ -271,6 +508,8 @@ class ToolContext exit 1 end + nit_dir = locate_nit_dir + if option_context.rest.is_empty and not accept_no_arguments then print tooldescription print "Use --help for help" @@ -280,15 +519,18 @@ class ToolContext # 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 - nit_dir = compute_nit_dir + # 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. @@ -304,31 +546,90 @@ class ToolContext if opt_set_dummy_tool.value then return "DUMMY_TOOL" end - return sys.program_name.basename("") + return sys.program_name.basename end - # The identified root directory of the Nit project - var nit_dir: nullable String = null + # The identified root directory of the Nit package + # + # It is assignable but is automatically set by `process_options` with `locate_nit_dir`. + var nit_dir: nullable String = null is writable + + # Shared files directory. + # + # Most often `nit/share/`. + var share_dir: String is lazy do + var sharedir = opt_share_dir.value + if sharedir == null then + sharedir = nit_dir / "share" + if not sharedir.file_exists then + fatal_error(null, "Fatal Error: cannot locate shared files directory in {sharedir}. Uses --share-dir to define it's location.") + end + end + return sharedir + end - private fun compute_nit_dir: nullable String + # Guess a possible nit_dir. + # + # It uses, in order: + # + # * the option `opt_nit_dir` + # * the environment variable `NIT_DIR` + # * the runpath of the program from argv[0] + # * the runpath of the process from /proc + # * the search in PATH + # + # If there is errors (e.g. the indicated path is invalid) or if no + # path is found, then an error is displayed and the program exits. + # + # The result is returned without being assigned to `nit_dir`. + # This function is automatically called by `process_options` + fun locate_nit_dir: String do - # a environ variable has precedence - var res = "NIT_DIR".environ - if not res.is_empty then return res + # the option has precedence + 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}.") + end + return res + end + + # then the environ variable has precedence + 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}.") + end + return res + end # find the runpath of the program from argv[0] res = "{sys.program_name.dirname}/.." - if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path + if check_nit_dir(res) then return res.simplify_path # find the runpath of the process from /proc var exe = "/proc/self/exe" if exe.file_exists then res = exe.realpath res = res.dirname.join_path("..") - if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path + if check_nit_dir(res) then return res.simplify_path end - return null + # search in the PATH + var path_sep = if is_windows then ";" else ":" + var ps = "PATH".environ.split(path_sep) + for p in ps do + res = p/".." + 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.") + abort + end + + private fun check_nit_dir(res: String): Bool + do + return res.file_exists and "{res}/src/nit.nit".file_exists end end @@ -336,16 +637,18 @@ end # # On some Linux systems `bash_completion` allow the program to control command line behaviour. # -# $ nitls [TAB][TAB] -# file1.nit file2.nit file3.nit +# ~~~sh +# $ nitls [TAB][TAB] +# file1.nit file2.nit file3.nit # -# $ nitls --[TAB][TAB] -# --bash-toolname --keep --path --tree -# --depends --log --project --verbose -# --disable-phase --log-dir --quiet --version -# --gen-bash-completion --no-color --recursive --warn -# --help --only-metamodel --source -# --ignore-visibility --only-parse --stop-on-first-error +# $ nitls --[TAB][TAB] +# --bash-toolname --keep --path --tree +# --depends --log --package --verbose +# --disable-phase --log-dir --quiet --version +# --gen-bash-completion --no-color --recursive --warn +# --help --only-metamodel --source +# --ignore-visibility --only-parse --stop-on-first-error +# ~~~ # # Generated file can be placed in system bash_completion directory `/etc/bash_completion.d/` # or source it in `~/.bash_completion`. @@ -354,10 +657,6 @@ class BashCompletion var toolcontext: ToolContext - init(toolcontext: ToolContext) do - self.toolcontext = toolcontext - end - private fun extract_options_names: Array[String] do var names = new Array[String] for option in toolcontext.option_context.options do @@ -378,7 +677,7 @@ class BashCompletion addn " COMPREPLY=()" addn " cur=\"$\{COMP_WORDS[COMP_CWORD]\}\"" addn " prev=\"$\{COMP_WORDS[COMP_CWORD-1]\}\"" - if option_names != null then + if not option_names.is_empty then addn " opts=\"{option_names.join(" ")}\"" addn " if [[ $\{cur\} == -* ]] ; then" addn " COMPREPLY=( $(compgen -W \"$\{opts\}\" -- $\{cur\}) )"