Skip to content
← Back

src/analysis/quick_fix_synthesizer.ghul

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