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