check-expect.lua revision 1.1 1 #! /usr/bin/lua
2 -- $NetBSD: check-expect.lua,v 1.1 2022/06/17 20:31:56 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
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 == fname 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 pp_fname = ppl_fname
95 pp_lineno = ppl_lineno
96 end
97 end
98
99 return comment_locations, comments_by_location
100 end
101
102
103 -- Load the expected raw lint output from a .exp file.
104 --
105 -- example return value: {
106 -- {
107 -- exp_lineno = "18",
108 -- location = "file.c(18)",
109 -- message = "not a constant expression [123]",
110 -- }
111 -- }
112 local function load_exp(exp_fname)
113
114 local lines = load_lines(exp_fname)
115 if lines == nil then return {} end
116
117 local messages = {}
118 for exp_lineno, line in ipairs(lines) do
119 for location, message in line:gmatch("(%S+%(%d+%)): (.+)$") do
120 table.insert(messages, {
121 exp_lineno = exp_lineno,
122 location = location,
123 message = message
124 })
125 end
126 end
127
128 return messages
129 end
130
131
132 ---@param comment string
133 ---@param pattern string
134 ---@return boolean
135 local function matches(comment, pattern)
136 if comment == "" then return false end
137
138 local any_prefix = pattern:sub(1, 3) == "..."
139 if any_prefix then pattern = pattern:sub(4) end
140 local any_suffix = pattern:sub(-3) == "..."
141 if any_suffix then pattern = pattern:sub(1, -4) end
142
143 if any_prefix and any_suffix then
144 return comment:find(pattern, 1, true) ~= nil
145 elseif any_prefix then
146 return pattern ~= "" and comment:sub(-#pattern) == pattern
147 elseif any_suffix then
148 return comment:sub(1, #pattern) == pattern
149 else
150 return comment == pattern
151 end
152 end
153
154 test(function()
155 assert_equals(matches("a", "a"), true)
156 assert_equals(matches("a", "b"), false)
157 assert_equals(matches("a", "aaa"), false)
158
159 assert_equals(matches("abc", "a..."), true)
160 assert_equals(matches("abc", "c..."), false)
161
162 assert_equals(matches("abc", "...c"), true)
163 assert_equals(matches("abc", "...a"), false)
164
165 assert_equals(matches("abc123xyz", "...a..."), true)
166 assert_equals(matches("abc123xyz", "...b..."), true)
167 assert_equals(matches("abc123xyz", "...c..."), true)
168 assert_equals(matches("abc123xyz", "...1..."), true)
169 assert_equals(matches("abc123xyz", "...2..."), true)
170 assert_equals(matches("abc123xyz", "...3..."), true)
171 assert_equals(matches("abc123xyz", "...x..."), true)
172 assert_equals(matches("abc123xyz", "...y..."), true)
173 assert_equals(matches("abc123xyz", "...z..."), true)
174 assert_equals(matches("pattern", "...pattern..."), true)
175 end)
176
177
178 local function check_test(c_fname)
179 local exp_fname = c_fname:gsub("%.c$", ".exp"):gsub(".+/", "")
180
181 local c_comment_locations, c_comments_by_location = load_c(c_fname)
182 if c_comment_locations == nil then return end
183
184 local exp_messages = load_exp(exp_fname) or {}
185
186 for _, exp_message in ipairs(exp_messages) do
187 local c_comments = c_comments_by_location[exp_message.location] or {}
188 local expected_message =
189 exp_message.message:gsub("/%*", "**"):gsub("%*/", "**")
190
191 local found = false
192 for i, c_comment in ipairs(c_comments) do
193 if c_comment ~= "" and matches(expected_message, c_comment) then
194 c_comments[i] = ""
195 found = true
196 break
197 end
198 end
199
200 if not found then
201 print_error("error: %s: missing /* expect+1: %s */",
202 exp_message.location, expected_message)
203 end
204 end
205
206 for _, c_comment_location in ipairs(c_comment_locations) do
207 for _, c_comment in ipairs(c_comments_by_location[c_comment_location]) do
208 if c_comment ~= "" then
209 print_error(
210 "error: %s: declared message \"%s\" is not in the actual output",
211 c_comment_location, c_comment)
212 end
213 end
214 end
215 end
216
217
218 local function main(args)
219 for _, name in ipairs(args) do
220 check_test(name)
221 end
222 end
223
224
225 main(arg)
226 os.exit(not had_errors)
227