Appearance
| 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 |