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. 6 import Base 7 import bend-lint-api@0.2.2.0/linter/lint.bend as Lint 8 import bend-lint-api@0.2.2.0/shared/lists.bend as Lists 9 10 # Paths 11 # ===== 12 def 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. 24 def 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 31 def 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`. 35 def 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. 43 def 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 50 def 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`. 58 def 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 ../. 62 def 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. 73 def is_hub(+path: String) -> Bool: 74 Bool.or(String.contains(path, "@"), String.starts_with(path, "0x")) 75 76 def 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. 91 def 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. 97 def 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 119 def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>): 120 Lint.done(paths(Lint.Here.nodes(here), Lint.Here.path(here), [])) 121 122 def rule() -> Lint.Rule: 123 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}