local standards = {} -- A standard (aka std) defines set of allowed globals, their fields, -- and whether they are mutable. -- -- A standard can be in several formats. Internal (normalized) format -- is a tree. Each node defines a global or its field. Each node may have -- boolean `read_only` and `other_fields`, and may contain definitions -- of nested fields in `fields` subtable, which maps field names -- to definition tables. For example, standard defining globals -- of some Lua version may start like this: -- { -- -- Most globals are read-only by default. -- read_only = true, -- fields = { -- -- The tree can't be recursive, just allow everything for `_G`. -- _G = {other_fields = true, read_only = false}, -- package = { -- fields = { -- -- `other_fields` is false by default, so that an empty table -- -- defines a field that can't be indexed further (a function in this case). -- loadlib = {}, -- -- Allow doing everything with `package.loaded`. -- loaded = {other_fields = true, read_only = false}, -- -- More fields here... -- } -- }, -- -- More globals here... -- } -- } -- -- A similar format is used to define standards in table form -- in config. There are two differences: -- first, top level table can have two fields, `globals` and `read_globals`, -- that map global names to definition tables. Default value of `read_only` field -- for the these tables depends on which table they come from (`true` for `read_globals` -- and `false` for `globals`). Additionally, all tables that map field or global names -- to definition tables may have non-string keys, their associated values are interpreted -- as names instead and their definition table allows indexing with any keys indefinitely. -- E.g. `{fields = {"foo"}}` is equivalent to `{fields = {foo = {other_fields = true}}}`. -- This feature makes it easier to create less strict standards that do not care about fields, -- to ease migration from the old format. -- -- Additionally, there are some predefined named standards in `luacheck.builtin_standards` module. -- In config and inline options its possible to use their names as strings to refer to them. -- Validates an optional table mapping field names to field definitions or non-string keys to names. -- `index` is an optional string specifying position of the field table in the root table. -- Returns a true if the table is valid, false, an error message, and index of the table with the error otherwise. local function validate_fields(fields, is_root, index) if fields == nil then return true end local field_type = is_root and "global" or "field" if type(fields) ~= "table" then return false, ("%ss table expected, got %s"):format(field_type, type(fields)), index end for key, value in pairs(fields) do if type(key) == "string" then local new_index = (index or "") .. "." .. key if type(value) ~= "table" then return false, ("%s description table expected, got %s"):format(field_type, type(value)), new_index end if value.read_only ~= nil and type(value.read_only) ~= "boolean" then local err = "invalid value of option 'read_only': boolean expected, got " .. type(value.read_only) return false, err, new_index end if value.other_fields ~= nil and type(value.other_fields) ~= "boolean" then local err = "invalid value of option 'other_fields': boolean expected, got " .. type(value.other_fields) return false, err, new_index end local ok, err, err_index = validate_fields(value.fields, false, new_index .. ".fields") if not ok then return false, err, err_index end elseif type(value) ~= "string" then local key_as_string = type(key) == "number" and ("%.20g"):format(key) or ("<%s>"):format(type(key)) local new_index = ("%s[%s]"):format(index or "", key_as_string) return false, ("string expected as %s name, got %s"):format(field_type, type(value)), new_index end end return true end -- Validates a field table. -- Returns true if the table is valid, false and an error message otherwise. function standards.validate_globals_table(globals_table) local ok, err, err_index = validate_fields(globals_table, true) if ok then return true end local err_prefix = err_index and ("in field %s: "):format(err_index) or "" return false, err_prefix .. err end -- Validates an std table in user-side format. -- Returns true if the table is valid, false and an error message otherwise. function standards.validate_std_table(std_table) local ok, err, err_index = validate_fields(std_table.globals, true, ".globals") if ok then ok, err, err_index = validate_fields(std_table.read_globals, true, ".read_globals") end if ok then return true end local err_prefix = ("in field %s: "):format(err_index) return false, err_prefix .. err end local infinitely_indexable_def = {other_fields = true} local function add_fields(def, fields, overwrite, ignore_array_part, default_read_only) if not fields then return end for field_name, field_def in pairs(fields) do if type(field_name) == "string" or not ignore_array_part then if type(field_name) ~= "string" then field_name = field_def field_def = infinitely_indexable_def end if not def.fields then def.fields = {} end if not def.fields[field_name] then def.fields[field_name] = {} end local existing_field_def = def.fields[field_name] local new_read_only = field_def.read_only if new_read_only == nil then new_read_only = default_read_only end if new_read_only ~= nil then if overwrite or new_read_only == false then existing_field_def.read_only = new_read_only end end if field_def.other_fields ~= nil then if overwrite or field_def.other_fields == true then existing_field_def.other_fields = field_def.other_fields end end add_fields(existing_field_def, field_def.fields, overwrite, false, nil) end end end -- Merges in an std table in user-side format. -- By default the new state of normalized std is a union of the standard tables being merged, -- e.g. if either table allows some field to be mutated, result should allow it, too. -- If `overwrite` is truthy, read-only statuses from the new std table overwrite existing values. -- If `ignore_top_array_part` is truthy, non-string keys in `globals` and `read_globals` tables -- in `std_table` are not processed. function standards.add_std_table(final_std, std_table, overwrite, ignore_top_array_part) add_fields(final_std, std_table.globals, overwrite, ignore_top_array_part, false) add_fields(final_std, std_table.read_globals, overwrite, ignore_top_array_part, true) end -- Overwrites or adds definition of a field with given read-only status and any nested keys. -- Field is specified as an array of field names. function standards.overwrite_field(final_std, field_names, read_only) local field_def = final_std for _, field_name in ipairs(field_names) do if not field_def.fields then field_def.fields = {} end if not field_def.fields[field_name] then field_def.fields[field_name] = {read_only = read_only} end field_def = field_def.fields[field_name] end for key in pairs(field_def) do field_def[key] = nil end field_def.read_only = read_only field_def.other_fields = true end -- Removes definition of a field from a normalized std table. -- Field is specified as an array of field names. function standards.remove_field(final_std, field_names) local field_def = final_std local parent_def for _, field_name in ipairs(field_names) do parent_def = field_def if not field_def.fields or not field_def.fields[field_name] then -- The field wasn't defined in the first place. return end field_def = field_def.fields[field_name] end if parent_def then parent_def.fields[field_names[#field_names]] = nil end end local function infer_deep_read_only_statuses(def, read_only) local deep_read_only = not def.other_fields or read_only if def.fields then for _, field_def in pairs(def.fields) do local field_read_only = read_only if field_def.read_only ~= nil then field_read_only = field_def.read_only end infer_deep_read_only_statuses(field_def, field_read_only) deep_read_only = deep_read_only and field_read_only and field_def.deep_read_only end end if deep_read_only then def.deep_read_only = true end end -- Finishes building a normalized std tables. -- Adds `deep_read_only` fields with `true` value to definition tables -- that do not have any writable fields, recursively. function standards.finalize(final_std) infer_deep_read_only_statuses(final_std, true) end local empty = {} -- Returns a definition table containing empty fields with given names. function standards.def_fields(...) local fields = {} for _, field in ipairs({...}) do fields[field] = empty end return {fields = fields} end return standards