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