local options = {} local builtin_standards = require "luacheck.builtin_standards" local standards = require "luacheck.standards" local utils = require "luacheck.utils" local boolean = utils.has_type("boolean") local number_or_false = utils.has_type_or_false("number") local array_of_strings = utils.array_of("string") -- Validates std string. -- Returns an array of std names with `add` field if there is `+` at the beginning of the string. -- On validation error returns `nil` and an error message. local function split_std(std, stds) local parts = utils.split(std, "+") if parts[1]:match("^%s*$") then parts.add = true table.remove(parts, 1) end for i, part in ipairs(parts) do parts[i] = utils.strip(part) if not stds[parts[i]] then return nil, ("unknown std '%s'"):format(parts[i]) end end return parts end local function std_or_array_of_strings(x, stds) if type(x) == "string" then local ok, err = split_std(x, stds) return not not ok, err elseif type(x) == "table" then return standards.validate_std_table(x) else return false, "string or table expected, got " .. type(x) end end local function field_map(x) if type(x) == "table" then return standards.validate_globals_table(x) else return false, "table expected, got " .. type(x) end end options.nullary_inline_options = { global = boolean, unused = boolean, redefined = boolean, unused_args = boolean, unused_secondaries = boolean, self = boolean, compat = boolean, allow_defined = boolean, allow_defined_top = boolean, module = boolean } options.variadic_inline_options = { globals = field_map, read_globals = field_map, new_globals = field_map, new_read_globals = field_map, not_globals = array_of_strings, ignore = array_of_strings, enable = array_of_strings, only = array_of_strings } options.all_options = { std = std_or_array_of_strings, max_line_length = number_or_false, max_code_line_length = number_or_false, max_string_line_length = number_or_false, max_comment_line_length = number_or_false, max_cyclomatic_complexity = number_or_false } utils.update(options.all_options, options.nullary_inline_options) utils.update(options.all_options, options.variadic_inline_options) -- Returns true if opts is valid option_set or is nil. -- Otherwise returns false and an error message. function options.validate(option_set, opts, stds) if opts == nil then return true end if type(opts) ~= "table" then return false, "option table expected, got " .. type(opts) end stds = stds or builtin_standards for option, validator in utils.sorted_pairs(option_set) do if opts[option] ~= nil then local ok, err = validator(opts[option], stds) if not ok then return false, ("invalid value of option '%s': %s"):format(option, err) end end end return true end -- Option stack is an array of options with options closer to end -- overriding options closer to beginning. -- Extracts sequence of active std tables from an option stack. local function get_std_tables(opts_stack, stds) local base_std local add_stds = {} local no_compat = false for _, opts in utils.ripairs(opts_stack) do if opts.compat and not no_compat then base_std = stds.max break elseif opts.compat == false then no_compat = true end if opts.std then if type(opts.std) == "table" then base_std = opts.std break else local parts = split_std(opts.std, stds) for _, part in ipairs(parts) do table.insert(add_stds, stds[part]) end if not parts.add then base_std = {} break end end end end table.insert(add_stds, 1, base_std or stds.max) return add_stds end -- Returns index of the last option table in a stack that uses given option, -- or zero if the option isn't used anywhere. local function index_of_last_option_usage(opts_stack, option_name) for index, opts in utils.ripairs(opts_stack) do if opts[option_name] then return index end end return 0 end local function split_field(field_name) return utils.split(field_name, "%.") end local function field_comparator(field1, field2) local parts1 = field1[1] local parts2 = field2[1] for i = 1, math.max(#parts1, #parts2) do local part1 = parts1[i] local part2 = parts2[i] if not part1 then return true elseif not part2 then return false end if part1 ~= part2 then return part1 < part2 end end return false end -- Combine all stds and global related options into one final definition table. -- A definition table may have fields `read_only` (boolean), `other_fields` (boolean), -- and `fields` (maps field names to definition tables). -- Std table format is similar, except at the top level there are two fields -- `globals` and `read_globals` mapping to top-level field tables. Also in field tables -- it's possible to use field names in array part as a shortcut: -- `{fields = {"foo"}}` is equivalent to `{fields = {foo = {}}}` or `{fields = {foo = {other_fields = true}}}` -- in top level fields tables. local function get_final_std(opts_stack, stds) local final_std = {} local std_tables = get_std_tables(opts_stack, stds) for _, std_table in ipairs(std_tables) do standards.add_std_table(final_std, std_table) end local last_new_globals = index_of_last_option_usage(opts_stack, "new_globals") local last_new_read_globals = index_of_last_option_usage(opts_stack, "new_read_globals") for index, opts in ipairs(opts_stack) do local globals = (index >= last_new_globals) and (opts.new_globals or opts.globals) local read_globals = (index >= last_new_read_globals) and (opts.new_read_globals or opts.read_globals) local new_fields = {} if globals then for _, global in ipairs(globals) do table.insert(new_fields, {split_field(global), false}) end end if read_globals then for _, read_global in ipairs(read_globals) do table.insert(new_fields, {split_field(read_global), true}) end end if globals and read_globals then -- If there are both globals and read-only globals defined in one options table, -- it's important that more general definitions are applied first, -- otherwise they will completely overwrite more specific definitions. -- E.g. `globals x` should be applied before `read globals x.y`. table.sort(new_fields, field_comparator) end for _, field in ipairs(new_fields) do standards.overwrite_field(final_std, field[1], field[2]) end standards.add_std_table(final_std, {globals = globals, read_globals = read_globals}, true, true) if opts.not_globals then for _, not_global in ipairs(opts.not_globals) do standards.remove_field(final_std, split_field(not_global)) end end end standards.finalize(final_std) return final_std end local function get_scalar_opt(opts_stack, option, default) for _, opts in utils.ripairs(opts_stack) do if opts[option] ~= nil then return opts[option] end end return default end local line_length_suboptions = {"max_code_line_length", "max_string_line_length", "max_comment_line_length"} local function get_max_line_opts(opts_stack) local res = {max_line_length = 120} for _, opt_name in ipairs(line_length_suboptions) do res[opt_name] = res.max_line_length end for _, opts in ipairs(opts_stack) do if opts.max_line_length ~= nil then res.max_line_length = opts.max_line_length for _, opt_name in ipairs(line_length_suboptions) do res[opt_name] = opts.max_line_length end end for _, opt_name in ipairs(line_length_suboptions) do if opts[opt_name] ~= nil then res[opt_name] = opts[opt_name] end end end return res end local function anchor_pattern(pattern, only_start) if not pattern then return end if pattern:sub(1, 1) == "^" or pattern:sub(-1) == "$" then return pattern else return "^" .. pattern .. (only_start and "" or "$") end end -- Returns {pair of normalized patterns for code and name}. -- `pattern` can be: -- string containing '/': first part matches warning code, second - variable name; -- string containing letters: matches variable name; -- otherwise: matches warning code. -- Unless anchored by user, pattern for name is anchored from both sides -- and pattern for code is only anchored at the beginning. local function normalize_pattern(pattern) local code_pattern, name_pattern local slash_pos = pattern:find("/") if slash_pos then code_pattern = pattern:sub(1, slash_pos - 1) name_pattern = pattern:sub(slash_pos + 1) elseif pattern:find("[_a-zA-Z]") then name_pattern = pattern else code_pattern = pattern end return {anchor_pattern(code_pattern, true), anchor_pattern(name_pattern)} end -- From most specific to less specific, pairs {option, pattern}. -- Applying macros in order is required to get deterministic results -- and get sensible results when intersecting macros are used. -- E.g. unused = false, unused_args = true should leave unused args enabled. local macros = { {"unused_args", "21[23]"}, {"global", "1"}, {"unused", "[23]"}, {"redefined", "4"} } -- Returns array of rules which should be applied in order. -- A rule is a table {{pattern*}, type}. -- `pattern` is a non-normalized pattern. -- `type` can be "enable", "disable" or "only". local function get_rules(opts_stack) local rules = {} local used_macros = {} for _, opts in utils.ripairs(opts_stack) do for _, macro_info in ipairs(macros) do local option, pattern = macro_info[1], macro_info[2] if not used_macros[option] then if opts[option] ~= nil then table.insert(rules, {{pattern}, opts[option] and "enable" or "disable"}) used_macros[option] = true end end end if opts.ignore then table.insert(rules, {opts.ignore, "disable"}) end if opts.only then table.insert(rules, {opts.only, "only"}) end if opts.enable then table.insert(rules, {opts.enable, "enable"}) end end return rules end local function normalize_patterns(rules) local res = {} for i, rule in ipairs(rules) do res[i] = {{}, rule[2]} for j, pattern in ipairs(rule[1]) do res[i][1][j] = normalize_pattern(pattern) end end return res end local scalar_options = { unused_secondaries = true, self = true, module = false, allow_defined = false, allow_defined_top = false, max_cyclomatic_complexity = false } -- Returns normalized options. -- Normalized options have fields: -- std: normalized std table, see `luacheck.standards` module; -- unused_secondaries, self, module, allow_defined, allow_defined_top: booleans; -- max_line_length: number or false; -- rules: see get_rules. function options.normalize(opts_stack, stds) local res = {} stds = stds or builtin_standards res.std = get_final_std(opts_stack, stds) for option, default in pairs(scalar_options) do res[option] = get_scalar_opt(opts_stack, option, default) end local max_line_opts = get_max_line_opts(opts_stack) utils.update(res, max_line_opts) res.rules = normalize_patterns(get_rules(opts_stack)) return res end return options