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