Skip to content

Default import prefixes for packages #60150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dickermoshe opened this issue Feb 16, 2025 · 6 comments
Open

Default import prefixes for packages #60150

dickermoshe opened this issue Feb 16, 2025 · 6 comments
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-assist Issues with analysis server assists P3 A lower priority bug or feature request type-design type-enhancement A request for a change that isn't a bug

Comments

@dickermoshe
Copy link

dickermoshe commented Feb 16, 2025

Problem

The dart extension is really helpful for automatically adding imports.
In fact, I rarely ever find myself writing imports manually.

When two packages import to items with the same name, dart does not let you reference them.
The solution for this is adding a prefix import 'package:foo/foo.dart' as foo;

This really rears its head in Flutter, where the default material widgets clutters the namespace.
Every flutter app is cluttered with widgets like MyFilledButton, MyDialog etc, because of these clashes.

It would be really nice if the auto-add-importer could be configured to add imports using a custom prefix.

Potential Solution

pubspec.yaml

prefix:
  path: p

The analyzer and extension would use this when adding imports.
For instance the path/path.dart library would use p as a prefix automatically.

@lrhn
Copy link
Member

lrhn commented Feb 17, 2025

My two worries about this is that:

  • It's unclear what the incentive is for a package author to add a default prefix.
  • It might cause more confusion because you'll have more prefix clashes than you have imported name clashes. Especially if following the example above and using a single-letter prefix.

The latter is not necessarily a problem. You can import more than one library with the same prefix, they then share the prefixed import scope, like they would have the top-level import scope, but without any other libraries to share them with.
It may still confuse people that they have to write both p.join for a path operation, and p.PackageConfig for a package configuration (packages path and package_config).

It's not clear which libraries should choose to have a default import prefix, or why the author would think that they do.
The problem only occurs when the user gets a conflict between two unrelated packages, which isn't something either package author has necessarily thought about. (I always worry when the incentives for using a feature isn't on the
person who needs to actually choose to do so.)

It may be libraries that contain many top-level getters or functions, or which have other names that are more likely to conflict Getters and functions have lower-cased names, which makes it easier to get shadowed by local variables, and easier to have a "simple conceptual name" that might be relevant in more than one context.
One could argue that path could have named its functions pathJoin and pathSuffix to give them context, and by not doing so, they're intended to be imported with a prefix like path (or, usually, p, because that's enough).
Most other libraries won't be in that situation. (Some would argue that dart:math is one and dart:developer could be another, mainly because there is a conflict between log from dart:math and dart:developer. Again it's a lower-case top-level name that causes the conflict, not an upper-camel-cased TypeName.)

Should this be per package, or per library?
If a package has multiple top-level libraries, should all the libraries use the same prefix?
Should you be able to declare a prefix per library, opting in individual libraries?

I probably wouldn't add a default prefix to any of my own packages.
They're all intended to be used directly, and top-level functions have context-bearing names (like loadPackageConfig, not just load), and I can't predict what they would conflict with anyway.

I guess package:path would be the obvious example for when a prefix is intended and expected.
I would probably go with path as the prefix, not just p.

That said, if you use the IDE for adding the import, where you get the option of adding an import, you could instead get the option of importing with or without a prefix. Or even three options: No prefix, package-name as prefix and first letter of package-name as prefix.
That is, you write join and press CTRL+Space for a suggestion, and the first item is "join(...) -> String", which is short for "Auto import from 'package:path/path.dart". The second item would be "path.join(...) -> String" short for "Auto import from 'package:path/path.dart' as 'path'", and maybe a third could `p.join(...) -> String".

It's more noise in the suggestion menu. If there are multiple possible places to import from, then one might want to put all the non-prefixed fixes at the top, followed by the prefixed ones. (But there being multiple places to import a name from is also the case where you may want a prefix, so maybe the prefixes should go first. I'll leave that to the usability experts.)

It does make a kind of sense for some packages, or individual libraries, to suggest that they're imported with a prefix. I'm not sure I'm sold on them getting to decide the prefix, because resonable people can disagree on which prefix-naming style to use (and the style guide doesn't say anything).
The package name, the first/last word of a multi-word package name, or the first letter are all reasonable choices.
But letting the package choose a default is easier than asking the user every time. It's easy enough to change afterwards anyway.

(Then there is the issue that it requires the analyzer to read the pubspec.yaml of every package that is available for import. From the language's perspective, the only thing that's necessarily available from a package dependecy is its lib/ directory. The pubspec.yaml doesn't need to exist after pub get has run, all the package information relevant to compiling the code is in the .dart_data/package_config.json file. Does the analyzer read dependency pubspec.yamls anyway, for other reasons, or would this be new?)

@lrhn lrhn added legacy-area-analyzer Use area-devexp instead. type-enhancement A request for a change that isn't a bug devexp-assist Issues with analysis server assists labels Feb 17, 2025
@dickermoshe
Copy link
Author

I agree, this doesn't seem very practical

It's unclear what the incentive is for a package author to add a default prefix.

I should have been more clear, I don't think this is something that library authors should be defining.
For instance, there are many clashes with code_builder and analyzer, but I wouldn't force everyone using code_builder to use a prefix

My suggestion is that this would go in the developers pubspec.yaml. This one of many possible solutions.

Should this be per package, or per library?
If a package has multiple top-level libraries, should all the libraries use the same prefix?
Should you be able to declare a prefix per library, opting in individual libraries?

This is something I haven't thought through. I think a configuration that works for both would be nice.

// All imports
import:
  path: p

// Just path/path.dart:
import:
  package:path/path.dart: p

// Could work for individual items
// Use the syntax from TypeChecker.fromUri(...) 
import:
  dart:developer#log: dev

That is, you write join and press CTRL+Space for a suggestion, and the first item is "join(...) -> String", which is short for "Auto import from 'package:path/path.dart". The second item would be "path.join(...) -> String" short for "Auto import from 'package:path/path.dart' as 'path'", and maybe a third could `p.join(...) -> String".

This would really clutter the namespace.


Alternative Ideas

Emmet Style

Another idea is to have the auto-complete use an Emmet style autocomplete.
It would look before the previous period to get an import prefix.

p.TextField
__________________________________________________
| TextField       package:flutter/material.dart as p|
|                                                  _|
|___________________________________________________|

Would add a import with the prefix

import "package:flutter/material.dart" as p;

Quick Fix

It would be really neat if there was a dart fix in the IDE that could fix these clashes by adding a prefix just for the conflict.

import "dart:math";
import 'dart:developer';

void main(){
  log("");
  ^^^^
}

A quickfix would:

  1. Ask which log method the user means to invoke.
  2. Changes only part of the import that's needed
  3. Adds the prefix.
import "dart:math";
import 'dart:developer' hide log;
import 'dart:developer' as developer show log;

void main(){
  developer .log("");
}

This would allow the developer library to be used without imports and only required for the conflicting log function.

@FMorschel
Copy link
Contributor

What version of Dart are you using @dickermoshe? As of Dart 3.7.0, you already have a quick-fix to import libraries with a prefix: #55863. This would not ensure you always use the same prefix but should help you add the import with the prefix without the name clashing by mistake.

@FMorschel
Copy link
Contributor

FMorschel commented Feb 17, 2025

It would be really neat if there was a dart fix in the IDE that could fix these clashes by adding a prefix just for the conflict.

I've also worked on this and is out on the main channel for the combinators show/hide #56830. I was still working on some other stuff and haven't had the time to think about the prefixed version yet. If you want to open an issue and CC me, feel free to do so. I'll gladly work on it.


Edit: My thoughts on this are to add an assist that does something like "Prefix import" or something like that, and this could be defined as a fix for ambiguous import.

@bwilkerson bwilkerson added area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. and removed legacy-area-analyzer Use area-devexp instead. labels Feb 21, 2025
@bwilkerson
Copy link
Member

We already have a mechanism for adding a prefix to an import, but it isn't discoverable and hence needs to be replaced.

I had already considered the question of whether to make this be an assist or a refactoring.

Making it a refactoring would allow us to prompt the user for the name of the prefix. If we make it an assist then we'd have to synthesize a name and some users would need to rename the prefix in order to get the code they want.

Making it a refactoring would also allow us to provide better feedback when the operation can't be completed (because of name shadowing). That probably isn't a factor in the assist case because we can synthesize a name that won't be shadowed, but we can't provide very good feedback on renames either.

I suspect that we want to do this as a refactoring.

That said, the idea of using an undefined identifier before the period as a prefix is an interesting one.

@FMorschel
Copy link
Contributor

Yes, @bwilkerson, we have an assist for adding an import with a prefix. It does use an undefined identifier as the suggested prefix. Added by df520c9 which closed:


@dickermoshe we also have a refactor-rename at the import keyword that asks for the prefix and adds it to all elements used from this import. On this, I agree with @bwilkerson that this needs to be more visible.


We also have this issue to track adding some work related to this at:

This also mentions this rename refactor. Basically, the talk there considers adding an "add"/"remove" prefix refactor/assist.

@pq pq added the P3 A lower priority bug or feature request label Mar 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-assist Issues with analysis server assists P3 A lower priority bug or feature request type-design type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

5 participants