-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Lint the situation where a noSuchMethod thrower is introduced because of privacy #58506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Recent changes to the language include a change to the semantics of these implicitly induced private members: They will throw, unconditionally, rather than forwarding to The main reason for this is that it would otherwise be unsafe to promote certain private instance variables, cf. dart-lang/language#2020 and https://github.com/dart-lang/language/labels/field-promotion, and the ability to perform these promotions is given a high priority by the language team. With the introduction of class modifiers, we can now use This lint will then be useful in several ways, albeit indirectly:
Note that it is not difficult to create this situation: // Library 'lib.dart'.
base class C {
void _m() {}
}
@reopen
abstract base class C2 implements C {
void _m();
}
void foo(C c) => c._m();
// Some other library.
import 'lib.dart';
base class D extends C2 {}
void main() => foo(D()); // Throws. @dart-lang/language-team, WDYT? Are you willing to give this lint request some support? |
As I understand it, there's two ways to get a no such method exception from a call to a private member: Implementing a class's interface in another library// lib_a.dart
class C {
void _m() {}
}
callM(C c) => c._m();
// lib_b.dart
class D implements C {}
main() {
callM(D()); // Ka-boom.
} Note that the only way this can happen is if Extending a class with an abstract method in another library// lib_a.dart
abstract class C {
void _m();
callM() => _m();
}
// lib_b.dart
class D extends C {}
main() {
D().callM(); // Ka-boom.
} Note that this can happen even with calls to private methods on In both cases, you have an instance invocation of a member that isn't actually there but the type checker doesn't catch it so it fails at runtime. I do like the idea of a lint to guard against these. But I worry it will be hard to come up with a precise set of semantics for the lint that don't lead to a lot of false positives. In practice, this seems to not be a large pain point for users, so tolerance for false positives is probably very low. Bad lint semanticsThe obvious semantics for the first example would be to report a lint error if:
If both of those are true, it's possible for a non- But I am sure that rule would have a ton of false positives. In my own code, I often define a class that I use as a public interface but that also has its own private implementation. It only accesses private members on Better lint semanticsSince it is the private member access that fails, that suggests that we should lint on those instead. So maybe report a lint error on a private member access where:
Note that with this, the lint would only ever fire in the library where the private members are declared, not in the other libraries that might be extending or implementing these classes. I think that's probably the right place for it. To fix the lint, the library author would have to mark the class I still worry that this lint would have a lot of annoying false positives, but it would be interesting to look into. I'm not sure if it's worth putting any significant effort into it, though. |
That's a nice analysis, @munificent! I agree that we're addressing the root cause by linting the class that declares a private member and allows external However, I'm suggesting that we also lint the other end of the relationship: We should lint any class that has a throwing noSuchMethod stub due to privacy. That is in a very real sense the root cause of the throwing behavior! The associated static analysis is very well-established (the compilers will generate those throwing stubs in the first place, so of course we know exactly on which classes they will exist). Moreover, this lint is also actionable in many cases. With // lib_a.dart
class C {
void _m() {}
}
callM(C c) => c._m();
// lib_b.dart
class D extends C {} // Lint is now gone.
main() {
callM(D()); // OK!
} With the other scenario, the external class I think it would be nice to have lints on all three: Consider a class
I made the lint on the throwing stub a bit more complex in order to avoid false positives. We can of course cut down on the complexity by approximating this rule in the safe direction (e.g., it's simpler to just require that To address the first lint, we can implement |
I'm with @munificent here: The problem is the private member invocation. If all such invocations are on I'd be very annoyed if a private member in another library causes any warnings in my library. I'd ask the other library author to fix that, but that's all I can do.
Why? Who benefits from getting a warning in the subclass? It's true that in some situations, you can fix the "problem" by using I'd go with just:
"Can have a throwing stub" is the case if:
(That is, generally I'd let the lint consider which cases are safe and which are not. It's allowed to use information that we won't have in the language specification, so I don't want to be too precise in defining when there is a problem, if some other available information proves that the problem isn't real.) |
OK, we all agree on the nature of the lints in the library that declares the private member. That's great! I'm sure we can sort out the details. The remaining part is that we don't agree on giving developers a heads-up at the point where they actually cause the throwing stub to be created.
I think that's a bit too optimistic. It is not a given that every organization/developer is able and willing to change every public class Still, we refuse to tell the developer who has a The developer of
You can just choose to say that it's a warning about using |
It sounds like this lint might have a lot of false positives, and that makes me wonder whether a lint is the right way to address this problem. Not all lints that discover ways that a runtime exception might occur are good to implement. If the probability of the exception is high enough, then finding it while writing the code can be very helpful. But if it's rare enough, then we might consider just improving the exception message to make it easier to debug. For example, could the message explain to the user that the problem is that a method was invoked on a subclass that caused a private, but unimplementable, method to be invoked, possibly with a link to documentation that describes the issue in more detail and suggests possible ways to fix the bug? Given that we're producing the stub at compile time we could presumably gather any information that the lint could while composing the message. It has the disadvantage that it might not be found if that code path isn't tested, but the advantage that there are no false positives. Also, the two options are not mutually exclusive. Given that users can disable lints, improvements to the exception message seem advisable anyway. |
I think the problem with this approach is how code evolves over time. Imagine:
I think it's an important invariant that adding a private member to a library shouldn't cause compile errors to appear in downstream code. |
@munificent wrote:
I think a compile-time error is preferable to a run-time error, and it isn't a safe bet that "maybe nobody will call that method, at least for a while". ;-) |
A productive language tells a developer everything they need to know through (1) API + static checking, and (2) documentation; other methods such as having them learn through runtime testing or tribal knowledge is less productive. Based on that I agree the check must be done on both sides of this error: (1) the library which has the private member and (2) the user of the library functionality that depends on that private member. There is no guarantee a library author will be using these static checks and ensure their code cannot lead to this error. Furthermore, this error is indirect and not communicated through API, static checks, or documentation, so it is not obvious to the user of the library either. A developer should not be surprised by an error generated by nonobvious language functionality when they use another library. Checking things from the user's end can notify them which API they may not be able to depend on at the moment. I say we take an approach that effectively combines what has been discussed in this thread so far:
|
[Edit 2024: Note that the implicitly induced member as of today will throw rather than invoke
noSuchMethod
. This just makes it even more important to introduce the lint as proposed, because it is even less likely that the implicitly induced member can be useful.]Cf. #47148.
It seems to contradict the overall static safety of Dart that we silently allow a concrete class
B
to implement/extend a classA
in a different library with an unimplemented private member_m
:In this situation, an implicitly induced member known as a noSuchMethod forwarder is added to
B
; this forwarder will invokenoSuchMethod
onthis
, and pass some data to describe the invocation.However, it is almost impossible to make that forwarder do anything other than throwing a
NoSuchMethodError
. In particular, we can't add anoSuchMethod
implementation that doesif (invocation.memberName == #_m) ..
toB
, because#_m
, being a private symbol, isn't equal to the given member name, because that's the#_m
of a different library.For example:
This issue is a request for a lint that flags any class (like
B
in the example) where a noSuchMethod forwarder is induced because of the privacy rule.The text was updated successfully, but these errors were encountered: