new lint: exhaustive_cases (#2090)

* new lint: exhaustive_cases

* ws

* fix desc

* review fixes

* + todo
diff --git a/example/all.yaml b/example/all.yaml
index 92a35c6..459925b 100644
--- a/example/all.yaml
+++ b/example/all.yaml
@@ -60,6 +60,7 @@
     - empty_catches
     - empty_constructor_bodies
     - empty_statements
+    - exhaustive_cases
     - file_names
     - flutter_style_todos
     - hash_and_equals
diff --git a/lib/src/rules.dart b/lib/src/rules.dart
index 502e537..11eba7b 100644
--- a/lib/src/rules.dart
+++ b/lib/src/rules.dart
@@ -61,6 +61,7 @@
 import 'rules/empty_catches.dart';
 import 'rules/empty_constructor_bodies.dart';
 import 'rules/empty_statements.dart';
+import 'rules/exhaustive_cases.dart';
 import 'rules/file_names.dart';
 import 'rules/flutter_style_todos.dart';
 import 'rules/hash_and_equals.dart';
@@ -115,7 +116,6 @@
 import 'rules/prefer_initializing_formals.dart';
 import 'rules/prefer_inlined_adds.dart';
 import 'rules/prefer_int_literals.dart';
-import 'rules/use_is_even_rather_than_modulo.dart';
 import 'rules/prefer_interpolation_to_compose_strings.dart';
 import 'rules/prefer_is_empty.dart';
 import 'rules/prefer_is_not_empty.dart';
@@ -164,6 +164,7 @@
 import 'rules/unsafe_html.dart';
 import 'rules/use_full_hex_values_for_flutter_colors.dart';
 import 'rules/use_function_type_syntax_for_parameters.dart';
+import 'rules/use_is_even_rather_than_modulo.dart';
 import 'rules/use_key_in_widget_constructors.dart';
 import 'rules/use_raw_strings.dart';
 import 'rules/use_rethrow_when_possible.dart';
@@ -234,6 +235,7 @@
     ..register(EmptyCatches())
     ..register(EmptyConstructorBodies())
     ..register(EmptyStatements())
+    ..register(ExhaustiveCases())
     ..register(FileNames())
     ..register(FlutterStyleTodos())
     ..register(HashAndEquals())
diff --git a/lib/src/rules/exhaustive_cases.dart b/lib/src/rules/exhaustive_cases.dart
new file mode 100644
index 0000000..12815da
--- /dev/null
+++ b/lib/src/rules/exhaustive_cases.dart
@@ -0,0 +1,146 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+
+import '../analyzer.dart';
+import '../util/dart_type_utilities.dart';
+
+const _desc = r'Define case clauses for all constants in enum-like classes.';
+
+const _details = r'''
+Switching on instances of enum-like classes should be exhaustive.
+
+Enum-like classes are defined as concrete (non-abstract) classes that have:
+  * only private non-factory constructors
+  * two or more static const fields whose type is the enclosing class and
+  * no subclasses of the class in the defining library
+
+**DO** define case clauses for all constants in enum-like classes.
+
+**BAD:**
+```
+class EnumLike {
+  final int i;
+  const E._(this.i);
+
+  static const e = E._(1);
+  static const f = E._(2);
+  static const g = E._(3);
+}
+
+void bad(EnumLike e) {
+  // Missing case.
+  switch(e) { // LINT
+    case E.e :
+      print('e');
+      break;
+    case E.f :
+      print('e');
+      break;
+  }
+}
+```
+
+**GOOD:**
+```
+class EnumLike {
+  final int i;
+  const E._(this.i);
+
+  static const e = E._(1);
+  static const f = E._(2);
+  static const g = E._(3);
+}
+
+void ok(E e) {
+  // All cases covered.
+  switch(e) { // OK
+    case E.e :
+      print('e');
+      break;
+    case E.f :
+      print('e');
+      break;
+    case E.g :
+      print('e');
+      break;
+  }
+}
+```
+''';
+
+class ExhaustiveCases extends LintRule implements NodeLintRule {
+  ExhaustiveCases()
+      : super(
+            name: 'exhaustive_cases',
+            description: _desc,
+            details: _details,
+            group: Group.style);
+
+  @override
+  void registerNodeProcessors(NodeLintRegistry registry,
+      [LinterContext context]) {
+    final visitor = _Visitor(this);
+    registry.addSwitchStatement(this, visitor);
+  }
+}
+
+class _Visitor extends SimpleAstVisitor {
+  static const LintCode lintCode = LintCode(
+    'exhaustive_cases',
+    "Missing case clause for '{0}'.",
+    correction: 'Try adding a case clause for the missing constant.',
+  );
+
+  final LintRule rule;
+
+  _Visitor(this.rule);
+
+  @override
+  void visitSwitchStatement(SwitchStatement statement) {
+    var expressionType = statement.expression.staticType;
+    if (expressionType is InterfaceType) {
+      var classElement = expressionType.element;
+      // Handled in analyzer.
+      if (classElement.isEnum) {
+        return;
+      }
+      var enumDescription = DartTypeUtilities.asEnumLikeClass(classElement);
+      if (enumDescription == null) {
+        return;
+      }
+
+      var enumConstantNames = enumDescription.enumConstantNames;
+      for (var member in statement.members) {
+        if (member is SwitchCase) {
+          var expression = member.expression;
+          // todo (pq): add a test to ensure that this handles prefixed identifiers.
+          if (expression is Identifier) {
+            var element = expression.staticElement;
+            if (element is PropertyAccessorElement) {
+              enumConstantNames.remove(element.name);
+            }
+          }
+        }
+      }
+
+      for (var constantName in enumConstantNames) {
+        // Use the same offset as MISSING_ENUM_CONSTANT_IN_SWITCH
+        var offset = statement.offset;
+        var end = statement.rightParenthesis.end;
+        // todo (pq): update to use reportLintForOffset when published (> 0.39.8)
+        rule.reporter.reportErrorForOffset(
+          lintCode,
+          offset,
+          end - offset,
+          [constantName],
+        );
+      }
+    }
+  }
+}
diff --git a/lib/src/util/dart_type_utilities.dart b/lib/src/util/dart_type_utilities.dart
index a2b4ec5..ffe2087 100644
--- a/lib/src/util/dart_type_utilities.dart
+++ b/lib/src/util/dart_type_utilities.dart
@@ -12,6 +12,56 @@
 typedef AstNodePredicate = bool Function(AstNode node);
 
 class DartTypeUtilities {
+  static EnumLikeClassDescription asEnumLikeClass(ClassElement classElement) {
+    // See discussion: https://github.com/dart-lang/linter/issues/2083
+    //
+
+    // Must be concrete.
+    if (classElement.isAbstract) {
+      return null;
+    }
+
+    // With only private non-factory constructors.
+    for (var cons in classElement.constructors) {
+      if (!cons.isPrivate || cons.isFactory) {
+        return null;
+      }
+    }
+
+    var type = classElement.thisType;
+
+    // And 2 or more static const fields whose type is the enclosing class.
+    var enumConstantNames = <String>[];
+    for (var field in classElement.fields) {
+      // Ensure static const.
+      if (field.isSynthetic || !field.isConst || !field.isStatic) {
+        continue;
+      }
+      // Check for type equality.
+      if (field.type != type) {
+        continue;
+      }
+      enumConstantNames.add(field.name);
+    }
+    if (enumConstantNames.length < 2) {
+      return null;
+    }
+
+    // And no subclasses in the defining library.
+    var compilationUnit = classElement.library.definingCompilationUnit;
+    for (var cls in compilationUnit.types) {
+      var classType = cls.thisType;
+      do {
+        classType = classType.superclass;
+        if (classType == type) {
+          return null;
+        }
+      } while (!classType.isDartCoreObject);
+    }
+
+    return EnumLikeClassDescription(enumConstantNames);
+  }
+
   /// Return whether the canonical elements of two elements are equal.
   static bool canonicalElementsAreEqual(Element element1, Element element2) =>
       getCanonicalElement(element1) == getCanonicalElement(element2);
@@ -448,6 +498,11 @@
   }
 }
 
+class EnumLikeClassDescription {
+  List<String> enumConstantNames;
+  EnumLikeClassDescription(this.enumConstantNames);
+}
+
 class InterfaceTypeDefinition {
   final String name;
   final String library;
diff --git a/test/rules/exhaustive_cases.dart b/test/rules/exhaustive_cases.dart
new file mode 100644
index 0000000..f5cfc59
--- /dev/null
+++ b/test/rules/exhaustive_cases.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// test w/ `pub run test -N exhaustive_cases`
+
+// Enum-like
+class E {
+  final int i;
+  const E._(this.i);
+
+  static const e = E._(1);
+  static const f = E._(2);
+  static const g = E._(3);
+}
+
+void e(E e) {
+  // Missing case.
+  switch(e) { // LINT
+    case E.e :
+      print('e');
+      break;
+    case E.f :
+      print('e');
+  }
+}
+
+void ok(E e) {
+  // All cases covered.
+  switch(e) { // OK
+    case E.e :
+      print('e');
+      break;
+    case E.f :
+      print('e');
+      break;
+    case E.g :
+      print('e');
+      break;
+  }
+}
+
+
+// Not Enum-like
+class Subclassed {
+  const Subclassed._();
+
+  static const e = Subclassed._();
+  static const f = Subclassed._();
+  static const g = Subclassed._();
+}
+
+class Subclass extends Subclassed {
+  Subclass() : super._();
+}
+
+void s(Subclassed e) {
+  switch(e) { // OK
+    case Subclassed.e :
+      print('e');
+  }
+}
+
+// Not Enum-like
+class TooFew {
+  const TooFew._();
+
+  static const e = TooFew._();
+}
+
+void t(TooFew e) {
+  switch(e) { // OK
+    case TooFew.e :
+      print('e');
+  }
+}
+
+// Not Enum-like
+class PublicCons {
+  const PublicCons();
+  static const e = PublicCons();
+  static const f = PublicCons();
+}
+
+void p(PublicCons e) {
+  switch(e) { // OK
+    case PublicCons.e :
+      print('e');
+  }
+}
+
+// Handled by analyzer
+enum ActualEnum {
+  e, f
+}
+void ae(ActualEnum e) {
+  // ignore: missing_enum_constant_in_switch
+  switch(e) { // OK
+    case ActualEnum.e :
+      print('e');
+  }
+}