Property definitions

nitc $ ToolContext :: defaultinit
# Global context for tools
class ToolContext
	# Number of errors
	var error_count: Int = 0 is writable

	# Number of warnings
	var warning_count: Int = 0

	# 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

	# 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, 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)

			for m in messages do
				if opt_no_color.value then
					sys.stderr.write("{m}\n")
				else
					sys.stderr.write("{m.to_color_string}\n")
				end
			end

			messages.clear
		end

		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
	#
	# Return the message (to add information)
	fun error(l: nullable Location, s: String): Message
	do
		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 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 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
	fun info(s: String, level: Int)
	do
		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
		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 = new OptionContext

	# Option --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 = new OptionBool("Do not show warnings", "-q", "--quiet")

	# Option --log
	var opt_log = new OptionBool("Generate various log files", "--log")

	# Option --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 = new OptionBool("Show Help (This screen)", "-h", "-?", "--help")

	# Option --version
	var opt_version = new OptionBool("Show version and exit", "--version")

	# Option --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 = new OptionCount("Additional messages from the tool", "-v", "--verbose")

	# Option --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 = new OptionBool("Do not use color to display errors and warnings", "--no-color")

	# Option --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_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.
	# It is mainly used in `usage`.
	# Should be correctly set by the client before calling `process_options`
	# A multi-line string is recommmended.
	#
	# eg. `"Usage: tool [OPTION]... [FILE]...\nDo some things."`
	var tooldescription: String = "Usage: [OPTION]... [ARG]..." is writable

	# Does `process_options` should accept an empty sequence of arguments.
	# ie. nothing except options.
	# Is `false` by default.
	#
	# If required, if should be set by the client before calling `process_options`
	var accept_no_arguments = false is writable

	# print the full usage of the tool.
	# Is called by `process_option` on `--help`.
	# It also could be called by the client.
	fun usage
	do
		print tooldescription
		option_context.usage
	end

	# Parse and process the options given on the command line
	fun process_options(args: Sequence[String])
	do
		self.opt_warn.value = 1

		# init options
		option_context.parse(args)

		if opt_help.value then
			usage
			exit 0
		end

		if opt_version.value then
			print version
			exit 0
		end

		if opt_bash_completion.value then
			var bash_completion = new BashCompletion(self)
			bash_completion.write_to(sys.stdout)
			exit 0
		end

		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 <http://nitlanguage.org>"""
			exit 0
		end

		var errors = option_context.errors
		if not errors.is_empty then
			for e in errors do print "Error: {e}"
			print tooldescription
			print "Use --help for help"
			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"
			exit 1
		end

		# 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

			# 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.
	fun version: String do
		if opt_set_dummy_tool.value then
			return "DUMMY_VERSION"
		end
		return nit_version
	end

	# Get the name of the tool or "DUMMY_TOOL" id `--set-dummy-tool` is set.
	fun toolname: String do
		if opt_set_dummy_tool.value then
			return "DUMMY_TOOL"
		end
		return sys.program_name.basename
	end

	# 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

	# 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
		# 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 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 check_nit_dir(res) then return res.simplify_path
		end

		# 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
src/toolcontext.nit:143,1--634,3