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
+}