Skip to content

Conversation

@stereotype441
Copy link
Contributor

@stereotype441 stereotype441 commented Aug 9, 2022

This change replaces NavigatorObserver._navigator (an instance field that points from a NavigatorObserver to the NavigatorState it's observing) with an expando that maps from NavigatorObserver to NavigatorState. There is no change to the public API, and no change to the behavior of users' production code.

The change ensures that if a customer's test replaces an instance of NavigatorObserver with a mock, but doesn't replace NavigatorState with a mock, the code in NavigatorState won't try to access the private _navigator field in NavigatorObserver. This is important because an upcoming change to the dart language is going to cause such accesses to throw exceptions (see #109237 and dart-lang/language#2020 for details).

Fixes #109237.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

The dart language team will soon be changing the behavior of the
language so that an invocation of a private class members won't get
dispatched to a `noSuchMethod` method in another library; instead an
exception will be thrown.  This is a necessary prerequisite to
allowing promotion of private fields
(dart-lang/language#2020).  It also closes
an important privacy loophole in the language, by ensuring that the
user can see all the possible behaviors of a private member invocation
without having to look outside of the library.

This has an important consequence for tests using mockito, in the
scenario where a private class member is referenced from outside of
its containing class, and the containing class instance is replaced
with a mock, but the reference isn't replaced with a mock.
Previously, in this scenario, the invocation would be dispatched to
Mock.noSuchMethod invocation; with this change it will throw an
exception.

This situation arises with NavigatorObserver._navigator, which is
referenced from the NavigatorState class; if a test mocks
NavigatorObserver but not NavigatorState, it will likely fail.  It
could be argued that such tests should be rewritten to use a less
brittle mocking strategy, but there are enough examples of this
pattern in Google's internal code base that it seems likely that
it exists in code belonging to other customers too.

To avoid breaking these customers, we replace
NavigatorObserver._navigator with a static expando.  Since references
to static class members aren't affected by mocking, this will ensure
that the upcoming dart language change won't trigger any new
exceptions in code following this testing pattern.

There is no behavioral change for production code.
@flutter-dashboard flutter-dashboard bot added f: routes Navigator, Router, and related APIs. framework flutter/packages/flutter repository. See also f: labels. labels Aug 9, 2022
@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for this that makes sure a class implementing NavigatorObserver and noSuchMethod can be used as a Navigator.observer without problems? That way, if somebody tries to refactor this back to what it was we will have a failing test to warn them once field promotion lands.

NavigatorState? get navigator => _navigator;
NavigatorState? _navigator;
NavigatorState? get navigator => _navigators[this];
static final _navigators = Expando<NavigatorState>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you leave a comment on this Expando property describing why we are doing this. I am sure the next guy looking at this will be somewhat confused as to why we do this here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

NavigatorState? get navigator => _navigator;
NavigatorState? _navigator;
NavigatorState? get navigator => _navigators[this];
static final _navigators = Expando<NavigatorState>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also - as the analyzer check will hopefully remind you - this field needs to be typed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@stereotype441
Copy link
Contributor Author

Could you add a test for this that makes sure a class implementing NavigatorObserver and noSuchMethod can be used as a Navigator.observer without problems? That way, if somebody tries to refactor this back to what it was we will have a failing test to warn them once field promotion lands.

Ok, done. I puzzled for a while over what the test should do, and what expectations it should check. I finally settled on replicating the actions in one of the other simple navigator tests, and testing that the sequence of invocations passed to noSuchMethod is as expected. Let me know if this seems reasonable.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once the checks are happy.

@stereotype441 stereotype441 merged commit 2a6f9ad into flutter:master Aug 10, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 10, 2022
camsim99 pushed a commit to camsim99/flutter that referenced this pull request Aug 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

f: routes Navigator, Router, and related APIs. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NavigatorObserver._navigator needs reworking due to an upcoming dart language change

2 participants