layout/imports.bend

bend-lint-rules@0.2.0.0 · 245 lines · 21 definitions

1# layout/imports: an import block out of order, with a file imported twice, or with blank lines. The
2# order: import Base, then the other imports by written path, then by alias, by code point; a comment
3# line stays with the import below it.
4# Fix: writes the block in order, drops a second import of a file, and points its uses at the first
5# one's alias.
6# Heuristics: two imports name one file if the check loads one file for both.
7import Base
8import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
9import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
10import bend-lint-api@0.2.0.0/shared/names.bend as Names
11import bend-lint-api@0.2.0.0/shared/strings.bend as Strings
12
13# Types
14# =====
15
16# One import: where its alias is, whether it is Base, its written path, its alias, the file it loads, and its text (the
17# comment lines above it, then its line).
18type Entry is Data:
19  Entry{at: Lint.Span, base: Bool, path: String, alias: String, loads: String, text: String}
20
21# The imports read so far, each file's first only: the files they load, each with its alias; the imports kept (last
22# first); and the operations that point a dropped import's uses at the kept one's alias.
23type Deduped is Data:
24  Deduped{
25    seen: List<&2, Names.Named<String>>,
26    kept: List<&2, Entry>,
27    renames: List<&2, Lint.Operation>
28  }
29
30# The block, as written: where it starts and ends, and its imports, last first.
31type Block is Data:
32  Block{beg: U32, end: U32, entries: List<&2, Entry>}
33
34# Text
35# ====
36def is_blank(s: String) -> Bool:
37  match s:
38    case SNil{}:
39      True{}
40    case SCon{' ', rest}:
41      is_blank(rest)
42    case SCon{'\t', rest}:
43      is_blank(rest)
44    case SCon{'\r', rest}:
45      is_blank(rest)
46    case _:
47      False{}
48
49def kept(+line: String) -> String:
50  Bool.pick(String, is_blank(line), "", line ++ "\n")
51
52# The comment lines of `s`, a run of whole lines, each with its newline; blank lines dropped. `line` is the current
53# line, last character first; `acc` the lines so far, last first.
54def comments(s: String, line: String, acc: List<&2, String>) -> String:
55  match s:
56    case SNil{}:
57      Strings.onto(kept(String.reverse(line)) <> acc, "")
58    case SCon{'\n', rest}:
59      comments(rest, "", kept(String.reverse(line)) <> acc)
60    case SCon{c, rest}:
61      comments(rest, SCon{c, line}, acc)
62
63def ended(+s: String) -> String:
64  Bool.pick(String, String.ends_with(s, "\n"), s, s ++ "\n")
65
66# Block
67# =====
68
69# The import on the line from `beg` to `end`, after the block so far.
70def add.line(
71  +text: String,
72  +beg: U32,
73  +end: U32,
74  at: Lint.Span,
75  base: Bool,
76  path: String,
77  alias: String,
78  loads: String,
79  b: Block
80) -> Block:
81  match b:
82    case Block{first, last, Nil{}}:
83      Block{beg, end, [Entry{at, base, path, alias, loads, ended(Strings.slice(text, beg, end))}]}
84    case Block{first, last, +entries}:
85      Block{
86        first,
87        end,
88        Entry{
89          at,
90          base,
91          path,
92          alias,
93          loads,
94          comments(Strings.slice(text, last, beg), "", []) ++ ended(Strings.slice(text, beg, end))
95        }
96          <> entries
97      }
98
99# The import whose alias is at `at`, after the block so far.
100def add(
101  +text: String,
102  at: Lint.Span,
103  base: Bool,
104  path: String,
105  alias: String,
106  loads: String,
107  b: Block
108) -> Block:
109  match at:
110    case Lint.Span{p, +a, e}:
111      +beg = Strings.line_start(text, a)
112      add.line(
113        text,
114        beg,
115        Strings.line_end(text, beg),
116        Lint.Span{p, a, e},
117        base,
118        path,
119        alias,
120        loads,
121        b
122      )
123
124# The block of the imports among `nodes`. A file's imports come first, so the first node of another kind ends them.
125def block(nodes: List<&2, Lint.Node>, +text: String, b: Block) -> Block:
126  match nodes:
127    case Con{
128      Lint.Node{
129        Lint.Import{loads},
130        alias,
131        Some{at},
132        Con{Lint.Node{Lint.ImportPath{}, path, s, k, f}, ks},
133        fs
134      },
135      rest
136    }:
137      block(rest, text, add(text, at, False{}, path, alias, loads, b))
138    case Con{Lint.Node{Lint.Import{loads}, alias, Some{at}, k, f}, rest}:
139      block(rest, text, add(text, at, True{}, "Base", alias, loads, b))
140    case _:
141      b
142
143# Duplicates and order
144# ====================
145def loads_of(e: Entry) -> String:
146  match e:
147    case Entry{at, base, path, alias, loads, text}:
148      loads
149
150# An import read, after those before it: kept, if its file has no import yet; else dropped, with its uses pointed at
151# `first`, the alias of the file's first import. A second import Base has none to point: its uses are bare names.
152def dedupe.kept(first: Maybe<&2, String>, e: Entry, d: Deduped) -> Deduped:
153  match first e d:
154    case None{} Entry{at, base, path, +alias, +loads, text} Deduped{seen, kept, renames}:
155      Deduped{
156        Names.Named{loads, alias} <> seen,
157        Entry{at, base, path, alias, loads, text} <> kept,
158        renames
159      }
160    case Some{a} Entry{at, False{}, path, alias, loads, text} Deduped{seen, kept, renames}:
161      Deduped{seen, kept, Lint.RenameUses{at, a} <> renames}
162    case Some{a} Entry{at, True{}, path, alias, loads, text} _:
163      d
164
165def dedupe.add(+e: Entry, d: Deduped) -> Deduped:
166  match d:
167    case Deduped{+seen, kept, renames}:
168      dedupe.kept(Names.lookup(String, loads_of(e), seen), e, Deduped{seen, kept, renames})
169
170# The imports of `es`, in order, after those of `d`, each file's first only.
171def dedupe(es: List<&2, Entry>, d: Deduped) -> Deduped:
172  match es:
173    case Nil{}:
174      d
175    case Con{e, rest}:
176      dedupe(rest, dedupe.add(e, d))
177
178# Whether `a` may come before `b`: Base first, then by path, then by alias.
179def is_before(a: Entry, b: Entry) -> Bool:
180  match a b:
181    case Entry{i, True{}, p, x, l, t} _:
182      True{}
183    case Entry{i, False{}, p, x, l, t} Entry{j, True{}, q, y, m, u}:
184      False{}
185    case Entry{i, False{}, +p, x, l, t} Entry{j, False{}, +q, y, m, u}:
186      Bool.pick(Bool, String.eq(p, q), String.is_le(x, y), String.is_lt(p, q))
187
188# The texts of `es`, after `acc`, the texts so far, last first.
189def texts(es: List<&2, Entry>, acc: List<&2, String>) -> String:
190  match es:
191    case Nil{}:
192      Strings.onto(acc, "")
193    case Con{Entry{at, base, path, alias, loads, text}, rest}:
194      texts(rest, text <> acc)
195
196# Rule
197# ====
198def verdict(
199  same: Bool,
200  +span: Lint.Span,
201  ordered: String,
202  renames: List<&2, Lint.Operation>
203) -> List<&2, Lint.Finding>:
204  match same:
205    case True{}:
206      []
207    case False{}:
208      [
209        Lint.Finding{
210          Lint.Warning{},
211          span,
212          "Order the imports: import Base first, then by path and alias, each file once.",
213          [Lint.Fix{"Order the imports", Lint.Replace{span, ordered} <> renames}]
214        }
215      ]
216
217# The block as written (`written`, at `span`), against its imports in order, each file once.
218def judged(span: Lint.Span, written: String, d: Deduped) -> List<&2, Lint.Finding>:
219  match d:
220    case Deduped{seen, kept, renames}:
221      +ordered = texts(Lists.sort(~Entry, ~is_before, List.reverse(&2, Entry, kept)), [])
222      verdict(String.eq(ordered, written), span, ordered, renames)
223
224def judge(path: String, text: String, b: Block) -> List<&2, Lint.Finding>:
225  match b:
226    case Block{beg, end, Nil{}}:
227      []
228    case Block{+beg, +end, entries}:
229      judged(
230        Lint.Span{path, beg, end},
231        Strings.slice(text, beg, end),
232        dedupe(List.reverse(&2, Entry, entries), Deduped{[], [], []})
233      )
234
235def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
236  Lint.done(
237    judge(
238      Lint.Here.path(here),
239      Lint.Here.text(here),
240      block(Lint.Here.nodes(here), Lint.Here.text(here), Block{0, 0, []})
241    )
242  )
243
244def rule() -> Lint.Rule:
245  Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}