no_runtimeType_toString (#1969)

* avoid_runtimeType_toString

* address review comments

* address review comments
diff --git a/example/all.yaml b/example/all.yaml
index 7f99b07..9578efa 100644
--- a/example/all.yaml
+++ b/example/all.yaml
@@ -74,6 +74,7 @@
     - no_adjacent_strings_in_list
     - no_duplicate_case_values
     - no_logic_in_create_state
+    - no_runtimeType_toString
     - non_constant_identifier_names
     - null_closures
     - omit_local_variable_types
diff --git a/lib/src/rules.dart b/lib/src/rules.dart
index 39fec22..57828c5 100644
--- a/lib/src/rules.dart
+++ b/lib/src/rules.dart
@@ -75,6 +75,7 @@
 import 'rules/no_adjacent_strings_in_list.dart';
 import 'rules/no_duplicate_case_values.dart';
 import 'rules/no_logic_in_create_state.dart';
+import 'rules/no_runtimeType_toString.dart';
 import 'rules/non_constant_identifier_names.dart';
 import 'rules/null_closures.dart';
 import 'rules/omit_local_variable_types.dart';
@@ -239,6 +240,7 @@
     ..register(NoDuplicateCaseValues())
     ..register(NonConstantIdentifierNames())
     ..register(NoLogicInCreateState())
+    ..register(NoRuntimeTypeToString())
     ..register(NullClosures())
     ..register(OmitLocalVariableTypes())
     ..register(OneMemberAbstracts())
diff --git a/lib/src/rules/no_runtimeType_toString.dart b/lib/src/rules/no_runtimeType_toString.dart
new file mode 100644
index 0000000..39781cb
--- /dev/null
+++ b/lib/src/rules/no_runtimeType_toString.dart
@@ -0,0 +1,110 @@
+// 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 '../analyzer.dart';
+
+const _desc = r'Avoid calling toString() on runtimeType.';
+
+const _details = r'''
+
+Calling `toString` on a runtime type is a non-trivial operation that can
+negatively impact performance. It's better to avoid it.
+
+**BAD:**
+```
+class A {
+  String toString() => '$runtimeType()';
+}
+```
+
+**GOOD:**
+```
+class A {
+  String toString() => 'A()';
+}
+```
+
+This lint has some exceptions where performance is not a problem or where real
+type information is more important than performance:
+
+* in assertion
+* in throw expressions
+* in catch clauses
+* in mixin declaration
+* in abstract class
+
+''';
+
+class NoRuntimeTypeToString extends LintRule implements NodeLintRule {
+  NoRuntimeTypeToString()
+      : super(
+            name: 'no_runtimeType_toString',
+            description: _desc,
+            details: _details,
+            group: Group.style);
+
+  @override
+  void registerNodeProcessors(NodeLintRegistry registry,
+      [LinterContext context]) {
+    final visitor = _Visitor(this);
+    registry.addInterpolationExpression(this, visitor);
+    registry.addMethodInvocation(this, visitor);
+  }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+
+  _Visitor(this.rule);
+
+  @override
+  void visitMethodInvocation(MethodInvocation node) {
+    if (_canSkip(node)) {
+      return;
+    }
+    if (node.methodName.name == 'toString' &&
+        _isRuntimeTypeAccess(node.realTarget)) {
+      rule.reportLint(node.methodName);
+    }
+  }
+
+  @override
+  void visitInterpolationExpression(InterpolationExpression node) {
+    if (_canSkip(node)) {
+      return;
+    }
+    if (_isRuntimeTypeAccess(node.expression)) {
+      rule.reportLint(node.expression);
+    }
+  }
+
+  bool _isRuntimeTypeAccess(Expression target) =>
+      target is PropertyAccess &&
+          (target.target is ThisExpression ||
+              target.target is SuperExpression) &&
+          target.propertyName.name == 'runtimeType' ||
+      target is SimpleIdentifier &&
+          target.name == 'runtimeType' &&
+          target.staticElement is PropertyAccessorElement;
+
+  bool _canSkip(AstNode node) =>
+      node.thisOrAncestorMatching((n) {
+        if (n is Assertion) return true;
+        if (n is ThrowExpression) return true;
+        if (n is CatchClause) return true;
+        if (n is MixinDeclaration) return true;
+        if (n is ClassDeclaration && n.isAbstract) return true;
+        if (n is ExtensionDeclaration) {
+          final extendedElement = n.declaredElement.extendedType.element;
+          return !(extendedElement is ClassElement &&
+              !extendedElement.isAbstract);
+        }
+        return false;
+      }) !=
+      null;
+}
diff --git a/test/rules/no_runtimeType_toString.dart b/test/rules/no_runtimeType_toString.dart
new file mode 100644
index 0000000..d59b0de
--- /dev/null
+++ b/test/rules/no_runtimeType_toString.dart
@@ -0,0 +1,54 @@
+// 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 no_runtimeType_toString`
+
+var o;
+
+class A {
+  var field;
+  String f() {
+    final s1 = '$runtimeType'; // LINT
+    final s2 = runtimeType.toString(); // LINT
+    final s3 = this.runtimeType.toString(); // LINT
+    final s4 = '${runtimeType}'; // LINT
+    final s5 = '${o.runtimeType}'; // OK
+    final s6 = o.runtimeType.toString(); // OK
+    final s7 = runtimeType == runtimeType; // OK
+    final s8 = field?.runtimeType?.toString(); // OK
+    try {
+      final s9 = '${runtimeType}'; // LINT
+    } catch (e) {
+      final s10 = '${runtimeType}'; // OK
+    }
+    final s11 = super.runtimeType.toString(); // LINT
+    throw '${runtimeType}'; // OK
+  }
+}
+
+abstract class B {
+  void f() {
+    final s1 = '$runtimeType'; // OK
+  }
+}
+
+mixin C {
+  void f() {
+    final s1 = '$runtimeType'; // OK
+  }
+}
+
+class D {
+  void f() {
+    var runtimeType = 'C';
+    print('$runtimeType'); // OK
+  }
+}
+
+extension on A {
+  String f() => '$runtimeType'; // LINT
+}
+extension on B {
+  String f() => '$runtimeType'; // OK
+}