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

Bind toplevel this assignments as global declarations #22891

Closed
wants to merge 5 commits into from

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Mar 26, 2018

Previously, this-assignments at the top level of a source file did not bind anything. The source file case was not handled in bindThisPropertyAssignment, however, so when #22643 added an assert that it handled every case, files like this started crashing on the assert:

this.a = 10

With the new commits, this fix correctly binds references to a and this.a in both script files and commonjs modules. Binding in Typescript files is unchanged.

Checking also understands toplevel this better in Javascript files. Now, this: { [s: string]: any } & globals. That means this.a will have the correct type, but also this.NaN. Checking in Typescript files is unchanged because I don't want to encourage this usage there.

Improves the fix for #22888

@sandersn sandersn requested review from mhegazy and a user March 26, 2018 23:18
@sandersn sandersn changed the title Bind toplevel this assignments as global decls Bind toplevel this assignments as global declarations Mar 26, 2018
case SyntaxKind.SourceFile:
// this.foo assignment in a source file
// Bind this property in the global namespace
declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following test doesn't work perfectly:

// @out: output.js
// @allowJs: true
// @Filename: a.js
this.a = 10;
a;

// @Filename: /b.js
this.a;
a;

In /b.js, this.a will be any, while a is still number.
I don't see how this ends up in globals? declareSourceFileMember seems to put it in the source file locals, but does that really point to the globals instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems half working, we either acknowledge that this at the top level is the global scope, and mirror that both for declarations and look ups, or we do not do either.

in other words, if this.a = 10 is a declaration of a global a, then should var a = 10; and both should be visible on this.a.

For this bug fix, i would say just avoid the assert by handling the SourceFile case.

Copy link

@Jessidhia Jessidhia Mar 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that top level this in a commonjs file is the initial exports object.

$ echo 'this.foo = "bar"' > a.js
$ node -p 'require("./a")'
{ foo: 'bar' }

this is only global in a traditional namespace-style file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binding

@andy-ms the single file case works the same way. file.locals declarations on a script (not module) source file will be available in the global namespace. As @Kovensky points out, we should put the declaration on file.exports when the source file is a module.

Type checking

@mhegazy Regarding the type checking, I guess you want us to start treating this as { [s: string]: any, a: number } instead of any? For the module case, we can treat it as the type of exports, which is actually the source file, I think. I guess we can look up members on top level this by looking them up in file.locals or file.exports, and just returning any if the member is not found.

Short term fix

@mhegazy It’s easy enough to no-op source files — do you think a short-term fix is worth it here? I guess the customer that @sheetalkamat was working with would benefit. I’ll create a separate PR with just the no-op and keep this one open for the complete fix.

@sandersn
Copy link
Member Author

See revised comments above; this now binds and checks toplevel this.

src/compiler/binder.ts Outdated Show resolved Hide resolved
if (isSourceFile(container)) {
// look up in the source file's locals or exports
const parent = getSymbolOfNode(container);
return createAnonymousType(parent, container.commonJsModuleIndicator ? parent.exports : globals, emptyArray, emptyArray, createIndexInfo(anyType, /*isReadonly*/ false), undefined);
Copy link

@ghost ghost Mar 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createAnonymousType looks expensive if we call it on every this expression not in a class/function. Don't we already get a type for the source file when doing import a = require("source-file");?
Also, we should maintain a type for globals; that would fix #14052. But until then createAnonymousType(globals) will be really expensive if there are 1000 globals, and there might be! Might be better to leave that case as a // TODO: GH#14052 and leave as any for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure i understand the need for the indexer.. why not just return a "global" type?

Copy link
Member Author

@sandersn sandersn Mar 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhegazy Do you want an error on any unknown toplevel this access in Javascript? I don't think we should be stricter than Typescript here.

@andy-ms I'll take a look and see what I can do. A global type seems like a good idea.

I don't think the current code is a real performance problem, though, for a couple of reasons:

  1. It only happens a couple of times per project, even in large code bases like chrome-devtools-frontend. Compare to checkObjectLiteral, which creates a fresh anonymous type for every object literal in a code base.
  2. createAnonymousType just saves a pointer to the globals symbol table (which does in fact have hundreds of entries just from JS built-ins). The property symbols themselves cache their own resolved type if it's requested.

1. Add a global type. This is only accessible via toplevel `this` in
javascript scripts, but could be the type of other values later, and
also given a typename later.
2. Use the type of the containing file in the export case.
@sandersn
Copy link
Member Author

sandersn commented Mar 29, 2018

Note that this PR creates a global type but doesn't give it a type name, and only makes it accessible via this in a js file. We can add a type name and additional values that have the type when the "global" proposal in TC39 decides on a value name. And we can define behaviour for both Typescript and Javascript.

@sandersn
Copy link
Member Author

sandersn commented Mar 30, 2018

After the design meeting, we decided to hold this change until the "global" proposal on TC39 finds a good syntax. Then we'll use that as the type and value name, both in Typescript and in Javascript.

@typescript-bot
Copy link
Collaborator

Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it.

@sandersn sandersn mentioned this pull request Jan 9, 2019
@jakebailey jakebailey deleted the bind-toplevel-this branch November 7, 2022 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants