xref: /netbsd/usr.bin/xlint/lint1/check-msgs.lua (revision 73ce3036)
1#! /usr/bin/lua
2-- $NetBSD: check-msgs.lua,v 1.19 2023/07/10 11:46:14 rillig Exp $
3
4--[[
5
6usage: lua ./check-msgs.lua *.c *.y
7
8Check that the message text in the comments of the C source code matches the
9actual user-visible message text in err.c.
10
11]]
12
13
14local 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*/%*%s*(Q?%d+)%s*%*/$")
20    if msg ~= nil then
21      msgs[id] = msg
22    end
23  end
24
25  f:close()
26
27  return msgs
28end
29
30
31local had_errors = false
32---@param fmt string
33function print_error(fmt, ...)
34  print(fmt:format(...))
35  had_errors = true
36end
37
38
39local 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)
62end
63
64local 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
76local 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()
100end
101
102
103local 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
108end
109
110
111-- Ensure that each test file for a particular message mentions the full text
112-- of that message and the message ID.
113local 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 = ("%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()
129end
130
131local function check_yacc_file(filename)
132  local decl = {}
133  local f = assert(io.open(filename, "r"))
134  local lineno = 0
135  for line in f:lines() do
136    lineno = lineno + 1
137    local type = line:match("^%%type%s+<[%w_]+>%s+(%S+)$") or
138      line:match("^/%* No type for ([%w_]+)%. %*/$")
139    if type then
140      decl[type] = lineno
141    end
142    local rule = line:match("^([%w_]+):")
143    if rule then
144      if decl[rule] then
145        decl[rule] = nil
146      else
147        print_error("%s:%d: missing type declaration for rule %q",
148          filename, lineno, rule)
149      end
150    end
151  end
152  for rule, decl_lineno in pairs(decl) do
153    print_error("%s:%d: missing rule %q", filename, decl_lineno, rule)
154  end
155  f:close()
156end
157
158local function main(arg)
159  local msgs = load_messages()
160  for _, fname in ipairs(arg) do
161    check_file(fname, msgs)
162    if fname:match("%.y$") then
163      check_yacc_file(fname)
164    end
165  end
166  check_test_files(msgs)
167end
168
169main(arg)
170os.exit(not had_errors)
171