~/bend-docscommunity

src/rules/suspicious/concat.bend source

src/rules/suspicious/concat.bend on the hub · documented module

# rule concat: a def passes itself a parameter grown at the end, `p ++ x` or# `List.append(.., p, ys)` (String.append(p, ..) too), as the argument in p's# own position, the slot it carries; p grown into another slot is not a# finding. The argument is that append written in place, inside any number# of parentheses (`(p ++ x)`), or a lone name read from a let: the nearest# `q = ..` or `+q = ..` of that name before the call in its block or an# enclosing one, whose right side is such an append (a later let of the name# shadows it; other binders, a typed or destructuring let and a do-bind do# not count). A String and a List are cons lists, so appending copies all of# p: the loop is quadratic, with right output (night-train's text step cost# three times the render; rootagi's JSON stringify). Prepend (`x <> acc`)# and reverse once at the end, build with `h <> go(t)`, or gather pieces and# join them once.import Baseimport ../../src.bend as Srcimport ../../finding.bend as Fimport ../../syntax/lex.bend as Leximport ../../syntax/tree.bend as Treeimport ../calls.bend as Callsimport ../../lazy/lazy.bend as Lazy# a leaf that is a lowercase name: its token. The rule matches a leaf's kind# here and in bound.is_eq only; the rest reads kinds through thesedef lone.name(nn: Tree.Node) -> Maybe<&2, Lex.Tok>:  match nn:    case Tree.Leaf{Lex.Tok{Lex.TName{}, t, l, c}}:      Some{Lex.Tok{Lex.TName{}, t, l, c}}    case other:      None{}# a chain that is exactly one lowercase name: its tokendef lone(nn: Tree.Node) -> Maybe<&2, Lex.Tok>:  match nn:    case Tree.NCons{h, Tree.NNil{}}:      lone.name(h)    case other:      None{}# the list a call appends onto: List.append's first list, String.append's firstdef appended(+tt: String, as: List<&2, Tree.Node>) -> Maybe<&2, Lex.Tok>:  +list = String.eq(tt, "List.append")  +str = String.eq(tt, "String.append")  Bool.pick(Maybe<&2, Lex.Tok>, Bool.or(list, str), lone(Calls.arg(as, Bool.pick(Nat, list, 2n, 0n))), None{})# a leaf, then a group: the call's appenddef onto.call(hh: Tree.Node, kids: Tree.Node) -> Maybe<&2, Lex.Tok>:  match hh:    case Tree.Leaf{Lex.Tok{k, +t, l, c}}:      appended(t, Calls.args(kids))    case other:      None{}# a first cell, then the second: a name then `++`, or a calldef onto.next(+hh: Tree.Node, h2: Tree.Node) -> Maybe<&2, Lex.Tok>:  match h2:    case Tree.Leaf{Lex.Tok{k, +o, _l, _c}}:      Bool.pick(Maybe<&2, Lex.Tok>, Bool.and(Calls.kind.oper(k), String.eq(o, "++")), lone.name(hh), None{})    case Tree.Group{open, kids, close}:      onto.call(hh, kids)    case other:      None{}# the name an argument appends onto, `p ++ ..` or an append call, inside any# number of parenthesesdef onto(aa: Tree.Node) -> Maybe<&2, Lex.Tok>:  match aa:    case Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, kids, _}, Tree.NNil{}}:      Lazy.stop(Maybe<&2, Lex.Tok>, Bool.not(String.eq(o, "(")), None{}, _u => onto(kids))    case Tree.NCons{hh, Tree.NCons{h2, rest}}:      onto.next(hh, h2)    case other:      None{}# a let of one plain name in scope: the name, and what its right side# appends ontotype Let is Data:  Let{name: String, onto: Maybe<&2, Lex.Tok>}# what the nearest let of the name appends onto; nothing when no let binds itdef find(lets: List<&2, Let>, +name: String) -> Maybe<&2, Lex.Tok>:  match lets:    case Nil{}:      None{}    case Con{Let{+nm, mm}, rest}:      Lazy.stop(Maybe<&2, Lex.Tok>, String.eq(nm, name), mm, _u => find(rest, name))# what the let of a lone name appends ontodef via.of(mm: Maybe<&2, Lex.Tok>, +lets: List<&2, Let>) -> Maybe<&2, Lex.Tok>:  match mm:    case None{}:      None{}    case Some{Lex.Tok{k, +t, l, c}}:      find(lets, t)# the name an argument appends onto, when it is a lone name read from a letdef via(aa: Tree.Node, +lets: List<&2, Let>) -> Maybe<&2, Lex.Tok>:  via.of(lone(aa), lets)# the first maybe when it holds a token, else the seconddef either(aa: Maybe<&2, Lex.Tok>, bb: Maybe<&2, Lex.Tok>) -> Maybe<&2, Lex.Tok>:  match aa:    case None{}:      bb    case Some{tok}:      Some{tok}# the name an argument appends onto, in place or through a letdef seen(+aa: Tree.Node, +lets: List<&2, Let>) -> Maybe<&2, Lex.Tok>:  either(onto(aa), via(aa, lets))# an `=`def bound.eq(kk: Lex.TokKind) -> Bool:  match kk:    case Lex.TEq{}:      True{}    case other:      False{}# a name, then `=`, then the right side: the letdef bound.of(mm: Maybe<&2, Lex.Tok>, +h2: Tree.Node, rhs: Tree.Node) -> Maybe<&2, Let>:  match mm:    case None{}:      None{}    case Some{Lex.Tok{k, +t, l, c}}:      Lazy.stop(Maybe<&2, Let>, Bool.not(Calls.kind.leaf(~bound.eq, h2)), None{}, _u => Some{Let{t, onto(rhs)}})# a plain name, then `=`, then the right side: the letdef bound.named(+hh: Tree.Node, +h2: Tree.Node, rhs: Tree.Node) -> Maybe<&2, Let>:  bound.of(lone.name(hh), h2, rhs)# `+`, then a plain name, `=` and the right side: the letdef bound.plus(+h1: Tree.Node, +h2: Tree.Node, rest: Tree.Node) -> Maybe<&2, Let>:  match rest:    case Tree.NCons{h3, rhs}:      Lazy.stop(Maybe<&2, Let>, Bool.not(String.eq(Calls.leaf.text(h1), "+")), None{},        _u => bound.named(h2, h3, rhs))    case other:      None{}# the let a statement's own tokens make, when it binds one plain name with# `=`, bare or `+`def bound(kids: Tree.Node) -> Maybe<&2, Let>:  match kids:    case Tree.NCons{+h1, Tree.NCons{+h2, +rest}}:      Lazy.either(Maybe<&2, Let>, Calls.kind.leaf(~Calls.kind.oper, h1), _u => bound.plus(h1, h2, rest),        _v => bound.named(h1, h2, rest))    case other:      None{}# the lets in scope after one more, when there is onedef push(mm: Maybe<&2, Let>, +lets: List<&2, Let>) -> List<&2, Let>:  match mm:    case None{}:      lets    case Some{lt}:      lt <> lets# a finding when the name is the parameter of the argument's own slotdef hit(  mm: Maybe<&2, Lex.Tok>,  +slot: String,  +path: String,  +acc: List<&2, F.Finding>) -> List<&2, F.Finding>:  match mm:    case None{}:      acc    case Some{Lex.Tok{k, +t, l, c}}:      Bool.pick(List<&2, F.Finding>, String.eq(t, slot),        F.Finding{path, l, c, U32.from_nat(String.length(t)), "concat",          t ++ " is appended to on every step, which copies it each time and makes the loop quadratic; prepend and reverse once at the end, or build with <>."} <> acc,        acc)# the findings on a self-call's arguments, each against the parameter in its# position (params, from the argument's own slot on)def hits(  as: List<&2, Tree.Node>,  +params: List<&2, String>,  +lets: List<&2, Let>,  +path: String,  acc: List<&2, F.Finding>) -> List<&2, F.Finding>:  match as:    case Nil{}:      List.reverse(&2, F.Finding, acc)    case Con{+a, rest}:      +slot = Maybe.default(&2, String, List.head(&2, String, params), "")      hits(rest, List.tail(&2, String, params), lets, path, hit(seen(a, lets), slot, path, acc))# the lets a statement leaves in scope for the statements after itdef after(kind: Tree.StmtKind, +kids: Tree.Node, +lets: List<&2, Let>) -> List<&2, Let>:  match kind:    case Tree.SLet{}:      push(bound(kids), lets)    case other:      lets# every self-call's arguments, with the lets in scopedef walk(  nn: Tree.Node,  +name: String,  +params: List<&2, String>,  +lets: List<&2, Let>,  +path: String) -> List<&2, F.Finding>:  match nn:    case Tree.NCons{Tree.Leaf{Lex.Tok{k, +t, l, c}}, Tree.NCons{Tree.Group{Lex.Tok{_, +o, _, _}, +kids, _}, rest}}:      +me = Bool.and(String.eq(o, "("), String.eq(t, name))      List.concat(&2, F.Finding, [        Lazy.stop(List<&2, F.Finding>, Bool.not(me), [], _u => hits(Calls.args(kids), params, lets, path, [])),        walk(kids, name, params, lets, path), walk(rest, name, params, lets, path)])    case Tree.NCons{Tree.Group{open, kids, close}, rest}:      List.concat(&2, F.Finding, [walk(kids, name, params, lets, path), walk(rest, name, params, lets, path)])    case Tree.NCons{Tree.Stmt{kind, +kids, body}, rest}:      List.concat(&2, F.Finding, [walk(kids, name, params, lets, path), walk(body, name, params, lets, path),        walk(rest, name, params, after(kind, kids, lets), path)])    case Tree.NCons{h, rest}:      walk(rest, name, params, lets, path)    case other:      Nil{}def check.go(ds: List<&2, Calls.Def>, +path: String, acc: List<&2, List<&2, F.Finding>>) -> List<&2, F.Finding>:  match ds:    case Nil{}:      List.concat(&2, F.Finding, List.reverse(&2, List<&2, F.Finding>, acc))    case Con{Calls.Def{+name, +sig, body}, rest}:      check.go(rest, path,        Lazy.stop(List<&2, F.Finding>, Calls.exempt(path, sig), [],          _u => walk(body, name, Calls.names(sig), [], path)) <> acc)# the ruledef check(ss: Src.Src) -> List<&2, F.Finding>:  Src.Src{path, text, toks, tree, bound, items} = ss  check.go(Calls.defs(tree), path, [])