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