layout/canon_import_path.bend

bend-lint-rules@0.2.0.0 · 123 lines · 14 definitions

1# layout/canon_import_path: an import path that is not the shortest path from the importer to the
2# file it names, or that does not start with ./ or ../.
3# Fix: writes the shortest path.
4# Heuristics: it compares real paths, links resolved, so a link to a file is that file. Base and hub
5# imports are not paths: it skips them.
6import Base
7import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
8import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
9
10# Paths
11# =====
12def segments.step(p: String, acc: List<&2, String>) -> List<&2, String>:
13  match p:
14    case SNil{}:
15      acc
16    case SCon{'.', SNil{}}:
17      acc
18    case SCon{'.', SCon{'.', SNil{}}}:
19      List.tail(&2, String, acc)
20    case SCon{c, rest}:
21      SCon{c, rest} <> acc
22
23# A path's segments, last first: "" and "." dropped, ".." applied.
24def segments.go(parts: List<&2, String>, acc: List<&2, String>) -> List<&2, String>:
25  match parts:
26    case Nil{}:
27      acc
28    case Con{p, rest}:
29      segments.go(rest, segments.step(p, acc))
30
31def segments(path: String) -> List<&2, String>:
32  List.reverse(&2, String, segments.go(String.split(path, '/'), []))
33
34# Each segment of `from` as "..", then `to`.
35def ups(from: List<&2, String>, to: List<&2, String>) -> List<&2, String>:
36  match from:
37    case Nil{}:
38      to
39    case Con{f, rest}:
40      ups(rest, ".." <> to)
41
42# Whether both lists start with the same segment.
43def is_same_head(from: List<&2, String>, to: List<&2, String>) -> Bool:
44  match from to:
45    case Con{a, as} Con{b, bs}:
46      String.eq(a, b)
47    case _ _:
48      False{}
49
50def relative.go(from: List<&2, String>, to: List<&2, String>, same: Bool) -> List<&2, String>:
51  match from to same:
52    case Con{a, +as} Con{b, +bs} True{}:
53      relative.go(as, bs, is_same_head(as, bs))
54    case f t _:
55      ups(f, t)
56
57# The segments that lead from the folder `from` to `to`.
58def relative(+from: List<&2, String>, +to: List<&2, String>) -> List<&2, String>:
59  relative.go(from, to, is_same_head(from, to))
60
61# The segments as a path that starts with ./ or ../.
62def canon(+parts: List<&2, String>) -> String:
63  match parts:
64    case Con{SCon{'.', SCon{'.', SNil{}}}, rest}:
65      String.join(parts, "/")
66    case _:
67      "./" ++ String.join(parts, "/")
68
69# Rule
70# ====
71
72# Hub imports name a package, not a path.
73def is_hub(+path: String) -> Bool:
74  Bool.or(String.contains(path, "@"), String.starts_with(path, "0x"))
75
76def verdict(fine: Bool, +want: String, +span: Lint.Span) -> List<&2, Lint.Finding>:
77  match fine:
78    case True{}:
79      []
80    case False{}:
81      [
82        Lint.Finding{
83          Lint.Warning{},
84          span,
85          "Write \"" ++ want ++ "\": the shortest path from this file, starting with ./ or ../.",
86          [Lint.Fix{"Use the shortest path", [Lint.Replace{span, want}]}]
87        }
88      ]
89
90# A written path, against the one from `file` to `loads`, the file its import loads.
91def judge(file: String, +written: String, loads: String, span: Lint.Span) -> List<&2, Lint.Finding>:
92  +want = canon(relative(segments(file ++ "/.."), segments(loads)))
93  verdict(Bool.or(is_hub(written), String.eq(want, written)), want, span)
94
95# The findings on the paths of `nodes`' imports, onto `out`. A file's imports come first, so the first node of another
96# kind ends them; `import Base` writes no path.
97def paths(
98  nodes: List<&2, Lint.Node>,
99  +file: String,
100  out: List<&2, Lint.Finding>
101) -> List<&2, Lint.Finding>:
102  match nodes:
103    case Con{
104      Lint.Node{
105        Lint.Import{loads},
106        n,
107        s,
108        Con{Lint.Node{Lint.ImportPath{}, written, Some{span}, k, f}, ks},
109        fs
110      },
111      rest
112    }:
113      paths(rest, file, Lists.onto(&2, Lint.Finding, judge(file, written, loads, span), out))
114    case Con{Lint.Node{Lint.Import{loads}, n, s, k, f}, rest}:
115      paths(rest, file, out)
116    case _:
117      List.reverse(&2, Lint.Finding, out)
118
119def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>):
120  Lint.done(paths(Lint.Here.nodes(here), Lint.Here.path(here), []))
121
122def rule() -> Lint.Rule:
123  Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}