Skip to content
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

Merged
merged 39 commits into from
Aug 17, 2020
Merged

Alias for commonjs require in JS #39770

merged 39 commits into from
Aug 17, 2020

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Jul 27, 2020

This PR binds commonjs const x = require('y') statements the same way as TS import 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

Limitations and open questions:

  1. There is special-case code in a few places for const x = require('y').z -- a require with a single suffix. But it doesn't work with const x = require('y').z.more.nested.accesses or const { 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.
  2. I didn't remove all of the existing special-case code. In particular, type checking of const s = require('fs').readFileSync("foo", "utf8") falls back to the old method of producing an anonymous type from require('fs') and then retrieving the property named readFileSync from that type.

The declaration emit change is easier to read with whitespace ignored because I indented a span of code without changing it.

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.
@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Jul 27, 2020
@@ -3185,7 +3185,10 @@ namespace ts {
}

if (!isBindingPattern(node.name)) {
if (isBlockOrCatchScoped(node)) {
if (isInJSFile(node) && isRequireVariableDeclaration(node, /*requireStringLiteralLikeArgument*/ true) && !getJSDocTypeTag(node)) {
Copy link
Member Author

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) {
Copy link
Member Author

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 Show resolved Hide resolved
// 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'
Copy link
Member Author

@sandersn sandersn Jul 27, 2020

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?

}
isRequireAlias = isCallExpression(expr) && isRequireCall(expr, /*requireStringLiteralLikeArgument*/ true) && !!valueType.symbol;
}
const isRequireAlias = isRequireVariableDeclaration(symbol.valueDeclaration, /*requireStringLiteralLikeArgument*/ true);
Copy link
Member Author

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.

Copy link
Contributor

@elibarzilay elibarzilay left a 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...

src/compiler/utilities.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
@sandersn
Copy link
Member Author

The latest merge from master shows that the nested-import generation in #39818 still does not work for require. I'll need to go find out why.

&& isAliasableOrJsExpression(node.parent.right)
|| node.kind === SyntaxKind.ShorthandPropertyAssignment
|| node.kind === SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as PropertyAssignment).initializer)
|| isRequireVariableDeclaration(node, /*requireStringLiteralLikeArgument*/ true);
Copy link
Member Author

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]);
Copy link
Member Author

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() { }
Copy link
Member Author

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.

@sandersn
Copy link
Member Author

@weswigham this should be ready to review now.

Copy link
Member

@weswigham weswigham left a 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?

src/compiler/checker.ts Show resolved Hide resolved
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(
Copy link
Member

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.

Copy link
Member

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?

src/compiler/checker.ts Show resolved Hide resolved
@sandersn
Copy link
Member Author

removing the value-side fallback lookup from the isJSDocTypeReference case in getTypeFromTypeReference - can we do that?

No, for two reasons. First, this doesn't make all exports into aliases.
Second, the value side fallback is inside of a module also used for things like

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Backlog Bug PRs that fix a backlog bug For Milestone Bug PRs that fix a bug with a specific milestone
Projects
None yet
4 participants