1#! /usr/bin/lua 2-- $NetBSD: check-expect.lua,v 1.17 2025/07/01 05:03:18 rillig Exp $ 3 4--[[ 5 6usage: lua ./check-expect.lua *.mk 7 8Check that the various 'expect' comments in the .mk files produce the 9expected text in the corresponding .exp file. 10 11# expect: <line> 12 Each <line> must occur in the .exp file. 13 The order in the .mk file must be the same as in the .exp file. 14 15# expect[+-]offset: <message> 16 Each <message> must occur in the .exp file and refer back to the 17 source line in the .mk file. 18 Each such line in the .exp file must have a corresponding expect line 19 in the .mk file. 20 The order in the .mk file must be the same as in the .exp file. 21 22# expect-reset 23 Search the following "expect:" and "expect[+-]offset:" comments 24 from the top of the .exp file again. 25 26# expect-not: <substring> 27 The <substring> must not occur as part of any line in the .exp file. 28 29# expect-not-matches: <pattern> 30 The <pattern> (see https://lua.org/manual/5.4/manual.html#6.4.1) 31 must not occur as part of any line in the .exp file. 32]] 33 34 35local had_errors = false 36---@param fmt string 37local function print_error(fmt, ...) 38 print(fmt:format(...)) 39 had_errors = true 40end 41 42 43---@return nil | string[] 44local function load_lines(fname) 45 local lines = {} 46 47 local f = io.open(fname, "r") 48 if f == nil then 49 return nil 50 end 51 52 for line in f:lines() do 53 table.insert(lines, line) 54 end 55 f:close() 56 57 return lines 58end 59 60 61--- @shape ExpLine 62--- @field filename string | nil 63--- @field lineno number | nil 64--- @field text string 65 66 67--- @param lines string[] 68--- @return ExpLine[] 69local function parse_exp(lines) 70 local exp_lines = {} 71 for _, line in ipairs(lines) do 72 local l_filename, l_lineno, l_text = 73 line:match('^make: ([^:]+%.mk):(%d+):%s+(.*)') 74 if not l_filename then 75 l_text = line 76 end 77 l_text = l_text:gsub("^%s+", ""):gsub("%s+$", "") 78 table.insert(exp_lines, { 79 filename = l_filename, 80 lineno = tonumber(l_lineno), 81 text = l_text, 82 }) 83 end 84 return exp_lines 85end 86 87---@param exp_lines ExpLine[] 88local function detect_missing_expect_lines(exp_fname, exp_lines, s, e) 89 for i = s, e do 90 local exp_line = exp_lines[i] 91 if exp_line.filename then 92 print_error("error: %s:%d requires in %s:%d: # expect+1: %s", 93 exp_fname, i, exp_line.filename, exp_line.lineno, exp_line.text) 94 end 95 end 96end 97 98local function check_mk(mk_fname) 99 local exp_fname = mk_fname:gsub("%.mk$", ".exp") 100 local mk_lines = load_lines(mk_fname) 101 local exp_raw_lines = load_lines(exp_fname) 102 if exp_raw_lines == nil then 103 return 104 end 105 local exp_lines = parse_exp(exp_raw_lines) 106 107 local exp_it = 1 108 109 for mk_lineno, mk_line in ipairs(mk_lines) do 110 111 local function match(pattern, action) 112 local _, n = mk_line:gsub(pattern, action) 113 if n > 0 then 114 match = function() end 115 end 116 end 117 118 match("^#%s+expect%-not:%s*(.*)", function(text) 119 for exp_lineno, exp_line in ipairs(exp_lines) do 120 if exp_line.text:find(text, 1, true) then 121 print_error("error: %s:%d: %s:%d must not contain '%s'", 122 mk_fname, mk_lineno, exp_fname, exp_lineno, text) 123 end 124 end 125 end) 126 127 match("^#%s+expect%-not%-matches:%s*(.*)", function(pattern) 128 for exp_lineno, exp_line in ipairs(exp_lines) do 129 if exp_line.text:find(pattern) then 130 print_error("error: %s:%d: %s:%d must not match '%s'", 131 mk_fname, mk_lineno, exp_fname, exp_lineno, pattern) 132 end 133 end 134 end) 135 136 match("^#%s+expect:%s*(.*)", function(text) 137 local i = exp_it 138 while i <= #exp_lines and text ~= exp_lines[i].text do 139 i = i + 1 140 end 141 if i <= #exp_lines then 142 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1) 143 exp_lines[i].text = "" 144 exp_it = i + 1 145 else 146 print_error("error: %s:%d: '%s:%d+' must contain '%s'", 147 mk_fname, mk_lineno, exp_fname, exp_it, text) 148 end 149 end) 150 151 match("^#%s+expect%-reset$", function() 152 exp_it = 1 153 end) 154 155 match("^#%s+expect([+%-]%d+):%s*(.*)", function(offset, text) 156 local msg_lineno = mk_lineno + tonumber(offset) 157 158 local i = exp_it 159 while i <= #exp_lines and text ~= exp_lines[i].text do 160 i = i + 1 161 end 162 163 if i <= #exp_lines and exp_lines[i].lineno == msg_lineno then 164 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1) 165 exp_lines[i].text = "" 166 exp_it = i + 1 167 elseif i <= #exp_lines then 168 print_error("error: %s:%d: expect%+d must be expect%+d", 169 mk_fname, mk_lineno, tonumber(offset), 170 exp_lines[i].lineno - mk_lineno) 171 else 172 print_error("error: %s:%d: %s:%d+ must contain '%s'", 173 mk_fname, mk_lineno, exp_fname, exp_it, text) 174 end 175 end) 176 177 match("^#%s+expect[+%-:]", function() 178 print_error("error: %s:%d: invalid \"expect\" line: %s", 179 mk_fname, mk_lineno, mk_line) 180 end) 181 182 end 183 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, #exp_lines) 184end 185 186for _, fname in ipairs(arg) do 187 check_mk(fname) 188end 189os.exit(not had_errors) 190