Home | History | Annotate | Line # | Download | only in lint1
check-expect.lua revision 1.10
      1 #!  /usr/bin/lua
      2 -- $NetBSD: check-expect.lua,v 1.10 2024/01/28 08:17:27 rillig Exp $
      3 
      4 --[[
      5 
      6 usage: lua ./check-expect.lua [-u] *.c
      7 
      8 Check that the /* expect+-n: ... */ comments in the .c source files match the
      9 actual messages found in the corresponding .exp files.  The .exp files are
     10 expected in the current working directory.
     11 
     12 The .exp files are generated on the fly during the ATF tests, see
     13 t_integration.sh.  During development, they can be generated using
     14 lint1/accept.sh.
     15 ]]
     16 
     17 
     18 local function test(func)
     19   func()
     20 end
     21 
     22 local function assert_equals(got, expected)
     23   if got ~= expected then
     24     assert(false, string.format("got %q, expected %q", got, expected))
     25   end
     26 end
     27 
     28 
     29 local had_errors = false
     30 ---@param fmt string
     31 function print_error(fmt, ...)
     32   print(fmt:format(...))
     33   had_errors = true
     34 end
     35 
     36 
     37 local function load_lines(fname)
     38   local lines = {}
     39 
     40   local f, err, errno = io.open(fname, "r")
     41   if f == nil then return nil, err, errno end
     42 
     43   for line in f:lines() do
     44     table.insert(lines, line)
     45   end
     46   f:close()
     47 
     48   return lines
     49 end
     50 
     51 
     52 local function save_lines(fname, lines)
     53   local f = io.open(fname, "w")
     54   for _, line in ipairs(lines) do
     55     f:write(line .. "\n")
     56   end
     57   f:close()
     58 end
     59 
     60 
     61 -- Load the 'expect:' comments from a C source file.
     62 --
     63 -- example return values:
     64 --   {
     65 --     ["file.c(18)"] = {"syntax error 'a' [249]", "syntax error 'b' [249]"},
     66 --     ["file.c(23)"] = {"not a constant expression [123]"},
     67 --   },
     68 --   { "file.c(18)", "file.c(23)" }
     69 local function load_c(fname)
     70   local basename = fname:match("([^/]+)$")
     71   local lines = load_lines(fname)
     72   if lines == nil then return nil, nil end
     73 
     74   local pp_fname = fname
     75   local pp_lineno = 0
     76   local comment_locations = {}
     77   local comments_by_location = {}
     78 
     79   local function add_expectation(offset, message)
     80     local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
     81     if comments_by_location[location] == nil then
     82       table.insert(comment_locations, location)
     83       comments_by_location[location] = {}
     84     end
     85     local trimmed_msg = message:match("^%s*(.-)%s*$")
     86     table.insert(comments_by_location[location], trimmed_msg)
     87   end
     88 
     89   for phys_lineno, line in ipairs(lines) do
     90 
     91     for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
     92       add_expectation(tonumber(offset), comment)
     93     end
     94 
     95     pp_lineno = pp_lineno + 1
     96 
     97     local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
     98     if ppl_lineno ~= nil then
     99       if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
    100         print_error("error: %s:%d: preprocessor line number must be %d",
    101           fname, phys_lineno, phys_lineno + 1)
    102       end
    103       if ppl_fname:match("%.c$") and ppl_fname ~= basename then
    104         print_error("error: %s:%d: preprocessor filename must be '%s'",
    105           fname, phys_lineno, basename)
    106       end
    107       pp_fname = ppl_fname
    108       pp_lineno = ppl_lineno
    109     end
    110   end
    111 
    112   return comment_locations, comments_by_location
    113 end
    114 
    115 
    116 -- Load the expected raw lint output from a .exp file.
    117 --
    118 -- example return value: {
    119 --   {
    120 --     exp_lineno = 18,
    121 --     location = "file.c(18)",
    122 --     message = "not a constant expression [123]",
    123 --   }
    124 -- }
    125 local function load_exp(exp_fname)
    126 
    127   local lines = load_lines(exp_fname)
    128   if lines == nil then
    129     print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
    130     return
    131   end
    132 
    133   local messages = {}
    134   for exp_lineno, line in ipairs(lines) do
    135     for location, message in line:gmatch("(.+%(%d+%)): (.+)$") do
    136       table.insert(messages, {
    137         exp_lineno = exp_lineno,
    138         location = location,
    139         message = message
    140       })
    141     end
    142   end
    143 
    144   return messages
    145 end
    146 
    147 
    148 local function matches(comment, pattern)
    149   if comment == "" then return false end
    150 
    151   local any_prefix = pattern:sub(1, 3) == "..."
    152   if any_prefix then pattern = pattern:sub(4) end
    153   local any_suffix = pattern:sub(-3) == "..."
    154   if any_suffix then pattern = pattern:sub(1, -4) end
    155 
    156   if any_prefix and any_suffix then
    157     return comment:find(pattern, 1, true) ~= nil
    158   elseif any_prefix then
    159     return pattern ~= "" and comment:sub(-#pattern) == pattern
    160   elseif any_suffix then
    161     return comment:sub(1, #pattern) == pattern
    162   else
    163     return comment == pattern
    164   end
    165 end
    166 
    167 test(function()
    168   assert_equals(matches("a", "a"), true)
    169   assert_equals(matches("a", "b"), false)
    170   assert_equals(matches("a", "aaa"), false)
    171 
    172   assert_equals(matches("abc", "a..."), true)
    173   assert_equals(matches("abc", "c..."), false)
    174 
    175   assert_equals(matches("abc", "...c"), true)
    176   assert_equals(matches("abc", "...a"), false)
    177 
    178   assert_equals(matches("abc123xyz", "...a..."), true)
    179   assert_equals(matches("abc123xyz", "...b..."), true)
    180   assert_equals(matches("abc123xyz", "...c..."), true)
    181   assert_equals(matches("abc123xyz", "...1..."), true)
    182   assert_equals(matches("abc123xyz", "...2..."), true)
    183   assert_equals(matches("abc123xyz", "...3..."), true)
    184   assert_equals(matches("abc123xyz", "...x..."), true)
    185   assert_equals(matches("abc123xyz", "...y..."), true)
    186   assert_equals(matches("abc123xyz", "...z..."), true)
    187   assert_equals(matches("pattern", "...pattern..."), true)
    188   assert_equals(matches("pattern", "... pattern ..."), false)
    189 end)
    190 
    191 
    192 -- Inserts the '/* expect */' lines to the .c file, so that the .c file
    193 -- matches the .exp file.
    194 --
    195 -- TODO: Fix crashes in tests with '# line file' preprocessing directives.
    196 local function insert_missing(missing)
    197   for fname, items in pairs(missing) do
    198     for i, item in ipairs(items) do
    199       item.stable_sort_rank = i
    200     end
    201     local function less(a, b)
    202       if a.lineno ~= b.lineno then
    203         return a.lineno > b.lineno
    204       end
    205       return a.stable_sort_rank > b.stable_sort_rank
    206     end
    207     table.sort(items, less)
    208     local lines = assert(load_lines(fname))
    209     local seen = {}
    210     for _, item in ipairs(items) do
    211       local lineno, message = item.lineno, item.message
    212       local indent = (lines[lineno] or ""):match("^([ \t]*)")
    213       local offset = 1 + (seen[lineno] or 0)
    214       local line = ("%s/* expect+%d: %s */"):format(indent, offset, message)
    215       print(("insert %s:%d %s"):format(fname, lineno, line))
    216       table.insert(lines, lineno, line)
    217       seen[lineno] = (seen[lineno] or 0) + 1
    218     end
    219     save_lines(fname, lines)
    220   end
    221 end
    222 
    223 
    224 local function check_test(c_fname, update)
    225   local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
    226 
    227   local c_comment_locations, c_comments_by_location = load_c(c_fname)
    228   if c_comment_locations == nil then return end
    229 
    230   local exp_messages = load_exp(exp_fname) or {}
    231   local missing = {}
    232 
    233   for _, exp_message in ipairs(exp_messages) do
    234     local c_comments = c_comments_by_location[exp_message.location] or {}
    235     local expected_message =
    236       exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
    237 
    238     local found = false
    239     for i, c_comment in ipairs(c_comments) do
    240       if c_comment ~= "" and matches(expected_message, c_comment) then
    241         c_comments[i] = ""
    242         found = true
    243         break
    244       end
    245     end
    246 
    247     if not found then
    248       print_error("error: %s: missing /* expect+1: %s */",
    249         exp_message.location, expected_message)
    250 
    251       if update then
    252         local fname = exp_message.location:match("^([^(]+)")
    253         local lineno = tonumber(exp_message.location:match("%((%d+)%)$"))
    254         if not missing[fname] then missing[fname] = {} end
    255         table.insert(missing[fname], {
    256           lineno = lineno,
    257           message = expected_message,
    258         })
    259       end
    260     end
    261   end
    262 
    263   for _, c_comment_location in ipairs(c_comment_locations) do
    264     for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
    265       if c_comment ~= "" then
    266         print_error(
    267           "error: %s: declared message \"%s\" is not in the actual output",
    268           c_comment_location, c_comment)
    269       end
    270     end
    271   end
    272 
    273   if missing then
    274     insert_missing(missing)
    275   end
    276 end
    277 
    278 
    279 local function main(args)
    280   local update = args[1] == "-u"
    281   if update then
    282     table.remove(args, 1)
    283   end
    284 
    285   for _, name in ipairs(args) do
    286     check_test(name, update)
    287   end
    288 end
    289 
    290 
    291 main(arg)
    292 os.exit(not had_errors)
    293