Home | History | Annotate | Line # | Download | only in lint1
check-expect.lua revision 1.8
      1  1.1  rillig #!  /usr/bin/lua
      2  1.8  rillig -- $NetBSD: check-expect.lua,v 1.8 2023/08/11 04:27:49 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.7  rillig   local f, err, errno = io.open(fname, "r")
     41  1.7  rillig   if f == nil then return nil, err, errno 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.6  rillig local function save_lines(fname, lines)
     53  1.6  rillig   local f = io.open(fname, "w")
     54  1.6  rillig   for _, line in ipairs(lines) do
     55  1.6  rillig     f:write(line .. "\n")
     56  1.6  rillig   end
     57  1.6  rillig   f:close()
     58  1.6  rillig end
     59  1.6  rillig 
     60  1.6  rillig 
     61  1.1  rillig -- Load the 'expect:' comments from a C source file.
     62  1.1  rillig --
     63  1.1  rillig -- example return values:
     64  1.1  rillig --   {
     65  1.8  rillig --     ["file.c(18)"] = {"syntax error 'a' [249]", "syntax error 'b' [249]"},
     66  1.1  rillig --     ["file.c(23)"] = {"not a constant expression [123]"},
     67  1.1  rillig --   },
     68  1.1  rillig --   { "file.c(18)", "file.c(23)" }
     69  1.1  rillig local function load_c(fname)
     70  1.4  rillig   local basename = fname:match("([^/]+)$")
     71  1.1  rillig   local lines = load_lines(fname)
     72  1.1  rillig   if lines == nil then return nil, nil end
     73  1.1  rillig 
     74  1.1  rillig   local pp_fname = fname
     75  1.1  rillig   local pp_lineno = 0
     76  1.1  rillig   local comment_locations = {}
     77  1.1  rillig   local comments_by_location = {}
     78  1.1  rillig 
     79  1.1  rillig   local function add_expectation(offset, message)
     80  1.1  rillig     local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
     81  1.1  rillig     if comments_by_location[location] == nil then
     82  1.1  rillig       table.insert(comment_locations, location)
     83  1.1  rillig       comments_by_location[location] = {}
     84  1.1  rillig     end
     85  1.1  rillig     local trimmed_msg = message:match("^%s*(.-)%s*$")
     86  1.1  rillig     table.insert(comments_by_location[location], trimmed_msg)
     87  1.1  rillig   end
     88  1.1  rillig 
     89  1.1  rillig   for phys_lineno, line in ipairs(lines) do
     90  1.1  rillig 
     91  1.1  rillig     for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
     92  1.1  rillig       add_expectation(tonumber(offset), comment)
     93  1.1  rillig     end
     94  1.1  rillig 
     95  1.1  rillig     pp_lineno = pp_lineno + 1
     96  1.1  rillig 
     97  1.1  rillig     local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
     98  1.1  rillig     if ppl_lineno ~= nil then
     99  1.3  rillig       if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
    100  1.1  rillig         print_error("error: %s:%d: preprocessor line number must be %d",
    101  1.1  rillig           fname, phys_lineno, phys_lineno + 1)
    102  1.1  rillig       end
    103  1.5  rillig       if ppl_fname:match("%.c$") and ppl_fname ~= basename then
    104  1.5  rillig         print_error("error: %s:%d: preprocessor filename must be '%s'",
    105  1.5  rillig           fname, phys_lineno, basename)
    106  1.5  rillig       end
    107  1.1  rillig       pp_fname = ppl_fname
    108  1.1  rillig       pp_lineno = ppl_lineno
    109  1.1  rillig     end
    110  1.1  rillig   end
    111  1.1  rillig 
    112  1.1  rillig   return comment_locations, comments_by_location
    113  1.1  rillig end
    114  1.1  rillig 
    115  1.1  rillig 
    116  1.1  rillig -- Load the expected raw lint output from a .exp file.
    117  1.1  rillig --
    118  1.1  rillig -- example return value: {
    119  1.1  rillig --   {
    120  1.5  rillig --     exp_lineno = 18,
    121  1.1  rillig --     location = "file.c(18)",
    122  1.1  rillig --     message = "not a constant expression [123]",
    123  1.1  rillig --   }
    124  1.1  rillig -- }
    125  1.1  rillig local function load_exp(exp_fname)
    126  1.1  rillig 
    127  1.1  rillig   local lines = load_lines(exp_fname)
    128  1.2  rillig   if lines == nil then
    129  1.2  rillig     print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
    130  1.2  rillig     return
    131  1.2  rillig   end
    132  1.1  rillig 
    133  1.1  rillig   local messages = {}
    134  1.1  rillig   for exp_lineno, line in ipairs(lines) do
    135  1.8  rillig     for location, message in line:gmatch("(.+%(%d+%)): (.+)$") do
    136  1.1  rillig       table.insert(messages, {
    137  1.1  rillig         exp_lineno = exp_lineno,
    138  1.1  rillig         location = location,
    139  1.1  rillig         message = message
    140  1.1  rillig       })
    141  1.1  rillig     end
    142  1.1  rillig   end
    143  1.1  rillig 
    144  1.1  rillig   return messages
    145  1.1  rillig end
    146  1.1  rillig 
    147  1.1  rillig 
    148  1.1  rillig local function matches(comment, pattern)
    149  1.1  rillig   if comment == "" then return false end
    150  1.1  rillig 
    151  1.1  rillig   local any_prefix = pattern:sub(1, 3) == "..."
    152  1.1  rillig   if any_prefix then pattern = pattern:sub(4) end
    153  1.1  rillig   local any_suffix = pattern:sub(-3) == "..."
    154  1.1  rillig   if any_suffix then pattern = pattern:sub(1, -4) end
    155  1.1  rillig 
    156  1.1  rillig   if any_prefix and any_suffix then
    157  1.1  rillig     return comment:find(pattern, 1, true) ~= nil
    158  1.1  rillig   elseif any_prefix then
    159  1.1  rillig     return pattern ~= "" and comment:sub(-#pattern) == pattern
    160  1.1  rillig   elseif any_suffix then
    161  1.1  rillig     return comment:sub(1, #pattern) == pattern
    162  1.1  rillig   else
    163  1.1  rillig     return comment == pattern
    164  1.1  rillig   end
    165  1.1  rillig end
    166  1.1  rillig 
    167  1.1  rillig test(function()
    168  1.1  rillig   assert_equals(matches("a", "a"), true)
    169  1.1  rillig   assert_equals(matches("a", "b"), false)
    170  1.1  rillig   assert_equals(matches("a", "aaa"), false)
    171  1.1  rillig 
    172  1.1  rillig   assert_equals(matches("abc", "a..."), true)
    173  1.1  rillig   assert_equals(matches("abc", "c..."), false)
    174  1.1  rillig 
    175  1.1  rillig   assert_equals(matches("abc", "...c"), true)
    176  1.1  rillig   assert_equals(matches("abc", "...a"), false)
    177  1.1  rillig 
    178  1.1  rillig   assert_equals(matches("abc123xyz", "...a..."), true)
    179  1.1  rillig   assert_equals(matches("abc123xyz", "...b..."), true)
    180  1.1  rillig   assert_equals(matches("abc123xyz", "...c..."), true)
    181  1.1  rillig   assert_equals(matches("abc123xyz", "...1..."), true)
    182  1.1  rillig   assert_equals(matches("abc123xyz", "...2..."), true)
    183  1.1  rillig   assert_equals(matches("abc123xyz", "...3..."), true)
    184  1.1  rillig   assert_equals(matches("abc123xyz", "...x..."), true)
    185  1.1  rillig   assert_equals(matches("abc123xyz", "...y..."), true)
    186  1.1  rillig   assert_equals(matches("abc123xyz", "...z..."), true)
    187  1.1  rillig   assert_equals(matches("pattern", "...pattern..."), true)
    188  1.1  rillig end)
    189  1.1  rillig 
    190  1.1  rillig 
    191  1.6  rillig -- Inserts the '/* expect */' lines to the .c file, so that the .c file matches
    192  1.6  rillig -- the .exp file.  Multiple 'expect' comments for a single line of code are not
    193  1.6  rillig -- handled correctly, but it's still better than doing the same work manually.
    194  1.6  rillig local function insert_missing(missing)
    195  1.6  rillig   for fname, items in pairs(missing) do
    196  1.6  rillig     table.sort(items, function(a, b) return a.lineno > b.lineno end)
    197  1.7  rillig     local lines = assert(load_lines(fname))
    198  1.6  rillig     for _, item in ipairs(items) do
    199  1.6  rillig       local lineno, message = item.lineno, item.message
    200  1.6  rillig       local indent = (lines[lineno] or ""):match("^([ \t]*)")
    201  1.6  rillig       local line = ("%s/* expect+1: %s */"):format(indent, message)
    202  1.6  rillig       table.insert(lines, lineno, line)
    203  1.6  rillig     end
    204  1.6  rillig     save_lines(fname, lines)
    205  1.6  rillig   end
    206  1.6  rillig end
    207  1.6  rillig 
    208  1.6  rillig 
    209  1.6  rillig local function check_test(c_fname, update)
    210  1.1  rillig   local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
    211  1.1  rillig 
    212  1.1  rillig   local c_comment_locations, c_comments_by_location = load_c(c_fname)
    213  1.1  rillig   if c_comment_locations == nil then return end
    214  1.1  rillig 
    215  1.1  rillig   local exp_messages = load_exp(exp_fname) or {}
    216  1.6  rillig   local missing = {}
    217  1.1  rillig 
    218  1.1  rillig   for _, exp_message in ipairs(exp_messages) do
    219  1.1  rillig     local c_comments = c_comments_by_location[exp_message.location] or {}
    220  1.1  rillig     local expected_message =
    221  1.1  rillig       exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
    222  1.1  rillig 
    223  1.1  rillig     local found = false
    224  1.1  rillig     for i, c_comment in ipairs(c_comments) do
    225  1.1  rillig       if c_comment ~= "" and matches(expected_message, c_comment) then
    226  1.1  rillig         c_comments[i] = ""
    227  1.1  rillig         found = true
    228  1.1  rillig         break
    229  1.1  rillig       end
    230  1.1  rillig     end
    231  1.1  rillig 
    232  1.1  rillig     if not found then
    233  1.1  rillig       print_error("error: %s: missing /* expect+1: %s */",
    234  1.1  rillig         exp_message.location, expected_message)
    235  1.6  rillig 
    236  1.6  rillig       if update then
    237  1.6  rillig         local fname = exp_message.location:match("^([^(]+)")
    238  1.6  rillig         local lineno = tonumber(exp_message.location:match("%((%d+)%)$"))
    239  1.6  rillig         if not missing[fname] then missing[fname] = {} end
    240  1.6  rillig         table.insert(missing[fname], {
    241  1.6  rillig           lineno = lineno,
    242  1.6  rillig           message = expected_message,
    243  1.6  rillig         })
    244  1.6  rillig       end
    245  1.1  rillig     end
    246  1.1  rillig   end
    247  1.1  rillig 
    248  1.1  rillig   for _, c_comment_location in ipairs(c_comment_locations) do
    249  1.1  rillig     for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
    250  1.1  rillig       if c_comment ~= "" then
    251  1.1  rillig         print_error(
    252  1.1  rillig           "error: %s: declared message \"%s\" is not in the actual output",
    253  1.1  rillig           c_comment_location, c_comment)
    254  1.1  rillig       end
    255  1.1  rillig     end
    256  1.1  rillig   end
    257  1.6  rillig 
    258  1.6  rillig   if missing then
    259  1.6  rillig     insert_missing(missing)
    260  1.6  rillig   end
    261  1.1  rillig end
    262  1.1  rillig 
    263  1.1  rillig 
    264  1.1  rillig local function main(args)
    265  1.6  rillig   local update = args[1] == "-u"
    266  1.6  rillig   if update then
    267  1.6  rillig     table.remove(args, 1)
    268  1.6  rillig   end
    269  1.6  rillig 
    270  1.1  rillig   for _, name in ipairs(args) do
    271  1.6  rillig     check_test(name, update)
    272  1.1  rillig   end
    273  1.1  rillig end
    274  1.1  rillig 
    275  1.1  rillig 
    276  1.1  rillig main(arg)
    277  1.1  rillig os.exit(not had_errors)
    278