-
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
Alias for commonjs require in JS #39770
Conversation
taken from #39558 as soon as it was created
1. Simplify alias resolution. 2. Simplify variable-like checking. 3. Make binding skip require calls with type tags -- they fall back to the old require-call code and then check from there. I haven't started on the declaration emit code since I don't know what is going on there nearly as well.
Found one missing feature, not sure it's worth adding.
@@ -3185,7 +3185,10 @@ namespace ts { | |||
} | |||
|
|||
if (!isBindingPattern(node.name)) { | |||
if (isBlockOrCatchScoped(node)) { | |||
if (isInJSFile(node) && isRequireVariableDeclaration(node, /*requireStringLiteralLikeArgument*/ true) && !getJSDocTypeTag(node)) { |
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.
note that once this works well enough, we could turn it on in TS as well, which would be pretty great.
if (!associatedDeclarationForContainingInitializerOrBindingName) { | ||
associatedDeclarationForContainingInitializerOrBindingName = location as BindingElement; | ||
} | ||
if (isParameterDeclaration(location as BindingElement) && !associatedDeclarationForContainingInitializerOrBindingName) { |
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.
driveby simplification to use isParameterDeclaration
src/compiler/checker.ts
Outdated
// const x = require('y').z | ||
const initializer = (node as VariableDeclaration).initializer! as PropertyAccessExpression; // require('y').z | ||
const uniqueName = factory.createUniqueName((getExternalModuleRequireArgument(node) as StringLiteral).text); // _y | ||
const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // 'y' |
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.
Since this is require('y').z
, target
is z
and target.parent
is require('y')
. I fall back to target
because, um, it MIGHT be right? And the declaration emitter seems to try to emit things even if they're not correct? Maybe this whole block should instead check that target.parent
is defined before proceeding. @weswigham what do you think?
src/compiler/checker.ts
Outdated
} | ||
isRequireAlias = isCallExpression(expr) && isRequireCall(expr, /*requireStringLiteralLikeArgument*/ true) && !!valueType.symbol; | ||
} | ||
const isRequireAlias = isRequireVariableDeclaration(symbol.valueDeclaration, /*requireStringLiteralLikeArgument*/ true); |
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.
another driveby simplification, this time to use isRequireVariableDeclaration.
Also, isRequireAlias
should never be true now, so I think this can be removed entirely. But I need to test it.
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.
It looks fine to me, but in the meat of the changes I probably followed around 25% of it so it's not worth much...
The latest merge from master shows that the nested-import generation in #39818 still does not work for |
Ad-hoc type reference resolution for `require` isn't needed anymore.
&& isAliasableOrJsExpression(node.parent.right) | ||
|| node.kind === SyntaxKind.ShorthandPropertyAssignment | ||
|| node.kind === SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as PropertyAssignment).initializer) | ||
|| isRequireVariableDeclaration(node, /*requireStringLiteralLikeArgument*/ true); |
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.
this is the new line of code, the rest is reformatting requested in the review
if (symbol && isInJSFile(node)) { | ||
const requireCall = forEach(symbol.declarations, d => isVariableDeclaration(d) && !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true) ? d.initializer : undefined); | ||
if (requireCall) { | ||
const moduleSymbol = checker.getSymbolAtLocation(requireCall.arguments[0]); |
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.
this ad-hoc code has been replaced by the call to checker.getAliasedSymbol. I just had to add require
calls to the list of aliases to skip past.
@@ -3,10 +3,11 @@ | |||
// @allowJs: true | |||
|
|||
// @Filename: /foo.js | |||
//// /*moduleDef*/class Blah { | |||
//// /*moduleDef*/function notExported() { } |
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.
this gotoDef test
appears to show that goto-def on the imported module symbol jumps to the single export of the module,
but actually shows that goto-def on the imported module symbol jumps to the very beginning of the module.
I added a non-exported function to make this clearer.
@weswigham this should be ready to review 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.
Looks like this ended up being way simpler than it looked. So, (at least one of) the goal(s) with doing these aliases properly is removing the value-side fallback lookup from the isJSDocTypeReference
case in getTypeFromTypeReference
- can we do that?
const uniqueName = factory.createUniqueName((getExternalModuleRequireArgument(node) as StringLiteral).text); // _y | ||
const specifier = getSpecifierForModuleSymbol(target.parent || target, context); // 'y' | ||
// import _y = require('y'); | ||
addResult(factory.createImportEqualsDeclaration( |
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.
So, this always needs to get made in the outermost scope, rather than the current (potentially nested) one, since import x = require('whatever')
isn't valid syntax within a namespace. includePrivateSymbol
handles that for specific symbols now (by adding the symbol we want to serialize to the private symbol list for the outermost scope), however we don't have a good mechanism (currently) for adding parts of serialized results to differing scopes. You might just need a sidechannel in the NodeBuilderContext
that you add this declaration do that you then grab and append to the start/end (probably doesn't matter which) of the file once you're doing serializing all symbols.
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.
As for a test case for this... something like a module.exports.A.B.C
export that relies on a require
'd type alias?
No, for two reasons. First, this doesn't make all exports into aliases. var exports = {}
/** @typedef {string} */
exports.SomeName;
/** @type {exports.SomeName} */
const myString = 'str';
const o = { a: 1, b: 2, c: 3 }
/** @type {keyof o} */
var x
var Outer = function O() { this.y = 2 }
Outer.Inner = class I {
constructor() {
this.x = 1
}
}
/** @type {Outer} */var z
/** @type {Outer.Inner} */var ka It does fix about 20% of the 50-odd failures we used to get without this fallback, but that's probably because we're importing classes correctly now. |
1. Fix indentation 2. Add comment for exported JSON emit 3. Add test case for nested-namespace exports.
This PR binds commonjs
const x = require('y')
statements the same way as TSimport x = require('y')
: as an alias. This improves the consistency of JS, and should fix a number of bugs that have been particularly hard to fix over the years. It should let us get rid of quite a bit of ad-hoc code too. See below for details.Fixes #38379 and
Fixes #37000
Fixes #25533
Maybe also
and some others I need to dig up, but I need to verify first.
Limitations and open questions:
const x = require('y').z
-- arequire
with a single suffix. But it doesn't work withconst x = require('y').z.more.nested.accesses
orconst { destructuring } = require('y').z
. This probably isn't much more, or much more complicated code -- I'm just not sure it's worth the effort, and I wanted to get an initial version done.const s = require('fs').readFileSync("foo", "utf8")
falls back to the old method of producing an anonymous type fromrequire('fs')
and then retrieving the property namedreadFileSync
from that type.The declaration emit change is easier to read with whitespace ignored because I indented a span of code without changing it.