Skip to content

Support @onlyOnThis: lint access to members on any receiver other than this #58722

Open
@eernstg

Description

@eernstg

Motivation

It is well-known that access to a private instance member _m in the declaring library is unsafe when a subtype of the enclosing class can be created in a different library:

// Library 'lib.dart'.

class C { String get _m => 'C from lib!'; }
void printIt(C c) => print("Instance of '${c._m}'");

// Library 'main.dart'.
import 'lib.dart';

class D implements C {}
void main() => printIt(D()); // Throws!

The basic issue is that privacy dominates soundness (for historic reasons), so we're allowed to have declarations like D, even though it is known at compile time that it does not have (and could not get) an implementation of _m that belongs to 'lib.dart' (if we write an _m in 'main.dart' then it belongs to 'main.dart', and it is completely unrelated to the one in 'lib.dart').

Let's consider this kind of safety violation for a public class C with a private member _m.

One way to make this safe is to introduce a notion of non-interface classes. If C does not have an interface then we can create subclasses using extends C (and, in some cases, we can mix it in using with C), but we cannot create a non-subclass subtype using implements C. This is safe because the dynamic type of c will be a class that inherits or overrides _m.

However, that approach makes it impossible to mock the class C, and it may also be overly restrictive for other purposes.

We do have another way ahead: If we can annotate _m with @onlyOnThis and then introduce a lint only_access_on_this that reports every case where _m is accessed (invoked or torn off) on a receiver which is not this, then the invocation is again safe because the run-time type of this will be a class that inherits or overrides _m (because other classes won't inherit the code that accesses _m).

It could also be useful to be able to specify that a given member (private or not) should only be accessed on this, simply because that's a useful business rule to have for certain software designs. You could say that this kind of member is "private to this object", and that concept could be useful when reasoning about the code.

Cf. #48918 for some recent discussions of a similar topic.

Proposal

This is a proposal for adding metadata @onlyOnThis, and adding a lint only_access_on_this.

@onlyOnThis may be added to any instance member m of a class, and this annotation is considered to be enabled on m as well as on every member that overrides m, directly or indirectly.

The lint would then flag every case where a member m is accessed (invoked or torn off) on a receiver which is not this (implicitly or explicitly), and @onlyOnThis is enabled on m.

We could then add another lint, perhaps named avoid_implementing_class_with_inaccessible_private_members, and use that to flag declarations where some inaccessible private members of a non-superclass superinterface do not have @onlyOnThis (that is, declarations that are unsafe in the same way as D in the original example).

Discussion

I believe it's useful to allow this diagnostic to be ignored, hence I'm proposing to use a lint rather than a compile-time error: There could be cases (like testing) where some private members remain unimplemented in a subtype (like a mock class), but the usage could be so simple and specific that said members are known to remain unused. Similarly, if @onlyOnThis is specified for software engineering reasons (and not for soundness reasons) then it might be justified to violate the constraint in some exceptional situations.

The metadata annotation @protected includes a similar constraint:

Tools, such as the analyzer, can provide feedback if
...

  • a reference to a member m which has this annotation, declared in
    a class or mixin C, is found outside of the declaring library and the
    receiver is something other than this.

However, there is no redundancy. That constraint never flags an access to m inside the declaring library, and hence it does nothing at all to help resolving the safety issue with unimplemented private members. You could perhaps even say that @onlyOnThis is orthogonal to protectedness, and @protected should stop flagging any member accesses based on whether the receiver is or is not this — and developers who do want this constraint would use both, as in @protected @onlyOnThis.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3A lower priority bug or feature requestarea-devexpFor issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages.devexp-linterIssues with the analyzer's support for the linter packagelinter-lint-requesttype-enhancementA request for a change that isn't a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions