Home | History | Annotate | Line # | Download | only in lint1
      1 #! /usr/bin/lua
      2 -- $NetBSD: check-msgs.lua,v 1.22 2024/03/01 17:22:55 rillig Exp $
      3 
      4 --[[
      5 
      6 usage: lua ./check-msgs.lua *.c *.y
      7 
      8 Check that the message text in the comments of the C source code matches the
      9 actual user-visible message text in err.c.
     10 
     11 ]]
     12 
     13 
     14 local function load_messages()
     15   local msgs = {} ---@type table<string>string
     16 
     17   local f = assert(io.open("err.c"))
     18   for line in f:lines() do
     19     local msg, id = line:match("%s*\"(.+)\",%s*// (Q?%d+)$")
     20     if msg ~= nil then
     21       msgs[id] = msg
     22     end
     23   end
     24 
     25   f:close()
     26 
     27   return msgs
     28 end
     29 
     30 
     31 local had_errors = false
     32 ---@param fmt string
     33 function print_error(fmt, ...)
     34   print(fmt:format(...))
     35   had_errors = true
     36 end
     37 
     38 
     39 local function check_message(fname, lineno, id, comment, msgs)
     40   local msg = msgs[id]
     41 
     42   if msg == nil then
     43     print_error("%s:%d: id=%s not found", fname, lineno, id)
     44     return
     45   end
     46 
     47   msg = msg:gsub("/%*", "**")
     48   msg = msg:gsub("%*/", "**")
     49   msg = msg:gsub("\\(.)", "%1")
     50 
     51   if comment == msg then
     52     return
     53   end
     54 
     55   local prefix = comment:match("^(.-)%s*%.%.%.$")
     56   if prefix ~= nil and msg:find(prefix, 1, 1) == 1 then
     57     return
     58   end
     59 
     60   print_error("%s:%d:   id=%-3s   msg=%-40s   comment=%s",
     61     fname, lineno, id, msg, comment)
     62 end
     63 
     64 local message_prefix = {
     65   error = "",
     66   error_at = "",
     67   warning = "",
     68   warning_at = "",
     69   query_message = "Q",
     70   c99ism = "",
     71   c11ism = "",
     72   c23ism = "",
     73   gnuism = "",
     74 }
     75 
     76 local function check_file(fname, msgs)
     77   local f = assert(io.open(fname, "r"))
     78   local lineno = 0
     79   local prev = ""
     80   for line in f:lines() do
     81     lineno = lineno + 1
     82 
     83     local func, id = line:match("^%s+([%w_]+)%((%d+)[),]")
     84     local prefix = message_prefix[func]
     85     if prefix then
     86       id = prefix .. id
     87       local comment = prev:match("^%s+/%* (.+) %*/$")
     88       if comment ~= nil then
     89         check_message(fname, lineno, id, comment, msgs)
     90       else
     91         print_error("%s:%d: missing comment for %s: /* %s */",
     92           fname, lineno, id, msgs[id])
     93       end
     94     end
     95 
     96     prev = line
     97   end
     98 
     99   f:close()
    100 end
    101 
    102 
    103 local function file_contains(filename, text)
    104   local f = assert(io.open(filename, "r"))
    105   local found = f:read("a"):find(text, 1, true)
    106   f:close()
    107   return found
    108 end
    109 
    110 
    111 -- Ensure that each test file for a particular message mentions the full text
    112 -- of that message and the message ID.
    113 local function check_test_files(msgs)
    114   local testdir = "../../../tests/usr.bin/xlint/lint1"
    115   local cmd = ("cd '%s' && printf '%%s\\n' msg_[0-9][0-9][0-9]*.c"):format(testdir)
    116   local filenames = assert(io.popen(cmd))
    117   for filename in filenames:lines() do
    118     local msgid = filename:match("^msg_(%d%d%d)")
    119     if msgs[msgid] then
    120       local unescaped_msg = msgs[msgid]:gsub("\\(.)", "%1")
    121       local expected_text = ("Test for message: %s [%s]"):format(unescaped_msg, msgid)
    122       local fullname = ("%s/%s"):format(testdir, filename)
    123       if not file_contains(fullname, expected_text) then
    124         print_error("%s must contain: // %s", fullname, expected_text)
    125       end
    126     end
    127   end
    128   filenames:close()
    129 end
    130 
    131 local function check_yacc_file(filename)
    132   local decl = {}
    133   local decl_list = {}
    134   local decl_list_index = 1
    135   local f = assert(io.open(filename, "r"))
    136   local lineno = 0
    137   for line in f:lines() do
    138     lineno = lineno + 1
    139     local type = line:match("^%%type%s+<[%w_]+>%s+(%S+)$") or
    140       line:match("^/%* No type for ([%w_]+)%. %*/$")
    141     if type then
    142       if decl[type] then
    143         print_error("%s:%d: duplicate type declaration for rule %q",
    144           filename, lineno, type)
    145       end
    146       decl[type] = lineno
    147       table.insert(decl_list, { lineno = lineno, rule = type })
    148     end
    149     local rule = line:match("^([%w_]+):")
    150     if rule then
    151       if decl[rule] then
    152         decl[rule] = nil
    153       else
    154         print_error("%s:%d: missing type declaration for rule %q",
    155           filename, lineno, rule)
    156       end
    157       if decl_list_index > 0 then
    158         local expected = decl_list[decl_list_index]
    159         if expected.rule == rule then
    160           decl_list_index = decl_list_index + 1
    161         else
    162           print_error("%s:%d: expecting rule %q (from line %d), got %q",
    163               filename, lineno, expected.rule, expected.lineno, rule)
    164           decl_list_index = 0
    165         end
    166       end
    167     end
    168   end
    169   for rule, decl_lineno in pairs(decl) do
    170     print_error("%s:%d: missing rule %q", filename, decl_lineno, rule)
    171   end
    172   f:close()
    173 end
    174 
    175 local function main(arg)
    176   local msgs = load_messages()
    177   for _, fname in ipairs(arg) do
    178     check_file(fname, msgs)
    179     if fname:match("%.y$") then
    180       check_yacc_file(fname)
    181     end
    182   end
    183   check_test_files(msgs)
    184 end
    185 
    186 main(arg)
    187 os.exit(not had_errors)
    188