Home | History | Annotate | Line # | Download | only in lint1
check-expect.lua revision 1.1
      1 #!  /usr/bin/lua
      2 -- $NetBSD: check-expect.lua,v 1.1 2022/06/17 20:31:56 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 
     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 == fname 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       pp_fname = ppl_fname
     95       pp_lineno = ppl_lineno
     96     end
     97   end
     98 
     99   return comment_locations, comments_by_location
    100 end
    101 
    102 
    103 -- Load the expected raw lint output from a .exp file.
    104 --
    105 -- example return value: {
    106 --   {
    107 --     exp_lineno = "18",
    108 --     location = "file.c(18)",
    109 --     message = "not a constant expression [123]",
    110 --   }
    111 -- }
    112 local function load_exp(exp_fname)
    113 
    114   local lines = load_lines(exp_fname)
    115   if lines == nil then return {} end
    116 
    117   local messages = {}
    118   for exp_lineno, line in ipairs(lines) do
    119     for location, message in line:gmatch("(%S+%(%d+%)): (.+)$") do
    120       table.insert(messages, {
    121         exp_lineno = exp_lineno,
    122         location = location,
    123         message = message
    124       })
    125     end
    126   end
    127 
    128   return messages
    129 end
    130 
    131 
    132 ---@param comment string
    133 ---@param pattern string
    134 ---@return boolean
    135 local function matches(comment, pattern)
    136   if comment == "" then return false end
    137 
    138   local any_prefix = pattern:sub(1, 3) == "..."
    139   if any_prefix then pattern = pattern:sub(4) end
    140   local any_suffix = pattern:sub(-3) == "..."
    141   if any_suffix then pattern = pattern:sub(1, -4) end
    142 
    143   if any_prefix and any_suffix then
    144     return comment:find(pattern, 1, true) ~= nil
    145   elseif any_prefix then
    146     return pattern ~= "" and comment:sub(-#pattern) == pattern
    147   elseif any_suffix then
    148     return comment:sub(1, #pattern) == pattern
    149   else
    150     return comment == pattern
    151   end
    152 end
    153 
    154 test(function()
    155   assert_equals(matches("a", "a"), true)
    156   assert_equals(matches("a", "b"), false)
    157   assert_equals(matches("a", "aaa"), false)
    158 
    159   assert_equals(matches("abc", "a..."), true)
    160   assert_equals(matches("abc", "c..."), false)
    161 
    162   assert_equals(matches("abc", "...c"), true)
    163   assert_equals(matches("abc", "...a"), false)
    164 
    165   assert_equals(matches("abc123xyz", "...a..."), true)
    166   assert_equals(matches("abc123xyz", "...b..."), true)
    167   assert_equals(matches("abc123xyz", "...c..."), true)
    168   assert_equals(matches("abc123xyz", "...1..."), true)
    169   assert_equals(matches("abc123xyz", "...2..."), true)
    170   assert_equals(matches("abc123xyz", "...3..."), true)
    171   assert_equals(matches("abc123xyz", "...x..."), true)
    172   assert_equals(matches("abc123xyz", "...y..."), true)
    173   assert_equals(matches("abc123xyz", "...z..."), true)
    174   assert_equals(matches("pattern", "...pattern..."), true)
    175 end)
    176 
    177 
    178 local function check_test(c_fname)
    179   local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
    180 
    181   local c_comment_locations, c_comments_by_location = load_c(c_fname)
    182   if c_comment_locations == nil then return end
    183 
    184   local exp_messages = load_exp(exp_fname) or {}
    185 
    186   for _, exp_message in ipairs(exp_messages) do
    187     local c_comments = c_comments_by_location[exp_message.location] or {}
    188     local expected_message =
    189       exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
    190 
    191     local found = false
    192     for i, c_comment in ipairs(c_comments) do
    193       if c_comment ~= "" and matches(expected_message, c_comment) then
    194         c_comments[i] = ""
    195         found = true
    196         break
    197       end
    198     end
    199 
    200     if not found then
    201       print_error("error: %s: missing /* expect+1: %s */",
    202         exp_message.location, expected_message)
    203     end
    204   end
    205 
    206   for _, c_comment_location in ipairs(c_comment_locations) do
    207     for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
    208       if c_comment ~= "" then
    209         print_error(
    210           "error: %s: declared message \"%s\" is not in the actual output",
    211           c_comment_location, c_comment)
    212       end
    213     end
    214   end
    215 end
    216 
    217 
    218 local function main(args)
    219   for _, name in ipairs(args) do
    220     check_test(name)
    221   end
    222 end
    223 
    224 
    225 main(arg)
    226 os.exit(not had_errors)
    227