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');
+ }
+}