linter: Allow dynamic calls on explicit dynamic- or Function-casts

Fixes https://github.com/dart-lang/linter/issues/4806

Change-Id: I6c900f9aa3d7c2cf8dc3547bfe035eab820e750b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/339101
Auto-Submit: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
diff --git a/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart b/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart
index 3218ccb..c62e870 100644
--- a/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart
+++ b/pkg/linter/lib/src/rules/avoid_dynamic_calls.dart
@@ -36,6 +36,8 @@
 to "dynamic", and calls to an object that is typed "Function" will also trigger
 this lint.
 
+Dynamic calls are allowed on cast expressions (`as dynamic` or `as Function`).
+
 **BAD:**
 ```dart
 void explicitDynamicType(dynamic object) {
@@ -183,11 +185,13 @@
       if (methodName == 'noSuchMethod' &&
           node.argumentList.arguments.length == 1 &&
           node.argumentList.arguments.first is! NamedExpression) {
-        // Special-cased; these exist on every object, even those typed "Object?".
+        // Special-cased; these exist on every object, even those typed
+        // "Object?".
         return;
       }
       if (methodName == 'toString' && node.argumentList.arguments.isEmpty) {
-        // Special-cased; these exist on every object, even those typed "Object?".
+        // Special-cased; these exist on every object, even those typed
+        // "Object?".
         return;
       }
     }
@@ -244,12 +248,16 @@
   }
 
   bool _lintIfDynamic(Expression? node) {
-    if (node?.staticType is DynamicType) {
-      rule.reportLint(node);
-      return true;
-    } else {
+    if (node == null || node.staticType is! DynamicType) {
       return false;
     }
+
+    if (node.unParenthesized is AsExpression) {
+      return false;
+    }
+
+    rule.reportLint(node);
+    return true;
   }
 
   void _lintIfDynamicOrFunction(Expression node, {DartType? staticType}) {
@@ -257,6 +265,9 @@
     if (staticType == null) {
       return;
     }
+    if (node.unParenthesized is AsExpression) {
+      return;
+    }
     if (staticType is DynamicType) {
       rule.reportLint(node);
     }
diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart
index 3f8071c..9ac005a 100644
--- a/pkg/linter/test/rules/all.dart
+++ b/pkg/linter/test/rules/all.dart
@@ -11,6 +11,7 @@
 import 'avoid_annotating_with_dynamic_test.dart'
     as avoid_annotating_with_dynamic;
 import 'avoid_catching_errors_test.dart' as avoid_catching_errors;
+import 'avoid_dynamic_calls_test.dart' as avoid_dynamic_calls;
 import 'avoid_equals_and_hash_code_on_mutable_classes_test.dart'
     as avoid_equals_and_hash_code_on_mutable_classes;
 import 'avoid_escaping_inner_quotes_test.dart' as avoid_escaping_inner_quotes;
@@ -260,6 +261,7 @@
   annotate_redeclares.main();
   avoid_annotating_with_dynamic.main();
   avoid_catching_errors.main();
+  avoid_dynamic_calls.main();
   avoid_equals_and_hash_code_on_mutable_classes.main();
   avoid_escaping_inner_quotes.main();
   avoid_final_parameters.main();
diff --git a/pkg/linter/test/rules/avoid_dynamic_calls_test.dart b/pkg/linter/test/rules/avoid_dynamic_calls_test.dart
new file mode 100644
index 0000000..130c86b
--- /dev/null
+++ b/pkg/linter/test/rules/avoid_dynamic_calls_test.dart
@@ -0,0 +1,151 @@
+// Copyright (c) 2023, 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:test_reflective_loader/test_reflective_loader.dart';
+
+import '../rule_test_support.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(AvoidDynamicCallsTest);
+  });
+}
+
+@reflectiveTest
+class AvoidDynamicCallsTest extends LintRuleTest {
+  @override
+  String get lintRule => 'avoid_dynamic_calls';
+
+  test_binaryExpression() async {
+    await assertDiagnostics(r'''
+void f(dynamic a) {
+  a + 1;
+}
+''', [
+      lint(22, 1),
+    ]);
+  }
+
+  test_binaryExpression_asDynamic() async {
+    await assertNoDiagnostics(r'''
+void f(Object? a) {
+  (a as dynamic) + 1;
+}
+''');
+  }
+
+  test_functionExpressionInvocation() async {
+    await assertDiagnostics(r'''
+void f(Function? g1, Function g2) {
+  (g1 ?? g2)();
+}
+''', [
+      lint(38, 10),
+    ]);
+  }
+
+  test_functionExpressionInvocation_asFunction() async {
+    await assertNoDiagnostics(r'''
+void f(Object? g1, Object? g2) {
+  ((g1 ?? g2) as Function)();
+}
+''');
+  }
+
+  test_functionInvocation() async {
+    await assertDiagnostics(r'''
+void f(Function g) {
+  g();
+}
+''', [
+      lint(23, 1),
+    ]);
+  }
+
+  test_functionInvocation_asFunction() async {
+    await assertNoDiagnostics(r'''
+void f(Object? g) {
+  (g as Function)();
+}
+''');
+  }
+
+  test_indexAssignmentExpression() async {
+    await assertDiagnostics(r'''
+void f(dynamic a) {
+  a[1] = 7;
+}
+''', [
+      lint(22, 1),
+    ]);
+  }
+
+  test_indexAssignmentExpression_asDynamic() async {
+    await assertNoDiagnostics(r'''
+void f(Object? a) {
+  (a as dynamic)[1] = 7;
+}
+''');
+  }
+
+  test_indexExpression() async {
+    await assertDiagnostics(r'''
+void f(dynamic a) {
+  a[1];
+}
+''', [
+      lint(22, 1),
+    ]);
+  }
+
+  test_indexExpression_asDynamic() async {
+    await assertNoDiagnostics(r'''
+void f(Object? a) {
+  (a as dynamic)[1];
+}
+''');
+  }
+
+  test_prefixedIdentifier() async {
+    await assertDiagnostics(r'''
+void f(dynamic a) {
+  a.foo;
+}
+''', [
+      lint(22, 1),
+    ]);
+  }
+
+  test_prefixedIdentifier_asDynamic() async {
+    await assertNoDiagnostics(r'''
+void f(Object? a) {
+  (a as dynamic).foo;
+}
+''');
+  }
+
+  test_propertyAccess() async {
+    await assertDiagnostics(r'''
+void f(C c) {
+  c.a.foo;
+}
+class C {
+  dynamic a;
+}
+''', [
+      lint(16, 3),
+    ]);
+  }
+
+  test_propertyAccess_asDynamic() async {
+    await assertNoDiagnostics(r'''
+void f(C c) {
+  (c.a as dynamic).foo;
+}
+class C {
+  Object? a;
+}
+''');
+  }
+}
diff --git a/pkg/linter/tool/machine/rules.json b/pkg/linter/tool/machine/rules.json
index 524cfd9..56cdc1d 100644
--- a/pkg/linter/tool/machine/rules.json
+++ b/pkg/linter/tool/machine/rules.json
@@ -20,7 +20,7 @@
     "incompatible": [],
     "sets": [],
     "fixStatus": "noFix",
-    "details": "**DO** avoid method calls or accessing properties on an object that is either\nexplicitly or implicitly statically typed \"dynamic\". Dynamic calls are treated\nslightly different in every runtime environment and compiler, but most\nproduction modes (and even some development modes) have both compile size and\nruntime performance penalties associated with dynamic calls.\n\nAdditionally, targets typed \"dynamic\" disables most static analysis, meaning it\nis easier to lead to a runtime \"NoSuchMethodError\" or \"NullError\" than properly\nstatically typed Dart code.\n\nThere is an exception to methods and properties that exist on \"Object?\":\n- a.hashCode\n- a.runtimeType\n- a.noSuchMethod(someInvocation)\n- a.toString()\n\n... these members are dynamically dispatched in the web-based runtimes, but not\nin the VM-based ones. Additionally, they are so common that it would be very\npunishing to disallow `any.toString()` or `any == true`, for example.\n\nNote that despite \"Function\" being a type, the semantics are close to identical\nto \"dynamic\", and calls to an object that is typed \"Function\" will also trigger\nthis lint.\n\n**BAD:**\n```dart\nvoid explicitDynamicType(dynamic object) {\n  print(object.foo());\n}\n\nvoid implicitDynamicType(object) {\n  print(object.foo());\n}\n\nabstract class SomeWrapper {\n  T doSomething<T>();\n}\n\nvoid inferredDynamicType(SomeWrapper wrapper) {\n  var object = wrapper.doSomething();\n  print(object.foo());\n}\n\nvoid callDynamic(dynamic function) {\n  function();\n}\n\nvoid functionType(Function function) {\n  function();\n}\n```\n\n**GOOD:**\n```dart\nvoid explicitType(Fooable object) {\n  object.foo();\n}\n\nvoid castedType(dynamic object) {\n  (object as Fooable).foo();\n}\n\nabstract class SomeWrapper {\n  T doSomething<T>();\n}\n\nvoid inferredType(SomeWrapper wrapper) {\n  var object = wrapper.doSomething<Fooable>();\n  object.foo();\n}\n\nvoid functionTypeWithParameters(Function() function) {\n  function();\n}\n```\n\n",
+    "details": "**DO** avoid method calls or accessing properties on an object that is either\nexplicitly or implicitly statically typed \"dynamic\". Dynamic calls are treated\nslightly different in every runtime environment and compiler, but most\nproduction modes (and even some development modes) have both compile size and\nruntime performance penalties associated with dynamic calls.\n\nAdditionally, targets typed \"dynamic\" disables most static analysis, meaning it\nis easier to lead to a runtime \"NoSuchMethodError\" or \"NullError\" than properly\nstatically typed Dart code.\n\nThere is an exception to methods and properties that exist on \"Object?\":\n- a.hashCode\n- a.runtimeType\n- a.noSuchMethod(someInvocation)\n- a.toString()\n\n... these members are dynamically dispatched in the web-based runtimes, but not\nin the VM-based ones. Additionally, they are so common that it would be very\npunishing to disallow `any.toString()` or `any == true`, for example.\n\nNote that despite \"Function\" being a type, the semantics are close to identical\nto \"dynamic\", and calls to an object that is typed \"Function\" will also trigger\nthis lint.\n\nDynamic calls are allowed on cast expressions (`as dynamic` or `as Function`).\n\n**BAD:**\n```dart\nvoid explicitDynamicType(dynamic object) {\n  print(object.foo());\n}\n\nvoid implicitDynamicType(object) {\n  print(object.foo());\n}\n\nabstract class SomeWrapper {\n  T doSomething<T>();\n}\n\nvoid inferredDynamicType(SomeWrapper wrapper) {\n  var object = wrapper.doSomething();\n  print(object.foo());\n}\n\nvoid callDynamic(dynamic function) {\n  function();\n}\n\nvoid functionType(Function function) {\n  function();\n}\n```\n\n**GOOD:**\n```dart\nvoid explicitType(Fooable object) {\n  object.foo();\n}\n\nvoid castedType(dynamic object) {\n  (object as Fooable).foo();\n}\n\nabstract class SomeWrapper {\n  T doSomething<T>();\n}\n\nvoid inferredType(SomeWrapper wrapper) {\n  var object = wrapper.doSomething<Fooable>();\n  object.foo();\n}\n\nvoid functionTypeWithParameters(Function() function) {\n  function();\n}\n```\n\n",
     "sinceDartSdk": "2.12.0"
   },
   {