dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Are import shorthands worth the cost in readability/cognitive load? #1941

Open leafpetersen opened 2 years ago

leafpetersen commented 2 years ago

Reading the import shorthand proposal, I am struck at the level of complexity for a user that this is introducing. Consider this text:


Examples:

- `import built_value;` means `import "package:built_value/built_value.dart";`
- `import built_value:serializer;` means `import "package:built_value/serializer.dart";`.
- `import :src/int_serializer;` means `import "package:built_value/src/int_serializer.dart";` when it occurs in the previous `serializer.dart` library, or anywhere else in the same Pub package.
- `import ./src/int_serializer;` means `import "./src/int_serializer.dart"`, aka.`import "src/int_serializer.dart"`, when it occurs inside the previous `serializer.dart` library.
- `import ../serializer;` means `import "../serializer.dart"` when it occurs inside the previous `src/int_serializer.dart` library.

This is an extraordinary amount of new cognitive load for a user. There are now somewhere around 8 ways to write an import of serializer.dart, some of which are only valid in certain locations, and all of which use overlapping syntax to mean different things (":something means the package part was elided, but foo:something means add the package: back in, change the : to a / and add a .dart"). There's numerous ways to write the same thing:foo and foo:foo and package:foo/foo.dart and :./foo and .foo and "./foo.dart" and "foo.dart" all might mean the same thing (I think? I'm a little lost in the weeds.).

Is this really worth the shorter syntax? I really worry that we're making code significantly less consistent and readable in the name of brevity.

cc @lrhn @munificent @eernstg @jakemac53 @natebosch @bwilkerson

lrhn commented 2 years ago

@leafpetersen Yes, that is correct.

The new syntax is not related to the old syntax. A package: URI contains two parts of information: the package name and the path inside the package. The new syntax allows you to specify those two parts in a clearly-separated way (separated by a : which cannot occur anywhere else in the allowed syntax). That's intended as an improvement over the original package: URI format where the package name is not really part of the path, it's closer to a host-name, so package:name/path is really interpreted more like package://name/path. Using : as separator is completely unrelated to the use of : in the URI syntax. It just came up naturally when I tried to find syntax that covers the necessary cases.

We want to be able to just say import test; to import "package:test/test.dart";. That's a very high impact abbreviation for the most common and most verbose kind of imports, and the one thing I don't want to compromise on.

We could stop at that. import words; means import "package:words/words.dart";. No other abbreviations. It's still going to be a win.

I would like to allow other files to have shorthands too, where you don't need to write .dart. Because it's nice.

The other kinds of imports that exist in practice are:

import "package:package_name/non_default_path.dart";
import "dart:corelibrary";
import "localpath.dart";  // aka "./localpath.dart"
import "../relativepath.dart";
import "/rootedpath.dart"; // Rare, only workes inside a package:-library since 2.12.

That's why I suggest shorthands for those as well:

Package imports:

Path imports:

In all these cases, we only allow the shorthand syntax if the path segments have a particularly simple format (which all path segments used in practice do, including the path ending with .dart), because that makes it parseable. Again, nothing that should be a problem in practice.

That covers all existing imports, and it makes them all shorter (if nothing else, because you omit the .dart).

It's not allowing anything new, it's not particularly magical, the syntax is new (and possibly eerily similar to URI syntax, while not actually being related, but it's not intended as such), and it works well for the most common cases. It's not trying to invent new concepts (like canonical library names or new hierarchies), it maps directly to the existing URIs.

Then it has also been suggested to have a :package_path shorthand which means the same thing as current_package:package_path, and which works in the entire Pub package, including test/ and bin/. That's new, we don't have anything like that already. It's not unreasonable, and I like not needing to hard-code my own package name into source files, but it's also more complicated than the rest. It's still consistent: A package import is package_name:path, but you can omit either the :path or the package_name and get a default (but can't omit both).

I could drop the /rootedpath and the :current_package_path and still be happy. I worry about having both because they mean the same thing inside lib/, but not outside (which likely means you should use /path inside lib/ and :path outside, which is annoying.)

Using / instead of : as separator between package_name and path in import package_name:path means that omitting the package-name would become import /path;. If used outside of the lib/ directory, inside a Pub package, then that is confusing since it means something different than import "/path.dart";. So, another separator is possible, but / is not a good choice.

Hixie commented 2 years ago

No, there's nothing non-ideal about these. These are all perfectly idiomatic, valid imports.

I mean, they violate the Dart style guide and the pubspec guidelines. I'm not sure how they can be described as idiomatic.

Comparing your proposal to Lasse's, I see:

  1. Lasse's proposal applies to more imports and more users.
  2. Yours has greater aesthetic appeal to you and possibly more users.

I agree. I think applying to many imports is irrelevant and I think aesthetic appeal is critical. More specifically, I think approachability is critical, and that Dart's success is closely tied to how similar it is to existing languages for syntactic choices that are otherwise unimportant.

I really don't understand why we care how many imports this applies to. The problem we're solving is about verbosity in some specific cases -- use of package:, redundancy of giving the package name twice -- which only apply to some imports and not others. There's really nothing wrong with today's import 'foo.dart' syntax, why would we break something that works fine?

Is there confusion that an import in a Dart library is going to import a Dart file?

I gave a specific example:

import ./foo.g

munificent commented 2 years ago

No, there's nothing non-ideal about these. These are all perfectly idiomatic, valid imports.

I mean, they violate the Dart style guide and the pubspec guidelines. I'm not sure how they can be described as idiomatic.

Dotted package names go against the external style guide, but there is a small Google-internal style guide that supercedes it which defines how dotted package names are mapped into the internal monorepo. This is similar to how Flutter has a separate style guide for its own internal use.

We don't consider our Google users to be living in sin because the packages they import have dots in their names. Even if we did, again, there's nothing they can do about it as regular Dart users. It's up to the team that maintains the internal Dart build infrastructure.

I agree. I think applying to many imports is irrelevant and I think aesthetic appeal is critical.

I presume you wouldn't be excited about an import shorthand syntax that only applied to 0.1% of the imports in the world's Dart code, right? We'd be spending time designing, implementing, and testing a feature that benefits almost no one. So at some level, the amount that a feature gets used must be part of its value proposition.

munificent commented 2 years ago

For package imports, there are two sections: the package name, and the path. ... The choice to use : as the separator token, is, I think going to be a source of confusion since it suggests a false connection to the full syntax, but it may be the only reasonable choice (are there other better choices?).

Right, one way to think of it is that for all imports, you specify a package name (which may be dotted internally), and a path within that package. Then the language gives you some sugar: If you omit the package name, it is inferred to be the current package and the path is treated as relative to the current library.

Here's an idea I had a while back: An obvious syntax that follows other languages is to make the package name unquoted (similar to Java, Swift, C#, Kotlin), and the path quoted (like JavaScript, Ruby, Lua, and other scripting languages). That means you don't need a separator between them. That leads to a full import syntax like:

import some_package 'path/within/it.dart';

I think it's reasonable to allow omitting the .dart like Ruby, Lua, etc. do:

import some_package 'path/within/it';

Then we'd say that you can elide the package name. If you do, the import is in the current package and the path is relative to the current library:

import 'relative/to/current';

(In other words, the current import syntax with .dart elided.)

You can omit the path, in which case the path is inferred to be a '.dart` file with the same name as the package:

import flutter_test;
// Same as: import flutter_test 'flutter_test.dart';

The main reason I didn't suggest this is that I think imports of named libraries within another package look weird:

import flutter 'material';
import analyzer 'dart/ast/visitor';
import widget.tla.proto 'client/component';

Technically, it works. I just think it looks strange and makes libraries in a package whose name doesn't match the package feel like second-class citizens, compared to something like:

import flutter:material;
import analyzer:dart/ast/visitor;
import widget.tla.proto:client/component;

But if other people like the way it looks, I'd be OK with it.

natebosch commented 2 years ago

How do other languages that do use . as a separator deal with this in bazel?

I'm not familiar with other languages built with bazel that have an analogous 2 level hierarchy like we do with Dart. We allow both hierarchy of packages using . in the names (the hierarchy is useful in bazel, but doesn't matter in Dart) as well as a path under a package.

Right, one way to think of it is that for all imports, you specify a package name (which may be dotted internally), and a path within that package. Then the language gives you some sugar: If you omit the package name, it is inferred to be the current package and the path is treated as relative to the current library.

This does not hold for relative imports in test/ or bin/

Hixie commented 2 years ago

This is similar to how Flutter has a separate style guide for its own internal use.

We should definitely not design language features for Flutter's internal use! That would be as absurd.

Of these three I think only the last one is clear:

import flutter 'material';
import flutter:material;
import flutter.material;

...but we probably need UX research to support this claim.

FWIW, even after participating in this discussion I still don't really know exactly what the proposals with a colon or slash actually do. (Like, do they skip src? How do you specify a file that doesn't end in .dart?) It's really not intuitive to me at all. It doesn't match any other mainstream language. I think we would be shooting ourselves in the foot if we go down this path that elevates the weird idiosyncratic bazel-like syntax. We're just going to make our language less approachable.

munificent commented 2 years ago

FWIW, even after participating in this discussion I still don't really know exactly what the proposals with a colon or slash actually do.

Here's a complete working implementation of the desugaring logic in vanilla Dart:

String desugar(String shorthand) {
  // Convenience function for matching against a regex string.
  String match1 = '';
  String match2 = '';
  bool matches(String pattern) {
    var match = RegExp('^$pattern\$').firstMatch(shorthand);
    if (match == null) return false;
    match1 = match[1]!;
    if (match.groupCount > 1) match2 = match[2]!;
    return true;
  }

  // Proposal rules here:
  // -------------------
  final identifier = r'[a-zA-Z0-9_$]+'; // Identifier characters.
  final dottedPrefix = '(?:$identifier\\.)*'; // Identifiers followed by ".".
  final dotted = '$dottedPrefix$identifier'; // Dotted identifier.
  final path = '$dotted(?:/$dotted)*'; // "/"-separated dotted identifiers.
  final relativeHead = r'(?:\./|(?:\.\./)+)'; // "./" or repeated "../".

  // A single shorthand Dart package name.
  if (matches('($dottedPrefix)($identifier)')) {
    return "'package:$match1$match2/$match2.dart'";
  }

  // A shorthand Dart package name followed by a colon, :, and a relative
  // shorthand path.
  if (matches('($dotted):($path)')) {
    // 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.
    if (match1 == 'dart') return "'dart:$match2'";
    return "'package:$match1/$match2.dart'";
  }

  // A : followed by a relative shorthand path.
  if (matches(':($path)')) return "'package:<current>/$match1.dart'";

  // A ./ or ../ followed by a relative shorthand path.
  if (matches('($relativeHead$path)')) return "'$match1.dart'";
  // -------------------

  throw 'Invalid shorthand.';
}

void main(List<String> arguments) {
  for (var argument in arguments) {
    print('import ${argument.padRight(40)} -> import ${desugar(argument)}');
  }
}

The actual logic is five if statements and some simple regex matching. If you run that and pass the example imports I have above, it prints:

import dart:isolate                             -> import 'dart:isolate'
import flutter_test                             -> import 'package:flutter_test/flutter_test.dart'
import path                                     -> import 'package:path/path.dart'
import flutter:material                         -> import 'package:flutter/material.dart'
import analyzer:dart/ast/visitor                -> import 'package:analyzer/dart/ast/visitor.dart'
import widget.tla.server                        -> import 'package:widget.tla.server/server.dart'
import widget.tla.proto:client/component        -> import 'package:widget.tla.proto/client/component.dart'
import ./test_utils                             -> import './test_utils.dart'
import ../util/dart_type_utilities              -> import '../util/dart_type_utilities.dart'
import ../../../room/member/membership          -> import '../../../room/member/membership.dart'
import ./src/assets/source_stylesheet           -> import './src/assets/source_stylesheet.dart'
import ./user_address.g                         -> import './user_address.g.dart'
leafpetersen commented 2 years ago

By my reading, this thread has diverged into several streams, and it's not entirely clear to me which stream is being discussed in each response. Specifically, I see discussion centering around the following questions:

I'd really prefer to keep the discussion of the third question separate, possibly in a separate issue - I do not believe that it fundamentally changes the issues that I'm concerned with here, and that caused me to raise the issue. That is, if we believe that the conceptual syntax is too confusing, I don't believe that / vs . changes that. This isn't to say that we shouldn't have that discussion: just that as best I can tell it's irrelevant until we've resolved the other issues. @Hixie do you disagree?

@Hixie Do you perceive this as a problem worth addressing? That is, are you of the opinion that there is no problem to solve here, or are you more concerned with the specifics of the solution?

@lrhn @munificent I believe we have some data from corpus analysis for this, is that correct? How many of the imports we've analyzed fall into the three categories above? What would be the impact of not doing the most contentious version (the leading :)?

Hixie commented 2 years ago

I believe the problem to solve is that simple imports of packages are more verbose than in other mainstream languages that do package imports. That's it. I don't think there's a problem with relative imports using strings.

I especially do not believe that the language should change to solve problems only experienced by a tiny minority of the language's users, and especially not when there is no reason why those problems can't be solved without changing the language, and especially not when those potential changes would require introducing syntaxes that are not already used in existing mainstream languages.

As a corollary to these beliefs, I think : should not appear in imports unless in a manner that is commonly understood (which I think in this context basically means only as a scheme delimiter in a URL), and I think paths should be quoted, and I think components of an import should use dot separators, and I don't think unquoted imports should be paths.

leafpetersen commented 2 years ago

I don't think there's a problem with relative imports using strings.

Ok, so this I think is an additional question for my list: do we allow unquoted relative imports. I have to say, I'm not seeing much of an argument for why not, but I'm open to hearing it. And in fairness, C++ uses a mix of quoted and unquoted, and I don't think it's a pain point there, so I'm perhaps also not seeing too much of an argument for why? :)

I especially do not believe that the language should change to solve problems only experienced by a tiny minority of the language's users,

Agreed, I don't want to rathole on this. Our internal users are certainly a factor, but they are not the deciding factor. We shouldn't ship a bad design based on that. The question I want to focus on is what the good designs are.

especially not when those potential changes would require introducing syntaxes that are not already used in existing mainstream language

I don't buy this. Any syntax we introduce will be one not already used in existing mainstream languages: no mainstream language has Dart's exact package system. Mainstream languages use ., and mainstream languages use /. I'm extremely skeptical of any claims that . will be understood better than / - the latter is literally the most common path separator on the planet (look at your browser URL... :) )

As a corollary to these beliefs, I think : should not appear in imports unless in a manner that is commonly understood (which I think in this context basically means only as a scheme delimiter in a URL)

This is the meat of the question I am personally highly focused on. Taken narrowly, I think your statement here leaves only the full shorthand (import foo -> import "package:foo/foo.dart") as an option. Is that your position? Or would you see a case to support a broader set of shorthands using a different token?

Separately, I don't think any syntax is going to be commonly understood: it's new, it requires some explanation. Dart's current syntax already requires explanation if you're new to it ("Where'd the lib go?"). But commonly understandable seems in reach.

leafpetersen commented 2 years ago

I think if we make a simple change to Lasse's proposal: in external imports, change ':' and '/' to dot - this will address the major concerns. That is, instead of "import packageName:foo/bar/baz" we write import packageName.foo.bar.baz;

I don't think this addresses my concerns, and I think pre-supposes answers to some of my questions above. Unless there's more to your proposal than you are saying here, this doesn't make anything significantly shorter: it just eliminates the quotes and the .dart. I don't think you can support the implicit package name with this syntax (which is fine, but is a design point that needs to be called out). Do you intend to support the bare "import packagename" shorthand?

Again, I'd really prefer to avoid bike-shedding on . vs / in this issue, from my standpoint that's an entirely separate issue.

Hixie commented 2 years ago

I don't think there's a problem with relative imports using strings.

Ok, so this I think is an additional question for my list: do we allow unquoted relative imports. I have to say, I'm not seeing much of an argument for why not, but I'm open to hearing it.

A better question in language design is "why". If we make any change for which we cannot answer "why not", we will be forever making changes that do not improve the language.

What problem does allowing unquoted relative imports solve?

The default decision for any language change should be "do not change". There is a cost to every change. It invalidates tutorials. Videos have to be redone. People have to learn new features and syntaxes. Code has to migrate. Fights are fought over whether to use new syntax or old syntax. Compatibility decisions must be made. Tools must be written to help migrations. Time must be spent forever more whenever dealing with online help content determining whether the syntax is newer than the developer knows, or obsolete and from before the time that the developer started learning.

And in fairness, C++ uses a mix of quoted and unquoted, and I don't think it's a pain point there, so I'm perhaps also not seeing too much of an argument for why? :)

C++20 is an interesting case study in that they have a much more complicated set of syntaxes for importing modules (and including header files, also still supported) which admittedly does involve colons, albeit in what I would consider a non-intuitive fashion (and one that doesn't really match what is proposed here for Dart either). I am not sure it's the model I would want to gamble Dart on.

especially not when those potential changes would require introducing syntaxes that are not already used in existing mainstream language

I don't buy this. Any syntax we introduce will be one not already used in existing mainstream languages: no mainstream language has Dart's exact package system.

The exact package system and the syntax are somewhat orthogonal.

Mainstream languages use ., and mainstream languages use /. I'm extremely skeptical of any claims that . will be understood better than / - the latter is literally the most common path separator on the planet (look at your browser URL... :) )

But that's the point. It's a path separator. That's not what's needed here. Conceptually people aren't importing files, they're importing packages. They don't want to think of the implementation details, if they did then we'd want to just give the whole path -- which is exactly what we're trying to move away from because it's so verbose.

As a corollary to these beliefs, I think : should not appear in imports unless in a manner that is commonly understood (which I think in this context basically means only as a scheme delimiter in a URL)

This is the meat of the question I am personally highly focused on. Taken narrowly, I think your statement here leaves only the full shorthand (import foo -> import "package:foo/foo.dart") as an option. Is that your position? Or would you see a case to support a broader set of shorthands using a different token?

I described what I'm proposing earlier: https://github.com/dart-lang/language/issues/1941#issuecomment-955870710

Separately, I don't think any syntax is going to be commonly understood: it's new, it requires some explanation.

I really don't think `import foo' is going to take much explanation. People will see it and immediately understand that this means importing the "foo" package, because that's how other languages work, and it's self-explanatory, and (given that they are already going to be learning about pubspec.yaml files) it's already an identifier they're familiar with.

Similarly import foo.bar is going to take very little explanation, just like import 'package:foo/bar.dart' gets by with only minimal explanation. Dot-separation is a common concept in Dart, so it fits well into our existing cognitive framework. There's nothing new to explain.

The only other syntax I propose to support is today's quoted path syntax, which again is highly intuitive and requires minimal explanation to anyone who is familiar with filesystems. It's how Dart deals with paths elsewhere too.

On the other hand, if we introduce a new "path literal" syntax, then we will have to explain when it can be used and when it cannot. We'll have to explain why import foo.g doesn't import the file named "foo.g" but instead imports the (equally non-existent) file "foo.g.dart". We'll have to explain why "foo.bar" here doesn't mean "the bar of the foo" but means the literal string "foo.bar" even though it's not quoted. We'll have to explain why ":", which has no place anywhere else in the description of packages and imports, suddenly makes an appearance, and why it doesn't match the way ":" is used in "dart run" (which itself is confusing and which I would remove, but that's a discussion for another bug). Or if we use "/" we'll have to explain why it doesn't mean "division" but only in this one context, and why "//" in the middle of a path starts a comment, rather than acting like it does in bash, and why local imports start with a "./" sigil. It's just full of weirdness and no benefit. These are the proposals for which I would ask "why" and for which I find no compelling answer.

lrhn commented 2 years ago

Package private libraries is a concept separate from how to specify them. You do want to import files in src/ in your test/ directory, so we need to be able to designate them somehow.

(We could simply say that library names must be the package name followed by . and then the . separated path segments down to the final file, without the .dart, and any library which does not declare its name like that cannot be imported. That means library package_name.package_name; makes your library importable by that name, but only when it also matches the path package:package_name/package_name.dart. I don't think it's worth it. We do import using paths. It works. I'm not a purist here (surprisingly), and I don't want to introduce a new concept of library identifier that you can use for imports, separate from the path, which you can also use.)

munificent commented 2 years ago

I don't know how to square this claim:

Conceptually people aren't importing files, they're importing packages. They don't want to think of the implementation details, if they did then we'd want to just give the whole path -- which is exactly what we're trying to move away from because it's so verbose.

And this one:

On the other hand, if we introduce a new "path literal" syntax, then we will have to explain when it can be used and when it cannot. We'll have to explain why import foo.g doesn't import the file named "foo.g" but instead imports the (equally non-existent) file "foo.g.dart".

If your mental model is that users import some conceptual "package" thing then both unquoted package names and eliding ".dart" makes sense. If your mental model is that users import library files, then writing an explicit path that ends in ".dart" makes sense. But I don't understand a mental that says users are "importing packages" and thus package names should be unquoted, but also sometimes users should have to write a ".dart" file extension on their imports so that it's clear what file they are importing.

Why do users with your mental model care about the file path sometimes and not others? What "package" is a user importing when they write "package:flutter/material.dart"? Or "some/other/code.dart"?

Hixie commented 2 years ago

If your mental model is that users import some conceptual "package" thing then both unquoted package names and eliding ".dart" makes sense.

If it's a package import there's nothing to elide. The package name doesn't have ".dart" in it.

I'm distinguishing importing a package (the thing you specify in your pubspec.yaml) from importing a file (local to your current package), because I think that's a distinction people make.

Some packages (like package:flutter) have multiple entry points (material, widgets, rendering, etc). I don't think most developers are thinking about the "widgets.dart" file with all the exports in it when they think of importing package:flutter/widgets.dart, they're just thinking about importing the "widgets" part of "flutter", just like a member on a class, hence flutter.widgets being IMHO a closer match to their mental model (and one that neatly fits the existing Dart concepts).

Meanwhile, when you're developing your own package, you think very much about the file you're creating, because they're part of the architecture of what you are building. So importing files is front of mind.

We should do UX research to prove or disprove this claim, since its validity, or the validity of its opposite, are pivotal to the proposals.

We should do UX research for this change regardless. It's a huge change to make whatever we do.

TimWhiting commented 2 years ago

Personally, I always think about importing files, package files are no different. Hence why I prefer the original proposal, and not the counter-proposals. But maybe that is because I'm a package author. I don't see how one would think that packages are any different than imports / exports within you app files though. There is nothing really special in the language about packages so far, only libraries.

Wdestroier commented 2 years ago

My brain is 2 smol to understand the new imports.

An interesting Go feature is that the import keyword only needs to be written once...

lrhn commented 2 years ago

Using packagename.dirname.filename for the import does make it appear like a package exposes a flat namespace of named libraries. I don't see it that way, but, I can see why someone might (or might want to).

We can allow - in the names too: package-name.dir-name.file-name is fine. We can parse that too. There are files/directories using - now, and they can be included.

It prevents the new syntax from being used for files/directories with . in them. That's annoying. Also for package-names with . in them (which is really annoying for some people, completely irrelevant for the rest).

leafpetersen commented 2 years ago

I described what I'm proposing earlier: #1941 (comment)

@Hixie That's more a set of examples than a proposal, but let me try to extract out a proposal from that. I think you are proposing the following:

All other cases must be specified as now. Is that your proposal?

My immediate question is why use . instead of : then? It feels like : is more consistent with the existing dart:core syntax and avoids adding a new concept. We simply say that the existing dart:foo shorthand is extended to packages, with a special case for foo:foo, and we're done.

lrhn commented 2 years ago

We simply say that the existing dart:foo shorthand is extended to packages, with a special case for foo:foo, and we're done.

(And we can support package names with .s in them!)

Wdestroier commented 2 years ago

My idea is to have a package import shorthand, with Go's multi line imports and JavaScript's relative imports...

Single line imports

import 'package:flutter/material.dart';

would become

import flutter:material;

Notice that package:flutter/ was changed to flutter: and the quotation marks were removed...

Allow multi line imports (a set of imports) to avoid repeating the import keyword over and over.

import 'package:flutter/material.dart';
import 'package:flutter_folio/commands/books/set_current_book_command.dart';
import 'package:flutter_folio/commands/books/set_current_page_command.dart';
import 'package:flutter_folio/core_packages.dart';

would become

import {
  flutter:material,
  flutter_folio:commands/books/set_current_book_command,
  flutter_folio:commands/books/set_current_page_command,
  flutter_folio:core_packages,
}

Relative imports

import '../themes/style.dart';

would become

import ../themes/style;

and

import 'foo.dart'
import 'bar.dart'
import '../../themes/style.dart';

would become

import { foo, bar, ../../themes/style }

Polemic questions (try not to have a heart attack):

Won't absolute path file and folder imports be a thing? Example: import C:\example_message.dart; import /tmp/example_message.dart;

and

import C:\flutter\.pub-cache\hosted\pub.dartlang.org\uuid-2.2.0;

Will remote git repository imports always be declared in pubspec.yaml?

Importing git repositories in every source file becomes hard to manage. At the same time, it's hard to write small scripts in Dart. Most of the time you need to create a new project to import the dependency in pubspec.yaml and then use it.

Maybe import github.com/dart-lang/test; should be a feature.

Entire git repositories can be imported, maybe single files should be allowed to be imported as well?

import pastebin.com/raw/2SW5kHcN;

void main() {
  print(message);
}

Should relative imports be kept?

They're important because Dart is meant to be a JavaScript substitute. They're specially important to help with the problem above. The only way to import files without creating a new project is by using relative imports.

Another feature that would help writing scripts would be absolute path folder and file imports (Absolute path folder imports covers local git repository imports).

Aren't recursive (on-demand) imports going to be a thing? To import all files from a folder and from subfolders.

import {
  portfolio:components/alignments,
  portfolio:components/rounded_card,
  portfolio:components/decorated_container,
  portfolio:components/no_glow_scroll_behavior,
  portfolio:components/no_animation_page,
}

It's probably not a good idea, but the statement above would become:

import portfolio:components/*;

Useful if you're not using an IDE. Another option is to create a class called components.dart and export all these classes. Or create a class called component.dart and put all these components in a single file.

EDIT: After all this brainstorming, I figured out that my conclusions were almost identical to the proposal.

The major differences were:

cedvdb commented 2 years ago

There are lots of fresh ideas in your comment. I really like the last one since it would eliminates the need to write barrels. Those are annoying to maintain too!

Hixie commented 2 years ago

@leafpetersen

I think you are proposing the following:

  • A dart core library, currently specified as dart:foo can be specified as dart.foo
  • Any file bar directly under lib of a package named foo can be imported as foo.bar
  • As a special case, Any file foo directly under lib of a package named foo can be imported as foo

All other cases must be specified as now. Is that your proposal?

Yes.

My immediate question is why use . instead of : then? It feels like : is more consistent with the existing dart:core syntax and avoids adding a new concept.

: is used in Dart for the following:

. is used in Dart for the following:

What we are proposing here is not a name-value pair in any sense, it's not in a string, and it's not part of another operator. It is a hierarchy separator.

We simply say that the existing dart:foo shorthand is extended to packages, with a special case for foo:foo, and we're done.

That's not a shorthand though, it's a URL. This would be equivalent to saying every package is now a URL scheme. That seems like a path that will lead to all kinds of trouble (e.g. https://pub.dev/packages/git colliding with the git: scheme).

leafpetersen commented 2 years ago

That's not a shorthand though, it's a URL.

Do you really think that any fraction of our developers think of "dart:async" as being a URL? I'd be extremely surprised.

This would be equivalent to saying every package is now a URL scheme. That seems like a path that will lead to all kinds of trouble (e.g. https://pub.dev/packages/git colliding with the git: scheme).

There's no collision if it's unquoted. The quoted syntax is always a URI, the unquoted syntax is always a package:library pair (with special case for when library and package are the same).

Hixie commented 2 years ago

I agree [the unquoted form is] not a URL, but then there's no parallel with 'dart:foo'.

Levi-Lesches commented 2 years ago

After not following this issue for about a week, I agree with @leafpetersen's request for answers to these guiding questions:

By my reading, this thread has diverged into several streams, and it's not entirely clear to me which stream is being discussed in each response. Specifically, I see discussion centering around the following questions:

  1. Is there significant value in doing any level of import shorthand?
  2. If we do import shorthands roughly following the ideas in this proposal, how general do we make them?

    • Only support import foo -> import "package:foo/foo.dart"
    • Also support import foo:bar -> import "package:foo/bar.dart"
    • Also support import :bar -> import "package:<current>/bar.dart"
  3. What should the path separator in the shorthand be? (/ as proposed, or . as counter-proposed)
  1. I think so, and I think most will agree. Whether the new form is simpler, or how much shorter it is, the need for some reasonable alternative to package imports is apparent in my opinion, and from the length of this discussion. On the other hand, as @Hixie keeps pointing out, the current syntax is perfectly valid and possibly the shortest reasonable syntax for most file imports.

  2. Regarding each shorthand form:

    • Importing foo to mean "package:foo/foo.dart" seems to be the import that everyone agrees is in need of shortening -- the only word that has any meaning in that phrase is foo, the rest can be inferred.
    • I also think foo:bar should be supported, if only for flutter:material.
    • I can't speak for everyone, but I use a heavily layered approach when building my apps and am constantly importing "package:myApp/data.dart" or "package:myApp/widgets.dart". The ability to simply import :data or :widgets would be invaluable. I also agree with @lrhn that hard-coding your own package name into any file but pubspec.yaml can be off-putting and redundant, not to mention can be confused with third-party package imports.
  3. Since dart run has taken on the : to mean package:file, I think it's only reasonable that imports do the same. As I pointed out earlier the supposed inconsistency between lib and bin is actually intuitive: when using dart run, we only care about executables inside bin, and when importing, we only care about libraries inside lib. Using package:file means "take the file I care about from package". That's perfectly reasonable and I like it. I don't particularly love the . because it reminds me of Python, and the comparison is not valid because the Python idea of a "package" is very different than Dart's. I like that a:b/c/d is easily parsed by the eye as package:path/to/file, and replacing all those with . or / wouldn't convey the same meaning.


@munificent:

Why do users with your mental model care about the file path sometimes and not others? What "package" is a user importing when they write "package:flutter/material.dart"? Or "some/other/code.dart"?

As someone with this mental model, hopefully I can explain: I draw a difference between files in my own package and files in other people's packages. The main difference is that I know about every file in my package, but I don't know anything about how someone else's package is laid out.

Hence, when I import "package:flutter/material.dart", yes I see I'm importing a file, but conceptually. I'm thinking of the material components of Flutter. Like @Hixie said:

Some packages (like package:flutter) have multiple entry points (material, widgets, rendering, etc). I don't think most developers are thinking about the "widgets.dart" file with all the exports in it when they think of importing package:flutter/widgets.dart, they're just thinking about importing the "widgets" part of "flutter", just like a member on a class, hence flutter.widgets being IMHO a closer match to their mental model (and one that neatly fits the existing Dart concepts).

On the other hand, when I import ../data.dart, I'm specifically thinking of "the file called data.dart that's one folder above the current file". Sure, each file has its purpose, but I know them and I want to import them the same way I created them -- by file path.

The difference between those two ideas is big enough, IMO, that they deserve to have their own syntax. Besides for the fact that not supporting strings would be a massive breaking change, it makes sense to specify files as a string path in my package. And when importing a file in another package, I don't care about the file path, so I want to highlight what I do know: the package name (flutter) and the "sub-package" name (material). The fact that there is an actual file flutter/lib/material.dart is just an implementation detail that allows me to import material widgets conveniently, and should not be reflected in my import.

So I support @Hixie's proposal of keeping package and file imports completely separate, with the personal preference that : be used instead of . or / for the reasons mentioned above, but that's bikeshedding I guess.

Wdestroier commented 2 years ago

Relative imports using dots look terrible... Dots could be reserved to imports from pub? I'm not sure if mixing both is a good idea

Levi-Lesches commented 2 years ago

@tatumizer sounds like you're endorsing Hixie's proposal of quoted paths vs unqouted dotted package names, sans the local keyword.

Hixie commented 2 years ago

I do think one weakness of my proposal is imports to the local package in tests. As I proposed things, that would have to be package: imports in strings still, which would be a bit weird. I wish those were relative imports, but being relative to the test file leads to some weird ../../../../lib/src/foo/bar.dart imports which isn't satisfying either...

cedvdb commented 2 years ago

relative import in the proposal always start with a .

If that's the case what do we need quotation + an extra keyword for ? There is 3 dinstinct token so far to distinguish them (dot, quote and now local keyword)

cedvdb commented 2 years ago

the leading dot is not the only thing disambiguating, the imports are organized and separated too with the linter, which further the differentiation. quote + different starting token + extra keyword + different placement seems unnecessary. Plus it's not just a leading dot, it is ./ or ../.

Imo your second argument is sound but maybe it's not a problem ? I can understand why quotes would be staying though

cedvdb commented 2 years ago

If you don't need the dot, how do you go back a directory ? You'd still need ../

cedvdb commented 2 years ago

Multiple comments here suggest you have to use ./ and just bar is not an option anymore. So you would have to write it explicitely.

Forcing the user to write ./ in import (nothing to do with the PATH) is equivalent to introducing a non-standard convention

Forcing might not be the gold standard, but it's used in many places like that, that are not related to dart. Further more ./ is much more common to devs than any other keyword you can come up with.

Anyway, I don't think this is moving either of us in a direction, so I'll drop it.

munificent commented 2 years ago

Import shorthand proposals break down imports into a few different styles and optimize each style (or not) differently. The styles are:

For package imports, this proposal and some others also have a special shorthand when the package being imported from is the current (i.e. surrounding) package.

To evaluate the usefulness of any proposal, it helps to know which of these styles are most common, so I wrote a script that scrapes all the imports (and exports, but I use "import" here to refer to both) in a corpus of pub packages and categorizes the URLs. Here's the results after looking at 6,412,685 lines in 45,838 files:

-- Style (152772 total) --
  55485 ( 36.319%): package:name/path.dart
  35492 ( 23.232%): package:simple/simple.dart
  30543 ( 19.993%): relative/path.dart
  16547 ( 10.831%): dart:lib
  14702 (  9.623%): ../relative/path.dart
      3 (  0.001%): (other erroneous URI)

So over a third of the imports are for a library in a package whose name doesn't match the package. This is libraries like "flutter/material.dart". About a quarter of imports are simple package imports where the package and library name match. Overall, more than half of all imports are "package:".

Relative paths are roughly another third of URIs, with "../" paths half as common as relative paths. The remaining tenth are "dart:" imports.

Of the package imports, how many are for the current package?

-- Package (90977 total) --
  52029 ( 57.189%): Other    ==============================
  38948 ( 42.811%): Current  ======================

So most package imports are not for the current package, but a large fraction are.

lrhn commented 2 years ago

@munificent Any chance you can split the same-package/other-package and name/path vs simple/simple into the four quadrants. I have a hunch that name/path is vastly more used inside the same package (where my :path shorthand would work) than into other packages. I'd like to know how wrong I am :)

munificent commented 2 years ago

Sure thing:

-- Package (90977 total) --
  29873 ( 32.836%): package:current_package/name/path.dart        =====
  26417 ( 29.037%): package:other_package/other_package.dart      =====
  25612 ( 28.152%): package:other_package/name/path.dart          ====
   9075 (  9.975%): package:current_package/current_package.dart  ==

name/path is common for both same package and other package imports. The latter is largely because of Flutter. Some of the most common import URIs are:

   8904 (  5.828%): package:flutter/material.dart
   1815 (  1.188%): package:flutter/services.dart
   1657 (  1.085%): package:flutter/widgets.dart
   1617 (  1.058%): package:flutter/foundation.dart
   1108 (  0.725%): package:flutter/cupertino.dart
    483 (  0.316%): package:flutter/rendering.dart

If you split out "package:other_package/name/path.dart" URIs for flutter separately from packages, you get:

-- Flutter (25612 total) --
  16194 ( 63.228%): package:flutter/name/path.dart  ==================
   9418 ( 36.772%): package:other/name/path.dart    ===========

So about 2/3rds of the other imports that have paths are from Flutter.

In case you're curious, the 30 most common "package:" imports are:

   8904 (  9.787%): package:flutter/material.dart
   4016 (  4.414%): package:test/test.dart
   2570 (  2.825%): package:flutter_test/flutter_test.dart
   1815 (  1.995%): package:flutter/services.dart
   1791 (  1.969%): package:meta/meta.dart
   1657 (  1.821%): package:flutter/widgets.dart
   1638 (  1.800%): package:json_annotation/json_annotation.dart
   1617 (  1.777%): package:flutter/foundation.dart
   1108 (  1.218%): package:flutter/cupertino.dart
    883 (  0.971%): package:path/path.dart
    578 (  0.635%): package:http/http.dart
    513 (  0.564%): package:mockito/mockito.dart
    483 (  0.531%): package:flutter/rendering.dart
    395 (  0.434%): package:over_react/over_react.dart
    331 (  0.364%): package:intl/intl.dart
    326 (  0.358%): package:yaml/yaml.dart
    316 (  0.347%): package:build/build.dart
    315 (  0.346%): package:logging/logging.dart
    291 (  0.320%): package:ml_linalg/dtype.dart
    289 (  0.318%): package:collection/collection.dart
    287 (  0.315%): package:provider/provider.dart
    264 (  0.290%): package:ffi/ffi.dart
    253 (  0.278%): package:flutter/gestures.dart
    242 (  0.266%): package:dcli/dcli.dart
    242 (  0.266%): package:analyzer/dart/element/element.dart
    228 (  0.251%): package:built_value/serializer.dart
    224 (  0.246%): package:freezed_annotation/freezed_annotation.dart
    215 (  0.236%): package:dio/dio.dart
    205 (  0.225%): package:built_value/built_value.dart
cedvdb commented 2 years ago

I personally use relative imports without ./ simply because that's what the IDE does by default. However I would not mind using ./.

My guess is that the data will be heavily skewed toward no dot ( the vast majority of imports are automatically added by IDE s). Probably something in the order of 95% vs 5% if I had to guess. Not sure what that data will show though, except that people rely on their IDE for imports

lrhn commented 2 years ago

@tatumizer I wouldn't expect any relative imports starting with ./.

The only character I've found in file/directory names other than [a-zA-Z0-9_.] (no $!) is -. Adding that seems reasonable, which is why I did so in the latest revision of my proposal.

lrhn commented 2 years ago

@lrhn: I can't believe you like this grammar.

Believe it! :) Or rather, ignore the grammar, look at the language. The grammar is an implementation detail.

For something non-quoted, I want a measure of regularity. It's basically [a-zA-Z0-9_]+ chunks which can be separated by . or - (so no leading, trailing or adjacent ./- characters). Then those "segments" are separated by / for paths and : between package name and path.

That covers all existing package names, directory names and file names in the Dart imports I checked. So, it's not like the result of the grammar is new, it's what everybody is doing already. It's not a new syntax. It's the language of package and file-system names that has already evolved, and this is just one possible grammar codifying what is already happening.

We could allow more, but at this point, I'm actually being opinionated and saying that you should stay inside that language if you want to use the quote-less imports.

And yes, dots are special in that:

Maybe we should invent something new, from scratch, instead of this rather incremental approach, but I actually think it's a nice increment. I have no urge to invent a completely new way to designate libraries, different from the URL. I just want a shorter way to write the URL, one which is compatible with as many existing imports as possible.

lrhn commented 2 years ago

@tatumizer

Now, suppose we are in a file a.dart under test directory, and we need to import b.dart from lib/src directory. There are 2 ways to do it: import '../lib/src/b'; import '/lib/src/b';

Both of those are wrong ways to do it. Files inside the lib/ directory should (going on must) be referenced using a package: URI. So, unless we recognize relative paths ending up inside the lib/ directory of the current package (or not just the current package?), and rewrite them to being package URIs, it's going to give a wrong result. (The wrong result is the same library imported both with a package: URI and a file: URI, meaning it's treated as two different libraries).

If you also want to change how Dart recognizes and canonicalizes library import paths, and how it determines whether two different URIs represent the same library, we can do that too, but that's a bigger feature than just doing shorthands for the current URIs. This is considered a "small feature" because it's not changing anything about resolution, it's literally just shorthands for URI references.

Package names: Dot-separated package names look more familiar (and arguably nicer) than the alternatives (slash- or colon-separated).

You probably mean "library names" there. Currently, all Pub package names are simple identifiers, but I don't actually believe that will scale forever. I personally expect that we'll allow .s in Pub package names at some point, to widen the namespace. I could be wrong, maybe underscores can be used instead, so you'd do myproject_mypackage instead of myproject.mypackage. Or maybe people will just have one package for their project, with multiple top-level libraries, to avoid namespacing. (What if Pub allowed .s in package names today. Would your preferences change? Why/why not? Would you want it to?)

That said, if we did that, I also think Pub should allow aliases/renaming for packages, so you can depend on foo.bar.baz as baz, and do imports as package:baz/... (with package-local package-names).

I can see how foo.bar.baz reads "cleaner" than foo:bar/baz, almost as if it was a name, not an unclearly separated package name and a path into that package. It is a package name and path, though. Maybe if it was both? What if we allowed you to import foo.bar.baz only if the target library (package:foo/bar/baz.dart) was actually named so, using library foo.bar.baz;. Then the author of a package can mark the libraries that are intended for external import by giving then the name corresponding to their path, and effectively make every other library package-private. (And it would give library names a reason to exist again). That would still be an indirect way to introduce package-private files, and I do prefer explicit to implicit when it comes to "enabling" features, it's a pain to enable some use of your library by accident, and then not be able to remove it again without breaking customers.

lrhn commented 2 years ago

The thing is that if a.b.c is a library name, with an implied package name of a, and path of b/c.dart, then a.b doesn't have to be anything. I don't see the .s as hierarchical, with that view it's just a dotted name which happen to match the path of the library with that name. If a.b.c exists, it doesn't mean that a.b is a library too, there does not have to be a library with path package:a/b.dart, or if there is, it doesn't have to have name a.b, and in either case, you can't import it as import a.b.

It's still a semi-magical link between package/path and dotted names, but all it means is that libraries without names, or with names that doesn't match their path, cannot be imported by their name. They can still be imported using their URI path. (And we can then, potentially, disallow importing from other packages by anything but name, which effectively makes every library package private if it doesn't have a library a.b.c; declaration matching its package name and path.)

TL;DR: That idea, entirely separate from what we are doing here, would be to allow you to import packages by their name iff their name is their package name and path segments, without the trailing .dart, separated by .s. (And only if those package names and path names contain no . characters.). It's different. Not necessarily bad.

lrhn commented 2 years ago

The problem here is that the compiler, seeing pkg.a.b.c.d.e must try all 16 possible interpretations in order to see if there is a file, and it can't even stop at the first one it finds if it has to check for ambiguities. That's certainly possible, and with a little look-ahead (if there is no a.b directory, we don't need to check for the ways c.d.e can be separated), it probably won't be bad in practice. You'd have to go out of your way to get actual exponential behavior. It's still more expensive than just finding the file directly, and you have to do it for every import in the program. (You'd even have to be careful about caching the result, because creating a new file somewhere else might change the result.) If you use / for actual path separation, and allow . in names, then there is no ambiguity.

munificent commented 2 years ago

OK, here's another pitch:

import dart/isolate;                      // -> 'dart:isolate';
import flutter_test;                      // -> 'package:flutter_test/flutter_test.dart';
import path;                              // -> 'package:path/path.dart';
import flutter/material;                  // -> 'package:flutter/material.dart';
import analyzer/dart/ast/visitor;         // -> 'package:analyzer/dart/ast/visitor/visitor.dart';
import widget.tla.server;                 // -> 'package:widget.tla.server/server.dart';
import widget.tla.proto/client/component; // -> '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';
import 'user_address.g.dart';

The rules are:

That's it.

In other words, it's the original proposal but with no provision for unquoted relative paths, no rule for the current package, and / instead of : for the package name separator.

It doesn't support unquoted relative imports because:

It uses a slash as the package name and library path separator because:

Aesthetically, I think it looks pretty nice.

Hixie commented 2 years ago

Here's what some files would look like:

https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/layout_builder_mutations_test.dart:

import flutter/src/rendering/sliver;
import flutter/src/widgets/basic;
import flutter/src/widgets/framework;
import flutter/src/widgets/layout_builder;
import flutter/src/widgets/scroll_view;
import flutter/src/widgets/sliver_layout_builder;
import flutter_test;

https://github.com/flutter/flutter/blob/master/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart::

import dart/async;
import dart/io as io;

import args/command_runner;
import file/memory;
import flutter_tools/src/base/common;
import flutter_tools/src/base/error_handling_io;
import flutter_tools/src/base/file_system;
import flutter_tools/src/base/io;
import flutter_tools/src/base/signals;
import flutter_tools/src/base/time;
import flutter_tools/src/build_info;
import flutter_tools/src/cache;
import flutter_tools/src/pub;
import flutter_tools/src/globals as globals;
import flutter_tools/src/pre_run_validator;
import flutter_tools/src/reporting/reporting;
import flutter_tools/src/runner/flutter_command;
import test/fake;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';

https://github.com/flutter/flutter/blob/master/dev/bots/prepare_package.dart:

import dart/async;
import dart/convert;
import dart/io hide Platform;
import dart/typed_data;

import args;
import crypto;
import crypto/src/digest_sink;
import http as http;
import path as path;
import platform show Platform, LocalPlatform;
import process;

https://github.com/flutter/flutter/blob/master/dev/integration_tests/flutter_gallery/test_driver/transitions_perf.dart:

import dart/convert show JsonEncoder;

import flutter/material;
import flutter_driver/driver_extension;
import flutter_gallery/demo_lists;
import flutter_gallery/gallery/app show GalleryApp;
import flutter_gallery/gallery/demos;
import flutter_test;

import 'run_demos.dart';

That's not too bad. The slash isn't as satisfying as the dot for a separator IMHO, but it's clearly intuitive, and there's a path component to this, so it is sound from a conceptual point of view. It solves the verbosity problems cleanly. I could get used to the slash, I think. The syntax is simple and the mapping from this syntax to the old syntax is easy to explain.

SGTM.

cedvdb commented 2 years ago

Imo something.g does not look stranger than something.utils or widget.tla.server if the rule is to assume a dart extension. I believe that's a non issue that was labeled as one.

Having 2 different rule set for relative and package imports does not make imports simpler. I don't want to have to think whether I should add the extension or not. I'd prefer dropping the extension in all cases for relative imports to keep both streamlined.

Also, and this is purely a preference, I'd prefer to use a dot for relative a la typescript A relative import is one that starts with ./ or ../. But quotes are fine too I guess, although quotes are more verbose.

import ./test_utils;
import ../util/dart_type_utilities;
import ../../../room/member/membership;
import ./src/assets/source_stylesheet;
import ./user_address.g;
munificent commented 2 years ago

Having 2 different rule set for relative and package imports does not make imports simpler.

That's already true today. Both forms are quoted, but you still have to know about the semi-magical "package:" URL scheme.

a14n commented 2 years ago

To refer to the current package (/lib) couldn't we use ~/ as prefix?

import dart/async;
import dart/io as io;

import args/command_runner;
import file/memory;
import test/fake;

import ~/src/base/common;
import ~/src/base/error_handling_io;
import ~/src/base/file_system;
import ~/src/base/io;
import ~/src/base/signals;
import ~/src/base/time;
import ~/src/build_info;
import ~/src/cache;
import ~/src/pub;
import ~/src/globals as globals;
import ~/src/pre_run_validator;
import ~/src/reporting/reporting;
import ~/src/runner/flutter_command;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';

If this prefix doesn't bring syntax problem, this proposal makes the package's renaming a no-op, makes imports of current package shorter, allows grouping current package imports and has a relatively obvious meaning imho.

lrhn commented 2 years ago

It might be a minor point, but isn't it odd that there's no way to refer to the root of the current package explicitly [in relative import]? The above expression has some room for improvement IMO. :-)

import '/src/helper.dart';

works today (has worked since SDK version 2.13).

There is no way to refer to the root of the pub package. I can live with that. As Bob makes a point of, his proposal only supports package URIs, and outside of lib/ is not a package URI. So, that works.

About using ~/, I assume it should work everywhere in the same Pub package, as a shorthand for "current package name/". That's not bad. Probably better than just using a leading slash if we use / as package-name/path separator. (If we use : as package-name:path separator, a leading slash is harder to mistake since slashes only occur in the path).

I actually think that distinguishing imports of the current package from other imports is a good thing for readability. When I look above and see:

import "package:helper/helper.dart";
import "package:foo/foo.dart";
import "package:test/test.dart";

it doesn't stand out that foo is the package I'm about to test, my own package. It's just another package in the pile.

Levi-Lesches commented 2 years ago

I like @munificent's proposal, with the personal preference of using : instead of / after the package.

  1. The precedence comes from the existing package:foo syntax and the dart run command
  2. It does a good job of visually separating the package name from the rest of the path
  3. It would more easily allow omitting either side of the : (the package name or the path)

In other words,

It's closest to the current syntax.

and

it makes it easy to migrate: just shave off the package and .dart and you're done.

Some examples:

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.dart';
import '../util/dart_type_utilities.dart';
import '../../../room/member/membership.dart';
import 'src/assets/source_stylesheet.dart';
import 'user_address.g.dart';
import dart:async;
import dart:io as io;

import args:command_runner;
import file:memory;
import :src/base/common;
import :src/base/error_handling_io;
import :src/base/file_system;
import :src/base/io;
import :src/base/signals;
import :src/base/time;
import :src/build_info;
import :src/cache;
import :src/pub;
import :src/globals as globals;
import :src/pre_run_validator;
import :src/reporting/reporting;
import :src/runner/flutter_command;
import test:fake;

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';
import dart:async;
import dart:convert;
import dart:io hide Platform;
import dart:typed_data;

import args;
import crypto;
import crypto:src/digest_sink;
import http as http;
import path as path;
import platform show Platform, LocalPlatform;
import process;
munificent commented 2 years ago

In other words,

It's closest to the current syntax.

and

it makes it easy to migrate: just shave off the package and .dart and you're done.

But using a : instead of / between the package name and path breaks both of these.