Skip to content
← Back

src/syntax/process/field_assignment_checker.ghul

1
namespace Syntax.Process is
2
use Collections.LIST
3
use Collections.MAP
4
use Collections.SET
5
6
use Ghul.Pipes
7
8
use Logging.Logger
9
use Logging.RELATED_LOCATION
10
11
use Source.LOCATION
12
13
use Semantic.Symbols.Symbol
14
use Semantic.Symbols.Classy
15
use Semantic.Symbols.Function
16
17
// What one method did, as the expression walk saw it.
18
class METHOD_SUMMARY(
19
location: LOCATION,
20
assigned: Collections.List[Symbol],
21
called: Collections.List[Symbol],
22
is_unreachable: bool
23
)
24
25
// Reports a field of non-optional reference type that a constructor
26
// leaves unassigned, which holds null in a slot whose type says it
27
// never does.
28
//
29
// A constructor is credited with what it assigns directly and with what
30
// the methods it cannot avoid calling assign, transitively. Both inputs
31
// are must-facts: the expression walk records the fields assigned, and
32
// the methods called on `self`, on every path out of a body, so a
33
// helper called from one arm of an `if` credits nothing.
34
//
35
// The closure is a least fixpoint from nothing assigned. A cycle of
36
// mutually recursive helpers therefore credits nothing rather than
37
// assuming its own conclusion, which reports where the truth is
38
// unclear instead of staying silent - the direction a check like this
39
// has to fail in.
40
//
41
// Three things are deliberately not credited, each because crediting
42
// them would hide a real null: a call on anything but `self`, which
43
// may reach another object entirely; a call to an overridable method,
44
// whose override may assign nothing; and a call reached on only some
45
// paths.
46
//
47
// A base class's fields are its own constructors' responsibility, so
48
// only the fields a class declares are checked here, and `super.init`
49
// needs no special treatment.
50
class FIELD_ASSIGNMENT_CHECKER is
51
_logger: Logger
52
53
_summaries: MAP[Symbol, METHOD_SUMMARY]
54
55
// Constructors still to report, in the order they were walked, so
56
// the reports come out in a stable order rather than a hash order.
57
// Deduplicated: a file recompiled on demand re-walks its
58
// constructors, and each must be reported once however many times
59
// it has been seen.
60
_constructors: LIST[Function]
61
_constructors_seen: SET[Symbol]
62
63
init(logger: Logger) is
64
_logger = logger
65
_summaries = MAP[Symbol, METHOD_SUMMARY]()
66
_constructors = LIST[Function]()
67
_constructors_seen = SET[Symbol]()
68
si
69
70
record(
71
function: Function,
72
location: LOCATION,
73
assigned: Collections.List[Symbol],
74
called: Collections.List[Symbol],
75
is_unreachable: bool
76
) is
77
_summaries[function] = METHOD_SUMMARY(location, assigned, called, is_unreachable)
78
79
if function.name =~ "init" /\ !_constructors_seen.contains(function) then
80
_constructors_seen.add(function)
81
_constructors.add(function)
82
fi
83
si
84
85
// Called once every method that is going to be walked has been.
86
report() is
87
for constructor in _constructors do
88
_report_constructor(constructor)
89
od
90
91
// Only the pending constructors are dropped. The summaries
92
// outlive the walk that produced them, because a file
93
// recompiled on its own still has to credit a constructor
94
// with what a helper in another file assigns, and that file
95
// is not being re-walked. A re-walk overwrites its own
96
// entries, so what is kept is never stale.
97
_constructors.clear()
98
_constructors_seen.clear()
99
si
100
101
_report_constructor(constructor: Function) is
102
let summary: METHOD_SUMMARY mut
103
104
if !_summaries.try_get_value(constructor, summary ref) then
105
return
106
fi
107
108
// Every path out throws or returns early - there is no exit at
109
// which a field could be observed unassigned.
110
if summary.is_unreachable then
111
return
112
fi
113
114
let owner = cast Classy?(constructor.owner)
115
116
if !owner? then
117
return
118
fi
119
120
let assigned = _close_over_calls(constructor)
121
let names = LIST[string]()
122
let related = LIST[RELATED_LOCATION]()
123
124
for member in owner.symbols do
125
if
126
isa Semantic.Symbols.Field(member) /\
127
member.is_instance /\
128
member.owner == owner
129
then
130
let `field = cast Semantic.Symbols.Field?(member)!
131
132
if _wants_assignment(`field) /\ !_covers(assigned, owner, `field) then
133
let name = _read_name(`field)
134
135
names.add(name)
136
related.add(RELATED_LOCATION(_declaration_location(owner, `field), "{name} declared here"))
137
fi
138
fi
139
od
140
141
if names.count == 0 then
142
return
143
fi
144
145
// One diagnostic per constructor, naming every missed field
146
// and carrying a related location for each declaration - a
147
// capable editor renders those as jump-to links, so the
148
// primary text stays the whole answer for a reader without
149
// related-location support (the CLI, an older editor).
150
let message =
151
if names.count == 1 then
152
"{names[0]} is not assigned on every path out of this constructor"
153
else
154
"{names |> join(", ")} are not assigned on every path out of this constructor"
155
fi
156
157
_logger.warn(summary.location, "field-definite-assignment", message, related)
158
si
159
160
// The fields the constructor assigns, plus those assigned by the
161
// methods it cannot avoid reaching.
162
_close_over_calls(constructor: Function) -> SET[Symbol] is
163
let assigned = SET[Symbol]()
164
let seen = SET[Symbol]()
165
let pending = Collections.STACK[Symbol]()
166
167
pending.push(constructor)
168
seen.add(constructor)
169
170
while pending.count > 0 do
171
let current = pending.pop()
172
let summary: METHOD_SUMMARY mut
173
174
// A callee walked in another file, or not walked at all,
175
// has no summary and so credits nothing.
176
if _summaries.try_get_value(current, summary ref) then
177
for v in summary.assigned do
178
assigned.add(v)
179
od
180
181
for callee in summary.called do
182
if !seen.contains(callee) then
183
seen.add(callee)
184
pending.push(callee)
185
fi
186
od
187
fi
188
od
189
190
return assigned
191
si
192
193
// A field worth checking: one whose type says it always holds a
194
// value, and which a default-initialised object leaves absent.
195
_wants_assignment(`field: Semantic.Symbols.Field) -> bool is
196
let type = `field.type
197
198
return
199
type? /\
200
!type.is_optional /\
201
!type.is_value_type /\
202
!type.is_sentinel /\
203
!type.is_error /\
204
!type.is_inferred /\
205
!type.is_type_variable
206
si
207
208
// An auto-property and its backing field are two symbols for one
209
// piece of state, and an assignment written `x = ...` records the
210
// property. Either standing for assigned answers for both.
211
_covers(assigned: SET[Symbol], owner: Classy, `field: Semantic.Symbols.Field) -> bool is
212
if assigned.contains(`field) then
213
return true
214
fi
215
216
let property = _associated_property(owner, `field)
217
218
return property? /\ assigned.contains(property)
219
si
220
221
// An auto-property's backing field is named for the property with a
222
// `$` in front. The reader wrote the property, so name that.
223
_read_name(`field: Semantic.Symbols.Field) -> string =>
224
if `field.name.starts_with('$') then
225
`field.name.substring(1)
226
else
227
`field.name
228
fi
229
230
// Where to put the diagnostic that names this field: its own
231
// declaration, or - for an auto-property - the property, since
232
// the backing field is compiler-synthesised and not something
233
// the reader wrote.
234
_declaration_location(owner: Classy, `field: Semantic.Symbols.Field) -> LOCATION is
235
let property = _associated_property(owner, `field)
236
237
return if property? then property.location else `field.location fi
238
si
239
240
// The property an auto-property's `$`-prefixed backing field
241
// belongs to, or null for an ordinary field.
242
_associated_property(owner: Classy, `field: Semantic.Symbols.Field) -> Symbol? is
243
if !`field.name.starts_with('$') then
244
return null
245
fi
246
247
return owner.find_member(`field.name.substring(1))
248
si
249
si
250
si