check-expect.lua revision 1.5 1 #! /usr/bin/lua
2 -- $NetBSD: check-expect.lua,v 1.5 2023/07/06 07:33:36 rillig Exp $
3
4 --[[
5
6 usage: lua ./check-expect.lua *.c
7
8 Check that the /* expect+-n: ... */ comments in the .c source files match the
9 actual messages found in the corresponding .exp files. The .exp files are
10 expected in the current working directory.
11
12 The .exp files are generated on the fly during the ATF tests, see
13 t_integration.sh. During development, they can be generated using
14 lint1/accept.sh.
15 ]]
16
17
18 local function test(func)
19 func()
20 end
21
22 local function assert_equals(got, expected)
23 if got ~= expected then
24 assert(false, string.format("got %q, expected %q", got, expected))
25 end
26 end
27
28
29 local had_errors = false
30 ---@param fmt string
31 function print_error(fmt, ...)
32 print(fmt:format(...))
33 had_errors = true
34 end
35
36
37 local function load_lines(fname)
38 local lines = {}
39
40 local f = io.open(fname, "r")
41 if f == nil then return nil end
42
43 for line in f:lines() do
44 table.insert(lines, line)
45 end
46 f:close()
47
48 return lines
49 end
50
51
52 -- Load the 'expect:' comments from a C source file.
53 --
54 -- example return values:
55 -- {
56 -- ["file.c(18)"] = {"invalid argument 'a'", "invalid argument 'b'"},
57 -- ["file.c(23)"] = {"not a constant expression [123]"},
58 -- },
59 -- { "file.c(18)", "file.c(23)" }
60 local function load_c(fname)
61 local basename = fname:match("([^/]+)$")
62 local lines = load_lines(fname)
63 if lines == nil then return nil, nil end
64
65 local pp_fname = fname
66 local pp_lineno = 0
67 local comment_locations = {}
68 local comments_by_location = {}
69
70 local function add_expectation(offset, message)
71 local location = ("%s(%d)"):format(pp_fname, pp_lineno + offset)
72 if comments_by_location[location] == nil then
73 table.insert(comment_locations, location)
74 comments_by_location[location] = {}
75 end
76 local trimmed_msg = message:match("^%s*(.-)%s*$")
77 table.insert(comments_by_location[location], trimmed_msg)
78 end
79
80 for phys_lineno, line in ipairs(lines) do
81
82 for offset, comment in line:gmatch("/%* expect([+%-]%d+): (.-) %*/") do
83 add_expectation(tonumber(offset), comment)
84 end
85
86 pp_lineno = pp_lineno + 1
87
88 local ppl_lineno, ppl_fname = line:match("^#%s*(%d+)%s+\"([^\"]+)\"")
89 if ppl_lineno ~= nil then
90 if ppl_fname == basename and tonumber(ppl_lineno) ~= phys_lineno + 1 then
91 print_error("error: %s:%d: preprocessor line number must be %d",
92 fname, phys_lineno, phys_lineno + 1)
93 end
94 if ppl_fname:match("%.c$") and ppl_fname ~= basename then
95 print_error("error: %s:%d: preprocessor filename must be '%s'",
96 fname, phys_lineno, basename)
97 end
98 pp_fname = ppl_fname
99 pp_lineno = ppl_lineno
100 end
101 end
102
103 return comment_locations, comments_by_location
104 end
105
106
107 -- Load the expected raw lint output from a .exp file.
108 --
109 -- example return value: {
110 -- {
111 -- exp_lineno = 18,
112 -- location = "file.c(18)",
113 -- message = "not a constant expression [123]",
114 -- }
115 -- }
116 local function load_exp(exp_fname)
117
118 local lines = load_lines(exp_fname)
119 if lines == nil then
120 print_error("check-expect.lua: error: file " .. exp_fname .. " not found")
121 return
122 end
123
124 local messages = {}
125 for exp_lineno, line in ipairs(lines) do
126 for location, message in line:gmatch("(%S+%(%d+%)): (.+)$") do
127 table.insert(messages, {
128 exp_lineno = exp_lineno,
129 location = location,
130 message = message
131 })
132 end
133 end
134
135 return messages
136 end
137
138
139 ---@param comment string
140 ---@param pattern string
141 ---@return boolean
142 local function matches(comment, pattern)
143 if comment == "" then return false end
144
145 local any_prefix = pattern:sub(1, 3) == "..."
146 if any_prefix then pattern = pattern:sub(4) end
147 local any_suffix = pattern:sub(-3) == "..."
148 if any_suffix then pattern = pattern:sub(1, -4) end
149
150 if any_prefix and any_suffix then
151 return comment:find(pattern, 1, true) ~= nil
152 elseif any_prefix then
153 return pattern ~= "" and comment:sub(-#pattern) == pattern
154 elseif any_suffix then
155 return comment:sub(1, #pattern) == pattern
156 else
157 return comment == pattern
158 end
159 end
160
161 test(function()
162 assert_equals(matches("a", "a"), true)
163 assert_equals(matches("a", "b"), false)
164 assert_equals(matches("a", "aaa"), false)
165
166 assert_equals(matches("abc", "a..."), true)
167 assert_equals(matches("abc", "c..."), false)
168
169 assert_equals(matches("abc", "...c"), true)
170 assert_equals(matches("abc", "...a"), false)
171
172 assert_equals(matches("abc123xyz", "...a..."), true)
173 assert_equals(matches("abc123xyz", "...b..."), true)
174 assert_equals(matches("abc123xyz", "...c..."), true)
175 assert_equals(matches("abc123xyz", "...1..."), true)
176 assert_equals(matches("abc123xyz", "...2..."), true)
177 assert_equals(matches("abc123xyz", "...3..."), true)
178 assert_equals(matches("abc123xyz", "...x..."), true)
179 assert_equals(matches("abc123xyz", "...y..."), true)
180 assert_equals(matches("abc123xyz", "...z..."), true)
181 assert_equals(matches("pattern", "...pattern..."), true)
182 end)
183
184
185 local function check_test(c_fname)
186 local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
187
188 local c_comment_locations, c_comments_by_location = load_c(c_fname)
189 if c_comment_locations == nil then return end
190
191 local exp_messages = load_exp(exp_fname) or {}
192
193 for _, exp_message in ipairs(exp_messages) do
194 local c_comments = c_comments_by_location[exp_message.location] or {}
195 local expected_message =
196 exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
197
198 local found = false
199 for i, c_comment in ipairs(c_comments) do
200 if c_comment ~= "" and matches(expected_message, c_comment) then
201 c_comments[i] = ""
202 found = true
203 break
204 end
205 end
206
207 if not found then
208 print_error("error: %s: missing /* expect+1: %s */",
209 exp_message.location, expected_message)
210 end
211 end
212
213 for _, c_comment_location in ipairs(c_comment_locations) do
214 for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
215 if c_comment ~= "" then
216 print_error(
217 "error: %s: declared message \"%s\" is not in the actual output",
218 c_comment_location, c_comment)
219 end
220 end
221 end
222 end
223
224
225 local function main(args)
226 for _, name in ipairs(args) do
227 check_test(name)
228 end
229 end
230
231
232 main(arg)
233 os.exit(not had_errors)
234