1 # rule strings: a `match` whose arms are string literals (`case "foo":`) with 2 # more than 64 literal characters in all. Each string arm lowers into nested 3 # per-char tests, and the checker's time and memory grow with the characters 4 # (bend 2.0.16: 45 characters take 0.8 s and 0.35 GB, 100 take 2 s and 1 GB, 5 # 480 take 12 s and 4.8 GB; pi-bend BEND-001/016 was 16 long event names). 6 # The number of arms barely matters, so only the characters count. Classify 7 # the string once through a lookup table into a sum type, and match on that. 8 # Only a case's first match column is read: string literals in a later 9 # column of `match a b:` do not count. A literal with no closing quote (an 10 # unterminated string) is not a string arm. 11 import Base 12 import ../../src.bend as Src 13 import ../../finding.bend as F 14 import ../../syntax/lex.bend as Lex 15 import ../../syntax/tree.bend as Tree 16 import ../../lazy/lazy.bend as Lazy 17 import ../tokens.bend as T 18 19 # the string arms of a match, and their characters 20 type Tally is Data: 21 Tally{arms: U32, chars: U32} 22 23 # does the text end on a closing quote, its escapes read as a backslash and 24 # the one char after it? esc: the char before was an unescaped backslash; 25 # shut: it was an unescaped quote 26 def closes(cs: List<&2, Char>, +esc: Bool, +shut: Bool) -> Bool: 27 match cs: 28 case Nil{}: 29 shut 30 case Con{ch, rest}: 31 +code = Char.to_u32(ch) 32 closes(rest, Bool.and(Bool.not(esc), U32.is_eq(code, 92)), Bool.and(Bool.not(esc), U32.is_eq(code, 34))) 33 34 # past its opening char, do the chars end on a closing quote? 35 def opened(cs: List<&2, Char>) -> Bool: 36 match cs: 37 case Nil{}: 38 False{} 39 case Con{_q, rest}: 40 closes(rest, False{}, False{}) 41 42 # is a string token closed: past its opening quote, does it end on a quote no 43 # backslash escapes? `"\"`, an unclosed string at the end of the file, is not 44 def terminated(+tt: String) -> Bool: 45 opened(String.to_list(tt)) 46 47 # the characters of the string a case pattern opens with, if it does and the 48 # string is closed 49 def literal(kids: Tree.Node) -> Maybe<&2, U32>: 50 match kids: 51 case Tree.NCons{Tree.Leaf{Lex.Tok{Lex.TStr{}, +t, l, c}}, rest}: 52 +w = T.width(t) 53 Bool.pick(Maybe<&2, U32>, terminated(t), Some{(w - 2 : U32)}, None{}) 54 case other: 55 None{} 56 57 # one for a string arm, else zero 58 def one(mm: Maybe<&2, U32>) -> U32: 59 match mm: 60 case None{}: 61 0 62 case Some{n}: 63 1 64 65 # a string arm's characters, else zero 66 def size(mm: Maybe<&2, U32>) -> U32: 67 match mm: 68 case None{}: 69 0 70 case Some{n}: 71 n 72 73 # the string arms among a match's statements 74 def tally(body: Tree.Node, +nn: U32, +chars: U32) -> Tally: 75 match body: 76 case Tree.NCons{Tree.Stmt{Tree.SCase{}, kids, b}, rest}: 77 +m = literal(T.pattern(kids)) 78 +k = one(m) 79 +ch = size(m) 80 tally(rest, (nn + k : U32), (chars + ch : U32)) 81 case Tree.NCons{h, rest}: 82 tally(rest, nn, chars) 83 case other: 84 Tally{nn, chars} 85 86 # a finding when the match's strings are too long in all 87 def report(tt: Tally, +path: String, +ll: U32, +cc: U32) -> List<&2, F.Finding>: 88 Tally{+n, +chars} = tt 89 Bool.pick(List<&2, F.Finding>, U32.is_gt(chars, 64), 90 [F.Finding{path, ll, cc, 5, "strings", "This match has " ++ U32.show(n) ++ " string-literal cases totalling " ++ U32.show(chars) 91 ++ " characters, over the limit of 64, which blows up compile time and memory; map the string to a sum type once and match on that."}], 92 []) 93 94 # a statement: a match is tallied 95 def on_stmt(+kids: Tree.Node, +body: Tree.Node, +path: String) -> List<&2, F.Finding>: 96 +hit = T.keyword(kids, "match") 97 Lazy.stop(List<&2, F.Finding>, Bool.not(hit), [], 98 _u => report(tally(body, 0, 0), path, Tree.line(kids), Tree.col(kids))) 99 100 # every statement, at any depth 101 def walk(nn: Tree.Node, +path: String) -> List<&2, F.Finding>: 102 match nn: 103 case Tree.NCons{Tree.Stmt{kind, kids, +body}, rest}: 104 List.concat(&2, F.Finding, [on_stmt(kids, body, path), walk(body, path), walk(rest, path)]) 105 case Tree.NCons{h, rest}: 106 walk(rest, path) 107 case other: 108 Nil{} 109 110 # the rule 111 def check(ss: Src.Src) -> List<&2, F.Finding>: 112 Src.Src{path, text, toks, tree, bound, items} = ss 113 walk(tree, path)