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

More special declaration types in JS #21974

Merged
merged 52 commits into from
Mar 8, 2018

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Feb 15, 2018

Object literals as declarations in JS

Starting in 2.7, Typescript supported empty object literals as namespace declarations in Javascript. Before that, it supported functions and classes as namespace declarations:

// before 2.7
var callable = function() {
}
callable.constant = 1

// after 2.7
var ns = {}
ns.constant = 1

This PR adds support for top-level assignments as declarations. A var or const declaration is not required.

app = {} // does NOT need to be “var app = {}”
app.C = class {
}
app.f = function() {
}
app.prop = 1

IIFEs as namespace declarations

This PR assumes that the IIFE returns a function, class or empty object literal, but does not verify it:

var C = (function () {
  function C(n) {
    this.p = n
  }
  return C;
})();
C.staticProperty = 1

Defaulted declarations

"Defaulted declarations" allow initializers that reference the declared name in the left side of a logical or:

my = window.my || {}
my.app = my.app || {}

Prototype assignment

You can assign an object literal directly to the prototype property. Invididual prototype assignments still work too:

var C = function (p) {
  this.p = p
}
C.prototype = {
  m() {
    console.log(this.p)
  }
}
C.prototype.q = function(r) {
  return this.p === r
}

Nested and merged declarations

Nesting works to any level now, and merges correctly across files. Previously neither was the case. The binder now speculatively creates nested symbols for top-level assignments and then the checker performs an extra merge of symbols with the new JSContainer flag.

Fix function containers

Previously functions weren't marked as a JS container when they didn't have any nested properties. This meant that type resolution didn't work:

var Outer = {}
Outer.Inner = function () { }

/** @type {Outer.Inner} */ // ok, Inner is treated as a type just like class { } is
var inner

Fixes #7632

Previously this only worked cross-file because it was a merge. Now it
works anywhere, and locally it is actually binding a new property on the
object literal symbol.
Less polymorphism, up-to-date generalised names and documentation.
This required fixing the predicates and the avoiding of contextual
typing loops. This is now done right, in
getContextualTypeOfBinaryExpression.

The predicates still need work.
Except for adding a newline at the end of file. That's required by
Linux, you know!
Turns out merging was incorrect even for non-nested declarations, but
tests didn't catch it before.
Also improve assert message in fourslash.
The add-intermediate-container-symbols loop is still quite confusing,
but it's not as bad as before.
Makes typingsInstaller compile without adding dependencies
return SpecialPropertyAssignmentKind.ExportsProperty;
}
if (innerPropertyAccess.name.escapedText === "prototype") {
if (lhs.expression.name.escapedText === "prototype") {
return SpecialPropertyAssignmentKind.PrototypeProperty;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

The preceding changes in this function are just a refactor, except for the following 3 lines.

else if (name.kind === SyntaxKind.PropertyAccessExpression) {
left = name.expression;
}
else {
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 else clause can never happen because on line 2004 we just checked that name.kind is QualifiedName or PropertyAccessExpression. So I deleted it.

Previously, this would assert:

```ts
exports.undeclared.n = 1;
```

Because undeclared was never declared in any recognised way. Now it no
longer asserts, but does not bind. That's because the full pattern
starts with the line `exports = require('./x')` and assumes that x.js
declares `undeclared`. I am not sure how to bind this. The new test
contains this pattern in case I figure it out.
Outer.Inner = function () {}
Outer.Inner.prototype = {
x: 1,
m() { }
Copy link
Member

Choose a reason for hiding this comment

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

We should also check/enforce that a __proto__ member in the RHS acts as a base type for the class type (this is also present in the devtools codebase).

Choose a reason for hiding this comment

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

Ah, Annex B... 😭

Copy link
Member

Choose a reason for hiding this comment

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

__proto__ was sensible ish in a pre-Object.setPrototypeOf world. :P

Choose a reason for hiding this comment

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

But there was Object.create :(

Copy link
Member Author

Choose a reason for hiding this comment

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

I’ll add it to the Big List of JavaScript We Could Do Better. I don’t wa t to put it in this PR though — I have t touched the inheritance code for a while.

Copy link
Member

Choose a reason for hiding this comment

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

K.

}

function forEachIdentifierInEntityName(e: EntityNameExpression, action: (e: Identifier, symbol: Symbol, k: EntityNameExpression) => Symbol): Symbol {
if (isIdentifier(e)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

should handle module.exports and exports as a terminal case instead of in the callers

declareSymbol(file.symbol.exports, file.symbol, <PropertyAccessExpression>node.left, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None);
const lhs = node.left as PropertyAccessEntityNameExpression;
const symbol = forEachIdentifierInEntityName(lhs.expression, (id, original, e) => {
if (isExportsOrModuleExportsOrAlias(file, e) || (isIdentifier(e) && e.escapedText === "module" && original === undefined)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not think we need to handle "module" in a special way. we probably should make forEachIdentifierInEntityName handle the terminal case correctly being identifier | exports | module.exports, and return the correct symbol in each case.

}

export function getJavascriptInitializer(initializer: Expression) {
if (isCallExpression(initializer)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we have a function getImmediatelyInvokedFunctionExpression that is almost the same

Copy link
Member Author

Choose a reason for hiding this comment

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

It is backwards: You give it a function and it climbs up past parentheses to check with there is a parent that is a CallExpression. This code starts with a CallExpression, climbs down past parentheses, and checks whether the expression is a function. I don't think it's worth undoing the work just to use getImmeidatelyInvokedFunctionExpression. It might be worthwhile to put this in utilities, except I'm not sure what to call it.

}
if (isObjectLiteralExpression(initializer) &&
(initializer.properties.length === 0 ||
isBinaryExpression(initializer.parent) && isPropertyAccessExpression(initializer.parent.left) && initializer.parent.left.name.escapedText === "prototype")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

kinda weird to be checking up the parent chain here.. can we do it higher up instead, e.g. getDeclaredJavascriptInitializer

Copy link
Member Author

Choose a reason for hiding this comment

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

We only check up the parent chain because non-empty object literals are allowed for protoype assignments. I think it is a little cleaner to calculate whether this is true and pass it in, although it's wordier, so I'll go with that.

As we discussed, I'll probably remove most restrictions on initializer kind soon anyway, so these functions will probably be simpler soon.

(getJavascriptInitializer(node.parent.right) || getDefaultedJavascriptInitializer(node.parent.left as EntityNameExpression, node.parent.right));
}

export function getJavascriptInitializer(initializer: Expression) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment listing the patterns that his function is supporting.

}
}

export function getAssignedJavascriptInitializer(node: Node) {
Copy link
Contributor

Choose a reason for hiding this comment

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

where else do we need the BarBartoken handling that is not in getDefaultedJavascriptInitializer? can we consolidate this in one place.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nowhere, it's just for defaulted initializers. I don't think the property access check is strictly needed, so I'll just move the BarBarToken check into getDefaultedJavascriptInitializer.

}
}

function getDefaultedJavascriptInitializer(name: EntityNameExpression, initializer: Expression) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can you add comments on all of these functions.. they have very similar names.

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

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

Also we need to handle the crashes you were running into before pushing this one.

sandersn added 2 commits March 7, 2018 14:51
1. Add documentation
2. Better organisation of concerns in utility functions
3. Better handling of module.exports and exports in the binder's new
code.
@sandersn
Copy link
Member Author

sandersn commented Mar 8, 2018

I addressed the comments and will take a look at the crashes tomorrow. I have fairly compact repros that should be easy to package as fourslash tests.

sandersn added 3 commits March 8, 2018 09:49
Note that the error location is misleading because it's reported inside
the merge step for the js initializer.
when the JS symbol is a JS initializer
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants