layout/canon_import_path.bend
bend-lint-rules@0.2.1.0 · 123 lines · 0 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.1.0/linter/lint.bend as Lint
8import bend-lint-api@0.2.1.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}}