1
Jianw
10 天以前 f6f5e6b632d6649386a380558d84003f3de7ec6c
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
local stages = require "luacheck.stages"
local utils = require "luacheck.utils"
 
local format = {}
 
local color_support = not utils.is_windows or os.getenv("ANSICON")
 
local function get_message_format(warning)
   local message_format = assert(stages.warnings[warning.code], "Unkown warning code " .. warning.code).message_format
 
   if type(message_format) == "function" then
      return message_format(warning)
   else
      return message_format
   end
end
 
local function plural(number)
   return (number == 1) and "" or "s"
end
 
local color_codes = {
   reset = 0,
   bright = 1,
   red = 31,
   green = 32
}
 
local function encode_color(c)
   return "\27[" .. tostring(color_codes[c]) .. "m"
end
 
local function colorize(str, ...)
   str = str .. encode_color("reset")
 
   for _, color in ipairs({...}) do
      str = encode_color(color) .. str
   end
 
   return encode_color("reset") .. str
end
 
local function format_color(str, color, ...)
   return color and colorize(str, ...) or str
end
 
local function format_number(number, color)
   return format_color(tostring(number), color, "bright", (number > 0) and "red" or "reset")
end
 
-- Substitutes markers within string format with values from a table.
-- "{field_name}" marker is replaced with `values.field_name`.
-- "{field_name!}" marker adds highlight or quoting depending on color
-- option.
local function substitute(string_format, values, color)
   return (string_format:gsub("{([_a-zA-Z0-9]+)(!?)}", function(field_name, highlight)
      local value = tostring(assert(values[field_name], "No field " .. field_name))
 
      if highlight == "!" then
         if color then
            return colorize(value, "bright")
         else
            return "'" .. value .. "'"
         end
      else
         return value
      end
   end))
end
 
local function format_message(event, color)
   return substitute(get_message_format(event), event, color)
end
 
-- Returns formatted message for an issue, without color.
function format.get_message(event)
   return format_message(event)
end
 
local function capitalize(str)
   return str:gsub("^.", string.upper)
end
 
local function fatal_type(file_report)
   return capitalize(file_report.fatal) .. " error"
end
 
local function count_warnings_errors(events)
   local warnings, errors = 0, 0
 
   for _, event in ipairs(events) do
      if event.code:sub(1, 1) == "0" then
         errors = errors + 1
      else
         warnings = warnings + 1
      end
   end
 
   return warnings, errors
end
 
local function format_file_report_header(report, file_name, opts)
   local label = "Checking " .. file_name
   local status
 
   if report.fatal then
      status = format_color(fatal_type(report), opts.color, "bright")
   elseif #report == 0 then
      status = format_color("OK", opts.color, "bright", "green")
   else
      local warnings, errors = count_warnings_errors(report)
 
      if warnings > 0 then
         status = format_color(tostring(warnings).." warning"..plural(warnings), opts.color, "bright", "red")
      end
 
      if errors > 0 then
         status = status and (status.." / ") or ""
         status = status..(format_color(tostring(errors).." error"..plural(errors), opts.color, "bright"))
      end
   end
 
   return label .. (" "):rep(math.max(50 - #label, 1)) .. status
end
 
local function format_location(file, location, opts)
   local res = ("%s:%d:%d"):format(file, location.line, location.column)
 
   if opts.ranges then
      res = ("%s-%d"):format(res, location.end_column)
   end
 
   return res
end
 
local function event_code(event)
   return (event.code:sub(1, 1) == "0" and "E" or "W")..event.code
end
 
local function format_event(file_name, event, opts)
   local message = format_message(event, opts.color)
 
   if opts.codes then
      message = ("(%s) %s"):format(event_code(event), message)
   end
 
   return format_location(file_name, event, opts) .. ": " .. message
end
 
local function format_file_report(report, file_name, opts)
   local buf = {format_file_report_header(report, file_name, opts)}
 
   if #report > 0 then
      table.insert(buf, "")
 
      for _, event in ipairs(report) do
         table.insert(buf, "    " .. format_event(file_name, event, opts))
      end
 
      table.insert(buf, "")
   elseif report.fatal then
      table.insert(buf, "")
      table.insert(buf, "    " .. file_name .. ": " .. report.msg)
      table.insert(buf, "")
   end
 
   return table.concat(buf, "\n")
end
 
local function escape_xml(str)
   str = str:gsub("&", "&")
   str = str:gsub('"', """)
   str = str:gsub("'", "'")
   str = str:gsub("<", "&lt;")
   str = str:gsub(">", "&gt;")
   return str
end
 
format.builtin_formatters = {}
 
function format.builtin_formatters.default(report, file_names, opts)
   local buf = {}
 
   if opts.quiet <= 2 then
      for i, file_report in ipairs(report) do
         if opts.quiet == 0 or file_report.fatal or #file_report > 0 then
            table.insert(buf, (opts.quiet == 2 and format_file_report_header or format_file_report) (
               file_report, file_names[i], opts))
         end
      end
 
      if #buf > 0 and buf[#buf]:sub(-1) ~= "\n" then
         table.insert(buf, "")
      end
   end
 
   local total = ("Total: %s warning%s / %s error%s in %d file%s"):format(
      format_number(report.warnings, opts.color), plural(report.warnings),
      format_number(report.errors, opts.color), plural(report.errors),
      #report - report.fatals, plural(#report - report.fatals))
 
   if report.fatals > 0 then
      total = total..(", couldn't check %s file%s"):format(
         report.fatals, plural(report.fatals))
   end
 
   table.insert(buf, total)
   return table.concat(buf, "\n")
end
 
function format.builtin_formatters.TAP(report, file_names, opts)
   opts.color = false
   local buf = {}
 
   for i, file_report in ipairs(report) do
      if file_report.fatal then
         table.insert(buf, ("not ok %d %s: %s"):format(#buf + 1, file_names[i], fatal_type(file_report)))
      elseif #file_report == 0 then
         table.insert(buf, ("ok %d %s"):format(#buf + 1, file_names[i]))
      else
         for _, warning in ipairs(file_report) do
            table.insert(buf, ("not ok %d %s"):format(#buf + 1, format_event(file_names[i], warning, opts)))
         end
      end
   end
 
   table.insert(buf, 1, "1.." .. tostring(#buf))
   return table.concat(buf, "\n")
end
 
function format.builtin_formatters.JUnit(report, file_names)
   -- JUnit formatter doesn't support any options.
   local opts = {}
   local buf = {[[<?xml version="1.0" encoding="UTF-8"?>]]}
   local num_testcases = 0
 
   for _, file_report in ipairs(report) do
      if file_report.fatal or #file_report == 0 then
         num_testcases = num_testcases + 1
      else
         num_testcases = num_testcases + #file_report
      end
   end
 
   table.insert(buf, ([[<testsuite name="Luacheck report" tests="%d">]]):format(num_testcases))
 
   for file_i, file_report in ipairs(report) do
      if file_report.fatal then
         table.insert(buf, ([[    <testcase name="%s" classname="%s">]]):format(
            escape_xml(file_names[file_i]), escape_xml(file_names[file_i])))
         table.insert(buf, ([[        <error type="%s"/>]]):format(escape_xml(fatal_type(file_report))))
         table.insert(buf, [[    </testcase>]])
      elseif #file_report == 0 then
         table.insert(buf, ([[    <testcase name="%s" classname="%s"/>]]):format(
            escape_xml(file_names[file_i]), escape_xml(file_names[file_i])))
      else
         for event_i, event in ipairs(file_report) do
            table.insert(buf, ([[    <testcase name="%s:%d" classname="%s">]]):format(
               escape_xml(file_names[file_i]), event_i, escape_xml(file_names[file_i])))
            table.insert(buf, ([[        <failure type="%s" message="%s"/>]]):format(
               escape_xml(event_code(event)), escape_xml(format_event(file_names[file_i], event, opts))))
            table.insert(buf, [[    </testcase>]])
         end
      end
   end
 
   table.insert(buf, [[</testsuite>]])
   return table.concat(buf, "\n")
end
 
local fatal_error_codes = {
   ["I/O"] = "F1",
   ["syntax"] = "F2",
   ["runtime"] = "F3"
}
 
function format.builtin_formatters.visual_studio(report, file_names)
   local buf = {}
 
   for i, file_report in ipairs(report) do
      if file_report.fatal then
         -- Older docs suggest that line number after a file name is optional; newer docs mark it as required.
         -- Just use tool name as origin and put file name into the message.
         table.insert(buf, ("luacheck : fatal error %s: couldn't check %s: %s"):format(
            fatal_error_codes[file_report.fatal], file_names[i], file_report.msg))
      else
         for _, event in ipairs(file_report) do
               -- Older documentation on the format suggests that it could support column range.
               -- Newer docs don't mention it. Don't use it for now.
               local event_type = event.code:sub(1, 1) == "0" and "error" or "warning"
               local message = format_message(event)
               table.insert(buf, ("%s(%d,%d) : %s %s: %s"):format(
                  file_names[i], event.line, event.column, event_type, event_code(event), message))
         end
      end
   end
 
   return table.concat(buf, "\n")
end
 
function format.builtin_formatters.plain(report, file_names, opts)
   opts.color = false
   local buf = {}
 
   for i, file_report in ipairs(report) do
      if file_report.fatal then
         table.insert(buf, ("%s: %s (%s)"):format(file_names[i], fatal_type(file_report), file_report.msg))
      else
         for _, event in ipairs(file_report) do
            table.insert(buf, format_event(file_names[i], event, opts))
         end
      end
   end
 
   return table.concat(buf, "\n")
end
 
--- Formats a report.
-- Recognized options:
--    `options.formatter`: name of used formatter. Default: "default".
--    `options.quiet`: integer in range 0-3. See CLI. Default: 0.
--    `options.color`: should use ansicolors? Default: true.
--    `options.codes`: should output warning codes? Default: false.
--    `options.ranges`: should output token end column? Default: false.
function format.format(report, file_names, options)
   return format.builtin_formatters[options.formatter or "default"](report, file_names, {
      quiet = options.quiet or 0,
      color = (options.color ~= false) and color_support,
      codes = options.codes,
      ranges = options.ranges
   })
end
 
return format