src/rules/correctness/put.bend

bolt@1.23.0.0 · 81 lines · 0 definitions

1# rule put: a call to `Map.put(`: among the significant tokens, a `Map.put`
2# token with a `(` token right after it. Base's Map.put is an internal helper
3# of Map.set: on a leaf it keeps the old key and replaces the value without
4# comparing keys, so putting a new key silently overwrites another entry
5# (bend-http c91bdf8: "b" into a map of "a" replaced "a"). Use Map.set. A
6# file that defines Map.put itself (Base's source), a `def` keyword with a
7# `Map.put` token right after it, is exempt.
8import Base
9import bolt@1.23.0.0/src/src.bend as Src
10import bolt@1.23.0.0/src/lazy/lazy.bend as Lazy
11import bolt@1.23.0.0/src/finding.bend as F
12import bolt@1.23.0.0/src/syntax/lex.bend as Lex
13import bolt@1.23.0.0/src/rules/tokens.bend as T
14
15# is the first token `Map.put`?
16def defines.named(toks: List<&2, Lex.Tok>) -> Bool:
17  match toks:
18    case Con{Lex.Tok{k, +t, l, c}, rest}:
19      String.eq(t, "Map.put")
20    case Nil{}:
21      False{}
22
23# a keyword? a token's kind is read here, its text in the tests beside it
24def defines.key(kk: Lex.TokKind) -> Bool:
25  match kk:
26    case Lex.TKey{}:
27      True{}
28    case other:
29      False{}
30
31# a dotted name?
32def calls.dotted(kk: Lex.TokKind) -> Bool:
33  match kk:
34    case Lex.TDotted{}:
35      True{}
36    case other:
37      False{}
38
39# does the file define Map.put: a `def` keyword right before a `Map.put`?
40def defines(toks: List<&2, Lex.Tok>) -> Bool:
41  match toks:
42    case Con{Lex.Tok{k, +d, l, c}, +rest}:
43      Lazy.or_else(Bool.and(defines.key(k), Bool.and(String.eq(d, "def"), defines.named(rest))), _u => defines(rest))
44    case Nil{}:
45      False{}
46
47# a dotted name then an open bracket: a finding when they are `Map.put` and
48# `(`, before what the rest reports
49def calls.one(
50  +tt: String,
51  +oo: String,
52  +ll: U32,
53  +cc: U32,
54  +path: String,
55  +more: List<&2, F.Finding>
56) -> List<&2, F.Finding>:
57  Bool.pick(List<&2, F.Finding>, Bool.and(String.eq(tt, "Map.put"), String.eq(oo, "(")),
58    F.Finding{path, ll, cc, 7, "put", "Map.put does not compare keys, so a new key can overwrite an existing entry; use Map.set."}
59      <> more,
60    more)
61
62# every `Map.put(` among the significant tokens
63def calls(toks: List<&2, Lex.Tok>, +path: String) -> List<&2, F.Finding>:
64  match toks:
65    case Con{Lex.Tok{k, +t, +l, +c}, +rest}:
66      match rest:
67        case Con{Lex.Tok{k2, +o, l2, c2}, r2}:
68          Lazy.either(List<&2, F.Finding>, Bool.and(calls.dotted(k), Lex.is_open(k2)), _u => calls.one(t, o, l, c, path,
69            calls(r2, path)), _v => calls(rest, path))
70        case Nil{}:
71          Nil{}
72    case Nil{}:
73      Nil{}
74
75def check.on(+toks: List<&2, Lex.Tok>, path: String) -> List<&2, F.Finding>:
76  Lazy.stop(List<&2, F.Finding>, defines(toks), [], _u => calls(toks, path))
77
78# the rule
79def check(ss: Src.Src) -> List<&2, F.Finding>:
80  Src.Src{path, text, toks, tree, bound, items} = ss
81  check.on(T.sig(toks), path)