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