Home | History | Annotate | Line # | Download | only in lint1
      1  1.14  rillig #! /usr/bin/lua
      2  1.14  rillig -- $NetBSD: check-expect.lua,v 1.14 2025/02/27 06:48:29 rillig Exp $
      3   1.1  rillig 
      4   1.1  rillig --[[
      5   1.1  rillig 
      6   1.9  rillig usage: lua ./check-expect.lua [-u] *.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.10  rillig   assert_equals(matches("pattern", "... pattern ..."), false)
    189   1.1  rillig end)
    190   1.1  rillig 
    191   1.1  rillig 
    192  1.10  rillig -- Inserts the '/* expect */' lines to the .c file, so that the .c file
    193  1.10  rillig -- matches the .exp file.
    194  1.10  rillig --
    195  1.10  rillig -- TODO: Fix crashes in tests with '# line file' preprocessing directives.
    196   1.6  rillig local function insert_missing(missing)
    197   1.6  rillig   for fname, items in pairs(missing) do
    198  1.10  rillig     for i, item in ipairs(items) do
    199  1.10  rillig       item.stable_sort_rank = i
    200  1.10  rillig     end
    201  1.10  rillig     local function less(a, b)
    202  1.10  rillig       if a.lineno ~= b.lineno then
    203  1.10  rillig         return a.lineno > b.lineno
    204  1.10  rillig       end
    205  1.10  rillig       return a.stable_sort_rank > b.stable_sort_rank
    206  1.10  rillig     end
    207  1.10  rillig     table.sort(items, less)
    208   1.7  rillig     local lines = assert(load_lines(fname))
    209  1.10  rillig     local seen = {}
    210   1.6  rillig     for _, item in ipairs(items) do
    211   1.6  rillig       local lineno, message = item.lineno, item.message
    212   1.6  rillig       local indent = (lines[lineno] or ""):match("^([ \t]*)")
    213  1.10  rillig       local offset = 1 + (seen[lineno] or 0)
    214  1.10  rillig       local line = ("%s/* expect+%d: %s */"):format(indent, offset, message)
    215   1.6  rillig       table.insert(lines, lineno, line)
    216  1.10  rillig       seen[lineno] = (seen[lineno] or 0) + 1
    217   1.6  rillig     end
    218   1.6  rillig     save_lines(fname, lines)
    219   1.6  rillig   end
    220   1.6  rillig end
    221   1.6  rillig 
    222   1.6  rillig 
    223   1.6  rillig local function check_test(c_fname, update)
    224   1.1  rillig   local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
    225   1.1  rillig 
    226   1.1  rillig   local c_comment_locations, c_comments_by_location = load_c(c_fname)
    227   1.1  rillig   if c_comment_locations == nil then return end
    228   1.1  rillig 
    229   1.1  rillig   local exp_messages = load_exp(exp_fname) or {}
    230   1.6  rillig   local missing = {}
    231   1.1  rillig 
    232   1.1  rillig   for _, exp_message in ipairs(exp_messages) do
    233   1.1  rillig     local c_comments = c_comments_by_location[exp_message.location] or {}
    234   1.1  rillig     local expected_message =
    235   1.1  rillig       exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
    236   1.1  rillig 
    237   1.1  rillig     local found = false
    238   1.1  rillig     for i, c_comment in ipairs(c_comments) do
    239  1.12  rillig       if c_comment ~= "" then
    240  1.12  rillig         if matches(expected_message, c_comment) then
    241  1.12  rillig           c_comments[i] = ""
    242  1.12  rillig           found = true
    243  1.12  rillig         end
    244   1.1  rillig         break
    245   1.1  rillig       end
    246   1.1  rillig     end
    247   1.1  rillig 
    248   1.1  rillig     if not found then
    249   1.1  rillig       print_error("error: %s: missing /* expect+1: %s */",
    250   1.1  rillig         exp_message.location, expected_message)
    251   1.6  rillig 
    252   1.6  rillig       if update then
    253   1.6  rillig         local fname = exp_message.location:match("^([^(]+)")
    254   1.6  rillig         local lineno = tonumber(exp_message.location:match("%((%d+)%)$"))
    255   1.6  rillig         if not missing[fname] then missing[fname] = {} end
    256   1.6  rillig         table.insert(missing[fname], {
    257   1.6  rillig           lineno = lineno,
    258   1.6  rillig           message = expected_message,
    259   1.6  rillig         })
    260   1.6  rillig       end
    261   1.1  rillig     end
    262   1.1  rillig   end
    263   1.1  rillig 
    264   1.1  rillig   for _, c_comment_location in ipairs(c_comment_locations) do
    265   1.1  rillig     for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
    266   1.1  rillig       if c_comment ~= "" then
    267   1.1  rillig         print_error(
    268   1.1  rillig           "error: %s: declared message \"%s\" is not in the actual output",
    269   1.1  rillig           c_comment_location, c_comment)
    270   1.1  rillig       end
    271   1.1  rillig     end
    272   1.1  rillig   end
    273   1.6  rillig 
    274   1.6  rillig   if missing then
    275   1.6  rillig     insert_missing(missing)
    276   1.6  rillig   end
    277   1.1  rillig end
    278   1.1  rillig 
    279   1.1  rillig 
    280   1.1  rillig local function main(args)
    281  1.13  rillig   local update = false
    282  1.13  rillig   for _, arg in ipairs(args) do
    283  1.13  rillig     if arg == "-u" then
    284  1.13  rillig       update = true
    285  1.13  rillig     end
    286   1.6  rillig   end
    287   1.6  rillig 
    288   1.1  rillig   for _, name in ipairs(args) do
    289  1.13  rillig     if name ~= "-u" then
    290  1.13  rillig       check_test(name, update)
    291  1.13  rillig     end
    292   1.1  rillig   end
    293   1.1  rillig end
    294   1.1  rillig 
    295   1.1  rillig 
    296   1.1  rillig main(arg)
    297   1.1  rillig os.exit(not had_errors)
    298