Appearance
| 1 | namespace Analysis is | |
| 2 | use Collections.Iterable; | |
| 3 | use Collections.LIST; | |
| 4 | use Collections.MAP; | |
| 5 | use Collections.SET; | |
| 6 | ||
| 7 | use Ghul.Pipes; | |
| 8 | ||
| 9 | use Trees = Syntax.Trees; | |
| 10 | ||
| 11 | use Source.LOCATION; | |
| 12 | ||
| 13 | // Attaches quick fixes to collected diagnostics before they go out on | |
| 14 | // the wire. The compiler is the sole author of fixes: clients render | |
| 15 | // whatever arrives here without any per-code knowledge, so adding or | |
| 16 | // changing a fix is a compiler-only change. | |
| 17 | // | |
| 18 | // Two kinds of fix are synthesized: | |
| 19 | // | |
| 20 | // - Removal of a redundant operator, for the redundant-presence-test / | |
| 21 | // redundant-unwrap / redundant-coalesce warnings. These diagnostics | |
| 22 | // are reported on the operator expression with the range ending at | |
| 23 | // the operator, so the '?' and '!' edits fall out of the diagnostic | |
| 24 | // range; the '?.' edit needs the member-access node to find where | |
| 25 | // the receiver ends. | |
| 26 | // | |
| 27 | // - A `@suppress("<code>")` insertion for every coded diagnostic, one | |
| 28 | // per enclosing scope the pragma can attach to (the pragma wraps the | |
| 29 | // next definition or statement, so anchors are the AST nodes of that | |
| 30 | // shape whose location contains the diagnostic). | |
| 31 | // | |
| 32 | // Edit coordinates use the wire convention of DIAGNOSTIC ranges: | |
| 33 | // 1-based, end column exclusive (AST LOCATION end columns are | |
| 34 | // inclusive, hence the +1s below). Every removal edit carries the text | |
| 35 | // it expects to delete so the client can drop a fix whose buffer has | |
| 36 | // drifted since the compile that produced it. | |
| 37 | class QUICK_FIX_SYNTHESIZER is | |
| 38 | attach_fixes( | |
| 39 | diagnostics: Collections.List[Protocol.DIAGNOSTIC], | |
| 40 | source_files: Iterable[Compiler.SOURCE_FILE] | |
| 41 | ) static is | |
| 42 | let targets_by_path = MAP[string, LIST[FIX_TARGET]](); | |
| 43 | ||
| 44 | for d in diagnostics do | |
| 45 | let code = d.code; | |
| 46 | ||
| 47 | if !code? \/ code.length == 0 then | |
| 48 | continue; | |
| 49 | fi | |
| 50 | ||
| 51 | let targets: LIST[FIX_TARGET] mut; | |
| 52 | ||
| 53 | if !targets_by_path.try_get_value(d.path, targets ref) then | |
| 54 | targets = LIST[FIX_TARGET](); | |
| 55 | targets_by_path.add(d.path, targets); | |
| 56 | fi | |
| 57 | ||
| 58 | targets.add(FIX_TARGET(d, code)); | |
| 59 | od | |
| 60 | ||
| 61 | if targets_by_path.count == 0 then | |
| 62 | return; | |
| 63 | fi | |
| 64 | ||
| 65 | for source_file in source_files do | |
| 66 | let targets: LIST[FIX_TARGET] mut; | |
| 67 | ||
| 68 | if | |
| 69 | targets_by_path.try_get_value(source_file.file_name, targets ref) | |
| 70 | then | |
| 71 | source_file.definition.walk(FIX_SITE_VISITOR(targets)); | |
| 72 | ||
| 73 | for target in targets do | |
| 74 | target.attach_fixes(); | |
| 75 | od | |
| 76 | fi | |
| 77 | od | |
| 78 | si | |
| 79 | si | |
| 80 | ||
| 81 | enum AnchorKind is | |
| 82 | BLOCK, // a statement | |
| 83 | MEMBER, // a variable definition (field) | |
| 84 | METHOD, // a function, property or indexer definition | |
| 85 | TYPE // a class, trait, struct, union or enum definition | |
| 86 | si | |
| 87 | ||
| 88 | class ANCHOR is | |
| 89 | location: LOCATION; | |
| 90 | kind: AnchorKind; | |
| 91 | ||
| 92 | init(location: LOCATION, kind: AnchorKind) is | |
| 93 | self.location = location; | |
| 94 | self.kind = kind; | |
| 95 | si | |
| 96 | si | |
| 97 | ||
| 98 | // Per-diagnostic accumulator: the fix-site walk records the enclosing | |
| 99 | // suppress anchors and (for redundant-coalesce) the receiver location | |
| 100 | // of the matching member access, then attach_fixes assembles the wire | |
| 101 | // DTOs. | |
| 102 | class FIX_TARGET is | |
| 103 | _diagnostic: Protocol.DIAGNOSTIC; | |
| 104 | _code: string; | |
| 105 | _anchors: LIST[ANCHOR]; | |
| 106 | _coalesce_receiver: LOCATION?; | |
| 107 | ||
| 108 | init(diagnostic: Protocol.DIAGNOSTIC, code: string) is | |
| 109 | _diagnostic = diagnostic; | |
| 110 | _code = code; | |
| 111 | _anchors = LIST[ANCHOR](); | |
| 112 | si | |
| 113 | ||
| 114 | // Node location contains the diagnostic's wire range. Wire end | |
| 115 | // columns are exclusive, LOCATION end columns inclusive. | |
| 116 | contains_diagnostic(location: LOCATION) -> bool => | |
| 117 | ( | |
| 118 | location.start_line < _diagnostic.start_line \/ | |
| 119 | (location.start_line == _diagnostic.start_line /\ location.start_column <= _diagnostic.start_column) | |
| 120 | ) /\ | |
| 121 | ( | |
| 122 | location.end_line > _diagnostic.end_line \/ | |
| 123 | (location.end_line == _diagnostic.end_line /\ location.end_column >= _diagnostic.end_column - 1) | |
| 124 | ); | |
| 125 | ||
| 126 | // Node location is exactly the diagnostic's wire range. | |
| 127 | matches_diagnostic(location: LOCATION) -> bool => | |
| 128 | location.start_line == _diagnostic.start_line /\ | |
| 129 | location.start_column == _diagnostic.start_column /\ | |
| 130 | location.end_line == _diagnostic.end_line /\ | |
| 131 | location.end_column == _diagnostic.end_column - 1; | |
| 132 | ||
| 133 | record_anchor(location: LOCATION, kind: AnchorKind) is | |
| 134 | if contains_diagnostic(location) then | |
| 135 | _anchors.add(ANCHOR(location, kind)); | |
| 136 | fi | |
| 137 | si | |
| 138 | ||
| 139 | record_coalesce_receiver(member_location: LOCATION, receiver_location: LOCATION) is | |
| 140 | if _code =~ "redundant-coalesce" /\ matches_diagnostic(member_location) then | |
| 141 | _coalesce_receiver = receiver_location; | |
| 142 | fi | |
| 143 | si | |
| 144 | ||
| 145 | attach_fixes() is | |
| 146 | let fixes = LIST[Protocol.QUICK_FIX](); | |
| 147 | ||
| 148 | let removal = _build_removal_fix(); | |
| 149 | ||
| 150 | if removal? then | |
| 151 | fixes.add(removal); | |
| 152 | fi | |
| 153 | ||
| 154 | _add_suppress_fixes(fixes); | |
| 155 | ||
| 156 | if fixes.count > 0 then | |
| 157 | _diagnostic.fixes = fixes; | |
| 158 | fi | |
| 159 | si | |
| 160 | ||
| 161 | _build_removal_fix() -> Protocol.QUICK_FIX? is | |
| 162 | if _code =~ "redundant-presence-test" then | |
| 163 | return _remove_trailing_operator("?"); | |
| 164 | elif _code =~ "redundant-unwrap" then | |
| 165 | return _remove_trailing_operator("!"); | |
| 166 | elif _code =~ "redundant-coalesce" /\ _coalesce_receiver? then | |
| 167 | // Deleting the '?' of '?.' turns the conditional access | |
| 168 | // into a plain one. The '?' is the character after the | |
| 169 | // receiver expression ends. | |
| 170 | let receiver = _coalesce_receiver; | |
| 171 | ||
| 172 | return _single_edit_fix( | |
| 173 | "Replace '?.' with '.'", | |
| 174 | true, | |
| 175 | Protocol.FIX_EDIT( | |
| 176 | receiver.end_line, receiver.end_column + 1, | |
| 177 | receiver.end_line, receiver.end_column + 2, | |
| 178 | "?", "" | |
| 179 | ) | |
| 180 | ); | |
| 181 | fi | |
| 182 | ||
| 183 | return null; | |
| 184 | si | |
| 185 | ||
| 186 | // The diagnostic range ends at the operator, so the operator is | |
| 187 | // its last character. | |
| 188 | _remove_trailing_operator(operator: string) -> Protocol.QUICK_FIX => | |
| 189 | _single_edit_fix( | |
| 190 | "Remove redundant '{operator}'", | |
| 191 | true, | |
| 192 | Protocol.FIX_EDIT( | |
| 193 | _diagnostic.end_line, _diagnostic.end_column - 1, | |
| 194 | _diagnostic.end_line, _diagnostic.end_column, | |
| 195 | operator, "" | |
| 196 | ) | |
| 197 | ); | |
| 198 | ||
| 199 | _add_suppress_fixes(fixes: LIST[Protocol.QUICK_FIX]) is | |
| 200 | // Locals sit inside a let statement, which is already the | |
| 201 | // innermost useful anchor; a variable anchor only stands on | |
| 202 | // its own for a field, where no statement contains it. | |
| 203 | let anchors = | |
| 204 | _anchors | |
| 205 | |> filter(a => a.kind != AnchorKind.MEMBER \/ !_is_inside_block_anchor(a)) | |
| 206 | |> sort((a, b) => _compare_innermost_first(a.location, b.location)) | |
| 207 | |> collect(); | |
| 208 | ||
| 209 | let emitted_lines = SET[int](); | |
| 210 | let emitted mut = 0; | |
| 211 | ||
| 212 | for anchor in anchors do | |
| 213 | if emitted >= 3 then | |
| 214 | break; | |
| 215 | fi | |
| 216 | ||
| 217 | if emitted_lines.contains(anchor.location.start_line) then | |
| 218 | continue; | |
| 219 | fi | |
| 220 | ||
| 221 | emitted_lines.add(anchor.location.start_line); | |
| 222 | ||
| 223 | let label = | |
| 224 | if emitted == 0 then | |
| 225 | "Suppress here"; | |
| 226 | elif anchor.kind == AnchorKind.METHOD then | |
| 227 | "Suppress for enclosing method"; | |
| 228 | elif anchor.kind == AnchorKind.TYPE then | |
| 229 | "Suppress for enclosing type"; | |
| 230 | else | |
| 231 | "Suppress for enclosing block"; | |
| 232 | fi; | |
| 233 | ||
| 234 | fixes.add(_build_suppress_fix(label, anchor.location)); | |
| 235 | ||
| 236 | emitted = emitted + 1; | |
| 237 | ||
| 238 | // Wider scopes than the enclosing method or type add | |
| 239 | // noise, not reach. | |
| 240 | if anchor.kind == AnchorKind.METHOD \/ anchor.kind == AnchorKind.TYPE then | |
| 241 | break; | |
| 242 | fi | |
| 243 | od | |
| 244 | si | |
| 245 | ||
| 246 | _is_inside_block_anchor(anchor: ANCHOR) -> bool => | |
| 247 | _anchors | |
| 248 | |> any( | |
| 249 | other => | |
| 250 | other.kind == AnchorKind.BLOCK /\ | |
| 251 | other.location.contains(anchor.location) | |
| 252 | ); | |
| 253 | ||
| 254 | _build_suppress_fix(label: string, anchor: LOCATION) -> Protocol.QUICK_FIX is | |
| 255 | let indent = "".pad_left(anchor.start_column - 1); | |
| 256 | ||
| 257 | return _single_edit_fix( | |
| 258 | "{label}: @suppress(\"{_code}\")", | |
| 259 | false, | |
| 260 | Protocol.FIX_EDIT( | |
| 261 | anchor.start_line, 1, | |
| 262 | anchor.start_line, 1, | |
| 263 | null, "{indent}@suppress(\"{_code}\")\n" | |
| 264 | ) | |
| 265 | ); | |
| 266 | si | |
| 267 | ||
| 268 | _single_edit_fix(title: string, is_preferred: bool, edit: Protocol.FIX_EDIT) -> Protocol.QUICK_FIX static is | |
| 269 | let edits = LIST[Protocol.FIX_EDIT](); | |
| 270 | ||
| 271 | edits.add(edit); | |
| 272 | ||
| 273 | return Protocol.QUICK_FIX(title, is_preferred, edits); | |
| 274 | si | |
| 275 | ||
| 276 | _compare_innermost_first(a: LOCATION, b: LOCATION) -> int static is | |
| 277 | // Nested anchors all contain the diagnostic, so the later a | |
| 278 | // span starts (and the earlier it ends) the more deeply | |
| 279 | // nested it is. | |
| 280 | if a.start_line != b.start_line then | |
| 281 | return b.start_line - a.start_line; | |
| 282 | fi | |
| 283 | ||
| 284 | if a.start_column != b.start_column then | |
| 285 | return b.start_column - a.start_column; | |
| 286 | fi | |
| 287 | ||
| 288 | if a.end_line != b.end_line then | |
| 289 | return a.end_line - b.end_line; | |
| 290 | fi | |
| 291 | ||
| 292 | return a.end_column - b.end_column; | |
| 293 | si | |
| 294 | si | |
| 295 | ||
| 296 | // Collects, for every target diagnostic in a file, the suppress | |
| 297 | // anchors that contain it and the coalescing member access it was | |
| 298 | // reported on. Statements arrive through the enter_node hook the | |
| 299 | // statement-list walk already provides; definition anchors need their | |
| 300 | // specific visit overloads because the definitions-list walk has no | |
| 301 | // per-child hook. | |
| 302 | class FIX_SITE_VISITOR: Syntax.Visitor is | |
| 303 | _targets: LIST[FIX_TARGET]; | |
| 304 | ||
| 305 | init(targets: LIST[FIX_TARGET]) is | |
| 306 | super.init(); | |
| 307 | ||
| 308 | _targets = targets; | |
| 309 | si | |
| 310 | ||
| 311 | _record_anchor(location: LOCATION, kind: AnchorKind) is | |
| 312 | for target in _targets do | |
| 313 | target.record_anchor(location, kind); | |
| 314 | od | |
| 315 | si | |
| 316 | ||
| 317 | enter_node(node: Trees.Node) is | |
| 318 | if isa Trees.Statements.Statement(node) then | |
| 319 | _record_anchor(node.location, AnchorKind.BLOCK); | |
| 320 | fi | |
| 321 | si | |
| 322 | ||
| 323 | visit(variable: Trees.Variables.VARIABLE) is | |
| 324 | _record_anchor(variable.location, AnchorKind.MEMBER); | |
| 325 | si | |
| 326 | ||
| 327 | visit(function: Trees.Definitions.FUNCTION) is | |
| 328 | _record_anchor(function.location, AnchorKind.METHOD); | |
| 329 | si | |
| 330 | ||
| 331 | visit(property: Trees.Definitions.PROPERTY) is | |
| 332 | _record_anchor(property.location, AnchorKind.METHOD); | |
| 333 | si | |
| 334 | ||
| 335 | visit(indexer: Trees.Definitions.INDEXER) is | |
| 336 | _record_anchor(indexer.location, AnchorKind.METHOD); | |
| 337 | si | |
| 338 | ||
| 339 | visit(`class: Trees.Definitions.CLASS) is | |
| 340 | _record_anchor(`class.location, AnchorKind.TYPE); | |
| 341 | si | |
| 342 | ||
| 343 | visit(`trait: Trees.Definitions.TRAIT) is | |
| 344 | _record_anchor(`trait.location, AnchorKind.TYPE); | |
| 345 | si | |
| 346 | ||
| 347 | visit(`struct: Trees.Definitions.STRUCT) is | |
| 348 | _record_anchor(`struct.location, AnchorKind.TYPE); | |
| 349 | si | |
| 350 | ||
| 351 | visit(`union: Trees.Definitions.UNION) is | |
| 352 | _record_anchor(`union.location, AnchorKind.TYPE); | |
| 353 | si | |
| 354 | ||
| 355 | visit(`enum: Trees.Definitions.ENUM) is | |
| 356 | _record_anchor(`enum.location, AnchorKind.TYPE); | |
| 357 | si | |
| 358 | ||
| 359 | visit(member: Trees.Expressions.MEMBER) is | |
| 360 | if member.is_coalesce then | |
| 361 | for target in _targets do | |
| 362 | target.record_coalesce_receiver(member.location, member.left.location); | |
| 363 | od | |
| 364 | fi | |
| 365 | si | |
| 366 | si | |
| 367 | si |