1 # layout/directive_order: directives that stack on consecutive comment-only lines, out of order: by 2 # rule, then severity, then keyword. 3 # Fix: writes them in that order, over the same comments. 4 # Heuristics: it reads the directives as written, and its fix writes the same comments over them, so a 5 # fix keeps the program. 6 import Base 7 import ../lib/linter/directives.bend as Directives 8 import bend-lint-api@0.2.2.0/linter/lint.bend as Lint 9 import bend-lint-api@0.2.2.0/shared/lists.bend as Lists 10 11 # Types 12 # ===== 13 14 # A directive of a stack: its key, where its comment is, and the comment's text. 15 type Slot is Data: 16 Slot{key: String, beg: U32, end: U32, text: String} 17 18 # A stack of directives: where it starts and ends, its last line, its last key, whether it is in order, and its 19 # directives (last first). 20 type Run is Data: 21 Run{beg: U32, end: U32, line: U32, key: String, sorted: Bool, slots: List<&2, Slot>} 22 23 # The stacks so far: the open one, and the findings about the closed ones that are out of order (last first). 24 type Runs is Data: 25 Runs{open: Maybe<&2, Run>, out: List<&2, Lint.Finding>} 26 27 # Functions 28 # ========= 29 30 # What a comment's directive is sorted by: its rule, then the severity it pins, then its keyword; none if it is no 31 # directive. 32 def key(draft: Maybe<&2, Maybe<&2, Directives.Draft>>) -> Maybe<&2, String>: 33 match draft: 34 case Some{ 35 Some{Directives.Draft{Directives.Keyword{a, s}, Directives.Target{r, pinned}, reasons}} 36 }: 37 Some{ 38 r 39 ++ "\t" 40 ++ Maybe.default(&2, String, pinned, "") 41 ++ "\t" 42 ++ Directives.Action.show(a) 43 ++ "-" 44 ++ Directives.Scope.show(s) 45 } 46 case _: 47 None{} 48 49 def Slot.is_le(a: Slot, b: Slot) -> Bool: 50 match a b: 51 case Slot{k, x, y, t} Slot{j, v, w, u}: 52 String.is_le(k, j) 53 54 # The edits that write the directives of `sorted` into the places of `slots` (both in order), onto `out`. 55 def Slot.order( 56 slots: List<&2, Slot>, 57 sorted: List<&2, Slot>, 58 +path: String, 59 out: List<&2, Lint.Operation> 60 ) -> List<&2, Lint.Operation>: 61 match slots sorted: 62 case Con{Slot{k, beg, end, t}, rest} Con{Slot{j, b, e, text}, more}: 63 Slot.order(rest, more, path, Lint.Replace{Lint.Span{path, beg, end}, text} <> out) 64 case _ _: 65 List.reverse(&2, Lint.Operation, out) 66 67 def Runs.close( 68 open: Maybe<&2, Run>, 69 +path: String, 70 out: List<&2, Lint.Finding> 71 ) -> List<&2, Lint.Finding>: 72 match open: 73 case Some{Run{beg, end, line, key, False{}, slots}}: 74 +ordered = List.reverse(&2, Slot, slots) 75 Lint.Finding{ 76 Lint.Warning{}, 77 Lint.Span{path, beg, end}, 78 "Directives that stack must be in order: by rule, then severity, then keyword.", 79 [ 80 Lint.Fix{ 81 "Order the directives", 82 Slot.order(ordered, Lists.sort(~Slot, ~Slot.is_le, ordered), path, []) 83 } 84 ] 85 } 86 <> out 87 case _: 88 out 89 90 # A stack of one directive: its key, and its comment's place, line and text after the #. 91 def Run.first(+key: String, +beg: U32, +end: U32, line: U32, body: String) -> Run: 92 Run{beg, end, line, key, True{}, [Slot{key, beg, end, "#" ++ body}]} 93 94 def Runs.extend( 95 continues: Bool, 96 run: Run, 97 comment: Directives.Comment, 98 +key: String, 99 path: String, 100 out: List<&2, Lint.Finding> 101 ) -> Runs: 102 match continues run comment: 103 case True{} Run{beg, end, line, last, sorted, slots} Directives.Comment{b, +e, l, t, body}: 104 Runs{ 105 Some{ 106 Run{ 107 beg, 108 e, 109 l, 110 key, 111 sorted && String.is_le(last, key), 112 Slot{key, b, e, "#" ++ body} <> slots 113 } 114 }, 115 out 116 } 117 case False{} _ Directives.Comment{b, e, l, t, body}: 118 Runs{Some{Run.first(key, b, e, l, body)}, Runs.close(Some{run}, path, out)} 119 120 # The stacks, with a comment read: a directive on its own line, `key`, stacks on the one on the line before it. 121 def Runs.step( 122 key: Maybe<&2, String>, 123 comment: Directives.Comment, 124 path: String, 125 runs: Runs 126 ) -> Runs: 127 match key comment runs: 128 case Some{ 129 k 130 } Directives.Comment{b, e, +l, False{}, body} Runs{Some{Run{rb, re, +rl, rk, rs, slots}}, out}: 131 Runs.extend( 132 U32.is_eq(l, (rl + 1: U32)), 133 Run{rb, re, rl, rk, rs, slots}, 134 Directives.Comment{b, e, l, False{}, body}, 135 k, 136 path, 137 out 138 ) 139 case Some{+k} Directives.Comment{b, e, l, False{}, body} Runs{None{}, out}: 140 Runs{Some{Run.first(k, b, e, l, body)}, out} 141 case _ _ _: 142 runs 143 144 def Runs.go( 145 comments: List<&2, Directives.Comment>, 146 +path: String, 147 runs: Runs 148 ) -> List<&2, Lint.Finding>: 149 match comments runs: 150 case Nil{} Runs{open, out}: 151 List.reverse(&2, Lint.Finding, Runs.close(open, path, out)) 152 case Con{Directives.Comment{b, e, l, t, +body}, rest} _: 153 Runs.go( 154 rest, 155 path, 156 Runs.step( 157 key(Directives.Draft.read(body)), 158 Directives.Comment{b, e, l, t, body}, 159 path, 160 runs 161 ) 162 ) 163 164 def stacks(scanned: Directives.Scanned, path: String) -> List<&2, Lint.Finding>: 165 match scanned: 166 case Directives.Scanned{comments, lines}: 167 Runs.go(comments, path, Runs{None{}, []}) 168 169 # The stacks out of order in a file that has a directive's mark. 170 def found(marked: Bool, text: String, path: String) -> List<&2, Lint.Finding>: 171 match marked: 172 case True{}: 173 stacks(Directives.Scan.of(text), path) 174 case False{}: 175 [] 176 177 def run(+here: Lint.Here) -> Lint.Ask(List<&2, Lint.Finding>): 178 +text = Lint.Here.text(here) 179 Lint.done(found(Directives.has_mark(text, False{}), text, Lint.Here.path(here))) 180 181 def rule() -> Lint.Rule: 182 Lint.Rule{Lint.Needs{False{}, False{}}, [], Lint.OnFile{run}}