local cache = require "luacheck.cache"
local options = require "luacheck.options"
local builtin_standards = require "luacheck.builtin_standards"
local fs = require "luacheck.fs"
local globbing = require "luacheck.globbing"
local standards = require "luacheck.standards"
local utils = require "luacheck.utils"
local luacheck = require "luacheck"
local config = {}
local function get_global_config_dir()
if utils.is_windows then
local local_app_data_dir = os.getenv("LOCALAPPDATA")
if not local_app_data_dir then
local user_profile_dir = os.getenv("USERPROFILE")
if user_profile_dir then
local_app_data_dir = fs.join(user_profile_dir, "Local Settings", "Application Data")
end
end
if local_app_data_dir then
return fs.join(local_app_data_dir, "Luacheck")
end
else
local fh = assert(io.popen("uname -s"))
local system = fh:read("*l")
fh:close()
if system == "Darwin" then
local home_dir = os.getenv("HOME")
if home_dir then
return fs.join(home_dir, "Library", "Application Support", "Luacheck")
end
else
local config_home_dir = os.getenv("XDG_CONFIG_HOME")
if not config_home_dir then
local home_dir = os.getenv("HOME")
if home_dir then
config_home_dir = fs.join(home_dir, ".config")
end
end
if config_home_dir then
return fs.join(config_home_dir, "luacheck")
end
end
end
end
config.default_path = ".luacheckrc"
function config.get_default_global_path()
local global_config_dir = get_global_config_dir()
if global_config_dir then
return fs.join(global_config_dir, config.default_path)
end
end
local function locate_config(path, global_path)
if path == false then
return
end
local is_default_path = not path
path = path or config.default_path
if fs.is_absolute(path) then
return path
end
local current_dir = fs.get_current_dir()
local anchor_dir, rel_dir = fs.find_file(current_dir, path)
if anchor_dir then
return fs.join(rel_dir, path), anchor_dir
end
if not is_default_path then
return nil, ("Couldn't find configuration file %s"):format(path)
end
if global_path == false then
return
end
global_path = global_path or config.get_default_global_path()
if global_path and fs.is_file(global_path) then
return global_path, (fs.split_base(global_path))
end
end
local function try_load(path)
local src = utils.read_file(path)
if not src then
return
end
local func, err = utils.load(src, nil, "@"..path)
return err or func
end
local function add_relative_loader(anchor_dir)
if not anchor_dir then
return
end
local function loader(modname)
local modpath = fs.join(anchor_dir, (modname:gsub("%.", utils.dir_sep)))
return try_load(modpath..".lua") or try_load(modpath..utils.dir_sep.."init.lua"), modname
end
table.insert(package.loaders or package.searchers, 1, loader) return loader
end
local function remove_relative_loader(loader)
if not loader then
return
end
for i, func in ipairs(package.loaders or package.searchers) do if func == loader then
table.remove(package.loaders or package.searchers, i) return
end
end
end
function config.relative_require(anchor_dir, modname)
local loader = add_relative_loader(anchor_dir)
local ok, mod_or_err = pcall(require, modname)
remove_relative_loader(loader)
return ok, mod_or_err
end
local special_mts = {
stds = {__index = builtin_standards},
files = {__index = function(files, key)
files[key] = {}
return files[key]
end}
}
local function make_config_env_mt()
local env_mt = {}
local special_values = {}
for key, mt in pairs(special_mts) do
special_values[key] = setmetatable({}, mt)
end
function env_mt.__index(_, key)
if special_mts[key] then
return special_values[key]
else
return _G[key]
end
end
function env_mt.__newindex(env, key, value)
if special_mts[key] then
if type(value) == "table" then
setmetatable(value, special_mts[key])
end
special_values[key] = value
else
rawset(env, key, value)
end
end
return env_mt, special_values
end
local function make_config_env()
local mt, special_values = make_config_env_mt()
return setmetatable({}, mt), special_values
end
local function remove_env_mt(env, special_values)
setmetatable(env, nil)
utils.update(env, special_values)
end
local function set_default_std(files, pattern, std)
local pattern_opts = {std = std}
if files[pattern] then
pattern_opts = utils.update(pattern_opts, files[pattern])
end
files[pattern] = pattern_opts
end
local function add_default_path_options(opts)
local files = {}
if opts.files then
files = utils.update(files, opts.files)
end
opts.files = files
set_default_std(files, "**/spec/**/*_spec.lua", "+busted")
set_default_std(files, "**/test/**/*_spec.lua", "+busted")
set_default_std(files, "**/tests/**/*_spec.lua", "+busted")
set_default_std(files, "**/*.rockspec", "+rockspec")
set_default_std(files, "**/*.luacheckrc", "+luacheckrc")
set_default_std(files, "**/config.ld", "+ldoc")
end
local fallback_config = {options = {}, anchor_dir = ""}
add_default_path_options(fallback_config.options)
function config.load_config(path, global_path)
local config_path, anchor_dir = locate_config(path, global_path)
if not config_path then
if anchor_dir then
return nil, anchor_dir
else
return fallback_config
end
end
local env, special_values = make_config_env()
local loader = add_relative_loader(anchor_dir)
local load_ok, ret, load_err = utils.load_config(config_path, env)
remove_relative_loader(loader)
if not load_ok then
return nil, ("Couldn't load configuration from %s: %s error (%s)"):format(config_path, ret, load_err)
end
if type(ret) == "table" then
utils.update(env, ret)
end
remove_env_mt(env, special_values)
add_default_path_options(env)
return {options = env, config_path = config_path, anchor_dir = anchor_dir}
end
function config.table_to_config(opts)
return {options = opts}
end
local function add_stds_from_config(conf, stds)
if conf.options.stds ~= nil then
if type(conf.options.stds) ~= "table" then
return nil, ("invalid option 'stds': table expected, got %s"):format(type(conf.options.stds))
end
local std_names = {}
for std_name in pairs(conf.options.stds) do
if type(std_name) == "string" then
table.insert(std_names, std_name)
end
end
table.sort(std_names)
for _, std_name in ipairs(std_names) do
local std = conf.options.stds[std_name]
if type(std) ~= "table" then
return nil, ("invalid custom std '%s': table expected, got %s"):format(std_name, type(std))
end
local ok, err = standards.validate_std_table(std)
if not ok then
return nil, ("invalid custom std '%s': %s"):format(std_name, err)
end
stds[std_name] = std
end
end
return true
end
local function error_prefix(conf)
if conf.config_path then
return ("in config loaded from %s: "):format(conf.config_path)
else
return ""
end
end
local function quiet_validator(x)
if type(x) == "number" then
if math.floor(x) == x and x >= 0 and x <= 3 then
return true
else
return false, ("integer in range 0..3 expected, got %.20g"):format(x)
end
else
return false, ("integer in range 0..3 expected, got %s"):format(type(x))
end
end
local function jobs_validator(x)
if type(x) == "number" then
if math.floor(x) == x and x >= 1 then
return true
else
return false, ("positive integer expected, got %.20g"):format(x)
end
else
return false, ("positive integer expected, got %s"):format(type(x))
end
end
config.format_options = {
quiet = quiet_validator,
color = utils.has_type("boolean"),
codes = utils.has_type("boolean"),
ranges = utils.has_type("boolean"),
formatter = utils.has_either_type("string", "function")
}
local top_options = {
cache = utils.has_either_type("string", "boolean"),
jobs = jobs_validator,
files = utils.has_type("table"),
stds = utils.has_type("table"),
exclude_files = utils.array_of("string"),
include_files = utils.array_of("string")
}
utils.update(top_options, config.format_options)
utils.update(top_options, options.all_options)
local function validate_config(conf, stds)
local ok, err = options.validate(top_options, conf.options, stds)
if not ok then
return nil, err
end
if conf.options.files then
for path, opts in pairs(conf.options.files) do
if type(path) == "string" then
ok, err = options.validate(options.all_options, opts, stds)
if not ok then
return nil, ("invalid options for path '%s': %s"):format(path, err)
end
end
end
end
return true
end
local ConfigStack = utils.class()
function ConfigStack:__init(configs, stds)
self._configs = configs
self._stds = stds
end
function ConfigStack:get_stds()
return self._stds
end
function config.stack_configs(configs)
local stds = utils.update({}, builtin_standards)
for _, conf in ipairs(configs) do
local ok, err = add_stds_from_config(conf, stds)
if not ok then
return nil, error_prefix(conf) .. err
end
end
for _, conf in ipairs(configs) do
local ok, err = validate_config(conf, stds)
if not ok then
return nil, error_prefix(conf) .. err
end
end
return ConfigStack(configs, stds)
end
function ConfigStack:get_top_options()
local res = {
quiet = 0,
color = true,
codes = false,
ranges = false,
formatter = "default",
cache = false,
jobs = false,
include_files = {},
exclude_files = {}
}
local current_dir = fs.get_current_dir()
local last_anchor_dir
for _, conf in ipairs(self._configs) do
for _, option in ipairs({"quiet", "color", "codes", "ranges", "jobs"}) do
if conf.options[option] ~= nil then
res[option] = conf.options[option]
end
end
last_anchor_dir = conf.anchor_dir or last_anchor_dir
if conf.options.formatter ~= nil then
res.formatter = conf.options.formatter
res.formatter_anchor_dir = last_anchor_dir
end
local anchor_dir = conf.anchor_dir or current_dir
for _, option in ipairs({"include_files", "exclude_files"}) do
if conf.options[option] ~= nil then
for _, glob in ipairs(conf.options[option]) do
table.insert(res[option], fs.normalize(fs.join(anchor_dir, glob)))
end
end
end
if conf.options.cache ~= nil then
if conf.options.cache == true then
if not res.cache then
res.cache = fs.normalize(fs.join(
last_anchor_dir or current_dir,
cache.get_default_dir(),
luacheck._VERSION
))
end
elseif conf.options.cache == false then
res.cache = false
else
res.cache = fs.normalize(fs.join(anchor_dir, conf.options.cache, luacheck._VERSION))
end
end
end
return res
end
local function add_applying_overrides(option_stack, conf, filename)
if not filename or not conf.options.files then
return
end
local current_dir = fs.get_current_dir()
local abs_filename = fs.normalize(fs.join(current_dir, filename))
local anchor_dir
if conf.anchor_dir == "" then
anchor_dir = fs.split_base(current_dir)
else
anchor_dir = conf.anchor_dir or current_dir
end
local matching_pairs = {}
for glob, opts in pairs(conf.options.files) do
if type(glob) == "string" then
local abs_glob = fs.normalize(fs.join(anchor_dir, glob))
if globbing.match(abs_glob, abs_filename) then
table.insert(matching_pairs, {
abs_glob = abs_glob,
opts = opts
})
end
end
end
table.sort(matching_pairs, function(pair1, pair2)
return globbing.compare(pair1.abs_glob, pair2.abs_glob)
end)
for _, pair in ipairs(matching_pairs) do
table.insert(option_stack, pair.opts)
end
end
function ConfigStack:get_options(filename)
local res = {}
for _, conf in ipairs(self._configs) do
table.insert(res, conf.options)
add_applying_overrides(res, conf, filename)
end
return res
end
return config