summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/expltools/explcheck-format.lua
blob: 5b64ab51b90437062f19d13c1fd77233a1b89cd5 (plain)
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
-- Formatting for the command-line interface of the static analyzer explcheck.

-- Transform a singular into plural if the count is zero or greater than two.
local function pluralize(singular, count)
  if count == 1 then
    return singular
  else
    return singular .. "s"
  end
end

-- Shorten a pathname, so that it does not exceed maximum length.
local function format_pathname(pathname, max_length)
  -- First, replace path segments with `/.../`, keeping other segments.
  local first_iteration = true
  while #pathname > max_length do
    local pattern
    if first_iteration then
      pattern = "([^/]*)/[^/]*/(.*)"
    else
      pattern = "([^/]*)/%.%.%./[^/]*/(.*)"
    end
    local prefix_start, _, prefix, suffix = pathname:find(pattern)
    if prefix_start == nil or prefix_start > 1 then
      break
    end
    pathname = prefix .. "/.../" .. suffix
    first_iteration = false
  end
  -- If this isn't enough, remove the initial path segment and prefix the filename with `...`.
  if #pathname > max_length then
    local pattern
    if first_iteration then
      pattern = "([^/]*/?)(.*)"
    else
      pattern = "([^/]*/%.%.%./)(.*)"
    end
    local prefix_start, _, _, suffix = pathname:find(pattern)
    if prefix_start == 1 then
      pathname = ".../" .. suffix
      if #pathname > max_length then
        pathname = "..." .. suffix:sub(-(max_length - #("...")))
      end
    end
  end
  return pathname
end

-- Colorize a string using ASCII color codes.
local function colorize(text, ...)
  local buffer = {}
  for _, color_code in ipairs({...}) do
    table.insert(buffer, "\27[")
    table.insert(buffer, tostring(color_code))
    table.insert(buffer, "m")
  end
  table.insert(buffer, text)
  table.insert(buffer, "\27[0m")
  return table.concat(buffer, "")
end

-- Remove ASCII color codes from a string.
local function decolorize(text)
  return text:gsub("\27%[[0-9]+m", "")
end

-- Convert a byte number in a file to a line and column number in a file.
local function convert_byte_to_line_and_column(line_starting_byte_numbers, byte_number)
  local line_number = 0
  for _, line_starting_byte_number in ipairs(line_starting_byte_numbers) do
    if line_starting_byte_number > byte_number then
      break
    end
    line_number = line_number + 1
  end
  assert(line_number > 0)
  local line_starting_byte_number = line_starting_byte_numbers[line_number]
  assert(line_starting_byte_number <= byte_number)
  local column_number = byte_number - line_starting_byte_number + 1
  return line_number, column_number
end

-- Print the results of analyzing a file.
local function print_results(pathname, issues, line_starting_byte_numbers, is_last_file)
  -- Display an overview.
  local all_issues = {}
  local status
  if(#issues.errors > 0) then
    status = (
      colorize(
        (
          tostring(#issues.errors)
          .. " "
          .. pluralize("error", #issues.errors)
        ), 1, 31
      )
    )
    table.insert(all_issues, issues.errors)
    if(#issues.warnings > 0) then
      status = (
        status
        .. ", "
        .. colorize(
          (
            tostring(#issues.warnings)
            .. " "
            .. pluralize("warning", #issues.warnings)
          ), 1, 33
        )
      )
      table.insert(all_issues, issues.warnings)
    end
  elseif(#issues.warnings > 0) then
    status = colorize(
      (
        tostring(#issues.warnings)
        .. " "
        .. pluralize("warning", #issues.warnings)
      ), 1, 33
    )
    table.insert(all_issues, issues.warnings)
  else
    status = colorize("OK", 1, 32)
  end

  local max_overview_length = 72
  local prefix = "Checking "
  local formatted_pathname = format_pathname(
    pathname,
    math.max(
      (
        max_overview_length
        - #prefix
        - #(" ")
        - #decolorize(status)
      ), 1
    )
  )
  local overview = (
    prefix
    .. formatted_pathname
    .. (" "):rep(
      math.max(
        (
          max_overview_length
          - #prefix
          - #decolorize(status)
          - #formatted_pathname
        ), 1
      )
    )
    .. status
  )
  io.write("\n" .. overview)

  -- Display the errors, followed by warnings.
  if #all_issues > 0 then
    for _, warnings_or_errors in ipairs(all_issues) do
      print()
      -- Display the warnings/errors.
      for _, issue in ipairs(issues.sort(warnings_or_errors)) do
        local code = issue[1]
        local message = issue[2]
        local range = issue[3]
        local position = ":"
        if range ~= nil then
          local line_number, column_number = convert_byte_to_line_and_column(line_starting_byte_numbers, range[1])
          position = position .. tostring(line_number) .. ":" .. tostring(column_number) .. ":"
        end
        local max_line_length = 88
        local reserved_position_length = 10
        local reserved_suffix_length = 30
        local label_indent = (" "):rep(4)
        local suffix = code:upper() .. " " .. message
        formatted_pathname = format_pathname(
          pathname,
          math.max(
            (
              max_line_length
              - #label_indent
              - reserved_position_length
              - #(" ")
              - math.max(#suffix, reserved_suffix_length)
            ), 1
          )
        )
        local line = (
          label_indent
          .. formatted_pathname
          .. position
          .. (" "):rep(
            math.max(
              (
                max_line_length
                - #label_indent
                - #formatted_pathname
                - #decolorize(position)
                - math.max(#suffix, reserved_suffix_length)
              ), 1
            )
          )
          .. suffix
          .. (" "):rep(math.max(reserved_suffix_length - #suffix, 0))
        )
        io.write("\n" .. line)
      end
    end
    if(not is_last_file) then
      print()
    end
  end
end

-- Print the summary results of analyzing multiple files.
local function print_summary(num_pathnames, num_warnings, num_errors)
  io.write("\n\nTotal: ")

  local errors_message = tostring(num_errors) .. " " .. pluralize("error", num_errors)
  errors_message = colorize(errors_message, 1, (num_errors > 0 and 31) or 32)
  io.write(errors_message .. ", ")

  local warnings_message = tostring(num_warnings) .. " " .. pluralize("warning", num_warnings)
  warnings_message = colorize(warnings_message, 1, (num_warnings > 0 and 33) or 32)
  io.write(warnings_message .. " in ")

  print(tostring(num_pathnames) .. " " .. pluralize("file", num_pathnames))
end

return {
  convert_byte_to_line_and_column = convert_byte_to_line_and_column,
  print_results = print_results,
  print_summary = print_summary,
}