-
Notifications
You must be signed in to change notification settings - Fork 1.7k
proposal: lint to flag when an exported API uses types that are not exported #58731
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
Attempting to make such a lint I face several questions/feedbacks: I think this lint shouldn't apply to libraries that are not reusable library. To detect those API-libraies I look for a Should the types used in parameter list be exported? On flutter codebase for instance there is On Flutter there is also cases with type alias where I'm not sure what to do. For instance Considering transitivity I'm not sure what the result should be. Finding what to export could be quite difficult and this could lead to a really large namespace at the end (with perhaps name clashes). Look at the following example: // a.dart
class A {}
// b.dart
import 'a.dart';
export 'a.dart' show A;
class B {
A a = A();
}
// c.dart
import 'c.dart';
export 'b.dart' show B;
// should we also export A ???
class C {
B b = B();
} |
It isn't clear to me that we want this lint. There's a high cost to code completion when the namespace of a library is bigger than it needs to be. While I understand the convenience benefit that led to Flutter's design, I'm not convinced that the benefit justifies the cost, especially for other packages. As a result, I'm not sure that this is a style that we want to promote among the larger community.
I agree. But I don't think the criteria you listed is sufficient, and maybe not necessary. First, the package must be a published package, then the library needs to be inside the
I would count parameter types as being used by an API, and I would count the types references in a type alias as being used as well.
When you import a library |
Thanks for your feedback.
I used |
Yes, I missed that detail. Sorry. I suspect then that the answer is 'yes', because a public API exported from |
Makes sense.
Yes seems like the right answer to me.
Interesting question. I guess
Yes,
I don't think this is Flutter-specific. It's annoying any time I use an API and it turns out that just importing the API isn't enough to use the API. I don't see why this should affect autocomplete, can you elaborate on that? Personally I've found autocomplete to be largely unusable in general, first because it interferes with what I can see of the code, second because it intercepts arrow key movements unpredictably, and third because it rarely suggests what I need so using it is slower than just typing what I want, but I'm probably not the target audience. |
I understand the usability concern driving this lint, but in general, packages should not export other packages. In fact, I would propose that we should instead have exactly that lint ( The problem is that it does not work well with semantic versioning and the package ecosystem as a whole. Basically, you are now tightly coupling your package with that package. Users of your package may have no idea the package you are exporting even exists (they don't import it), and they are unlikely to have an explicit dependency on it (for the same reason). But they do actually use it, and do depend on its API, so they should have a dependency on it. You can attempt to resolve this, but ultimately you run into problems: Option 1: You pin to exact versions of that dependency you are exporting
Option 2: You pin to minor release ranges (
|
Fwiw if lint is restricted to only suggest exporting libraries from within the same package then that doesn't have the above problems regarding versioning. But we would want to make that an explicit part of the proposal then. |
Proposed lint name: batteries_included_library. |
The Dart SDK, including If we start making some libraries supersets of others we will need tools to help discover when a dependency is too high level. If I depend on |
What @jakemac53 says! Your package's public API (which is probably the exported API of the libraries in the root of the |
Great conversation! FWIW, I'm very much w/ @jakemac53. Exploring something like what @lrhn proposed sounds valuable to me (though a little adjacent to what @Hixie was after in the OP). |
In order to maximize the benefit of autocompletion, the tooling needs to make the text that the user is typing be as close to the top of the list of suggestions as often as possible. If there were only one possible valid choice for what the user is trying to type then the analysis server could predict the text with 100% accuracy. If there were 10 possible valid choices, with nothing to indicate which is more likely, then the analysis server could predict the correct text 10% of the time (assuming a random order for the suggestions). Fortunately, there are usually indicators that can help it choose correctly more often than that, such as the required type of the expression at the completion location. But when the target type is We face the same challenge when suggesting static fields on classes like |
Wouldn't making sure that we export the relevant types make it more likely that autocomplete could find the relevant types, if it's limited to things that are currently in scope? In my experience, we currently show lots of stuff in the autocomplete dropdown that are not relevant [1]. It's not clear to me that changing what's in scope slightly would have any notable impact on autocomplete. If it really is that sensitive to what's in scope, then we should be thinking much more carefully about what we export (for example, maybe [1] For example, in my brand new |
<rule_name>
Incidentally I remembered the other day that we have another very similar lint already, that flags privates in public APIs. Making this consistent with that lint makes sense to me. |
Some additional feedbacks from my experiment with this lint on Flutter (personal thought: I'm now rather against this lint) : Transitivity issuesHere's a example of diff after implementing the transitivity feature (if +++ b/packages/flutter/lib/src/foundation/consolidate_response.dart
@@ -7,8 +7,60 @@ import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
-export 'dart:io' show HttpClientResponse;
-export 'dart:typed_data' show Uint8List;
+export 'dart:async'
+ show
+ EventSink,
+ StreamConsumer,
+ StreamSink,
+ StreamSubscription,
+ StreamTransformer,
+ StreamTransformerBase;
+export 'dart:convert'
+ show
+ Codec,
+ Converter,
+ Encoding;
+export 'dart:io'
+ show
+ ConnectionTask,
+ ContentType,
+ Cookie,
+ HeaderValue,
+ HttpClientResponse,
+ HttpClientResponseCompressionState,
+ HttpConnectionInfo,
+ HttpHeaders,
+ IOSink,
+ InternetAddress,
+ InternetAddressType,
+ RawSocketOption,
+ RedirectInfo,
+ Socket,
+ SocketOption,
+ X509Certificate;
+export 'dart:typed_data'
+ show
+ ByteBuffer,
+ ByteData,
+ Endian,
+ Float32List,
+ Float32x4,
+ Float32x4List,
+ Float64List,
+ Float64x2,
+ Float64x2List,
+ Int16List,
+ Int32List,
+ Int32x4,
+ Int32x4List,
+ Int64List,
+ Int8List,
+ TypedData,
+ Uint16List,
+ Uint32List,
+ Uint64List,
+ Uint8ClampedList,
+ Uint8List; This leads to several issues. namespace grothWith this change a lib using name clashesAfter this change the Flutter codebase contains several errors about class name conflicts (eg. there are 2 If a lib has those 2 CleanupWith a lot of export, it's easy to make a mistake by exporting a name that is not in the public API. How to detect that? We could make a lint for that ( |
Do you have concrete examples of the transitivity thing? e.g. what API does Flutter expose which exposes dart:convert's Codec? Float32x4List may be an even better example; what API chain leads to that one? |
From my previous example on Uint8List has a constructor dart:convert's Codec comes from the path : HttpClientResponse has a method |
Hmm. Interesting conundrum. For the first one, I would argue that since it returns a Uint8List we don't care about Uint8List's constructor (though I have no idea how the lint is supposed to know that). For the second though, ugh, that's pretty bad. I can't come up with a plausible reason why Codec is clearly not part of the HttpClientResponse API, but you're right that we clearly don't really want to export that whole API... It's kind of like you want to draw a line around the API, like, "export HttpClientResponse but ignore detach, that's not really an API you'll need to use this one", or "export Uint8List but ignore the constructor, you don't need to build it to use me". What are the kinds of transitive walk that you're doing? The earlier discussion was about superclasses, and specifically the types of inherited public members, which seem to me to obviously be part of the API. But maybe we don't go any deeper than just that? |
Interesting! I'm now wondering what was your initial problem that makes you request such a lint. Is it because of always_specify_types that forces you to write the type every time you declare a variable (and add the corresponding import)? Given that
You can take a look at my current implementation ( far from perfect :) ). Before adding transitivity I was collecting types on constructors, fields, methods, top level variables/functions. After adding transitivity I collect recursivelly also types from classes already collected by exploring their superclass, interfaces, constructors, fields, methods. |
When you look at APIs, you look at the types it exposes. Class types represent interfaces, not the entire class declaration (that's why subclasses are subtypes, even if statics are not inherited). That means its only instance members, not static members or constructors, which are relevant to the transitive type closure of an API. If you then start exporting the declaration of those transitive type dependencies, then their class APIs become part of your library API too, and then you do need to look at the exposed constructors and static members. So, the transitive type closure of a library is not the same as the transitive type closure you get by exporting your transitively used types. The transitive type closure is not closed under export. Which is why exporting your dependencies might not be the solution you're looking for. It introduces more problems that needs solutions. |
@a14n asked about the motivation for proposing the lint which is the topic of this issue, and suggested that it could be the simultaneous use of Following that line of thinking, we could perhaps describe the purpose of the lint as follows: We want each exported namespace In that sense it's a nice usability property that a widely used library could have, a new kind of 'batteries included' property that we might call 'all reachable imports included'. It is possible that I took too many steps at once here, so maybe the intention is just that we get 'all imports reachable in one step', or something like that—that is, if we import a function I can understand why the 'closedness' property looks good, but I'd also like to mention that the situation has a structure which is quite similar to the situation that we encountered with reflection several years ago: Anything that is based on automatically following the transitive closure of a software dependency could give rise to a space explosion. With reflection we'd get reflection support for all kinds of things even in the case where only a few things are used, and with 'all reachable imports included' (and maybe even 'all imports reachable in k steps' for some k) we'd get access to a large set of reachable declarations, even in the case where only a few of them are used. I don't know how costly it is to import a far larger set of libraries than what is actually required in order to be able to compile the library, so we'd need more input on that from the relevant tool teams. In any case, this makes me think that it could be better to improve on the support for adding further imports to the current library based on the fact that a given member access has a type that involves a declaration which is not imported. For example, we're writing This would make it possible for developers to maintain a certain discipline with imports: If a member access |
The usual motivation for me is things like calling |
(Originally I would maintain this property of the APIs myself, making sure we exported what mattered every time we added anything to each library's APIs, but as the team has grown this has not scaled and new contributors often don't know about this facet of API design and so don't enforce it and so quickly nobody is enforcing it because the precedent is you don't export the types you use and so on.) |
I'd say it's something that should be addressed by auto-complete. The static anaylisis is aware that after typing Given my current implementation of this lint, I'm not sure it goes somewhere. As you confirmed, too much elements are exported and it's quite impossible to have a logic to automatically cut some branches. |
I've never experienced autocomplete that doesn't frustrate me, so that wouldn't be my first choice. :-) |
I think there's value in this lint even if it's just one layer deep (i.e. not transitive). The PRs you landed in the Flutter framework were all improvements, IMHO. |
If we go in this direction we will face cleanup issue in the future if some APIs change. For instance if we widden a parameter type of a constructor from |
Sure. |
Created the issue flutter/flutter#100444 a while ago to highlight the problem of re-exports in public apis . In short, it is very difficult to get an overview of an api with so many re-exports. |
Does the lint library_private_types_in_public_api satisfy this request? |
I don't believe this is the same thing no - the request is to have a lint telling you to re-export public types that appear in your API (but to be clear I don't think that is a good idea because you can't reasonably generalize when you should do that or not, I think it should be a human choice). |
For anyone looking for such functionality - it's now available as DCM command https://dcm.dev/docs/cli/check-exports-completeness. |
It would be great to have a lint that flags when an exported API uses types that are not themselves exported.
For example:
Library b should lint that Foo is not exported, correction would be:
The text was updated successfully, but these errors were encountered: