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