-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Proposal: If there’s a package.json, only auto-import things in it, more or less #31893
Proposal: If there’s a package.json, only auto-import things in it, more or less #31893
Conversation
let seenDeps: Map<true> | undefined; | ||
function *readPackageJsonDependencies() { | ||
type PackageJson = Record<typeof dependencyKeys[number], Record<string, string> | undefined>; | ||
const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies"] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw string completions consider peerDependencies
, but I was on the fence about whether that makes sense here. Usually a peer dependency is also a dev dependency; if it’s not, we probably don’t have typings for it anyway, and would never offer an auto-import in the first place. It seems unlikely to me that anyone will notice/care whether we look there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think excluding peer deps is the right thing to do here, if it's not in your dependencies (or through other deps) then it won't be in the tree at all
src/services/completions.ts
Outdated
// If `!!d.name.originalKeywordKind`, this is `export { _break as break };` -- skip this and prefer the keyword completion. | ||
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check). | ||
isExportSpecifier(d) && (d.propertyName ? isIdentifierANonContextualKeyword(d.name) : !!d.parent.parent.moduleSpecifier))) { | ||
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to look more at this case and see if we can still always continue—I think we might need to do something similar to below, but forgot to investigate further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I was right—fixed in de8ef32
src/services/completions.ts
Outdated
} | ||
else { | ||
// This is not a re-export, so see if we have any aliases pending and remove them | ||
potentialDuplicateSymbols.delete(getSymbolId(symbol).toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was unable to come up with a test case where this happens. I think a well-behaved language service host probably tends to order source files in such a way that we always see the original symbol first and the re-exports later, but I think it’s possible that with circular imports or a less savvy language service host, we would need this branch.
cc @mjbvz |
Not sure if this is covered (I didn't see it being mentioned), but please remember about monorepos, which are configured as a single project (i.e. single In all cases we should use the |
Hey @niieani, thanks for the feedback! The behavior I’ve coded here is to look for all package.jsons in scope for the current file and offer auto-imports from all of them. You’re right that using the one closest to the project root would be insufficient, but using only the closest one I think would be too restrictive—for example, electron-react-typescript-boilerplate uses one package.json at the root for most dependencies, and a second one in (Once I hear some feedback from the team about whether we want to go forward with this, I’ll add a test case for this behavior too) |
@@ -620,4 +666,73 @@ namespace ts.codefix { | |||
// Need `|| "_"` to ensure result isn't empty. | |||
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`; | |||
} | |||
|
|||
function createLazyPackageJsonDependencyReader(fromFile: SourceFile, host: LanguageServiceHost) { | |||
const packageJsonPaths = findPackageJsons(getDirectoryPath(fromFile.fileName), host); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think from tsserver perspective caching packageJson should be fairely ok since we do watch failed lookup locations and any changes in there result in recomputing program. But that's not guaranteed with other hosts since they can have their own module resolution or use the default resolution cache which doesn't watch these locations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we do watch failed lookup locations and any changes in there result in recomputing program
Just to make sure I’ve interpreted this correctly: you’re saying that if a user runs npm install new-package
which changes the package.json, synchronizeHostData
will be called so I could update the cache there?
But that's not guaranteed with other hosts
Maybe we can implement a fast check to determine whether the cache is up-to-date (last modified timestamp? content hash?). Actually, that might be better than reading eagerly on recomputing the program, since the program will change more often than the package.json.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the overall structure of the code that's changed in completions.ts. Can you explain what alias-vs-not-alias is and exported-vs-reexported? I need to get a big picture before I can understand the specific uses of things like potentatialDuplicateSymbol and coveredReExportedSymbols.
Also a few style suggestions.
Thanks for the review @sandersn. I added a much more cohesive comment with diagrams and examples. It may be worth mentioning separately, though, that most of the apparent complexity is just a performance optimization. The goal is simply “don’t add aliases to the list iff the symbol they’re aliasing is in the list,” but the trick is doing that in a single pass in an arbitrary module order, aggressively caching work the type checker has already done. If we didn’t care about time complexity or calling |
Ping @mjbvz—could I get someone from the VS Code side to give this idea a quick look? I would love to get this in before we cut the beta release. If that doesn’t happen I’ll shoot for typescript@experimental, but either way we’ll want to get VS Code’s feedback since most requests/issues in this area come via the Code repo. Thanks! 😄 |
@andrewbranch I think the idea makes a lot of sense but we will need to see how it works for some real world projects. If this PR is merged, we can ask users on VS Code 1.36+ to installed this extension and see if there are any issues reported |
* occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move | ||
* everything from Bucket C to Bucket A. | ||
*/ | ||
function getSymbolsFromOtherSourceFileExports(/** Bucket A */ symbols: Symbol[], tokenText: string, target: ScriptTarget, host: LanguageServiceHost): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still need to re-read completions.ts, but here are some style nits for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, here are the nits this time. I hope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HERE are my minor nits, which I worked so hard to reproduce!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more question and some minor suggestions for completions.ts.
cc3615c
to
ac0f70a
Compare
…in it, more or less (microsoft#31893)" This reverts commit 60a1b1d.
Component commits: f6cb90a Revert "Proposal: If there’s a package.json, only auto-import things in it, more or less (microsoft#31893)" This reverts commit 60a1b1d.
This started out as an attempt to solve #30713 in the general case, as an alternative to #31065. As it stands, it’s possible that this doesn’t change the status of #30713 at all, depending on the specifics of the project, but my hunch is that this is, in other ways, an improvement over the status quo.
You shouldn’t import node_modules that aren’t in your package.json
Your node_modules folder has bazillions of things in it as dependencies of dependencies that you shouldn’t import directly without explicitly adding them to your own top-level dependencies (in your package.json). And yet, if any of these second-order dependencies have typings (whether included or in
@types
), TypeScript happily offers to import it for you. This is especially frustrating when the correct import would be offered to you were it not shadowed by a wrong one. For example, I installed@emotion/core
which re-exports some symbols from@emotion/css
. Obviously I want to import from my explicit dependency of@emotion/core
, not one of its dependencies:Before (GIF)
Imports from
@emotion/css
because that’s the shorter path. ESLint ruleno-extraneous-dependencies
enabled for emphasis.After (GIF)
Imports from
@emotion/core
because that’s an explicit dependency in my package.json.Special handling for Node core modules
Core modules like
os
andcrypto
are the one exception to the rule. Actually, in a TypeScript project, they’re almost not an exception, because the user is expected to have@types/node
in their package.json if they’re using those modules. But in a JavaScript project supported by type acquisition in VS Code, the user will probably not explicitly install@types/node
, but will still expect IntelliSense and auto-import to work for those modules if they’re writing for Node. If they’re purely writing for the browser, they expect not to see auto-import suggestions for them (#30713).In a JavaScript project, especially one that tends not to install
@types
and doesn’t have ajsconfig.json
, I haven’t found a bulletproof heuristic for whether or not to offer Node core module imports. Today, they’re offered if you have@types/node
in your program for any reason, including as a dependency of a dependency, which happens pretty frequently.My proposed heuristic is “are you already importing Node core modules.” If you are, we should probably help you import more of them. If you’re not, either you don’t want to import any, or we’ll help you after you write the first one by hand. We can also define this more or less conservatively, depending on where we think is the best balance between minimizing false positives, minimizing false negatives, and maximizing performance:
JS auto-import from node core heuristic (GIF)
writeFile
is offered as an auto-import once there’s another import from a Node core module in the file.Open questions
package.json
stays the same, which is obviously much longer than script files on average. Thoughts?