language
language copied to clipboard
Import shorthand syntax
Most recent version: https://github.com/dart-lang/language/blob/main/accepted/future-releases/unquoted-imports/feature-specification.md
Original design below (which also has some good parts, a final result might be something in-between).
This is a proposal for a shorter import syntax for Dart. It is defined as a shorthand syntax which expands to, and coexists with, the existing import syntax. It avoids unnecessary repetitions and uses short syntax for the most common imports.
Motivation
Dart package imports are fairly verbose because they are based on URIs with no shorthands. A fairly typical import would be:
import "package:built_value/built_value.dart";
The repetition alone is grating, and Dart imports can typically be split into three groups:
- Platform libraries,
import "dart:async";. - Third-party packages,
import "package:built_value/built_value.dart";. - Same package relative import,
import "src/helper.dart";.
The package imports are the ones with most overhead. For the rest, the surrounding quotes and trailing .dart are still so ubiquitous that they might as well be assumed.
Syntax
The new syntax uses no quotes. Each shorthand library reference is provided as a URI-like character sequence containing no whitespace, and consisting only of identifiers/reserved words separated or prefixed by colons (:), dots (.) and slashes (/).
The allowed formats are:
- A single shorthand Dart package name.
- A shorthand Dart package name followed by a colon,
:, and a relative shorthand path. - A
/,./or../followed by a relative shorthand path.
A shorthand Dart package name is a dotted name: A non-empty . separated sequence of Dart identifiers or reserved words. Such a sequence can have just a single element and no separator.
A relative shorthand path is a non-empty / separated sequence of dotted names.
The grammar would be:
# Any sequence of letters, digits, `_` and `$`.
<SHORTHAND_IDENTIFIER> ::=
<INTEGER_LITERAL> | <INTEGER_LITERAL>? (<IDENTIFIER> | <RESERVED_WORD>)
<DOTTED_IDENTIFIER> ::=
<SHORTHAND_IDENTIFIER> | <DOTTED_IDENTIFIER> '.' <SHORTHAND_IDENTIFIER>
<SHORTHAND_PATH> ::=
<DOTTED_IDENTIFIER> | <SHORTHAND_PATH> '/' <DOTTED_IDENTIFIER>
<SHORTHAND_URI> ::=
<DOTTED_IDENTIFIER> (':' <SHORTHAND_PATH>)?
| './' <SHORTHAND_PATH>
| '../' <SHORTHAND_PATH>
| '/' <SHORTHAND_PATH>
<uri> ::= ...
| <SHORTHAND_URI>
Since a shorthand URI can only occur where a URI is expected, and a URI is currently always a string, there is no ambiguity in parsing. Tokenization is doable, but will probably initially allow whitespace between tokens because it doesn't yet know that it's a shorthand sequence. When it recognizes that a URI is expected and a non-string follows, it must combine the following tokens only as long as there is no space between them.
We can allow spaces between identifiers/keywords and :, . and /, but it will be harder to read and it makes the grammar less extensible.
The shorthand syntax can also be used for export and part declarations. It does not work for part of declarations because part of foo.bar.baz; is already valid syntax. We could allow only relative (./ or ../) shorthands for part of declarations, or we may want to disallow this existing syntax so that you can use the full shorthand syntax with no exceptions.
(Please do disallow the old part-of format where you use the parent library name).
(Not sure this works as written. Something like x.2.4e2 can be parsed as containing a double literal. The grammar above doesn't allow that. A less distinguishing approach could be:
- Require a shorthand to start with
[a-zA-Z_$0-9./]. - Include every character up to the first following
;, whitespace, quote or comment. (Tokenization will be very confusing if the shorthand URI can contain//or/*, or a string quite.) - Check later whether its valid according to the grammar above.
That seems more viable than trying to guess which tokens can be accepted, just accept any that can be included whole into the combined lexeme.)
Semantics
An import of a single-identifier package name, name, is equivalent to an import of "package:name/name.dart". This is the most common form of package imports, and it gets the shortest syntax.
An import of a dot-separated package name, some.prefix.last, is equivalent to an import of "package:some.prefix.last/last.dart". The single-identifier case is just the special case where there is no prefix.
An import of a package-colon-path sequence, name:path is equivalent to an import of "package:name/path.dart". (Notice the added .dart). This is used for packages which expose more than one library.
An import of a relative URI path, path, one starting with /, ./ or ../, is equivalent to an import of "path.dart".
The package name dart is special cased so that an import of dart:async will import "dart:async", and an import of just dart is not allowed because there is no dart:dart library. This allows us to treat dart: as a platform supplied package with libraries core.dart, async.dart, etc., which may actually be an improvement over the current special-casing that we do. It does mean that dart is not available as a package name for user packages.
Examples:
import built_value;meansimport "package:built_value/built_value.dart";import built_value:serializer;meansimport "package:built_value/serializer.dart";.import dart:async;meansimport "dart:async";.import ./src/helper;meansimport "src/helper.dart";.import /src/helper;meansimport "package:current_package/src/helper.dart";.
The leading ./ for relative files in the same directory, is needed because otherwise we cannot distinguish whether import foo; means import the foo package or the local foo file.
import hide hide hide;is valid and meansimport "package:hide/hide.dart" hide hide;.import pkg1 if (dart.libraries.io) pkg2;works too, each URI is expanded individually.
Consequences
Programmers can write less code. There will be some paths which cannot be written in the shorthand syntax, perhaps because they contain non-identifier characters or path segments starting with a digit. Those will still have to be written the old way, inside delimited strings.
The parser needs to be a little clever. If it tokenizes identifiers, reserved words, dots, colons and slashes first, then it has to combine them back into a single shorthand URI and check for separating whitespace. The reason this proposal does not allow even more complicated shorthand URIs is that it would make parsing even more problematic. The chosen design attempts a trade-off between allowing most existing package URIs to be written with the new syntax and allow the syntax to be parsed without too much overhead.
We can always make the choice to enforce extra rules for whitespace, but I don't even think that's particularly important. The following approach is grammar based (so whitespace is allowed everywhere), and it parses the examples without issues, as well as all the usual test files (so the grammar isn't broken): https://dart-review.googlesource.com/123407.
This is one of the things that drove me crazy from the start. Would love to see this change.
I'd rather see dart.async instead of dart:async though.
Overall, yes, I like this and think it can work.
This is mostly a matter of taste, but I find it hard to like the leading ./ for relative paths and using / as a separator in an unquoted "path".
It's a shame to give the elegant foo.bar.baz syntax over to only be used for the internal dotted package names. It would nice if that could mean package:foo/bar/baz.dart' since that would benefit external users too.
To get a better picture, I scraped a corpus and tried to gather (or in the case of internal code, fake) a representative set of imports. Here is the current syntax:
import 'dart:isolate';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart';
import 'package:flutter/material.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:widget.tla.server/server.dart';
import 'package:widget.tla.proto/client/component.dart';
import 'test_utils.dart';
import '../util/dart_type_utilities.dart';
import '../../../room/member/membership.dart';
import 'src/assets/source_stylesheet.dart';
I think that covers all the different forms. With this proposal, those become:
import dart:isolate;
import flutter_test;
import path;
import flutter:material;
import analyzer:dart/ast/visitor;
import widget.tla.server;
import widget.tla.proto:client/component;
import ./test_utils;
import ../util/dart_type_utilities;
import ../../../room/member/membership;
import ./src/assets/source_stylesheet;
To me, the unequivocal wins are flutter_test and path. I think dart:isolate and flutter:material look pretty good. The rest are OK, though the slashes look a little strange to me.
Here is an alternate idea:
import dart:isolate;
import flutter_test;
import path;
import flutter:material;
import analyzer:dart.ast.visitor;
import widget.tla.server;
import widget.tla.proto:client.component;
import 'test_utils';
import '../util/dart_type_utilities';
import '../../../room/member/membership';
import 'src/assets/source_stylesheet';
The rules here are:
- Package imports are unquoted.
- Use a
:to separate package name from path. - Use
.as the path separator for packages. - Non-package imports stay quoted and use
/for separators. They do not need.dart.
Using the . as a package path separator makes it more like a "logical" path and makes it look more natural to me when not quoted. Quoting file paths makes 'test_utils' unambiguous without needing a leading ./ (and is just as long, though ../ imports get two characters longer). I don't mind quoting file paths—I like that it makes the / feel a little more natural to me and doesn't require clever parsing tricks. It reminds me of the distinction between #include <foo> and #include "foo" in C.
Using quoted file imports does mean implicitly adding .dart to any existing file import that lacks it, which could potentially be a breaking change. I searched the 1,000 most recent pub packages and the only non-"dart:" I could find that didn't end up "package:" were:
Dart:async
src/notadotdartfile
dart-ext:ejdb2dart
So I think we're fine there.
Again, I think the style proposed here is workable too. Either of these options would be a net improvement, I believe.
Both are an improvement, but I'd prefer the latter. The former is weird with unquoted paths, particularly files from the same directory with the leading ./
I must admit that using . as path separator is not something that feels natural to me.
It also has the issue that . might be a part of a directory or file name. It already happens for, say, generated protobuf files, something.pb.dart. Using import foo:src.generated.something.pb; becomes ambiguous.
A / can't be part of a directory name, not even on Windows.
A dotted identifier also looks like a library name, which is confusing. If you write foo:pkg.foo.lib, I'd be inclined to read it as trying to import the library named pkg.foo.lib.
Using dots does make it look like a namespace. It's just that it isn't, It's not an internal Dart scope thing, but rather navigating in an external hierarchical path structure where the parts are not necessarily Dart identifiers, not even today, and making it look like it's Dart names worries me.
As for:
import 'src/assets/source_stylesheet';
without the trailing .dart, that's a breaking change because Dart files don't need to end in .dart. They typically do, but that import could already be valid, and now there'd be no way to actually import that file.
I think quoted strings should keep their current meaning, an unquoted imports (or differently quoted strings) are shorthands for an actual URI reference.
We might want to special-case dart-ext: as well, or maybe we won't, and you have to write it as a URI. It's rare enough that it probably doesn't matte.
We'd have to preserve the ability to write imports with quotes, because file names can contain special characters (even a plain space would make parsing hard, e.g., hide hide hide could be a file name). We might then conclude that we should avoid "magic" rules (including adding .dart at the end) in these old-style imports. which would make old-style imports a safe option, usable for code generators and with tricky file names.
Considering the "nice" paths that don't contain spaces and other obstacles, I don't see a problem with the slashes, and I actually tend to think it's good for readability that the syntax is similar to the URIs and paths that we see in many other contexts. Of course, focusing only on these "nice" cases and with no existing code to break, it's obviously an attractive idea to leave out .dart at the end.
This leaves the initial ./ in paths like ./test_utils or ./subdir/test_utils as one of the main controversies.
How about reusing the colon, e.g., import :test_utils;?
We would use : to indicate that the following [a-zA-Z0-9_/]+ is a relative path. It's one char shorter! :smile:
You might want to think that it's justified by "the current package can be denoted by the empty string", but that is actually not true (e.g., for a relative import of a library in myPackage/test/subdir from myPackage/test, using import :subdir/myLibrary; has a different meaning than import myPackage:subdir/myLibrary;). Still, that shouldn't be too hard to get used to.
I'd actually expect :foo to mean <current package>:foo, an absolute path into the current package. It seems arbitrary to make it relative to the current location, and different from the other uses of : to delimit package from path.
Then
import :src/helper.dart
would be another way to refer to local files, absolutely rather than relatively, but still relative on the package name. It's equivalent to the current
import "/src/helper.dart";
We could consider adding that as a feature too.
I'd actually expect
:footo mean<current package>:foo, an absolute path into the current package.
Me too.
Then
import :src/helper.dartwould be another way to refer to local files, absolutely rather than relatively, but still relative on the package name.
I considered a proposal where that was the only new sugar for "relative" imports. Basically tell users to make everything a package import, even for libraries in their own code. That obviously doesn't work for libraries outside lib. But even ignoring that, I found many many imports in a corpus that would become egregiously long if you had to use a full path from the root of the package.
It's equivalent to the current
import "/src/helper.dart";We could consider adding that as a feature too.
That... that works? :-O
It's equivalent to the current
import "/src/helper.dart"; We could consider adding that as a feature too.That... that works? :-O
Ack, no. I thought I had changed package: URI resolution to keep the name absolute. Apparently I didn't finish that yet.
https://dart-review.googlesource.com/c/sdk/+/117542
(Update: It works!)
@lrhn @munificent @eernstg What do you think of index.dart for folder widgets? like index.js
widgets/button.dart
widgets/index.dart => export 'widgets/button.dart';
some.dart => import 'widgets';
The idea, from a very cursory glance, seems to be that a directory can contain a "default" file that is imported if you import the directory. In Dart, it would mean that
import "package:foo/bar/";
would automatically import "package:foo/bar/index.dart".
We have traditionally used the package name as the default file in dart, so "package:foo/foo.dart" is the default name for "package:foo".
We could extend that to any directory, so if you refer to foo/bar/baz and that turns out to be a directory, we "complete" it to "foo/bar/baz/baz.dart".
Not sure whether that's better or worse than what is proposed here. You'll have to write less for sub-directories, but it requires the compiler to be able to recognize directories, something it can't if it fetches source from an HTTP URI.
We could extend that to any directory, so if you refer to foo/bar/baz and that turns out to be a directory, we "complete" it to "foo/bar/baz/baz.dart".
I think this is also a good option
I'm personally not a fan of adding syntax that relies on a convention for organizing libraries that doesn't already exist. I think this convention is already pretty well-established:
widgets/button.dart
widgets.dart => export 'widgets/button.dart';
some.dart => import 'widgets.dart';
I'd rather have a syntax assume that convention and then users don't have to reorganize their code to get the greatest benefit from the new syntax.
widgets/button.dart widgets.dart => export 'widgets/button.dart'; some.dart => import 'widgets.dart';
@munificent that's how I now use, but it's not so convenient. My personal subjective opinion.
Is this mean that the following example will be valid?
import /module/foo/xy.dart; means import 'package:my_app/module/foo/xy.dart';
An absolute import option from the root of the project/package would greatly increase the reusability of the code.
I think that should work. The current proposal doesn't include a package-root-relative path, but I see no reason it can't.
(So I added it).
also import @async or import :async for import 'dart:async' and import _/some.dart for current package.
One thing useful in typescript is the ability to create aliases for imports.
so you can import like this
import '~core';
import '~models';
I've written a prototype fix flag in dart_style that converts current URIs to the proposed shorthand syntax here when possible. You can run it like:
$ git clone https://github.com/dart-lang/dart_style.git
$ cd dart_style
$ git checkout temp-import-shorthand
$ dart tool/command_shell.dart format --fix-uri-shorthand <path>
Of course, because the syntax is not actually supported by Dart, running this will give you broken code! It's only useful for humans to get a feel for what the syntax could look like.
Just used the tool on a large code base. Did you mean to not quote the relative imports? I'm seeing:
import /src/debug_utils;
I wish
import 'package:main/src/debug_utils.dart';
could be:
import src/debug_utils;
But it's already a very nice improvement.
@mnordine, see @lrhn's earlier comment that
import foo;
would be ambiguous between
import "package:foo/foo.dart";
// or
import "foo.dart";
So some leading character like : or / is needed to disambiguate.
Yes, make src special, so you can leave out the leading /. Not a huge deal, though.
I would also like it that if there was no package:foo/foo.dart in .dart_tool/package_config.json, I could use:
import foo;
for the foo.dart in the same directory, and have the compiler alert me to the conflict if I ever import package:foo/foo.dart, or perhaps prefer the foo.dart in the same directory.
Both of those approaches are pretty special-cased and can easily break -- it's probably simpler if we be explicit and lead with / or :, especially since /directory is already pretty commonly used.
Yeah, I don't feel strongly about it, just a nit.
It would be great if the decision to implement this feature would take some UX testing into consideration.
I don't think that this feature is a great fit for Dart as it would make Dart much harder to learn for new developers.
Right now It is mostly obvious where imported/exported resources are and how imports/exports work, but with this feature that wouldn't be the case anymore.
It would be great if the decision to implement this feature would take some UX testing into consideration.
I don't think that this feature is a great fit for Dart as it would make Dart much harder to learn for new developers.
Right now It is mostly obvious where imported/exported resources are and how imports/exports work, but with this feature that wouldn't be the case anymore.
I completely agree with this comment. It should not be downvoted. I like the current verbosity of imports. There are too much cases to think about how the import works with the current proposal. The only benefit is to write less code but actually this is not a valid reason because we have autocompletion.
I disagree. Aestetics is not a reason to be a good language. Original proposal does not talk about aestetics. Moreover Dart has more urgent issues. I hope dart team does not lose time with this issue.
When I started learning Dart I loved the fact that its syntax and concepts were straightforward and intuitive (null safety, classes, constants, finals, parameters, constructors, mixins, etc.). It's pretty much adopted the best of every previous language and added it's own taste to it. I hope it stays in the right path doesn't become too hacky like JavaScript, TypeScript or PHP. I think this feature makes Dart's imports too much complicated (some sort of JavaScript-ish thing).