src/rules/suspicious/prefix.bend
bolt@1.26.0.0 · 293 lines · 0 definitions
1# rule prefix: a String.take compared to a literal of its length:
2# `String.eq(String.take(s, k), "..")` or `String.eq("..", String.take(s,
3# k))`. Among the significant tokens (spaces, newlines and comments dropped):
4# a dotted name `String.eq`, `(`, `String.take` and `(`, then the take's
5# first argument s, any tokens up to the first comma outside the brackets
6# they open (a call such as `String.drop(m, 4n)` is one argument), then a
7# Nat literal k, `)`, `,`, a string literal and `)`; or the dotted name
8# `String.eq`, `(`, the string literal, `,`, `String.take`, `(`, s, the
9# comma, k, `)` and `)`. Only when k's text is the literal's length (an
10# escape, `\n` or `\u{1B}`, one char) written in decimal with an `n`, as
11# `4n` for `"ERR "`. A bracket that closes the take before any such comma
12# ends the scan with no finding. One finding, on the `String.eq` token, for
13# each such run, and nothing else: a take of another length is another bug.
14# String.take copies k chars into a new String only to compare them;
15# `String.starts_with(s, "ERR ")` (Base) walks s in place and gives the same
16# answer for every s, False when s is shorter, s any expression. A LAWS.bend
17# or a PROOF.bend, which never runs, is exempt. A comment is never read, and
18# a string literal is one token, so the shape written inside either is not
19# the shape.
20import Base
21import bolt@1.26.0.0/src/src.bend as Src
22import bolt@1.26.0.0/src/paths.bend as Paths
23import bolt@1.26.0.0/src/role.bend as Role
24import bolt@1.26.0.0/src/lazy/lazy.bend as Lazy
25import bolt@1.26.0.0/src/finding.bend as F
26import bolt@1.26.0.0/src/syntax/lex.bend as Lex
27import bolt@1.26.0.0/src/rules/tokens.bend as T
28
29# where the length of a string literal's text is: before its opening
30# quote, in plain text, just after a backslash, inside a `\u{..}`, or past
31# its closing quote
32type Mode is Data:
33 MOpen{}
34 MPlain{}
35 MEsc{}
36 MUni{}
37 MDone{}
38
39# is the char the code point?
40def is_cp(+cc: Char, +xx: U32) -> Bool:
41 U32.is_eq(Char.to_u32(cc), xx)
42
43# what the char adds to the length: one in plain text, but not the closing
44# quote; nothing anywhere else
45def len.add(mm: Mode, +cc: Char) -> Nat:
46 match mm:
47 case MPlain{}:
48 Bool.pick(Nat, is_cp(cc, 34), 0n, 1n)
49 case other:
50 0n
51
52# where the char leaves the length: a backslash starts an escape, a quote
53# closes the literal, `u` after a backslash opens a `\u{..}` and `}` closes
54# it
55def len.next(mm: Mode, +cc: Char) -> Mode:
56 match mm:
57 case MOpen{}:
58 MPlain{}
59 case MPlain{}:
60 Bool.pick(Mode, is_cp(cc, 92), MEsc{}, Bool.pick(Mode, is_cp(cc, 34), MDone{}, MPlain{}))
61 case MEsc{}:
62 Bool.pick(Mode, is_cp(cc, 117), MUni{}, MPlain{})
63 case MUni{}:
64 Bool.pick(Mode, is_cp(cc, 125), MPlain{}, MUni{})
65 case MDone{}:
66 MDone{}
67
68# the length of the rest of a literal's text, from where it is
69def len.go(tt: String, +mm: Mode) -> Nat:
70 match tt:
71 case SNil{}:
72 0n
73 case SCon{+cc, rest}:
74 Nat.add(len.add(mm, cc), len.go(rest, len.next(mm, cc)))
75
76# the length of a string literal, its text as the lexer keeps it (quotes
77# and escapes as written): each escape one char
78def len(tt: String) -> Nat:
79 len.go(tt, MOpen{})
80
81# is the Nat literal's text the literal's length?
82def fits(+kk: String, lit: String) -> Bool:
83 String.eq(kk, Nat.show(len(lit)) ++ "n")
84
85# what the finding says: the take, and the starts_with that replaces it
86def say(+ss: String, +kk: String, +lit: String) -> String:
87 "String.eq(String.take(" ++ ss ++ ", " ++ kk ++ "), " ++ lit ++ ") copies " ++ kk
88 ++ " chars only to compare them; String.starts_with(" ++ ss ++ ", " ++ lit ++ ") gives the same answer in place."
89
90# where the scan of a take's first argument stands: inside it at a depth of
91# brackets it opened, just past the comma that ends it, or at the bracket
92# that closes the take before any such comma
93type Skip is Data:
94 SGo{depth: Nat}
95 SComma{}
96 SClose{}
97
98# the argument as the finding writes it, and the tokens after its comma
99type Arg is Data:
100 Arg{text: String, rest: List<&2, Lex.Tok>}
101
102# a comma at the depth: the argument's end at depth zero, else inside it
103def skip.comma(dd: Nat) -> Skip:
104 match dd:
105 case 0n:
106 SComma{}
107 case 1n+p:
108 SGo{1n+p}
109
110# a closing bracket at the depth: the take's own at depth zero, else one
111# bracket the argument opened
112def skip.close(dd: Nat) -> Skip:
113 match dd:
114 case 0n:
115 SClose{}
116 case 1n+p:
117 SGo{p}
118
119# where a token of the kind leaves the scan at the depth
120def skip.step(kk: Lex.TokKind, dd: Nat) -> Skip:
121 match kk:
122 case Lex.TComma{}:
123 skip.comma(dd)
124 case Lex.TOpen{}:
125 SGo{1n+dd}
126 case Lex.TClose{}:
127 skip.close(dd)
128 case other:
129 SGo{dd}
130
131# a token as the finding writes it: a comma with a space after it, an
132# operator with one on each side
133def piece(kk: Lex.TokKind, +tt: String) -> String:
134 match kk:
135 case Lex.TComma{}:
136 ", "
137 case Lex.TOp{}:
138 " " ++ tt ++ " "
139 case other:
140 tt
141
142# what a token adds to the argument's text, given where it leaves the scan:
143# nothing for the comma that ends the argument
144def skip.piece(sk: Skip, kk: Lex.TokKind, +tt: String) -> String:
145 match sk:
146 case SComma{}:
147 ""
148 case other:
149 piece(kk, tt)
150
151# the take's first argument, scanned from where the scan stands: what it
152# says (acc: its pieces so far, last first) and the tokens after the comma
153# that ends it; none when the take
154# closes first or the tokens run out
155def skip(toks: List<&2, Lex.Tok>, sk: Skip, +acc: List<&2, String>) -> Maybe<&2, Arg>:
156 match toks:
157 case Nil{}:
158 None{}
159 case Con{Lex.Tok{+k, +t, l, c}, rest}:
160 match sk:
161 case SComma{}:
162 Some{Arg{String.join(List.reverse(&2, String, acc), ""), Lex.Tok{k, t, l, c} <> rest}}
163 case SClose{}:
164 None{}
165 case SGo{dd}:
166 +nx = skip.step(k, dd)
167 skip(rest, nx, skip.piece(nx, k, t) <> acc)
168
169# a dotted name: what `String.eq` is
170def dotted(kk: Lex.TokKind) -> Bool:
171 match kk:
172 case Lex.TDotted{}:
173 True{}
174 case other:
175 False{}
176
177# after the argument's comma, k, `)`, `,`, a string literal and `)`: is k
178# the literal's length?
179def tail_a.ok(+kk: String, +t7: String, +t8: String, k9: Lex.TokKind, +t9: String, +t10: String) -> Bool:
180 Bool.and(String.eq(t7, ")"), Bool.and(String.eq(t8, ","), Bool.and(Lex.is_str(k9),
181 Bool.and(String.eq(t10, ")"), fits(kk, t9)))))
182
183# the take-first shape's end, after the argument: what the finding says
184def tail_a(ma: Maybe<&2, Arg>) -> Maybe<&2, String>:
185 match ma:
186 case None{}:
187 None{}
188 case Some{Arg{+ss, r}}:
189 match r:
190 case Con{Lex.Tok{k6, +t6, l6, c6}, Con{Lex.Tok{k7, +t7, l7, c7}, Con{Lex.Tok{k8, +t8, l8, c8},
191 Con{Lex.Tok{k9, +t9, l9, c9}, Con{Lex.Tok{k10, +t10, l10, c10}, rest}}}}}:
192 Bool.pick(Maybe<&2, String>, tail_a.ok(t6, t7, t8, k9, t9, t10), Some{say(ss, t6, t9)}, None{})
193 case other:
194 None{}
195
196# after the argument's comma, k, `)` and `)`: is k the literal's length?
197def tail_b.ok(+kk: String, +t9: String, +t10: String, +lit: String) -> Bool:
198 Bool.and(String.eq(t9, ")"), Bool.and(String.eq(t10, ")"), fits(kk, lit)))
199
200# the literal-first shape's end, after the argument: what the finding says
201def tail_b(ma: Maybe<&2, Arg>, +lit: String) -> Maybe<&2, String>:
202 match ma:
203 case None{}:
204 None{}
205 case Some{Arg{+ss, r}}:
206 match r:
207 case Con{Lex.Tok{k8, +t8, l8, c8}, Con{Lex.Tok{k9, +t9, l9, c9}, Con{Lex.Tok{k10, +t10, l10, c10}, rest}}}:
208 Bool.pick(Maybe<&2, String>, tail_b.ok(t8, t9, t10, lit), Some{say(ss, t8, lit)}, None{})
209 case other:
210 None{}
211
212# a dotted `String.eq`, `(`, `String.take` and `(`?
213def head_a(k0: Lex.TokKind, +t0: String, +t1: String, +t2: String, +t3: String) -> Bool:
214 Bool.and(dotted(k0), Bool.and(String.eq(t0, "String.eq"), Bool.and(String.eq(t1, "("),
215 Bool.and(String.eq(t3, "("), String.eq(t2, "String.take")))))
216
217# the take-first shape, its head read: the end, after the argument
218def at_a.go(ok: Bool, r3: List<&2, Lex.Tok>) -> Maybe<&2, String>:
219 match ok:
220 case True{}:
221 tail_a(skip(r3, SGo{0n}, []))
222 case False{}:
223 None{}
224
225# a token of kind k0 and text t0, then these: the take-first shape?
226def at_a(k0: Lex.TokKind, +t0: String, toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
227 match toks:
228 case Con{Lex.Tok{k1, +t1, l1, c1}, Con{Lex.Tok{k2, +t2, l2, c2}, Con{Lex.Tok{k3, +t3, l3, c3}, r3}}}:
229 at_a.go(head_a(k0, t0, t1, t2, t3), r3)
230 case other:
231 None{}
232
233# a dotted `String.eq`, `(`, a string literal, `,`, `String.take` and `(`?
234def head_b(k0: Lex.TokKind, +t0: String, +t1: String, k2: Lex.TokKind, +t3: String, +t4: String, +t5: String) -> Bool:
235 Bool.and(dotted(k0), Bool.and(String.eq(t0, "String.eq"), Bool.and(String.eq(t1, "("), Bool.and(Lex.is_str(k2),
236 Bool.and(String.eq(t3, ","), Bool.and(String.eq(t4, "String.take"), String.eq(t5, "(")))))))
237
238# the literal-first shape, its head read: the end, after the argument
239def at_b.go(ok: Bool, r5: List<&2, Lex.Tok>, +lit: String) -> Maybe<&2, String>:
240 match ok:
241 case True{}:
242 tail_b(skip(r5, SGo{0n}, []), lit)
243 case False{}:
244 None{}
245
246# a token of kind k0 and text t0, then these: the literal-first shape?
247def at_b(k0: Lex.TokKind, +t0: String, toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
248 match toks:
249 case Con{Lex.Tok{k1, +t1, l1, c1}, Con{Lex.Tok{k2, +t2, l2, c2}, Con{Lex.Tok{k3, +t3, l3, c3},
250 Con{Lex.Tok{k4, +t4, l4, c4}, Con{Lex.Tok{k5, +t5, l5, c5}, r5}}}}}:
251 at_b.go(head_b(k0, t0, t1, k2, t3, t4, t5), r5, t2)
252 case other:
253 None{}
254
255# the first message, else the second
256def either(ma: Maybe<&2, String>, mb: Maybe<&2, String>) -> Maybe<&2, String>:
257 match ma:
258 case None{}:
259 mb
260 case Some{x}:
261 Some{x}
262
263# a token of kind k0 and text t0, then these: either shape? what the finding
264# says when they make one
265def at(+k0: Lex.TokKind, +t0: String, +toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
266 either(at_a(k0, t0, toks), at_b(k0, t0, toks))
267
268# the finding a run makes, if any, before the rest
269def hit(mm: Maybe<&2, String>, +path: String, +ll: U32, +cc: U32, more: List<&2, F.Finding>) -> List<&2, F.Finding>:
270 match mm:
271 case None{}:
272 more
273 case Some{msg}:
274 F.Finding{path, ll, cc, 9, "prefix", msg} <> more
275
276# every run of either shape among the tokens, in order
277def walk(toks: List<&2, Lex.Tok>, +path: String) -> List<&2, F.Finding>:
278 match toks:
279 case Nil{}:
280 Nil{}
281 case Con{Lex.Tok{+k, +t, +l, +c}, +rest}:
282 hit(at(k, t, rest), path, l, c, walk(rest, path))
283
284# the rule
285def check(ss: Src.Src) -> List<&2, F.Finding>:
286 Src.Src{+path, text, toks, tree, bound, items} = ss
287 Lazy.stop(List<&2, F.Finding>, Paths.is_law_file(path), [], _u => walk(T.sig(toks), path))
288
289# the rule, told the file's role (src/role.bend): a law or proof file is
290# skipped
291def check.role(rr: Role.Role, ss: Src.Src) -> List<&2, F.Finding>:
292 Src.Src{path, text, toks, tree, bound, items} = ss
293 Lazy.stop(List<&2, F.Finding>, Role.is_law_file(rr), [], _u => walk(T.sig(toks), path))