rust-lang / style-team

Home of the Rust style team
Apache License 2.0
454 stars 56 forks source link

imports (`use`) #24

Closed nrc closed 6 years ago

solson commented 7 years ago

For one thing, if you're looking up the origin of a name imported via a glob import, it's immediately necessary to know whether there is more than one glob import. If there are two glob imports and you only notice one of them, you might incorrectly assume it's the source and start barking up the wrong tree.

LukasKalbertodt commented 7 years ago

My take on glob imports:

Problems

1. Unexpected breaking changes

Using glob imports can lead to unexpected compiler errors when merely adding or renaming a symbol in one module (example example). This is not a problem when glob importing (crate-)local symbols, but can be a big problem when glob importing symbols from foreign crates.

2. Lack of explicitness/documentation

  1. When reading code, one often wants to know the definition of a used symbol. Non-glob imports allow to search for the symbol and to quickly see where it comes from. Glob imports make this impossible.
  2. "Imports as documentation about a module's dependencies": one might argue that glob imports don't really say anything about the module's dependencies.

Rules

(additional explanation in [square brackets])

  1. Generally, glob imports should be avoided. However, it can make sense to use them in the certain situations. As long as the remaining rules are complied with, it is OK to use glob imports in the following cases:
    1. importing all symbols from a prelude [prelude's only purpose is to import everything from them; otherwise the "real" paths could be used instead]
    2. (nearly) all symbols from a module with many symbols are used [in this case the glob import is a fitting documentation of the module's dependencies: "use all symbols from that module" is a valid thing to state]
    3. (nearly) all variants from an enum with many variants are used in a narrow scope (e.g. one function) [especially methods of enum types can be extremely verbose when the programmer has to repeat the type's name many times]
  2. Only glob import symbols from crate-local modules or prelude modules. [everything else can easily lead to build breakage, because patches considered "non breaking" are actually a breaking change]
  3. Only one glob import per scope. [Avoids confusion about where a symbol is coming from (problem 2.1)]

Appendix: as a consequence of rule (2.), changing the set of symbols in a public prelude is considered a breaking change and should lead to a major version bump.

petrochenkov commented 7 years ago

@LukasKalbertodt

Using glob imports can lead to unexpected compiler errors when merely adding or renaming a symbol in one module (example).

The example doesn't break on nightly, RFC 1560 solved the glob conflict problem and was stabilized recently.

LukasKalbertodt commented 7 years ago

@petrochenkov

The example doesn't break on nightly, RFC 1560 solved the glob conflict problem and was stabilized recently.

I forgot to use bar() ... my bad! I updated my original comment. I agree, that the RFC makes the problem a lot less bad, but it's still possible to break builds like that.

nathan-myers-mongo commented 7 years ago

I can't see how vertical space on import lines matters one way or the other. Vertical space matters when you need to see them along with what's above and below them, and nothing is above them.

ssokolow commented 7 years ago

I'm sorry, but I can't agree on "ASCIIbetical" order (I've always heard it called "lexicographic order").

That puts all-caps things like simple integer constants at the front of the list, which makes the imports look top/front heavy and makes them more difficult to scan since the least significant parts (compared to types and functions) come first.

(Sort of a "big endian is easier to read in hex dumps" sort of thing, though not as severe.)

This correction just looks too wrong and I currently keep my imports sorted by hand because of it.

- use self::libc::{access, c_char, c_int, W_OK};
+ use self::libc::{W_OK, access, c_char, c_int};

Given that I'm a stubborn [omitted], if rustfmt tried enforcing it more strongly, I'd write some kind of formatting canary and a CI hook to refuse commits in which it got removed or modified. (Heck, I still might add such a thing to my project templates, since, as I remember, the only thing stopping it right now is a rustfmt default)

Also, in the examples @nrc lists, I use and prefer the "Single import" variants. Visual indent looks nicer, but I'd also accept block indent. (As long as I'm not seeing the repeated foo:: clutter or a lot of wasted screen space from the "one name per line" variants)

The underlying theme being "It's all the same to the compiler, so what's easiest for me to scan when I'm coming back to an old codebase and need to re-familiarize myself with how it works? ...especially when Vim is hard-coded to drag the cursor along for the ride when I have to scroll."

strega-nil commented 7 years ago

@ssokolow I agree with that. I really don't like ASCIIbetical.

heycam commented 7 years ago

I am in favour of "multiple imports, one per line":

ssokolow commented 7 years ago

@heycam

Sorry, but I just can't agree. When I had to code in Java in uni., it was one of the things which grated on me the most and I perceive it as not only distracting, but primitive in a "It's 2016 and the language is still penalizing non-autocomplete users because the compiler is stupid?" sort of way.

EDIT: ...or, in less "It's 5AM and I'm overdue for bed" terms...

Even with autocomplete, it's still more awkward to expect the user to learn some IDE-specific autocomplete pseudo-macro grammar to avoid having to type EnterFTabbTabbTab (assuming the first completion match is the desired one) in place of , if they want to do a bunch of imports from Foo::bar::baz::.(Or risk driving them to wildcard imports, which also add unnecessary reliance on tooling to understand the origin of symbols in the code.)

Better to optimize the part the human must unavoidably do (communicating what is to be imported) at the expense of work that rustfmt is demonstrably able to take responsibility for (syntax-aware sorting) rather than burdening them down with focusing on their autocomplete (or clipboard) invocations when it's not necessary.

joshtriplett commented 7 years ago

I could live with dropping ASCIIbetical sorting. I personally prefer it because it puts all the constants first, which matches the order of definitions in the module itself; I put constants at the top of modules, so having them at the top of imports makes sense to me.

ssokolow commented 7 years ago

I put constants at the top of my modules too, but I find that different concerns inform the ordering of definitions and imports.

steveklabnik commented 7 years ago

Back in november, @nrc said

I think this is ready for FCP. Could someone write up a summary please?

It doesn't seem that anyone has done this. I can't right this moment, but maybe we could do that to move this along?

TedDriggs commented 7 years ago

@Screwtapello makes a good point about common names such as Connection. Encouraging people to - at declaration or consumption - duplicate the module name in the type name runs counter to this style recommendation and the existing behavior of the standard library. It also increases the likelihood of stale aliases when renames happen during refactoring.

Separately; the recommendation against using super has the unfortunate side-effect of making directory-level refactoring harder by requiring updates in more places. This may be something tooling can address, but at the moment it's tedious to fix.

nrc commented 7 years ago

Bit late to the party here but we should consider other forms of change, should Rustfmt change:

I say yes to both (we currently do both of these)

solson commented 7 years ago

@nrc I agree with both, but I know at least one person who prefers use ::foo everywhere, so there may be a desire for an option.

I can't imagine anyone wanting foo::{bar}.

joshtriplett commented 7 years ago

@nrc Definitely yes to the first. For the second, do you mean changing use ::foo to use foo? Can rustfmt definitively determine the equivalence?

solson commented 7 years ago

@joshtriplett I don't think there's any determining necessary. They're always equivalent.

nrc commented 7 years ago

Can rustfmt definitively determine the equivalence?

They are always equivalent since paths in imports are always absolute

nrc commented 7 years ago

And another:

iliekturtles commented 7 years ago

I like to leave use foo::{Bar}; as-is or even use foo::{Bar,}; to reduce diff noise as additional items are added/removed: use foo::{Bar, Baz,};

nrc commented 7 years ago

We've changed our process: rather than requiring a full RFC, a PR to close this issue only requires making the changes discussed here to the style guide. You can see more details about the process in the readme for this repo.

If you'd like to help with this work, let me know, I'd be very happy to help you help us.

alobb commented 7 years ago

I'd be interested in working on this. If I understand the readme correctly, is the next step to implement the agreed-upon style in rustfmt (i.e., the 4th bullet point here)? If so, I don't see what the consensus on style was in the above comments.

cc @nrc, as I'm not sure if you get a notification otherwise

nrc commented 7 years ago

@alobb The Rustfmt tracking issue is https://github.com/rust-lang-nursery/rustfmt/issues/1361. I think the next steps are doing the implementation and creating a PR in this repo against the style guide, these are meant to be sequential, but could be done in parallel.

I'm afraid there is no good summary in this thread. It might be a good to start is by writing such a summary. On re-reading this thread it is hard to decide exactly where consensus is, so anything that fits at least some of the comments is probably a good place to start.

I would include the following:

alobb commented 7 years ago

To summarize the discussion in this thread:

  1. Grouping The general consensus seems to be three groups of imports: (1) standard library imports, (2) external library imports, and (3) internal imports. @nrc, you mention that changes to the compiler would allow rustfmt to perform gather this data; what is the state of that work? Can you link to any issues regarding it?
  2. Normalization Based on reactions to https://github.com/rust-lang-nursery/fmt-rfcs/issues/24#issuecomment-273632135, it looks like most people are in favor of having rustfmt normalize paths in all of the following cases:
use foo::{bar} // becomes `use foo::bar`
use ::foo // becomes `use foo`
use foo::self // becomes `use foo`
  1. Advice
    • Glob imports are acceptable in a few circumstances:
      1. importing from a prelude
      2. Using nearly, or all symbols in a module
    • Avoid using super:: to refer to anything outside the current file
    • Avoid importing each variant within an enum; the enum name should be used in the path, except when the variant is part of the prelude.
    • In general, import types and traits directly, but qualify functions by their modules.
      • Exceptions to importing types directly can happen when this would result in aliased types:
      • If types would shadow prelude types, prefer to qualify them by their modules rather than importing directly.
use std::fmt; // and use fmt::Write
use std::io; // and use io::Write
  1. Ordering
    • If self is included in a group of paths, it should be the first path listed (If self is the only path, it will be normalized and removed)
    • All imports within a module (other than self) will be sorted alphabetically
    • Glob imports appear before other imports from the same module as in the following example:
      use foo::*;
      use foo::bar::baz;
  2. Layout
    • This remains the most controversial topic. The following block contains the different styles considered. I have attempted to order the styles from most popular to least, but there was no clear consensus from the comments.
    • Glob imports have their own use statement.
    • If the length of a use statement fits within one line, it is not split into multiple lines.
    • On all styles that split a single use statement over multiple lines there is also the question of how far the indent should go; it can be 1 or 2 indents; 2 indents has the benefit of avoiding import names lining up with module names.
      
      // Single import, one name per line.
      use foo::{
      bar,
      baz,
      qux,
      dssdfas,
      };

// Multiple imports, maximum names per line. use foo::{bar, baz, qux}; use foo::{dssdfasf};

// Single import, full block indent use foo::{ bar, baz, qux, dssdfasf, dsfsdfs, };

// Single import, visual indent. use foo::{bar, baz, qux, dssdfasf, dsfsdfs};

// Multiple imports, one name per line. use foo::bar; use foo::baz; use foo::qux;

// Single import, block indent. use foo::{bar, baz, qux, dssdfasf, dfsdfs};



If anyone feels that I have misrepresented any of the above (_especially_ preferences of import layout), please comment on this saying so; I've attempted to pull all shared ideas, but may missed something.
joshtriplett commented 7 years ago

@alobb That looks like an accurate summary to me. Three nits:

1) The styles you mentioned only apply if the names take up more room than will fit on a single line. If the entire import statement fits on one line, put it on one line.

2) On any of the examples that have a continuation line (which includes "single import, one name per line" and "single import, block indent"), I'd personally suggest using two levels of block indentation on the names, rather than one, so that they don't line up with the module name; otherwise, the module name and the imported names from that module appear at the same level.

3) The description of "Types and functions should appear ahead of constants within a use statement.", while not directly tied to any particular consensus I could find in this issue, does sound reasonable to me. (While I prefer "constants first" instead of "constants last", as long as constants get grouped together separately from types/functions, that works for me.)

alobb commented 7 years ago

Thanks for the feedback, updated the summary!

nrc commented 7 years ago

you mention that changes to the compiler would allow rustfmt to perform gather this data; what is the state of that work?

The changes I'm thinking of are moving name resolution into the same phase as macro expansion. That work is complete (or almost complete). However, leveraging this in rustfmt seems like hard work at the moment. So, I think for now we should expect to specify but not implement this part.

nrc commented 7 years ago

Thanks for the great summary @alobb

Thoughts:

At a function or block level

I would not even recommend this. Although I would frown on it less than at module level.

Avoid using super:: to refer to anything outside the current file

Not sure how I feel about this. I agree in principle, but there are cases where it is more succinct and just as clear.

I would add to advice - avoid importing individual variants or all variants of an enum; use the name of the enum in paths (except for variants in the prelude).

either types and functions appear first, or constants appear first

I'm having second thoughts about this. I think this is hard to implement - we could only rely on convention in order to enforce it. Alphabetical sorting would get close to this and I think is good enough.

It is unclear if prelude glob imports should go ahead of other crate imports

They should not.

For sorting, I think it would pay to be explicit about sorting of import statements, and of sorting names within a single list import.

In particular, we should probably say that glob imports get sorted before other imports from the same module, e.g.

use foo::*;
use foo::bar::baz;

how far the indent should go; it can be 1 or 2 indents

I prefer one - we don't use two anywhere else and I don't see a strong argument for starting here.

For layout, I nominate 'Multiple imports, maximum names per line.' (I personally prefer 'Single import, block indent.' (or visual indent), but they seem unpopular).

alobb commented 7 years ago

Thanks for the feedback, I'm integrating it now. I'm not sure indentation level has enough consensus (I don't think it was discussed enough) to choose either way in the summary.

Also, it looked like there was quite a bit of support for limiting usage of super::, so I've left that as-is for now.

nrc commented 7 years ago

To be clear, most of my comment was intended as how we should move forward (IMO) with the tricky bits, rather than claiming consensus now. I should have been more explicit about which was which.

joshtriplett commented 7 years ago

@nrc

I'm having second thoughts about this. I think this is hard to implement - we could only rely on convention in order to enforce it. Alphabetical sorting would get close to this and I think is good enough.

Alphabetical sorting wouldn't do this. ASCIIbetical sorting would come closer, but it'd sort types and constants together rather than separating them, and two comments in this issue disliked ASCIIbetical order. I think "separate constants from types/functions/etc, and sort those two groups separately" seems promising (no matter which order those two groups go in). I don't mind doing that by convention if rustfmt can't do it by type.

alobb commented 7 years ago

Although it might not be possible to get the grouping exactly correct without the compiler, would it be enough to consider all paths that contain ['A'-'Z' | '_'] to be constants (and therefore sorted and grouped together), and everything else to be types and functions? I may be misunderstanding something, but if the users are following the naming conventions, it seems to me that this should work. (This may be what was meant by relying on convention, not sure)

Edit: I am also in favor of clearly separating constants from functions and types, as long it doesn't make the implementation too complex or inefficient.

nrc commented 7 years ago

I don't mind doing that by convention if rustfmt can't do it by type.

The trouble is that if rustfmt does any sorting at all, then it break the convention even if the user is abiding by it, and it has no way to know whether the source code is following the convention which it can't support or just out of order.

nrc commented 7 years ago

I am also in favor of clearly separating constants from functions and types

I do not think this is possible since rustfmt doesn't know the difference between the name of a type and the name of a function

nrc commented 7 years ago

I don't mind doing that by convention

Actually, perhaps I misunderstood. Are you proposing we group the imports based on ALL_CAPS vs CamelCase vs snake_case and order each group? (With or without merging CamelCase/snake_case).

That could work, the question then is should we do that or alphabetical or ASCIIbetical?

joshtriplett commented 7 years ago

@nrc I would suggest grouping those and sorting alphabetically within groups, rather than sorting ASCIIbetically. I could live with any ordering of those groups, though I'd tend to favor constants then types then functions.

solson commented 7 years ago

It took me a while to realize what the actual ordering problem was, so here's a concrete example for anyone else in my position. This is plain "ASCIIbetical" or lexicographic order:

use foo::{BAZ_QUUX, BazQuuz, FOO_BAR, FooBar, baz_quux, foo_bar};

I didn't realize earlier that this simple approach would interleave ALL_CAPS and MixedCase identifiers. I agree with the people suggesting something like this:

use foo::{BAZ_QUUX, FOO_BAR, BazQuux, FooBar, baz_quux, foo_bar};

However, like @joshtriplett, I don't care strongly about the order between the groups. I just want them cleanly separated and independently sorted. I think this would be easy to support.

nrc commented 7 years ago

OK, that (alphabetically sorted groups) sounds fine to me too. I don't feel too strongly about the ordering either, but I think we should define one. If I were to do this myself, I would go functions/modules, types, constants. But I don't really know why.

jimmycuadra commented 7 years ago

I order them as constants, types, functions/modules, because this is what sort does.

joshtriplett commented 7 years ago

I can see arguments for both. I tend to put constants/types/functions in that order, both because it tends to match the order I declare things in a module, and because I'm used to ASCIIbetical order in ls placing things like Makefile and README first. On the other hand, the more I think about it, the more I can understand wanting to put constants last, because seeing them can potentially front-load information without context ("why do you need that constant, oh, you're calling that function"). I could understand an "outer-to-inner-usage" argument for putting constants last. On the other other hand, the kinds of constants you pass to functions would make sense to put inside appropriate enum types, while the kinds of constants you declare at the top level might not actually get passed to functions, so the "outside-in" approach might not make sense.

In the end, though, the argument that makes the most sense to me is "order the use list in the same order as the declarations in the module", which would argue for constants/types/functions.

nrc commented 7 years ago

So, given that we recommend functions not be imported but called via their module, it is probably worth thinking of the groups as modules/types/constants. As well as @joshtriplett's "outer-to-inner-usage" argument, I would argue that the modules/types/constants order puts the most frequently used things earlier (I rarely use constants, frequently use modules (esp to scope functions), and types are in between.

ssokolow commented 7 years ago

As long as constants are at the end (because they're the least "significant"), I'm flexible.

johnthagen commented 7 years ago

I've been working on a C++11 project using ClangFormat, and found that it does the alphabetical sort of #include groups, which is really nice (even though it can and did break my code once).

I didn't realize how much manual time I actually spent keeping those sections all sorted nicely, so I am now greatly looking forward to when rustfmt does this. So big +1 for that feature.

retep998 commented 7 years ago

This is how I currently handle imports that are too long for one line in winapi. I'm fine with this not being the default, as long as it is provided as an option.

use um::d3dcommon::{
    D3D_CBUFFER_TYPE, D3D_INCLUDE_TYPE, D3D_NAME, D3D_REGISTER_COMPONENT_TYPE,
    D3D_RESOURCE_RETURN_TYPE, D3D_SHADER_CBUFFER_FLAGS, D3D_SHADER_INPUT_FLAGS,
    D3D_SHADER_INPUT_TYPE, D3D_SHADER_MACRO, D3D_SHADER_VARIABLE_CLASS, D3D_SHADER_VARIABLE_FLAGS,
    D3D_SHADER_VARIABLE_TYPE, ID3DInclude,
};

This is what I'd prefer to be the default for rustfmt.

use um::d3dcommon::{
    D3D_CBUFFER_TYPE,
    D3D_INCLUDE_TYPE,
    D3D_NAME,
    D3D_REGISTER_COMPONENT_TYPE,
    D3D_RESOURCE_RETURN_TYPE,
    D3D_SHADER_CBUFFER_FLAGS,
    D3D_SHADER_INPUT_FLAGS,
    D3D_SHADER_INPUT_TYPE,
    D3D_SHADER_MACRO,
    D3D_SHADER_VARIABLE_CLASS,
    D3D_SHADER_VARIABLE_FLAGS,
    D3D_SHADER_VARIABLE_TYPE,
    ID3DInclude,
};
bluss commented 7 years ago

rustfmt issue for linebroken use rust-lang-nursery/rustfmt/issues/788

nrc commented 7 years ago

Thread is summarised in https://github.com/rust-lang-nursery/fmt-rfcs/issues/24#issuecomment-289649096

Within each import, we sort in groups: snake_case, CamelCase, SCREAMING_SNAKE_CASE, within each group, sort alphabetically

torkleyy commented 7 years ago

In an internal module, should I prefer importing by path (e.g. use foo::Foo) over importing the re-exported item (e.g. use Foo) or the other way around?

nrc commented 7 years ago

cc https://github.com/rust-lang-nursery/rustfmt/pull/1562 which implements grouping based on whitespace rather than semantics of names

Nemo157 commented 7 years ago

One thing I haven't seen mentioned at all is spaces inside the braces in a multi-item use. Everyone seems to take it for granted that there aren't any spaces, but personally I find that very jarring as in all other cases an open brace is always succeeded by whitespace and a close brace is always preceded by whitespace. Specifically instead of use foo::{self, bar, baz}; I write use foo::{ self, bar, baz };.

At this point from the amount of code I've seen in the wild using the former I guess I'm wrong and I need to retrain my internal parser to not always expect whitespace in braced blocks, but just thought this could be noted in case there was a silent majority it may provoke to rise up.

ssokolow commented 7 years ago

To be honest, my formatting intuition has been parsing it as a Python set literal.

Even ignoring that, I see the fact that it's a comma-separated list as being more significant than that it uses curly braces... thus, it should use whitespace rules consistent with [arrays], (tuples), and fn(arguments).