check-expect.lua revision 1.16 1 #! /usr/bin/lua
2 -- $NetBSD: check-expect.lua,v 1.16 2025/07/01 04:24:20 rillig Exp $
3
4 --[[
5
6 usage: lua ./check-expect.lua *.mk
7
8 Check that the various 'expect' comments in the .mk files produce the
9 expected 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
35 local had_errors = false
36 ---@param fmt string
37 local function print_error(fmt, ...)
38 print(fmt:format(...))
39 had_errors = true
40 end
41
42
43 ---@return nil | string[]
44 local 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
58 end
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[]
69 local 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
85 end
86
87 ---@param exp_lines ExpLine[]
88 local 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
96 end
97
98 local 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 mk_line:gsub("^#%s+expect%-not:%s*(.*)", function(text)
112 for exp_lineno, exp_line in ipairs(exp_lines) do
113 if exp_line.text:find(text, 1, true) then
114 print_error("error: %s:%d: %s:%d must not contain '%s'",
115 mk_fname, mk_lineno, exp_fname, exp_lineno, text)
116 end
117 end
118 end)
119
120 mk_line:gsub("^#%s+expect%-not%-matches:%s*(.*)", function(pattern)
121 for exp_lineno, exp_line in ipairs(exp_lines) do
122 if exp_line.text:find(pattern) then
123 print_error("error: %s:%d: %s:%d must not match '%s'",
124 mk_fname, mk_lineno, exp_fname, exp_lineno, pattern)
125 end
126 end
127 end)
128
129 mk_line:gsub("^#%s+expect:%s*(.*)", function(text)
130 local i = exp_it
131 while i <= #exp_lines and text ~= exp_lines[i].text do
132 i = i + 1
133 end
134 if i <= #exp_lines then
135 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
136 exp_lines[i].text = ""
137 exp_it = i + 1
138 else
139 print_error("error: %s:%d: '%s:%d+' must contain '%s'",
140 mk_fname, mk_lineno, exp_fname, exp_it, text)
141 end
142 end)
143
144 if mk_line:match("^#%s*expect%-reset$") then
145 exp_it = 1
146 end
147
148 mk_line:gsub("^#%s+expect([+%-]%d+):%s*(.*)", function(offset, text)
149 local msg_lineno = mk_lineno + tonumber(offset)
150
151 local i = exp_it
152 while i <= #exp_lines and text ~= exp_lines[i].text do
153 i = i + 1
154 end
155
156 if i <= #exp_lines and exp_lines[i].lineno == msg_lineno then
157 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, i - 1)
158 exp_lines[i].text = ""
159 exp_it = i + 1
160 elseif i <= #exp_lines then
161 print_error("error: %s:%d: expect%+d must be expect%+d",
162 mk_fname, mk_lineno, tonumber(offset),
163 exp_lines[i].lineno - mk_lineno)
164 else
165 print_error("error: %s:%d: %s:%d+ must contain '%s'",
166 mk_fname, mk_lineno, exp_fname, exp_it, text)
167 end
168 end)
169 end
170 detect_missing_expect_lines(exp_fname, exp_lines, exp_it, #exp_lines)
171 end
172
173 for _, fname in ipairs(arg) do
174 check_mk(fname)
175 end
176 os.exit(not had_errors)
177