local argparse = require "argparse"
local cache = require "luacheck.cache"
local config = require "luacheck.config"
local luacheck = require "luacheck"
local multithreading = require "luacheck.multithreading"
local profiler = require "luacheck.profiler"
local runner = require "luacheck.runner"
local utils = require "luacheck.utils"
local version = require "luacheck.version"
local exit_codes = {
ok = 0,
warnings = 1,
errors = 2,
fatals = 3,
critical = 4
}
local function critical(msg)
io.stderr:write("Critical error: "..msg.."\n")
os.exit(exit_codes.critical)
end
local function get_parser()
local parser = argparse(
"luacheck", "luacheck " .. luacheck._VERSION .. ", a linter and a static analyzer for Lua.", [[
Links:
Luacheck on GitHub: https://github.com/lunarmodules/luacheck
Luacheck documentation: https://luacheck.readthedocs.org]])
:help_max_width(80)
parser:argument("files", "List of files, directories and rockspecs to check. Pass '-' to check stdin.")
:args "+"
:argname "<file>"
parser:group("Options for filtering warnings",
parser:flag("-g --no-global", "Filter out warnings related to global variables. " ..
"Equivalent to --ignore 1."):target("global"):action("store_false"),
parser:flag("-u --no-unused", "Filter out warnings related to unused variables and values. " ..
"Equivalent to --ignore [23]."):target("unused"):action("store_false"),
parser:flag("-r --no-redefined", "Filter out warnings related to redefined variables. " ..
"Equivalent to --ignore 4."):target("redefined"):action("store_false"),
parser:flag("-a --no-unused-args", "Filter out warnings related to unused arguments and " ..
"loop variables. Equivalent to --ignore 21[23]."):target("unused_args"):action("store_false"),
parser:flag("-s --no-unused-secondaries", "Filter out warnings related to unused variables set " ..
"together with used ones."):target("unused_secondaries"):action("store_false"),
parser:flag("--no-self", "Filter out warnings related to implicit self argument.")
:target("self"):action("store_false"),
parser:option("--ignore -i", "Filter out warnings matching these patterns.\n" ..
"If a pattern contains slash, part before slash matches warning code and part after it matches name of " ..
"related variable. Otherwise, if the pattern contains letters or underscore, it matches name of " ..
"related variable. Otherwise, the pattern matches warning code.")
:args "+"
:count "*"
:argname "<patt>"
:action "concat"
:init(nil),
parser:option("--enable -e", "Do not filter out warnings matching these patterns.")
:args "+"
:count "*"
:argname "<patt>"
:action "concat"
:init(nil),
parser:option("--only -o", "Filter out warnings not matching these patterns.")
:args "+"
:count "*"
:argname "<patt>"
:action "concat"
:init(nil),
parser:option("--operators", "Allow compound operators matching patterns")
:args "+"
:count "*"
:argname "<patt>"
:action "concat"
:init(nil))
parser:group("Options for configuring allowed globals",
parser:option("--std", "Set standard globals, default is max. <std> can be one of:\n" ..
" max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" ..
" min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.x;\n" ..
" lua51 - globals of Lua 5.1 without deprecated ones;\n" ..
" lua51c - globals of Lua 5.1;\n" ..
" lua52 - globals of Lua 5.2;\n" ..
" lua52c - globals of Lua 5.2 with LUA_COMPAT_ALL;\n" ..
" lua53 - globals of Lua 5.3;\n" ..
" lua53c - globals of Lua 5.3 with LUA_COMPAT_5_2;\n" ..
" lua54 - globals of Lua 5.4;\n" ..
" lua54c - globals of Lua 5.4 with LUA_COMPAT_5_3;\n" ..
" luajit - globals of LuaJIT 2.x;\n" ..
" ngx_lua - globals of Openresty lua-nginx-module 0.10.10, including standard LuaJIT 2.x globals;\n" ..
" love - globals added by LÖVE;\n" ..
" luanti - globals added by Luanti (formerly Minetest);\n" ..
" playdate - globals added by the Playdate SDK;\n" ..
" busted - globals added by Busted 2.0, by default added for files ending with _spec.lua within spec, " ..
"test, and tests subdirectories;\n" ..
" rockspec - globals allowed in rockspecs, by default added for files ending with .rockspec;\n" ..
" luacheckrc - globals allowed in Luacheck configs, by default added for files ending with .luacheckrc;\n" ..
" none - no standard globals.\n\n" ..
"Sets can be combined using '+'. Extra sets can be defined in config by " ..
"adding to `stds` global in config."),
parser:flag("-c --compat", "Equivalent to --std max."),
parser:option("--globals", "Add custom global variables (e.g. foo) or fields (e.g. foo.bar) " ..
"on top of standard ones.")
:args "*"
:count "*"
:argname "<name>"
:action "concat"
:init(nil),
parser:option("--read-globals", "Add read-only global variables or fields.")
:args "*"
:count "*"
:argname "<name>"
:action "concat"
:init(nil),
parser:option("--new-globals", "Set custom global variables or fields. Removes custom globals added previously.")
:args "*"
:count "*"
:argname "<name>"
:action "concat"
:init(nil),
parser:option("--new-read-globals", "Set read-only global variables or fields. " ..
"Removes read-only globals added previously.")
:args "*"
:count "*"
:argname "<name>"
:action "concat"
:init(nil),
parser:option("--not-globals", "Remove custom and standard global variables or fields.")
:args "*"
:count "*"
:argname "<name>"
:action "concat"
:init(nil),
parser:flag("-d --allow-defined", "Allow defining globals implicitly by setting them."),
parser:flag("-t --allow-defined-top",
"Allow defining globals implicitly by setting them in the top level scope."),
parser:flag("-m --module", "Limit visibility of implicitly defined globals to their files."))
parser:group("Options for configuring line length limits",
parser:option("--max-line-length", "Set maximum allowed line length (default: 120).")
:argname "<length>"
:convert(tonumber),
parser:flag("--no-max-line-length", "Do not limit line length.")
:action "store_false"
:target "max_line_length",
parser:option("--max-code-line-length", "Set maximum allowed length for lines ending with code (default: 120).")
:argname "<length>"
:convert(tonumber),
parser:flag("--no-max-code-line-length", "Do not limit code line length.")
:action "store_false"
:target "max_code_line_length",
parser:option("--max-string-line-length", "Set maximum allowed length for lines within a string (default: 120).")
:argname "<length>"
:convert(tonumber),
parser:flag("--no-max-string-line-length", "Do not limit string line length.")
:action "store_false"
:target "max_string_line_length",
parser:option("--max-comment-line-length", "Set maximum allowed length for comment lines (default: 120).")
:argname "<length>"
:convert(tonumber),
parser:flag("--no-max-comment-line-length", "Do not limit comment line length.")
:action "store_false"
:target "max_comment_line_length")
parser:option("--max-cyclomatic-complexity", "Set maximum cyclomatic complexity for functions.")
:argname "<complexity>"
:convert(tonumber)
parser:flag("--no-max-cyclomatic-complexity", "Do not limit function cyclomatic complexity (default).")
:action "store_false"
:target "max_cyclomatic_complexity"
local default_global_path = config.get_default_global_path()
local config_opt = parser:option("--config", "Path to configuration file. (default: "..config.default_path..")")
local no_config_opt = parser:flag("--no-config", "Do not look up configuration file.")
:action "store_false"
:target "config"
parser:mutex(config_opt, no_config_opt)
local default_config_opt = parser:option("--default-config", ("Path to configuration file to use if --[no-]config "..
"is not used and project-specific %s is not found. (default: %s)"):format(
config.default_path, default_global_path or "could not detect"))
local no_default_config_opt = parser:flag("--no-default-config", "Do not use default configuration file.")
:action "store_false"
:target "default_config"
parser:mutex(default_config_opt, no_default_config_opt)
parser:group("Configuration file options",
config_opt,
no_config_opt,
default_config_opt,
no_default_config_opt)
parser:group("File filtering options",
parser:option("--exclude-files", "Do not check files matching these globbing patterns.")
:args "+"
:count "*"
:argname "<glob>"
:action "concat"
:init(nil),
parser:option("--include-files", "Do not check files not matching these globbing patterns.")
:args "+"
:count "*"
:argname "<glob>"
:action "concat"
:init(nil))
parser:option("--filename", "Use another filename in output and for selecting configuration overrides.")
local cache_opt = parser:option("--cache", ("Path to cache directory. (default: %s)"):format(
cache.get_default_dir()))
:args "?"
local no_cache_opt = parser:flag("--no-cache", "Do not use cache.")
:action "store_false"
:target "cache"
parser:mutex(cache_opt, no_cache_opt)
local lanes_notice = ""
if not multithreading.has_lanes then
lanes_notice = "\nWarning: LuaLanes not found, parallel checking disabled."
end
parser:group("Performance optimization options",
cache_opt,
no_cache_opt,
parser:option(
"-j --jobs", "Check <jobs> files in parallel (default: " ..
tostring(multithreading.default_jobs) .. ")." .. lanes_notice):convert(tonumber))
parser:group("Output formatting options",
parser:option("--formatter" , "Use custom formatter. <formatter> must be a module name or one of:\n" ..
" TAP - Test Anything Protocol formatter;\n" ..
" JUnit - JUnit XML formatter;\n" ..
" visual_studio - MSBuild/Visual Studio aware formatter;\n" ..
" plain - simple warning-per-line formatter;\n" ..
" default - standard formatter."),
parser:flag("-q --quiet", "Suppress output for files without warnings.\n" ..
"-qq: Suppress output of warnings.\n" ..
"-qqq: Only print total number of warnings and errors.")
:count "0-3",
parser:flag("--codes", "Show warning codes."),
parser:flag("--ranges", "Show ranges of columns related to warnings."),
parser:flag("--no-color", "Do not color output.")
:action "store_false"
:target "color")
parser:flag("--profile", "Show performance statistics."):hidden(true)
parser:flag("-v --version", "Show version info and exit.")
:action(function() print(version.string) os.exit(exit_codes.ok) end)
return parser
end
local function main()
local parser = get_parser()
local ok, args = parser:pparse()
if not ok then
io.stderr:write(("%s\n\nError: %s\n"):format(parser:get_usage(), args))
os.exit(exit_codes.critical)
end
if args.profile then
args.jobs = 1
profiler.init()
end
if args.quiet == 0 then
args.quiet = nil
end
if args.cache then
args.cache = args.cache[1] or true
end
local checker, err, is_invalid_args_error = runner.new(args)
if not checker then
if is_invalid_args_error then
io.stderr:write(("%s\n\nError: %s\n"):format(parser:get_usage(), err))
os.exit(exit_codes.critical)
else
critical(err)
end
end
local inputs = {}
for _, file in ipairs(args.files) do
local input = {filename = args.filename}
if file == "-" then
input.file = io.stdin
elseif file:find("%.rockspec$") then
input.rockspec_path = file
else
input.path = file
end
table.insert(inputs, input)
end
local report, check_err = checker:check(inputs)
if not report then
critical(check_err)
end
for _, file_report in ipairs(report) do
if not file_report.filename then
file_report.filename = "stdin"
end
end
local output, format_err = checker:format(report)
if not output then
critical(format_err)
end
io.stdout:write(output)
if args.profile then
profiler.report()
end
if report.fatals > 0 then
os.exit(exit_codes.fatals)
elseif report.errors > 0 then
os.exit(exit_codes.errors)
elseif report.warnings > 0 then
os.exit(exit_codes.warnings)
else
os.exit(exit_codes.ok)
end
end
local _, error_wrapper = utils.try(main)
local err = error_wrapper.err
local traceback = error_wrapper.traceback
if utils.is_instance(err, utils.InvalidPatternError) then
critical(("Invalid pattern '%s'"):format(err.pattern))
elseif type(err) == "string" and err:find("interrupted!$") then
critical("Interrupted")
else
local msg = ("Luacheck %s bug (please report at https://github.com/lunarmodules/luacheck/issues):\n%s\n%s"):format(
luacheck._VERSION, err, traceback)
critical(msg)
end