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. 7 import Base 8 import bend-lint-api@0.2.1.0/linter/lint.bend as Lint 9 import bend-lint-api@0.2.1.0/shared/lists.bend as Lists 10 import bend-lint-api@0.2.1.0/shared/names.bend as Names 11 import bend-lint-api@0.2.1.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). 18 type 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. 23 type Deduped is Data: 24 Deduped{ 25 seen: List<&2, Names.Named>, 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. 31 type Block is Data: 32 Block{beg: U32, end: U32, entries: List<&2, Entry>} 33 34 # Text 35 # ==== 36 def 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 49 def 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. 54 def 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 63 def 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. 70 def 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. 100 def 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. 125 def 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 # ==================== 145 def 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. 152 def 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 165 def 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. 171 def 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. 179 def 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. 189 def 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 # ==== 198 def 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. 218 def 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 224 def 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 235 def 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 244 def rule() -> Lint.Rule: 245 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}