Description
Today I spent some time writing some toy analysis code using patterns. I was building up a switch statement on an as-needed basis, so I had a default
clause to handle the cases I hadn't written yet. The code looked like this:
ir.CodeReference lowerElement(ExecutableElement element) {
switch (element.enclosingElement) {
case ClassElement class_:
return ir.ClassMemberReference(
Uri.parse(element.library.definingCompilationUnit.uri!),
class_.name,
element.name);
default:
throw 'TODO(paulberry): ${element.enclosingElement.runtimeType}';
}
}
Then I got the bright idea that instead of a default clause, I ought to be able to use an object pattern, i.e.:
ir.CodeReference lowerElement(ExecutableElement element) { // (1)
switch (element.enclosingElement) {
case ClassElement class_:
return ir.ClassMemberReference(
Uri.parse(element.library.definingCompilationUnit.uri!),
class_.name,
element.name);
case Object(:var runtimeType):
throw 'TODO(paulberry): $runtimeType';
}
}
But this didn't work. The analyzer produced an error at (1): "The body might complete normally, causing 'null' to be returned, but the return type, 'CodeReference', is a potentially non-nullable type." But in truth, the switch statement is exhaustive (because the type of element.enclosingElement
is non-nullable). Flow analysis just wasn't smart enough to see that it was.
In the general case, it's not possible for flow analysis to be as smart as the exhaustiveness algorithm. This is a known limitation of the design of the compilation pipeline, and we have discussed it elsewhere (for example, dart-lang/sdk#51926 (comment)).
However, flow analysis does understand that variable and wildcard patterns might always match (because the type of the pattern is a supertype of the matched value type), and in those cases it considers the switch to be exhaustive. I think it would be worth expanding the set of patterns that flow analysis recognizes as "always matching" to include:
- A cast pattern, if the subpattern is always matching
- A list pattern of the form
[...]
, if the required type is a supertype of the matched value type - A logical and pattern, if both subpatterns are always matching
- A logical or pattern, if either of the subpatterns is always matching
- A null check pattern, if the matched value type is non-nullable and the subpattern is always matching
- A null assert pattern, if the subpattern is always matching
- An object pattern, if the required type is a supertype of the matched value type and all subpatterns are always matching
- A record pattern, if the required type is a supertype of the matched value type and all subpatterns are always matching
That would allow the example above to work. It would also reduce the number of situations where flow analysis and the exhaustiveness algorithm disagree about whether a switch is exhaustive, which should reduce user pain (see #2977).
(Note that if we were to implement this functionality prior to removing support for mixed-mode null safety, we would have to be careful to make sure that this didn't lead to unsoundness escalation (see #1143).