Make main executable a module
This commit is contained in:
parent
101553882b
commit
d6ad8cfdda
610
bin/luacheck.lua
610
bin/luacheck.lua
@ -1,610 +1,2 @@
|
||||
#!/usr/bin/env lua
|
||||
local luacheck = require "luacheck"
|
||||
local argparse = require "luacheck.argparse"
|
||||
local stds = require "luacheck.stds"
|
||||
local options = require "luacheck.options"
|
||||
local expand_rockspec = require "luacheck.expand_rockspec"
|
||||
local multithreading = require "luacheck.multithreading"
|
||||
local cache = require "luacheck.cache"
|
||||
local format = require "luacheck.format"
|
||||
local version = require "luacheck.version"
|
||||
local fs = require "luacheck.fs"
|
||||
local utils = require "luacheck.utils"
|
||||
|
||||
local function fatal(msg)
|
||||
io.stderr:write("Fatal error: "..msg.."\n")
|
||||
os.exit(3)
|
||||
end
|
||||
|
||||
local function global_error_handler(err)
|
||||
if type(err) == "table" and err.pattern then
|
||||
fatal("Invalid pattern '" .. err.pattern .. "'")
|
||||
else
|
||||
fatal(debug.traceback(
|
||||
("Luacheck %s bug (please report at github.com/mpeterv/luacheck/issues):\n%s"):format(luacheck._VERSION, err), 2))
|
||||
end
|
||||
end
|
||||
|
||||
local function main()
|
||||
local default_config = ".luacheckrc"
|
||||
local default_cache_path = ".luacheckcache"
|
||||
|
||||
local function get_parser()
|
||||
local parser = argparse "luacheck"
|
||||
:description ("luacheck " .. luacheck._VERSION .. ", a simple static analyzer for Lua.")
|
||||
:epilog [[
|
||||
Links:
|
||||
|
||||
Luacheck on GitHub: https://github.com/mpeterv/luacheck
|
||||
Luacheck documentation: http://luacheck.readthedocs.org]]
|
||||
|
||||
parser:argument "files"
|
||||
:description (fs.has_lfs and [[List of files, directories and rockspecs to check.
|
||||
Pass "-" to check stdin.]] or [[List of files and rockspecs to check.
|
||||
Pass "-" to check stdin.]])
|
||||
:args "+"
|
||||
:argname "<file>"
|
||||
|
||||
parser:flag "-g" "--no-global"
|
||||
:description [[Filter out warnings related to global variables.
|
||||
Equivalent to --ignore 1.]]
|
||||
parser:flag "-u" "--no-unused"
|
||||
:description [[Filter out warnings related to unused variables and values.
|
||||
Equivalent to --ignore [23].]]
|
||||
parser:flag "-r" "--no-redefined"
|
||||
:description [[Filter out warnings related to redefined variables.
|
||||
Equivalent to --ignore 4.]]
|
||||
|
||||
parser:flag "-a" "--no-unused-args"
|
||||
:description [[Filter out warnings related to unused arguments and loop variables.
|
||||
Equivalent to --ignore 21[23].]]
|
||||
parser:flag "-s" "--no-unused-secondaries"
|
||||
:description "Filter out warnings related to unused variables set together with used ones."
|
||||
parser:flag "--no-self"
|
||||
:description "Filter out warnings related to implicit self argument."
|
||||
|
||||
parser:option "--std"
|
||||
:description [[Set standard globals. <std> must be one of:
|
||||
_G - globals of the current Lua interpreter (default);
|
||||
lua51 - globals of Lua 5.1;
|
||||
lua52 - globals of Lua 5.2;
|
||||
lua52c - globals of Lua 5.2 compiled with LUA_COMPAT_ALL;
|
||||
lua53 - globals of Lua 5.3;
|
||||
lua53c - globals of Lua 5.3 compiled with LUA_COMPAT_5_2;
|
||||
luajit - globals of LuaJIT 2.0;
|
||||
min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0;
|
||||
max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0;
|
||||
none - no standard globals.]]
|
||||
parser:option "--globals"
|
||||
:description "Add custom globals on top of standard ones."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--read-globals"
|
||||
:description "Add read-only globals."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--new-globals"
|
||||
:description "Set custom globals. Removes custom globals added previously."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--new-read-globals"
|
||||
:description "Set read-only globals. Removes read-only globals added previously."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:flag "-c" "--compat"
|
||||
:description "Equivalent to --std max."
|
||||
parser:flag "-d" "--allow-defined"
|
||||
:description "Allow defining globals implicitly by setting them."
|
||||
parser:flag "-t" "--allow-defined-top"
|
||||
:description "Allow defining globals implicitly by setting them in the top level scope."
|
||||
parser:flag "-m" "--module"
|
||||
:description "Limit visibility of implicitly defined globals to their files."
|
||||
|
||||
parser:option "--ignore" "-i"
|
||||
:description [[Filter out warnings matching these patterns.
|
||||
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>"
|
||||
parser:option "--enable" "-e"
|
||||
:description "Do not filter out warnings matching these patterns."
|
||||
:args "+"
|
||||
:count "*"
|
||||
:argname "<patt>"
|
||||
parser:option "--only" "-o"
|
||||
:description "Filter out warnings not matching these patterns."
|
||||
:args "+"
|
||||
:count "*"
|
||||
:argname "<patt>"
|
||||
|
||||
parser:flag "--no-inline"
|
||||
:description "Disable inline options."
|
||||
|
||||
local config_opt = parser:option "--config"
|
||||
:description ("Path to configuration file. (default: "..default_config..")")
|
||||
|
||||
local no_config_opt = parser:flag "--no-config"
|
||||
:description "Do not look up configuration file."
|
||||
|
||||
parser:mutex(config_opt, no_config_opt)
|
||||
|
||||
if fs.has_lfs then
|
||||
local cache_opt = parser:option "--cache"
|
||||
:description "Path to cache file."
|
||||
:default (default_cache_path)
|
||||
:defmode "arg"
|
||||
|
||||
local no_cache_opt = parser:flag "--no-cache"
|
||||
:description "Do not use cache."
|
||||
|
||||
parser:mutex(cache_opt, no_cache_opt)
|
||||
end
|
||||
|
||||
if multithreading.has_lanes then
|
||||
parser:option "-j" "--jobs"
|
||||
:description "Check <jobs> files in parallel."
|
||||
:convert(tonumber)
|
||||
end
|
||||
|
||||
parser:option "--formatter"
|
||||
:description [[Use custom formatter. <formatter> must be a module name or one of:
|
||||
TAP - Test Anything Protocol formatter;
|
||||
JUnit - JUnit XML formatter;
|
||||
plain - simple warning-per-line formatter;
|
||||
default - standard formatter.]]
|
||||
|
||||
parser:flag "-q" "--quiet"
|
||||
:count "0-3"
|
||||
:description [[Suppress output for files without warnings.
|
||||
-qq: Suppress output of warnings.
|
||||
-qqq: Only print total number of warnings and errors.]]
|
||||
|
||||
parser:flag "--codes"
|
||||
:description "Show warning codes."
|
||||
|
||||
parser:flag "--no-color"
|
||||
:description "Do not color output."
|
||||
|
||||
parser:flag "-v" "--version"
|
||||
:description "Show version info and exit."
|
||||
:action(function()
|
||||
print(version.string)
|
||||
os.exit(0)
|
||||
end)
|
||||
|
||||
return parser
|
||||
end
|
||||
|
||||
-- Expands folders, rockspecs, -
|
||||
-- Returns new array of filenames and table mapping indexes of bad rockspecs to error messages.
|
||||
-- Removes "./" in the beginnings of file names.
|
||||
local function expand_files(files)
|
||||
local res, bad_rockspecs = {}, {}
|
||||
|
||||
local function add(file)
|
||||
table.insert(res, (file:gsub("^./", "")))
|
||||
end
|
||||
|
||||
for _, file in ipairs(files) do
|
||||
if file == "-" then
|
||||
table.insert(res, io.stdin)
|
||||
elseif fs.is_dir(file) then
|
||||
for _, nested_file in ipairs(fs.extract_files(file, "%.lua$")) do
|
||||
add(nested_file)
|
||||
end
|
||||
elseif file:sub(-#".rockspec") == ".rockspec" then
|
||||
local related_files, err = expand_rockspec(file)
|
||||
|
||||
if related_files then
|
||||
for _, related_file in ipairs(related_files) do
|
||||
add(related_file)
|
||||
end
|
||||
else
|
||||
add(file)
|
||||
bad_rockspecs[#res] = err
|
||||
end
|
||||
else
|
||||
add(file)
|
||||
end
|
||||
end
|
||||
|
||||
return res, bad_rockspecs
|
||||
end
|
||||
|
||||
-- Config must support special metatables for some keys:
|
||||
-- autovivification for `files`, fallback to built-in stds for `stds`.
|
||||
-- Save values assigned to these globals to hidden keys.
|
||||
local special_keys = {stds = {}, files = {}}
|
||||
local special_mts = {stds = {__index = stds}, files = {__index = function(self, k)
|
||||
self[k] = {}
|
||||
return self[k]
|
||||
end}}
|
||||
local config_env_mt = {__newindex = function(self, k, v)
|
||||
if special_keys[k] then
|
||||
if type(v) == "table" then
|
||||
setmetatable(v, special_mts[k])
|
||||
end
|
||||
|
||||
k = special_keys[k]
|
||||
end
|
||||
|
||||
rawset(self, k, v)
|
||||
end, __index = function(self, k)
|
||||
if special_keys[k] then
|
||||
return self[special_keys[k]]
|
||||
end
|
||||
end}
|
||||
|
||||
local function make_config_env()
|
||||
local env = setmetatable({}, config_env_mt)
|
||||
env.files = {}
|
||||
env.stds = {}
|
||||
-- Expose `require` so that custom stds or configuration could be loaded from modules.
|
||||
env.require = require
|
||||
return env
|
||||
end
|
||||
|
||||
-- Returns nil or config, config_path, optional relative path from config to current directory.
|
||||
local function get_config(config_path)
|
||||
local rel_path
|
||||
|
||||
if not config_path then
|
||||
local current_dir = fs.current_dir()
|
||||
local config_dir = fs.find_file(current_dir, default_config)
|
||||
|
||||
if not config_dir then
|
||||
return
|
||||
end
|
||||
|
||||
if config_dir == current_dir then
|
||||
config_path = default_config
|
||||
else
|
||||
config_path = config_dir .. default_config
|
||||
rel_path = current_dir:sub(#config_dir + 1)
|
||||
end
|
||||
end
|
||||
|
||||
local env = make_config_env()
|
||||
local ok, ret = utils.load_config(config_path, env)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't load configuration from %s: %s error"):format(config_path, ret))
|
||||
end
|
||||
|
||||
if type(ret) == "table" then
|
||||
utils.update(env, ret)
|
||||
end
|
||||
|
||||
rawset(env, "files", env[special_keys.files])
|
||||
|
||||
if type(env[special_keys.stds]) == "table" then
|
||||
utils.update(stds, env[special_keys.stds])
|
||||
end
|
||||
|
||||
return env, config_path, rel_path
|
||||
end
|
||||
|
||||
local function validate_args(args, parser)
|
||||
if args.jobs and args.jobs < 1 then
|
||||
parser:error("<jobs> must be at least 1")
|
||||
end
|
||||
|
||||
if args.std and not options.split_std(args.std) then
|
||||
parser:error("<std> must name a standard library")
|
||||
end
|
||||
end
|
||||
|
||||
local function get_options(args)
|
||||
local res = {}
|
||||
|
||||
for _, argname in ipairs {"allow_defined", "allow_defined_top", "module", "compat", "std"} do
|
||||
if args[argname] then
|
||||
res[argname] = args[argname]
|
||||
end
|
||||
end
|
||||
|
||||
for _, argname in ipairs {"global", "unused", "redefined", "unused", "unused_args",
|
||||
"unused_secondaries", "self", "inline"} do
|
||||
if args["no_"..argname] then
|
||||
res[argname] = false
|
||||
end
|
||||
end
|
||||
|
||||
for _, argname in ipairs {"globals", "read_globals", "new_globals", "new_read_globals",
|
||||
"ignore", "enable", "only"} do
|
||||
if #args[argname] > 0 then
|
||||
res[argname] = utils.concat_arrays(args[argname])
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Applies cli-specific options from config to args.
|
||||
local function combine_config_and_args(config, args)
|
||||
if args.no_color then
|
||||
args.color = false
|
||||
else
|
||||
args.color = not config or (config.color ~= false)
|
||||
end
|
||||
|
||||
args.codes = args.codes or config and config.codes
|
||||
args.formatter = args.formatter or (config and config.formatter) or "default"
|
||||
|
||||
if args.no_cache or not fs.has_lfs then
|
||||
args.cache = false
|
||||
else
|
||||
args.cache = args.cache or (config and config.cache)
|
||||
end
|
||||
|
||||
if args.cache == true then
|
||||
args.cache = default_cache_path
|
||||
end
|
||||
|
||||
args.jobs = args.jobs or (config and config.jobs)
|
||||
end
|
||||
|
||||
-- Returns sparse array of mtimes and map of filenames to cached reports.
|
||||
local function get_mtimes_and_cached_reports(cache_filename, files, bad_files)
|
||||
local cache_files = {}
|
||||
local cache_mtimes = {}
|
||||
local sparse_mtimes = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if not bad_files[i] and file ~= io.stdin then
|
||||
table.insert(cache_files, file)
|
||||
local mtime = fs.mtime(file)
|
||||
table.insert(cache_mtimes, mtime)
|
||||
sparse_mtimes[i] = mtime
|
||||
end
|
||||
end
|
||||
|
||||
return sparse_mtimes, cache.load(cache_filename, cache_files, cache_mtimes) or fatal(
|
||||
("Couldn't load cache from %s: data corrupted"):format(cache_filename))
|
||||
end
|
||||
|
||||
-- Returns sparse array of sources of files that need to be checked, updates bad_files with files that had I/O issues.
|
||||
local function get_srcs_to_check(cached_reports, files, bad_files)
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if not bad_files[i] and not cached_reports[file] then
|
||||
local src = utils.read_file(file)
|
||||
|
||||
if src then
|
||||
res[i] = src
|
||||
else
|
||||
bad_files[i] = "I/O"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function get_report(source)
|
||||
local report, err = luacheck.get_report(source)
|
||||
|
||||
if report then
|
||||
return report
|
||||
else
|
||||
err.error = "syntax"
|
||||
return err
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns sparse array of new reports.
|
||||
local function get_new_reports(files, srcs, jobs)
|
||||
local dense_srcs = {}
|
||||
local dense_to_sparse = {}
|
||||
|
||||
for i in ipairs(files) do
|
||||
if srcs[i] then
|
||||
table.insert(dense_srcs, srcs[i])
|
||||
dense_to_sparse[#dense_srcs] = i
|
||||
end
|
||||
end
|
||||
|
||||
local map = jobs and multithreading.has_lanes and multithreading.pmap or utils.map
|
||||
local dense_res = map(get_report, dense_srcs, jobs)
|
||||
|
||||
local res = {}
|
||||
|
||||
for i in ipairs(dense_srcs) do
|
||||
res[dense_to_sparse[i]] = dense_res[i]
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Updates cache with new_reports. Updates bad_files for which mtime is absent.
|
||||
local function update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports)
|
||||
local cache_files = {}
|
||||
local cache_mtimes = {}
|
||||
local cache_reports = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if srcs[i] and file ~= io.stdin then
|
||||
if not mtimes[i] then
|
||||
bad_files[i] = "I/O"
|
||||
else
|
||||
table.insert(cache_files, file)
|
||||
table.insert(cache_mtimes, mtimes[i])
|
||||
table.insert(cache_reports, new_reports[i] or false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return cache.update(cache_filename, cache_files, cache_mtimes, cache_reports) or fatal(
|
||||
("Couldn't save cache to %s: I/O error"):format(cache_filename))
|
||||
end
|
||||
|
||||
-- Returns array of reports for files.
|
||||
local function get_reports(cache_filename, files, bad_rockspecs, jobs)
|
||||
local bad_files = utils.update({}, bad_rockspecs)
|
||||
local mtimes
|
||||
local cached_reports
|
||||
|
||||
if cache_filename then
|
||||
mtimes, cached_reports = get_mtimes_and_cached_reports(cache_filename, files, bad_files)
|
||||
else
|
||||
cached_reports = {}
|
||||
end
|
||||
|
||||
local srcs = get_srcs_to_check(cached_reports, files, bad_files)
|
||||
local new_reports = get_new_reports(files, srcs, jobs)
|
||||
|
||||
if cache_filename then
|
||||
update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports)
|
||||
end
|
||||
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if bad_files[i] then
|
||||
res[i] = {error = bad_files[i]}
|
||||
else
|
||||
res[i] = cached_reports[file] or new_reports[i]
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function combine_config_and_options(config, config_path, config_rel_path, cli_opts, files)
|
||||
if not config then
|
||||
return cli_opts
|
||||
end
|
||||
|
||||
config_rel_path = config_rel_path or ""
|
||||
|
||||
local function validate(option_set, opts)
|
||||
local ok, invalid_field = options.validate(option_set, opts)
|
||||
|
||||
if not ok then
|
||||
if invalid_field then
|
||||
fatal(("Couldn't load configuration from %s: invalid value of option '%s'"):format(
|
||||
config_path, invalid_field))
|
||||
else
|
||||
fatal(("Couldn't load configuration from %s: validation error"):format(config_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
validate(options.top_config_options, config)
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
res[i] = {config}
|
||||
|
||||
if type(config.files) == "table" and type(file) == "string" then
|
||||
file = config_rel_path .. file
|
||||
local overriding_paths = {}
|
||||
|
||||
for path in pairs(config.files) do
|
||||
if file:sub(1, #path) == path then
|
||||
table.insert(overriding_paths, path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Since all paths are prefixes of path, sorting by len is equivalent to regular sorting.
|
||||
table.sort(overriding_paths)
|
||||
|
||||
-- Apply overrides from less specific (shorter prefixes) to more specific (longer prefixes).
|
||||
for _, path in ipairs(overriding_paths) do
|
||||
local overriding_config = config.files[path]
|
||||
validate(options.config_options, overriding_config)
|
||||
table.insert(res[i], overriding_config)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(res[i], cli_opts)
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function normalize_filenames(files)
|
||||
for i, file in ipairs(files) do
|
||||
if type(file) ~= "string" then
|
||||
files[i] = "stdin"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local builtin_formatters = utils.array_to_set({"TAP", "JUnit", "plain", "default"})
|
||||
|
||||
local function pformat(report, file_names, args)
|
||||
if builtin_formatters[args.formatter] then
|
||||
return format(report, file_names, args)
|
||||
end
|
||||
|
||||
local formatter = args.formatter
|
||||
local ok, output
|
||||
|
||||
if type(formatter) == "string" then
|
||||
ok, formatter = pcall(require, formatter)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't load custom formatter '%s': %s"):format(args.formatter, formatter))
|
||||
end
|
||||
end
|
||||
|
||||
ok, output = pcall(formatter, report, file_names, args)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't run custom formatter '%s': %s"):format(tostring(args.formatter), output))
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
local parser = get_parser()
|
||||
local args = parser:parse()
|
||||
local opts = get_options(args)
|
||||
local config
|
||||
local config_path
|
||||
local config_rel_path
|
||||
|
||||
if not args.no_config then
|
||||
config, config_path, config_rel_path = get_config(args.config)
|
||||
end
|
||||
|
||||
validate_args(args, parser)
|
||||
combine_config_and_args(config, args)
|
||||
|
||||
local files, bad_rockspecs = expand_files(args.files)
|
||||
local reports = get_reports(args.cache, files, bad_rockspecs, args.jobs)
|
||||
local report = luacheck.process_reports(reports, combine_config_and_options(config, config_path, config_rel_path, opts, files))
|
||||
normalize_filenames(files)
|
||||
|
||||
local output = pformat(report, files, args)
|
||||
|
||||
if #output > 0 and output:sub(-1) ~= "\n" then
|
||||
output = output .. "\n"
|
||||
end
|
||||
|
||||
io.stdout:write(output)
|
||||
|
||||
local exit_code
|
||||
|
||||
if report.errors > 0 then
|
||||
exit_code = 2
|
||||
elseif report.warnings > 0 then
|
||||
exit_code = 1
|
||||
else
|
||||
exit_code = 0
|
||||
end
|
||||
|
||||
os.exit(exit_code)
|
||||
end
|
||||
|
||||
xpcall(main, global_error_handler)
|
||||
require "luacheck.main"
|
||||
|
@ -99,6 +99,7 @@ print(" Installing luacheck modules into " .. luacheck_src_dir)
|
||||
mkdir(luacheck_lib_dir)
|
||||
|
||||
for _, filename in ipairs {
|
||||
"main.lua",
|
||||
"init.lua",
|
||||
"linearize.lua",
|
||||
"analyze.lua",
|
||||
|
@ -19,6 +19,7 @@ build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
luacheck = "src/luacheck/init.lua",
|
||||
["luacheck.main"] = "src/luacheck/main.lua",
|
||||
["luacheck.linearize"] = "src/luacheck/linearize.lua",
|
||||
["luacheck.analyze"] = "src/luacheck/analyze.lua",
|
||||
["luacheck.reachability"] = "src/luacheck/reachability.lua",
|
||||
|
609
src/luacheck/main.lua
Normal file
609
src/luacheck/main.lua
Normal file
@ -0,0 +1,609 @@
|
||||
local luacheck = require "luacheck"
|
||||
local argparse = require "luacheck.argparse"
|
||||
local stds = require "luacheck.stds"
|
||||
local options = require "luacheck.options"
|
||||
local expand_rockspec = require "luacheck.expand_rockspec"
|
||||
local multithreading = require "luacheck.multithreading"
|
||||
local cache = require "luacheck.cache"
|
||||
local format = require "luacheck.format"
|
||||
local version = require "luacheck.version"
|
||||
local fs = require "luacheck.fs"
|
||||
local utils = require "luacheck.utils"
|
||||
|
||||
local function fatal(msg)
|
||||
io.stderr:write("Fatal error: "..msg.."\n")
|
||||
os.exit(3)
|
||||
end
|
||||
|
||||
local function global_error_handler(err)
|
||||
if type(err) == "table" and err.pattern then
|
||||
fatal("Invalid pattern '" .. err.pattern .. "'")
|
||||
else
|
||||
fatal(debug.traceback(
|
||||
("Luacheck %s bug (please report at github.com/mpeterv/luacheck/issues):\n%s"):format(luacheck._VERSION, err), 2))
|
||||
end
|
||||
end
|
||||
|
||||
local function main()
|
||||
local default_config = ".luacheckrc"
|
||||
local default_cache_path = ".luacheckcache"
|
||||
|
||||
local function get_parser()
|
||||
local parser = argparse "luacheck"
|
||||
:description ("luacheck " .. luacheck._VERSION .. ", a simple static analyzer for Lua.")
|
||||
:epilog [[
|
||||
Links:
|
||||
|
||||
Luacheck on GitHub: https://github.com/mpeterv/luacheck
|
||||
Luacheck documentation: http://luacheck.readthedocs.org]]
|
||||
|
||||
parser:argument "files"
|
||||
:description (fs.has_lfs and [[List of files, directories and rockspecs to check.
|
||||
Pass "-" to check stdin.]] or [[List of files and rockspecs to check.
|
||||
Pass "-" to check stdin.]])
|
||||
:args "+"
|
||||
:argname "<file>"
|
||||
|
||||
parser:flag "-g" "--no-global"
|
||||
:description [[Filter out warnings related to global variables.
|
||||
Equivalent to --ignore 1.]]
|
||||
parser:flag "-u" "--no-unused"
|
||||
:description [[Filter out warnings related to unused variables and values.
|
||||
Equivalent to --ignore [23].]]
|
||||
parser:flag "-r" "--no-redefined"
|
||||
:description [[Filter out warnings related to redefined variables.
|
||||
Equivalent to --ignore 4.]]
|
||||
|
||||
parser:flag "-a" "--no-unused-args"
|
||||
:description [[Filter out warnings related to unused arguments and loop variables.
|
||||
Equivalent to --ignore 21[23].]]
|
||||
parser:flag "-s" "--no-unused-secondaries"
|
||||
:description "Filter out warnings related to unused variables set together with used ones."
|
||||
parser:flag "--no-self"
|
||||
:description "Filter out warnings related to implicit self argument."
|
||||
|
||||
parser:option "--std"
|
||||
:description [[Set standard globals. <std> must be one of:
|
||||
_G - globals of the current Lua interpreter (default);
|
||||
lua51 - globals of Lua 5.1;
|
||||
lua52 - globals of Lua 5.2;
|
||||
lua52c - globals of Lua 5.2 compiled with LUA_COMPAT_ALL;
|
||||
lua53 - globals of Lua 5.3;
|
||||
lua53c - globals of Lua 5.3 compiled with LUA_COMPAT_5_2;
|
||||
luajit - globals of LuaJIT 2.0;
|
||||
min - intersection of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0;
|
||||
max - union of globals of Lua 5.1, Lua 5.2, Lua 5.3 and LuaJIT 2.0;
|
||||
none - no standard globals.]]
|
||||
parser:option "--globals"
|
||||
:description "Add custom globals on top of standard ones."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--read-globals"
|
||||
:description "Add read-only globals."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--new-globals"
|
||||
:description "Set custom globals. Removes custom globals added previously."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:option "--new-read-globals"
|
||||
:description "Set read-only globals. Removes read-only globals added previously."
|
||||
:args "*"
|
||||
:count "*"
|
||||
:argname "<global>"
|
||||
parser:flag "-c" "--compat"
|
||||
:description "Equivalent to --std max."
|
||||
parser:flag "-d" "--allow-defined"
|
||||
:description "Allow defining globals implicitly by setting them."
|
||||
parser:flag "-t" "--allow-defined-top"
|
||||
:description "Allow defining globals implicitly by setting them in the top level scope."
|
||||
parser:flag "-m" "--module"
|
||||
:description "Limit visibility of implicitly defined globals to their files."
|
||||
|
||||
parser:option "--ignore" "-i"
|
||||
:description [[Filter out warnings matching these patterns.
|
||||
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>"
|
||||
parser:option "--enable" "-e"
|
||||
:description "Do not filter out warnings matching these patterns."
|
||||
:args "+"
|
||||
:count "*"
|
||||
:argname "<patt>"
|
||||
parser:option "--only" "-o"
|
||||
:description "Filter out warnings not matching these patterns."
|
||||
:args "+"
|
||||
:count "*"
|
||||
:argname "<patt>"
|
||||
|
||||
parser:flag "--no-inline"
|
||||
:description "Disable inline options."
|
||||
|
||||
local config_opt = parser:option "--config"
|
||||
:description ("Path to configuration file. (default: "..default_config..")")
|
||||
|
||||
local no_config_opt = parser:flag "--no-config"
|
||||
:description "Do not look up configuration file."
|
||||
|
||||
parser:mutex(config_opt, no_config_opt)
|
||||
|
||||
if fs.has_lfs then
|
||||
local cache_opt = parser:option "--cache"
|
||||
:description "Path to cache file."
|
||||
:default (default_cache_path)
|
||||
:defmode "arg"
|
||||
|
||||
local no_cache_opt = parser:flag "--no-cache"
|
||||
:description "Do not use cache."
|
||||
|
||||
parser:mutex(cache_opt, no_cache_opt)
|
||||
end
|
||||
|
||||
if multithreading.has_lanes then
|
||||
parser:option "-j" "--jobs"
|
||||
:description "Check <jobs> files in parallel."
|
||||
:convert(tonumber)
|
||||
end
|
||||
|
||||
parser:option "--formatter"
|
||||
:description [[Use custom formatter. <formatter> must be a module name or one of:
|
||||
TAP - Test Anything Protocol formatter;
|
||||
JUnit - JUnit XML formatter;
|
||||
plain - simple warning-per-line formatter;
|
||||
default - standard formatter.]]
|
||||
|
||||
parser:flag "-q" "--quiet"
|
||||
:count "0-3"
|
||||
:description [[Suppress output for files without warnings.
|
||||
-qq: Suppress output of warnings.
|
||||
-qqq: Only print total number of warnings and errors.]]
|
||||
|
||||
parser:flag "--codes"
|
||||
:description "Show warning codes."
|
||||
|
||||
parser:flag "--no-color"
|
||||
:description "Do not color output."
|
||||
|
||||
parser:flag "-v" "--version"
|
||||
:description "Show version info and exit."
|
||||
:action(function()
|
||||
print(version.string)
|
||||
os.exit(0)
|
||||
end)
|
||||
|
||||
return parser
|
||||
end
|
||||
|
||||
-- Expands folders, rockspecs, -
|
||||
-- Returns new array of filenames and table mapping indexes of bad rockspecs to error messages.
|
||||
-- Removes "./" in the beginnings of file names.
|
||||
local function expand_files(files)
|
||||
local res, bad_rockspecs = {}, {}
|
||||
|
||||
local function add(file)
|
||||
table.insert(res, (file:gsub("^./", "")))
|
||||
end
|
||||
|
||||
for _, file in ipairs(files) do
|
||||
if file == "-" then
|
||||
table.insert(res, io.stdin)
|
||||
elseif fs.is_dir(file) then
|
||||
for _, nested_file in ipairs(fs.extract_files(file, "%.lua$")) do
|
||||
add(nested_file)
|
||||
end
|
||||
elseif file:sub(-#".rockspec") == ".rockspec" then
|
||||
local related_files, err = expand_rockspec(file)
|
||||
|
||||
if related_files then
|
||||
for _, related_file in ipairs(related_files) do
|
||||
add(related_file)
|
||||
end
|
||||
else
|
||||
add(file)
|
||||
bad_rockspecs[#res] = err
|
||||
end
|
||||
else
|
||||
add(file)
|
||||
end
|
||||
end
|
||||
|
||||
return res, bad_rockspecs
|
||||
end
|
||||
|
||||
-- Config must support special metatables for some keys:
|
||||
-- autovivification for `files`, fallback to built-in stds for `stds`.
|
||||
-- Save values assigned to these globals to hidden keys.
|
||||
local special_keys = {stds = {}, files = {}}
|
||||
local special_mts = {stds = {__index = stds}, files = {__index = function(self, k)
|
||||
self[k] = {}
|
||||
return self[k]
|
||||
end}}
|
||||
local config_env_mt = {__newindex = function(self, k, v)
|
||||
if special_keys[k] then
|
||||
if type(v) == "table" then
|
||||
setmetatable(v, special_mts[k])
|
||||
end
|
||||
|
||||
k = special_keys[k]
|
||||
end
|
||||
|
||||
rawset(self, k, v)
|
||||
end, __index = function(self, k)
|
||||
if special_keys[k] then
|
||||
return self[special_keys[k]]
|
||||
end
|
||||
end}
|
||||
|
||||
local function make_config_env()
|
||||
local env = setmetatable({}, config_env_mt)
|
||||
env.files = {}
|
||||
env.stds = {}
|
||||
-- Expose `require` so that custom stds or configuration could be loaded from modules.
|
||||
env.require = require
|
||||
return env
|
||||
end
|
||||
|
||||
-- Returns nil or config, config_path, optional relative path from config to current directory.
|
||||
local function get_config(config_path)
|
||||
local rel_path
|
||||
|
||||
if not config_path then
|
||||
local current_dir = fs.current_dir()
|
||||
local config_dir = fs.find_file(current_dir, default_config)
|
||||
|
||||
if not config_dir then
|
||||
return
|
||||
end
|
||||
|
||||
if config_dir == current_dir then
|
||||
config_path = default_config
|
||||
else
|
||||
config_path = config_dir .. default_config
|
||||
rel_path = current_dir:sub(#config_dir + 1)
|
||||
end
|
||||
end
|
||||
|
||||
local env = make_config_env()
|
||||
local ok, ret = utils.load_config(config_path, env)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't load configuration from %s: %s error"):format(config_path, ret))
|
||||
end
|
||||
|
||||
if type(ret) == "table" then
|
||||
utils.update(env, ret)
|
||||
end
|
||||
|
||||
rawset(env, "files", env[special_keys.files])
|
||||
|
||||
if type(env[special_keys.stds]) == "table" then
|
||||
utils.update(stds, env[special_keys.stds])
|
||||
end
|
||||
|
||||
return env, config_path, rel_path
|
||||
end
|
||||
|
||||
local function validate_args(args, parser)
|
||||
if args.jobs and args.jobs < 1 then
|
||||
parser:error("<jobs> must be at least 1")
|
||||
end
|
||||
|
||||
if args.std and not options.split_std(args.std) then
|
||||
parser:error("<std> must name a standard library")
|
||||
end
|
||||
end
|
||||
|
||||
local function get_options(args)
|
||||
local res = {}
|
||||
|
||||
for _, argname in ipairs {"allow_defined", "allow_defined_top", "module", "compat", "std"} do
|
||||
if args[argname] then
|
||||
res[argname] = args[argname]
|
||||
end
|
||||
end
|
||||
|
||||
for _, argname in ipairs {"global", "unused", "redefined", "unused", "unused_args",
|
||||
"unused_secondaries", "self", "inline"} do
|
||||
if args["no_"..argname] then
|
||||
res[argname] = false
|
||||
end
|
||||
end
|
||||
|
||||
for _, argname in ipairs {"globals", "read_globals", "new_globals", "new_read_globals",
|
||||
"ignore", "enable", "only"} do
|
||||
if #args[argname] > 0 then
|
||||
res[argname] = utils.concat_arrays(args[argname])
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Applies cli-specific options from config to args.
|
||||
local function combine_config_and_args(config, args)
|
||||
if args.no_color then
|
||||
args.color = false
|
||||
else
|
||||
args.color = not config or (config.color ~= false)
|
||||
end
|
||||
|
||||
args.codes = args.codes or config and config.codes
|
||||
args.formatter = args.formatter or (config and config.formatter) or "default"
|
||||
|
||||
if args.no_cache or not fs.has_lfs then
|
||||
args.cache = false
|
||||
else
|
||||
args.cache = args.cache or (config and config.cache)
|
||||
end
|
||||
|
||||
if args.cache == true then
|
||||
args.cache = default_cache_path
|
||||
end
|
||||
|
||||
args.jobs = args.jobs or (config and config.jobs)
|
||||
end
|
||||
|
||||
-- Returns sparse array of mtimes and map of filenames to cached reports.
|
||||
local function get_mtimes_and_cached_reports(cache_filename, files, bad_files)
|
||||
local cache_files = {}
|
||||
local cache_mtimes = {}
|
||||
local sparse_mtimes = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if not bad_files[i] and file ~= io.stdin then
|
||||
table.insert(cache_files, file)
|
||||
local mtime = fs.mtime(file)
|
||||
table.insert(cache_mtimes, mtime)
|
||||
sparse_mtimes[i] = mtime
|
||||
end
|
||||
end
|
||||
|
||||
return sparse_mtimes, cache.load(cache_filename, cache_files, cache_mtimes) or fatal(
|
||||
("Couldn't load cache from %s: data corrupted"):format(cache_filename))
|
||||
end
|
||||
|
||||
-- Returns sparse array of sources of files that need to be checked, updates bad_files with files that had I/O issues.
|
||||
local function get_srcs_to_check(cached_reports, files, bad_files)
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if not bad_files[i] and not cached_reports[file] then
|
||||
local src = utils.read_file(file)
|
||||
|
||||
if src then
|
||||
res[i] = src
|
||||
else
|
||||
bad_files[i] = "I/O"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function get_report(source)
|
||||
local report, err = luacheck.get_report(source)
|
||||
|
||||
if report then
|
||||
return report
|
||||
else
|
||||
err.error = "syntax"
|
||||
return err
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns sparse array of new reports.
|
||||
local function get_new_reports(files, srcs, jobs)
|
||||
local dense_srcs = {}
|
||||
local dense_to_sparse = {}
|
||||
|
||||
for i in ipairs(files) do
|
||||
if srcs[i] then
|
||||
table.insert(dense_srcs, srcs[i])
|
||||
dense_to_sparse[#dense_srcs] = i
|
||||
end
|
||||
end
|
||||
|
||||
local map = jobs and multithreading.has_lanes and multithreading.pmap or utils.map
|
||||
local dense_res = map(get_report, dense_srcs, jobs)
|
||||
|
||||
local res = {}
|
||||
|
||||
for i in ipairs(dense_srcs) do
|
||||
res[dense_to_sparse[i]] = dense_res[i]
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Updates cache with new_reports. Updates bad_files for which mtime is absent.
|
||||
local function update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports)
|
||||
local cache_files = {}
|
||||
local cache_mtimes = {}
|
||||
local cache_reports = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if srcs[i] and file ~= io.stdin then
|
||||
if not mtimes[i] then
|
||||
bad_files[i] = "I/O"
|
||||
else
|
||||
table.insert(cache_files, file)
|
||||
table.insert(cache_mtimes, mtimes[i])
|
||||
table.insert(cache_reports, new_reports[i] or false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return cache.update(cache_filename, cache_files, cache_mtimes, cache_reports) or fatal(
|
||||
("Couldn't save cache to %s: I/O error"):format(cache_filename))
|
||||
end
|
||||
|
||||
-- Returns array of reports for files.
|
||||
local function get_reports(cache_filename, files, bad_rockspecs, jobs)
|
||||
local bad_files = utils.update({}, bad_rockspecs)
|
||||
local mtimes
|
||||
local cached_reports
|
||||
|
||||
if cache_filename then
|
||||
mtimes, cached_reports = get_mtimes_and_cached_reports(cache_filename, files, bad_files)
|
||||
else
|
||||
cached_reports = {}
|
||||
end
|
||||
|
||||
local srcs = get_srcs_to_check(cached_reports, files, bad_files)
|
||||
local new_reports = get_new_reports(files, srcs, jobs)
|
||||
|
||||
if cache_filename then
|
||||
update_cache(cache_filename, files, bad_files, srcs, mtimes, new_reports)
|
||||
end
|
||||
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
if bad_files[i] then
|
||||
res[i] = {error = bad_files[i]}
|
||||
else
|
||||
res[i] = cached_reports[file] or new_reports[i]
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function combine_config_and_options(config, config_path, config_rel_path, cli_opts, files)
|
||||
if not config then
|
||||
return cli_opts
|
||||
end
|
||||
|
||||
config_rel_path = config_rel_path or ""
|
||||
|
||||
local function validate(option_set, opts)
|
||||
local ok, invalid_field = options.validate(option_set, opts)
|
||||
|
||||
if not ok then
|
||||
if invalid_field then
|
||||
fatal(("Couldn't load configuration from %s: invalid value of option '%s'"):format(
|
||||
config_path, invalid_field))
|
||||
else
|
||||
fatal(("Couldn't load configuration from %s: validation error"):format(config_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
validate(options.top_config_options, config)
|
||||
local res = {}
|
||||
|
||||
for i, file in ipairs(files) do
|
||||
res[i] = {config}
|
||||
|
||||
if type(config.files) == "table" and type(file) == "string" then
|
||||
file = config_rel_path .. file
|
||||
local overriding_paths = {}
|
||||
|
||||
for path in pairs(config.files) do
|
||||
if file:sub(1, #path) == path then
|
||||
table.insert(overriding_paths, path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Since all paths are prefixes of path, sorting by len is equivalent to regular sorting.
|
||||
table.sort(overriding_paths)
|
||||
|
||||
-- Apply overrides from less specific (shorter prefixes) to more specific (longer prefixes).
|
||||
for _, path in ipairs(overriding_paths) do
|
||||
local overriding_config = config.files[path]
|
||||
validate(options.config_options, overriding_config)
|
||||
table.insert(res[i], overriding_config)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(res[i], cli_opts)
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
local function normalize_filenames(files)
|
||||
for i, file in ipairs(files) do
|
||||
if type(file) ~= "string" then
|
||||
files[i] = "stdin"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local builtin_formatters = utils.array_to_set({"TAP", "JUnit", "plain", "default"})
|
||||
|
||||
local function pformat(report, file_names, args)
|
||||
if builtin_formatters[args.formatter] then
|
||||
return format(report, file_names, args)
|
||||
end
|
||||
|
||||
local formatter = args.formatter
|
||||
local ok, output
|
||||
|
||||
if type(formatter) == "string" then
|
||||
ok, formatter = pcall(require, formatter)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't load custom formatter '%s': %s"):format(args.formatter, formatter))
|
||||
end
|
||||
end
|
||||
|
||||
ok, output = pcall(formatter, report, file_names, args)
|
||||
|
||||
if not ok then
|
||||
fatal(("Couldn't run custom formatter '%s': %s"):format(tostring(args.formatter), output))
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
local parser = get_parser()
|
||||
local args = parser:parse()
|
||||
local opts = get_options(args)
|
||||
local config
|
||||
local config_path
|
||||
local config_rel_path
|
||||
|
||||
if not args.no_config then
|
||||
config, config_path, config_rel_path = get_config(args.config)
|
||||
end
|
||||
|
||||
validate_args(args, parser)
|
||||
combine_config_and_args(config, args)
|
||||
|
||||
local files, bad_rockspecs = expand_files(args.files)
|
||||
local reports = get_reports(args.cache, files, bad_rockspecs, args.jobs)
|
||||
local report = luacheck.process_reports(reports, combine_config_and_options(config, config_path, config_rel_path, opts, files))
|
||||
normalize_filenames(files)
|
||||
|
||||
local output = pformat(report, files, args)
|
||||
|
||||
if #output > 0 and output:sub(-1) ~= "\n" then
|
||||
output = output .. "\n"
|
||||
end
|
||||
|
||||
io.stdout:write(output)
|
||||
|
||||
local exit_code
|
||||
|
||||
if report.errors > 0 then
|
||||
exit_code = 2
|
||||
elseif report.warnings > 0 then
|
||||
exit_code = 1
|
||||
else
|
||||
exit_code = 0
|
||||
end
|
||||
|
||||
os.exit(exit_code)
|
||||
end
|
||||
|
||||
xpcall(main, global_error_handler)
|
Loading…
x
Reference in New Issue
Block a user