Language: Import shorthand syntax.

Created on 29 Oct 2019  路  17Comments  路  Source: dart-lang/language

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 is still so ubiquitous that it 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).

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".

An import of an absolute path with no scheme, /path is equivalent to an import of "package:current_package/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; means import "package:built_value/built_value.dart";
  • import built_value:serializer; means import "package:built_value/serializer.dart";.
  • import dart:async; means import "dart:async";.
  • import ./src/helper; means import "src/helper.dart";.
  • import /src/helper; means import "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 means import "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.

feature small-feature

Most helpful comment

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.

All 17 comments

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鈥擨 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 the 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 :foo to mean <current package>:foo, an absolute path into the current package.

Me too.

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.

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
```dart
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

@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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

leonsenft picture leonsenft  路  4Comments

dev-aentgs picture dev-aentgs  路  3Comments

wytesk133 picture wytesk133  路  4Comments

ShivamArora picture ShivamArora  路  3Comments

kevmoo picture kevmoo  路  3Comments