xref: /netbsd-src/usr.bin/xlint/lint1/check-msgs.lua (revision 9e211f359920a7b95ff05505ca6ee27fa088acb5)
1#! /usr/bin/lua
2-- $NetBSD: check-msgs.lua,v 1.22 2024/03/01 17:22:55 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*// (Q?%d+)$")
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 = ("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()
129end
130
131local 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()
173end
174
175local 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)
184end
185
186main(arg)
187os.exit(not had_errors)
188