Home | History | Annotate | Line # | Download | only in lint1
check-expect.lua revision 1.5
      1 #!  /usr/bin/lua
      2 -- $NetBSD: check-expect.lua,v 1.5 2023/07/06 07:33:36 rillig Exp $
      3 
      4 --[[
      5 
      6 usage: lua ./check-expect.lua *.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 = io.open(fname, "r")
     41   if f == nil then return nil 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 -- Load the 'expect:' comments from a C source file.
     53 --
     54 -- example return values:
     55 --   {
     56 --     ["file.c(18)"] = {"invalid argument 'a'", "invalid argument 'b'"},
     57 --     ["file.c(23)"] = {"not a constant expression [123]"},
     58 --   },
     59 --   { "file.c(18)", "file.c(23)" }
     60 local function load_c(fname)
     61   local basename = fname:match("([^/]+)$")
     62   local lines = load_lines(fname)
     63   if lines == nil then return nil, nil end
     64 
     65   local pp_fname = fname
     66   local pp_lineno = 0
     67   local comment_locations = {}
     68   local comments_by_location = {}
     69 
     70   local function add_expectation(offset, message)
     71     local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
     72     if comments_by_location[location] == nil then
     73       table.insert(comment_locations, location)
     74       comments_by_location[location] = {}
     75     end
     76     local trimmed_msg = message:match("^%s*(.-)%s*$")
     77     table.insert(comments_by_location[location], trimmed_msg)
     78   end
     79 
     80   for phys_lineno, line in ipairs(lines) do
     81 
     82     for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
     83       add_expectation(tonumber(offset), comment)
     84     end
     85 
     86     pp_lineno = pp_lineno + 1
     87 
     88     local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
     89     if ppl_lineno ~= nil then
     90       if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
     91         print_error("error: %s:%d: preprocessor line number must be %d",
     92           fname, phys_lineno, phys_lineno + 1)
     93       end
     94       if ppl_fname:match("%.c$") and ppl_fname ~= basename then
     95         print_error("error: %s:%d: preprocessor filename must be '%s'",
     96           fname, phys_lineno, basename)
     97       end
     98       pp_fname = ppl_fname
     99       pp_lineno = ppl_lineno
    100     end
    101   end
    102 
    103   return comment_locations, comments_by_location
    104 end
    105 
    106 
    107 -- Load the expected raw lint output from a .exp file.
    108 --
    109 -- example return value: {
    110 --   {
    111 --     exp_lineno = 18,
    112 --     location = "file.c(18)",
    113 --     message = "not a constant expression [123]",
    114 --   }
    115 -- }
    116 local function load_exp(exp_fname)
    117 
    118   local lines = load_lines(exp_fname)
    119   if lines == nil then
    120     print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
    121     return
    122   end
    123 
    124   local messages = {}
    125   for exp_lineno, line in ipairs(lines) do
    126     for location, message in line:gmatch("(%S+%(%d+%)): (.+)$") do
    127       table.insert(messages, {
    128         exp_lineno = exp_lineno,
    129         location = location,
    130         message = message
    131       })
    132     end
    133   end
    134 
    135   return messages
    136 end
    137 
    138 
    139 ---@param comment string
    140 ---@param pattern string
    141 ---@return boolean
    142 local function matches(comment, pattern)
    143   if comment == "" then return false end
    144 
    145   local any_prefix = pattern:sub(1, 3) == "..."
    146   if any_prefix then pattern = pattern:sub(4) end
    147   local any_suffix = pattern:sub(-3) == "..."
    148   if any_suffix then pattern = pattern:sub(1, -4) end
    149 
    150   if any_prefix and any_suffix then
    151     return comment:find(pattern, 1, true) ~= nil
    152   elseif any_prefix then
    153     return pattern ~= "" and comment:sub(-#pattern) == pattern
    154   elseif any_suffix then
    155     return comment:sub(1, #pattern) == pattern
    156   else
    157     return comment == pattern
    158   end
    159 end
    160 
    161 test(function()
    162   assert_equals(matches("a", "a"), true)
    163   assert_equals(matches("a", "b"), false)
    164   assert_equals(matches("a", "aaa"), false)
    165 
    166   assert_equals(matches("abc", "a..."), true)
    167   assert_equals(matches("abc", "c..."), false)
    168 
    169   assert_equals(matches("abc", "...c"), true)
    170   assert_equals(matches("abc", "...a"), false)
    171 
    172   assert_equals(matches("abc123xyz", "...a..."), true)
    173   assert_equals(matches("abc123xyz", "...b..."), true)
    174   assert_equals(matches("abc123xyz", "...c..."), true)
    175   assert_equals(matches("abc123xyz", "...1..."), true)
    176   assert_equals(matches("abc123xyz", "...2..."), true)
    177   assert_equals(matches("abc123xyz", "...3..."), true)
    178   assert_equals(matches("abc123xyz", "...x..."), true)
    179   assert_equals(matches("abc123xyz", "...y..."), true)
    180   assert_equals(matches("abc123xyz", "...z..."), true)
    181   assert_equals(matches("pattern", "...pattern..."), true)
    182 end)
    183 
    184 
    185 local function check_test(c_fname)
    186   local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
    187 
    188   local c_comment_locations, c_comments_by_location = load_c(c_fname)
    189   if c_comment_locations == nil then return end
    190 
    191   local exp_messages = load_exp(exp_fname) or {}
    192 
    193   for _, exp_message in ipairs(exp_messages) do
    194     local c_comments = c_comments_by_location[exp_message.location] or {}
    195     local expected_message =
    196       exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
    197 
    198     local found = false
    199     for i, c_comment in ipairs(c_comments) do
    200       if c_comment ~= "" and matches(expected_message, c_comment) then
    201         c_comments[i] = ""
    202         found = true
    203         break
    204       end
    205     end
    206 
    207     if not found then
    208       print_error("error: %s: missing /* expect+1: %s */",
    209         exp_message.location, expected_message)
    210     end
    211   end
    212 
    213   for _, c_comment_location in ipairs(c_comment_locations) do
    214     for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
    215       if c_comment ~= "" then
    216         print_error(
    217           "error: %s: declared message \"%s\" is not in the actual output",
    218           c_comment_location, c_comment)
    219       end
    220     end
    221   end
    222 end
    223 
    224 
    225 local function main(args)
    226   for _, name in ipairs(args) do
    227     check_test(name)
    228   end
    229 end
    230 
    231 
    232 main(arg)
    233 os.exit(not had_errors)
    234