[cfe] Support noSuchMethod forwarder for invalid implementation through inheritance

This adds support for adding a noSuchMethod forwarder when an inherited private method from a different library doesn't implement the inherited interface.

Closes #61717

Change-Id: I932b47cd007219cce1b31e05406f2d08b28e588e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/454601
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/lib/src/builder/library_builder.dart b/pkg/front_end/lib/src/builder/library_builder.dart
index 8f16fde..3024326 100644
--- a/pkg/front_end/lib/src/builder/library_builder.dart
+++ b/pkg/front_end/lib/src/builder/library_builder.dart
@@ -107,7 +107,7 @@
   /// Lookups the required member [name] declared in this library.
   ///
   /// If no member is found an internal problem is reported.
-  NamedBuilder? lookupRequiredLocalMember(String name);
+  NamedBuilder lookupRequiredLocalMember(String name);
 
   void recordAccess(
     CompilationUnit accessor,
@@ -257,7 +257,7 @@
   }
 
   @override
-  NamedBuilder? lookupRequiredLocalMember(String name) {
+  NamedBuilder lookupRequiredLocalMember(String name) {
     NamedBuilder? builder = libraryNameSpace.lookup(name)?.getable;
     if (builder == null) {
       internalProblem(
diff --git a/pkg/front_end/lib/src/kernel/hierarchy/class_member.dart b/pkg/front_end/lib/src/kernel/hierarchy/class_member.dart
index a64ab9f..8c887a2 100644
--- a/pkg/front_end/lib/src/kernel/hierarchy/class_member.dart
+++ b/pkg/front_end/lib/src/kernel/hierarchy/class_member.dart
@@ -2,6 +2,7 @@
 // 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:front_end/src/kernel/hierarchy/members_node.dart';
 import 'package:kernel/ast.dart';
 import 'package:kernel/type_algebra.dart';
 
@@ -871,15 +872,18 @@
 
   @override
   bool get isNoSuchMethodForwarder {
-    // [implementedInterfaceMember] can only be a noSuchMethod forwarder if
-    // [inheritedClassMember] also is, since we don't allow overriding a regular
-    // member with a noSuchMethodForwarder.
+    // [implementedInterfaceMember] can be a noSuchMethod forwarder in two
+    // scenarios:
     //
-    // If the current class is abstract, [inheritedClassMember] can be a
-    // a noSuchMethod forwarder while [implementedInterfaceMember] is not,
-    // because we only insert noSuchMethod forwarders into non-abstract classes.
+    //  * If [inheritedClassMember] also is, since we don't allow overriding a
+    //    regular member with a noSuchMethodForwarder.
     //
-    // For instance
+    //    If the current class is abstract, [inheritedClassMember] can be a
+    //    a noSuchMethod forwarder while [implementedInterfaceMember] is not,
+    //    because we only insert noSuchMethod forwarders into non-abstract
+    //    classes.
+    //
+    //    For instance
     //
     //     class Super {
     //       noSuchMethod(_) => null;
@@ -899,15 +903,43 @@
     //                       // and this will be an error.
     //     }
     //
+    //  * If [name] is not visible in the library of [classBuilder].
+    //
+    //    If the [inheritedClassMember] is private wrt. a different library,
+    //    there is no requirement that it implements the
+    //    [implementedInterfaceMember], and we will create a noSuchMethod
+    //    forwarder if it doesn't.
+    //
+    //    For instance
+    //
+    //     // library a.dart
+    //     class A {
+    //       void _m(int i) {}
+    //     }
+    //     abstract class B extends A {
+    //       void _m(num i);
+    //     }
+    //
+    //     // library b.dart
+    //     import 'a.dart';
+    //     class C extends B {
+    //       /* _m(num i) */ // A._m doesn't implement B._m so a noSuchMethod
+    //                       // forwarder is created.
+    //     }
+    //
     assert(
-      !(implementedInterfaceMember.isNoSuchMethodForwarder &&
-          !inheritedClassMember.isNoSuchMethodForwarder),
+      !implementedInterfaceMember.isNoSuchMethodForwarder ||
+          inheritedClassMember.isNoSuchMethodForwarder ||
+          !isNameVisibleIn(name, classBuilder.libraryBuilder),
       "The inherited $inheritedClassMember has "
       "isNoSuchMethodForwarder="
       "${inheritedClassMember.isNoSuchMethodForwarder} but "
       "the implemented $implementedInterfaceMember has "
       "isNoSuchMethodForwarder="
-      "${implementedInterfaceMember.isNoSuchMethodForwarder}.",
+      "${implementedInterfaceMember.isNoSuchMethodForwarder} "
+      " and the name $name is "
+      "${!isNameVisibleIn(name, classBuilder.libraryBuilder) ? " not " : ""} "
+      "visible.",
     );
     return inheritedClassMember.isNoSuchMethodForwarder;
   }
diff --git a/pkg/front_end/lib/src/kernel/hierarchy/members_node.dart b/pkg/front_end/lib/src/kernel/hierarchy/members_node.dart
index 0a4a5b6..bb003b2 100644
--- a/pkg/front_end/lib/src/kernel/hierarchy/members_node.dart
+++ b/pkg/front_end/lib/src/kernel/hierarchy/members_node.dart
@@ -2505,11 +2505,58 @@
         interfaceMembers.addAll(_implementedMembers);
 
         ClassMember? noSuchMethodTarget;
-        if (_extendedMember.isNoSuchMethodForwarder &&
-            !classBuilder.isAbstract &&
-            (userNoSuchMethodMember != null ||
-                !isNameVisibleIn(name, classBuilder.libraryBuilder))) {
-          noSuchMethodTarget = noSuchMethodMember;
+        if (!classBuilder.isAbstract) {
+          if (_extendedMember.isNoSuchMethodForwarder &&
+              userNoSuchMethodMember != null) {
+            // If we inherit a noSuchMethod forwarder we might need to create
+            // a new one in this class.
+            //
+            // For instance
+            //
+            //     class Super {
+            //       noSuchMethod(_) => null;
+            //       method1(); // noSuchMethod forwarder created for this.
+            //       method2(int i); // noSuchMethod forwarder created for this.
+            //       method3(int i); // noSuchMethod forwarder created for this.
+            //       method4(int i) {}
+            //     }
+            //     abstract class Abstract extends Super {
+            //       method2(num i); // No noSuchMethod forwarder will be
+            //                       // inserted here.
+            //     }
+            //     class Class extends Abstract {
+            //       method1(); // noSuchMethod forwarder from Super is valid.
+            //       /* method2(num i) */ // A new noSuchMethod forwarder is
+            //                            // created.
+            //       method3(num i); // A new noSuchMethod forwarder is created.
+            //       method4(num i); // No noSuchMethod forwarder will be
+            //                       // inserted and this will be an error.
+            //     }
+            noSuchMethodTarget = noSuchMethodMember;
+          }
+          if (!isNameVisibleIn(name, classBuilder.libraryBuilder)) {
+            // [name] is not visible, so  there is no requirement that
+            // [_extendedMember]  implements the [interfaceMembers]. We will
+            // create a noSuchMethod forwarder if it doesn't.
+            //
+            // For instance
+            //
+            //     // library a.dart
+            //     class A {
+            //       void _m(int i) {}
+            //     }
+            //     abstract class B extends A {
+            //       void _m(num i);
+            //     }
+            //
+            //     // library b.dart
+            //     import 'a.dart';
+            //     class C extends B {
+            //       /* _m(num i) */ // A._m doesn't implement B._m so a
+            //                       // noSuchMethod forwarder is created.
+            //     }
+            noSuchMethodTarget = noSuchMethodMember;
+          }
         }
 
         /// Normally, if only one member defines the interface member there
diff --git a/pkg/front_end/testcases/general/issue61717.dart b/pkg/front_end/testcases/general/issue61717.dart
new file mode 100644
index 0000000..0404c48
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2025, 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 'issue61717_lib.dart';
+
+base class C extends B {}
+
+void main() {
+  try {
+    C().public();
+  } catch (_) {
+    print('C.public throws');
+  }
+}
diff --git a/pkg/front_end/testcases/general/issue61717.dart.strong.expect b/pkg/front_end/testcases/general/issue61717.dart.strong.expect
new file mode 100644
index 0000000..add930e
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.strong.expect
@@ -0,0 +1,47 @@
+library;
+import self as self;
+import "issue61717_lib.dart" as iss;
+import "dart:core" as core;
+
+import "org-dartlang-testcase:///issue61717_lib.dart";
+
+base class C extends iss::B {
+  synthetic constructor •() → self::C
+    : super iss::B::•()
+    ;
+  synthetic no-such-method-forwarder method iss::_m(core::num _#wc1#formal) → void
+    return throw{for-error-handling} core::NoSuchMethodError::withInvocation(this, new core::_InvocationMirror::_withType(#C1, 0, #C2, core::List::unmodifiable<dynamic>(<dynamic>[_#wc1#formal]), core::Map::unmodifiable<core::Symbol, dynamic>(#C3)));
+}
+static method main() → void {
+  try {
+    new self::C::•().{iss::A::public}(){() → void};
+  }
+  on core::Object catch(final wildcard core::Object _#wc0#formal) {
+    core::print("C.public throws");
+  }
+}
+
+library;
+import self as iss;
+import "dart:core" as core;
+
+base class A extends core::Object {
+  synthetic constructor •() → iss::A
+    : super core::Object::•()
+    ;
+  method _m(wildcard core::int _#wc0#formal) → void {}
+  method public() → void
+    return this.{iss::A::_m}(0){(core::int) → void};
+}
+abstract base class B extends iss::A {
+  synthetic constructor •() → iss::B
+    : super iss::A::•()
+    ;
+  abstract method _m(wildcard core::num _#wc1#formal) → void;
+}
+
+constants  {
+  #C1 = #org-dartlang-testcase:///issue61717.dart::_m
+  #C2 = <core::Type>[]
+  #C3 = <core::Symbol, dynamic>{}
+}
diff --git a/pkg/front_end/testcases/general/issue61717.dart.strong.modular.expect b/pkg/front_end/testcases/general/issue61717.dart.strong.modular.expect
new file mode 100644
index 0000000..add930e
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.strong.modular.expect
@@ -0,0 +1,47 @@
+library;
+import self as self;
+import "issue61717_lib.dart" as iss;
+import "dart:core" as core;
+
+import "org-dartlang-testcase:///issue61717_lib.dart";
+
+base class C extends iss::B {
+  synthetic constructor •() → self::C
+    : super iss::B::•()
+    ;
+  synthetic no-such-method-forwarder method iss::_m(core::num _#wc1#formal) → void
+    return throw{for-error-handling} core::NoSuchMethodError::withInvocation(this, new core::_InvocationMirror::_withType(#C1, 0, #C2, core::List::unmodifiable<dynamic>(<dynamic>[_#wc1#formal]), core::Map::unmodifiable<core::Symbol, dynamic>(#C3)));
+}
+static method main() → void {
+  try {
+    new self::C::•().{iss::A::public}(){() → void};
+  }
+  on core::Object catch(final wildcard core::Object _#wc0#formal) {
+    core::print("C.public throws");
+  }
+}
+
+library;
+import self as iss;
+import "dart:core" as core;
+
+base class A extends core::Object {
+  synthetic constructor •() → iss::A
+    : super core::Object::•()
+    ;
+  method _m(wildcard core::int _#wc0#formal) → void {}
+  method public() → void
+    return this.{iss::A::_m}(0){(core::int) → void};
+}
+abstract base class B extends iss::A {
+  synthetic constructor •() → iss::B
+    : super iss::A::•()
+    ;
+  abstract method _m(wildcard core::num _#wc1#formal) → void;
+}
+
+constants  {
+  #C1 = #org-dartlang-testcase:///issue61717.dart::_m
+  #C2 = <core::Type>[]
+  #C3 = <core::Symbol, dynamic>{}
+}
diff --git a/pkg/front_end/testcases/general/issue61717.dart.strong.outline.expect b/pkg/front_end/testcases/general/issue61717.dart.strong.outline.expect
new file mode 100644
index 0000000..5355dde
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.strong.outline.expect
@@ -0,0 +1,40 @@
+library;
+import self as self;
+import "issue61717_lib.dart" as iss;
+import "dart:core" as core;
+
+import "org-dartlang-testcase:///issue61717_lib.dart";
+
+base class C extends iss::B {
+  synthetic constructor •() → self::C
+    ;
+  synthetic no-such-method-forwarder method iss::_m(core::num _#wc1#formal) → void
+    return throw{for-error-handling} core::NoSuchMethodError::withInvocation(this, new core::_InvocationMirror::_withType(#_m, 0, const <core::Type>[], core::List::unmodifiable<dynamic>(<dynamic>[_#wc1#formal]), core::Map::unmodifiable<core::Symbol, dynamic>(const <core::Symbol, dynamic>{})));
+}
+static method main() → void
+  ;
+
+library;
+import self as iss;
+import "dart:core" as core;
+
+base class A extends core::Object {
+  synthetic constructor •() → iss::A
+    ;
+  method _m(wildcard core::int _#wc0#formal) → void
+    ;
+  method public() → void
+    ;
+}
+abstract base class B extends iss::A {
+  synthetic constructor •() → iss::B
+    ;
+  abstract method _m(wildcard core::num _#wc1#formal) → void;
+}
+
+
+Extra constant evaluation status:
+Evaluated: SymbolLiteral @ org-dartlang-testcase:///issue61717.dart:7:12 -> SymbolConstant(#_m)
+Evaluated: ListLiteral @ org-dartlang-testcase:///issue61717.dart:7:12 -> ListConstant(const <Type>[])
+Evaluated: MapLiteral @ org-dartlang-testcase:///issue61717.dart:7:12 -> MapConstant(const <Symbol, dynamic>{})
+Extra constant evaluation: evaluated: 11, effectively constant: 3
diff --git a/pkg/front_end/testcases/general/issue61717.dart.strong.transformed.expect b/pkg/front_end/testcases/general/issue61717.dart.strong.transformed.expect
new file mode 100644
index 0000000..de52a8c
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.strong.transformed.expect
@@ -0,0 +1,47 @@
+library;
+import self as self;
+import "issue61717_lib.dart" as iss;
+import "dart:core" as core;
+
+import "org-dartlang-testcase:///issue61717_lib.dart";
+
+base class C extends iss::B {
+  synthetic constructor •() → self::C
+    : super iss::B::•()
+    ;
+  synthetic no-such-method-forwarder method iss::_m(core::num _#wc1#formal) → void
+    return throw{for-error-handling} core::NoSuchMethodError::withInvocation(this, new core::_InvocationMirror::_withType(#C1, 0, #C2, core::List::unmodifiable<dynamic>(core::_GrowableList::_literal1<dynamic>(_#wc1#formal)), core::Map::unmodifiable<core::Symbol, dynamic>(#C3)));
+}
+static method main() → void {
+  try {
+    new self::C::•().{iss::A::public}(){() → void};
+  }
+  on core::Object catch(final wildcard core::Object _#wc0#formal) {
+    core::print("C.public throws");
+  }
+}
+
+library;
+import self as iss;
+import "dart:core" as core;
+
+base class A extends core::Object {
+  synthetic constructor •() → iss::A
+    : super core::Object::•()
+    ;
+  method _m(wildcard core::int _#wc0#formal) → void {}
+  method public() → void
+    return this.{iss::A::_m}(0){(core::int) → void};
+}
+abstract base class B extends iss::A {
+  synthetic constructor •() → iss::B
+    : super iss::A::•()
+    ;
+  abstract method _m(wildcard core::num _#wc1#formal) → void;
+}
+
+constants  {
+  #C1 = #org-dartlang-testcase:///issue61717.dart::_m
+  #C2 = <core::Type>[]
+  #C3 = <core::Symbol, dynamic>{}
+}
diff --git a/pkg/front_end/testcases/general/issue61717.dart.textual_outline.expect b/pkg/front_end/testcases/general/issue61717.dart.textual_outline.expect
new file mode 100644
index 0000000..3131f03
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.textual_outline.expect
@@ -0,0 +1,5 @@
+import 'issue61717_lib.dart';
+
+base class C extends B {}
+
+void main() {}
diff --git a/pkg/front_end/testcases/general/issue61717.dart.textual_outline_modelled.expect b/pkg/front_end/testcases/general/issue61717.dart.textual_outline_modelled.expect
new file mode 100644
index 0000000..3131f03
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717.dart.textual_outline_modelled.expect
@@ -0,0 +1,5 @@
+import 'issue61717_lib.dart';
+
+base class C extends B {}
+
+void main() {}
diff --git a/pkg/front_end/testcases/general/issue61717_lib.dart b/pkg/front_end/testcases/general/issue61717_lib.dart
new file mode 100644
index 0000000..88a086d
--- /dev/null
+++ b/pkg/front_end/testcases/general/issue61717_lib.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2025, 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.
+
+base class A {
+  void _m(int _) {}
+  void public() => _m(0);
+}
+
+abstract base class B extends A {
+  void _m(num _);
+}