src/rules/suspicious/strict.bend

bolt@1.20.0.0 · 130 lines · 0 definitions

1# rule strict: a def calls itself inside an operand of `Bool.and(..)` or
2# `Bool.or(..)`, or in a comma-separated stretch holding `&&` or `||` (either
3# side of the operator), matched by that exact text
4# (a qualified `Base.Bool.or` is not seen). They are functions, not
5# short-circuits: both sides always run (timed: `False{} && f(x)` costs all of
6# f), so `ok(h) && go(rest)` walks the whole input after the answer is known,
7# and the pending operator keeps a frame per step (the JS lane overflowed at
8# ~64K this way). Match on the Bool in a helper, or carry it as an argument:
9# `go(rest, ok(h))`. A self-call bound by a let above is not flagged: binding
10# it says both run. Nor is one in a lambda body (`_u => go(rest)`, as
11# `Lazy.or_else` takes): a thunk does not run eagerly, unless the body itself
12# holds `&&` or `||`. A `=>` ends the stretch on its left, so a body's `&&` or
13# `||` never reaches back past it: `go(x) <> _u => a && b` is not flagged.
14import Base
15import bolt@1.20.0.0/src/src.bend as Src
16import bolt@1.20.0.0/src/finding.bend as F
17import bolt@1.20.0.0/src/syntax/lex.bend as Lex
18import bolt@1.20.0.0/src/syntax/tree.bend as Tree
19import bolt@1.20.0.0/src/rules/calls.bend as Calls
20import bolt@1.20.0.0/src/lazy/lazy.bend as Lazy
21
22# is the operator `&&` or `||`?
23def logic(+tt: String) -> Bool:
24  Bool.or(String.eq(tt, "&&"), String.eq(tt, "||"))
25
26# a comma or a `=>`: the end of a stretch
27def seg_op.ends(kk: Lex.TokKind) -> Bool:
28  match kk:
29    case Lex.TComma{}:
30      True{}
31    case Lex.TLam{}:
32      True{}
33    case other:
34      False{}
35
36# this leaf's `&&` or `||`, or one further on in the stretch
37def seg_op.or(+here: Bool, more: Bool) -> Bool:
38  Bool.or(here, more)
39
40# does the chain, up to its first top-level comma or `=>`, hold `&&` or `||`?
41def seg_op(nn: Tree.Node) -> Bool:
42  match nn:
43    case Tree.NCons{+h, rest}:
44      Lazy.stop(Bool, Calls.kind.leaf(~seg_op.ends, h), False{},
45        _u => seg_op.or(Bool.and(Calls.kind.leaf(~Calls.kind.oper, h), logic(Calls.leaf.text(h))), seg_op(rest)))
46    case other:
47      False{}
48
49# is the group after a name an operand: the stretch is hot, or it is the `(`
50# of `Bool.and` or `Bool.or`
51def inner(+hot: Bool, +oo: String, +tt: String) -> Bool:
52  Bool.or(hot, Bool.and(String.eq(oo, "("), Bool.or(String.eq(tt, "Bool.and"), String.eq(tt, "Bool.or"))))
53
54# a name, then a group: a finding when the name is the def, the group a `(`
55# call and the stretch hot, then what the group and the rest report
56def call(
57  +tt: String,
58  +ll: U32,
59  +cc: U32,
60  +oo: String,
61  +name: String,
62  +path: String,
63  +hot: Bool,
64  kids: List<&2, F.Finding>,
65  rest: List<&2, F.Finding>
66) -> List<&2, F.Finding>:
67  +more = List.concat(&2, F.Finding, [kids, rest])
68  Bool.pick(List<&2, F.Finding>, Bool.and(Bool.and(String.eq(oo, "("), String.eq(tt, name)), hot),
69    F.Finding{path, ll, cc, U32.from_nat(String.length(name)), "strict",
70      name ++ " calls itself inside Bool.and or Bool.or, which evaluate both sides, so there is no short-circuit; match on the Bool in a helper."} <> more,
71    more)
72
73# a leaf read by its kind: past a comma, past a `=>`, or past any other
74# token; only the branch taken runs
75def leaf(
76  kk: Lex.TokKind,
77  comma: Unit -> List<&2, F.Finding>,
78  lam: Unit -> List<&2, F.Finding>,
79  plain: Unit -> List<&2, F.Finding>
80) -> List<&2, F.Finding>:
81  match kk:
82    case Lex.TComma{}:
83      comma(Unit{})
84    case Lex.TLam{}:
85      lam(Unit{})
86    case _k:
87      plain(Unit{})
88
89# the self-calls in operands: outer is whether the enclosing group is an
90# operand, hot whether the current comma segment is; a `=>` starts a lambda
91# body, hot only by its own `&&`/`||`. A leaf is read by its kind in `leaf`,
92# once the cell after it is known
93def walk(nn: Tree.Node, +name: String, +path: String, +outer: Bool, +hot: Bool) -> List<&2, F.Finding>:
94  match nn:
95    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, +l, +c}}, +rest}:
96      match rest:
97        case Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, r2}:
98          leaf(k, _u => walk(rest, name, path, outer, Bool.or(outer, seg_op(rest))),
99            _v => walk(rest, name, path, outer, seg_op(rest)),
100            _w => call(t, l, c, o, name, path, hot,
101              walk(kids, name, path, inner(hot, o, t), Bool.or(inner(hot, o, t), seg_op(kids))),
102              walk(r2, name, path, outer, hot)))
103        case _r:
104          leaf(k, _u => walk(rest, name, path, outer, Bool.or(outer, seg_op(rest))),
105            _v => walk(rest, name, path, outer, seg_op(rest)), _w => walk(rest, name, path, outer, hot))
106    case Tree.NCons{Tree.Group{open, +kids, close}, rest}:
107      List.concat(&2, F.Finding, [walk(kids, name, path, hot, Bool.or(hot, seg_op(kids))),
108        walk(rest, name, path, outer, hot)])
109    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:
110      List.concat(&2, F.Finding, [walk(kids, name, path, False{}, seg_op(kids)),
111        walk(body, name, path, False{}, False{}),
112        walk(rest, name, path, outer, hot)])
113    case Tree.NCons{h, rest}:
114      walk(rest, name, path, outer, hot)
115    case other:
116      Nil{}
117
118def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>:
119  match ds:
120    case Nil{}:
121      List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))
122    case Con{Calls.Def{+name, +sig, body}, rest}:
123      check.go(rest, path,
124        Lazy.stop(List<&2, F.Finding>, Calls.exempt(path, sig), [],
125          _u => walk(body, name, path, False{}, False{})) <> acc)
126
127# the rule
128def check(ss: Src.Src) -> List<&2, F.Finding>:
129  Src.Src{path, text, toks, tree, bound, items} = ss
130  check.go(Calls.defs(tree), path, [])