Jianw
2025-05-13 3b39fe3810c3ee2ec9ec97236c1769c5c85e062c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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