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
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
8bd66a0
JS Object literal assignments are declarations
sandersn Nov 29, 2017
c6a7751
Test:js object literal assignment as declaration
sandersn Nov 29, 2017
4f07f58
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Feb 7, 2018
9e8d59c
Support `var x = x || {}` declarations in JS
sandersn Feb 7, 2018
a51bce0
Test:basic `var x = x || {}` support in JS
sandersn Feb 7, 2018
2f8c237
Support `o.x = o.x || {}` assignments in JS
sandersn Feb 7, 2018
7e3fdc2
Test:`o.x = o.x || {}` assignments in JS
sandersn Feb 7, 2018
e0596ad
Improve contextual type skip in checkObjectLiteral
sandersn Feb 7, 2018
d3b02be
Update baselines
sandersn Feb 7, 2018
4998b99
getSpecialPropertyAssignmentKind uses type guards
sandersn Feb 8, 2018
d0b08a2
Refactor JS static property assignment binding
sandersn Feb 8, 2018
b0aebb4
Recursive object-literal-assignment declarations
sandersn Feb 8, 2018
a09c239
4-nested object-literal assignment works in JS
sandersn Feb 9, 2018
8ac94f5
Support function/class in JS nested decls
sandersn Feb 9, 2018
33c084f
Return baselines to original state
sandersn Feb 9, 2018
61ea026
Allow window. prefix in default-assignment JS decl
sandersn Feb 9, 2018
15554d7
Fix bogus jsdoc error
sandersn Feb 10, 2018
03d155f
Update tests and baselines
sandersn Feb 10, 2018
fc08e20
Correctly merge JS decls
sandersn Feb 13, 2018
88c67fa
Refactor binder and update baselines.
sandersn Feb 13, 2018
76a9ac4
Restrict declaration initializers too
sandersn Feb 13, 2018
bad155f
Clean up bindPropertyAssignment
sandersn Feb 14, 2018
0cadfcf
Clean up js decl code in checker+utilities
sandersn Feb 14, 2018
4fdef85
Naming and duplication cleanup
sandersn Feb 15, 2018
d2b933e
Cleanup in binder: rename and move
sandersn Feb 15, 2018
0191b70
Further cleanup
sandersn Feb 15, 2018
8f98c77
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Feb 15, 2018
8bfcf33
Add symbols for just-added test
sandersn Feb 15, 2018
d180117
Move skipParentheses to utilities
sandersn Feb 15, 2018
54a89ac
Simplify bindPropertyAssignment inner loop
sandersn Feb 15, 2018
518f651
Remove assert hit by chrome devtools+update baselines
sandersn Feb 20, 2018
ad43240
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Feb 20, 2018
5af91a9
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Feb 20, 2018
116a8a8
Support nested prototype declarations
sandersn Feb 20, 2018
8e424f9
Cleanup new code in binder
sandersn Feb 20, 2018
01f2ee3
Set up structure of prototype assignments
sandersn Feb 21, 2018
b14cf4e
First draft of prototype assignment
sandersn Feb 22, 2018
41fba6f
Incremental prototype+prototype assignment work
sandersn Feb 22, 2018
d55aa22
Code cleanup
sandersn Feb 22, 2018
aa88f71
Fix js-prototype-assignment on declarations
sandersn Feb 22, 2018
dd25236
Fix nested js-containers+proto assignment in types space
sandersn Feb 23, 2018
5d32a31
Merge branch 'js-prototype-assignment' into js-object-literal-assignm…
sandersn Feb 23, 2018
aa6b76f
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Feb 23, 2018
4099d48
Update chrome-devtools baseline
sandersn Feb 23, 2018
c3143d2
Support js nested namespace decls on exports
sandersn Feb 27, 2018
c318089
Remove assert for undeclared js-nested-exports
sandersn Feb 27, 2018
125a317
Fix lint
sandersn Feb 27, 2018
d86440f
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Mar 7, 2018
239f214
Address PR comments
sandersn Mar 8, 2018
04ceb3d
Disallow JS/non-JS merge without crashing
sandersn Mar 8, 2018
35730f2
Improve error span:duplicate symbols cross-js/ts
sandersn Mar 8, 2018
f8134d0
Merge branch 'master' into js-object-literal-assignments-as-declarations
sandersn Mar 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 78 additions & 65 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2037,19 +2037,19 @@ namespace ts {
const specialKind = getSpecialPropertyAssignmentKind(node as BinaryExpression);
switch (specialKind) {
case SpecialPropertyAssignmentKind.ExportsProperty:
bindExportsPropertyAssignment(<BinaryExpression>node);
bindExportsPropertyAssignment(node as BinaryExpression);
break;
case SpecialPropertyAssignmentKind.ModuleExports:
bindModuleExportsAssignment(<BinaryExpression>node);
bindModuleExportsAssignment(node as BinaryExpression);
break;
case SpecialPropertyAssignmentKind.PrototypeProperty:
bindPrototypePropertyAssignment(<BinaryExpression>node);
bindPrototypePropertyAssignment(node as BinaryExpression);
break;
case SpecialPropertyAssignmentKind.ThisProperty:
bindThisPropertyAssignment(<BinaryExpression>node);
bindThisPropertyAssignment(node as BinaryExpression);
break;
case SpecialPropertyAssignmentKind.Property:
bindStaticPropertyAssignment(<BinaryExpression>node);
bindSpecialPropertyAssignment(node as BinaryExpression);
break;
case SpecialPropertyAssignmentKind.None:
// Nothing to do
Expand Down Expand Up @@ -2356,7 +2356,7 @@ namespace ts {
}
else if ((node.expression.kind === SyntaxKind.Identifier || node.expression.kind === SyntaxKind.PropertyAccessExpression) &&
node.parent.parent.kind === SyntaxKind.SourceFile) {
bindStaticPropertyAssignment(node);
bindStaticPropertyAssignment(node as PropertyAccessEntityNameExpression);
}
}

Expand All @@ -2365,88 +2365,101 @@ namespace ts {

// Look up the function in the local scope, since prototype assignments should
// follow the function declaration
const leftSideOfAssignment = node.left as PropertyAccessExpression;
const classPrototype = leftSideOfAssignment.expression as PropertyAccessExpression;
const leftSideOfAssignment = node.left as PropertyAccessEntityNameExpression;
const classPrototype = leftSideOfAssignment.expression as PropertyAccessEntityNameExpression;
const constructorFunction = classPrototype.expression as Identifier;

// Fix up parent pointers since we're going to use these nodes before we bind into them
leftSideOfAssignment.parent = node;
constructorFunction.parent = classPrototype;
classPrototype.parent = leftSideOfAssignment;

bindPropertyAssignment(constructorFunction.escapedText, leftSideOfAssignment, /*isPrototypeProperty*/ true);
bindPropertyAssignment(constructorFunction, leftSideOfAssignment, /*isPrototypeProperty*/ true);
}

/**
* For nodes like `x.y = z`, declare a member 'y' on 'x' if x is a function or class, or not declared.
* Also works for expression statements preceded by JSDoc, like / ** @type number * / x.y;
*/
function bindStaticPropertyAssignment(node: BinaryExpression | PropertyAccessExpression) {
// Look up the function in the local scope, since static assignments should
// follow the function declaration
const leftSideOfAssignment = node.kind === SyntaxKind.PropertyAccessExpression ? node : node.left as PropertyAccessExpression;
const target = leftSideOfAssignment.expression;

if (isIdentifier(target)) {
// Fix up parent pointers since we're going to use these nodes before we bind into them
target.parent = leftSideOfAssignment;
if (node.kind === SyntaxKind.BinaryExpression) {
leftSideOfAssignment.parent = node;
}
if (container === file && isNameOfExportsOrModuleExportsAliasDeclaration(file, target)) {
// This can be an alias for the 'exports' or 'module.exports' names, e.g.
// var util = module.exports;
// util.property = function ...
bindExportsPropertyAssignment(node as BinaryExpression);
}
else {
bindPropertyAssignment(target.escapedText, leftSideOfAssignment, /*isPrototypeProperty*/ false);
}
}
}

function lookupSymbolForName(name: __String) {
return lookupSymbolForNameWorker(container, name);
}

function bindPropertyAssignment(functionName: __String, propertyAccess: PropertyAccessExpression, isPrototypeProperty: boolean) {
const symbol = lookupSymbolForName(functionName);
let targetSymbol = symbol && isDeclarationOfFunctionOrClassExpression(symbol) ?
(symbol.valueDeclaration as VariableDeclaration).initializer.symbol :
symbol;
Debug.assert(propertyAccess.parent.kind === SyntaxKind.BinaryExpression || propertyAccess.parent.kind === SyntaxKind.ExpressionStatement);
let isLegalPosition: boolean;
if (propertyAccess.parent.kind === SyntaxKind.BinaryExpression) {
const initializerKind = (propertyAccess.parent as BinaryExpression).right.kind;
isLegalPosition = (initializerKind === SyntaxKind.ClassExpression || initializerKind === SyntaxKind.FunctionExpression) &&
propertyAccess.parent.parent.parent.kind === SyntaxKind.SourceFile;
function bindSpecialPropertyAssignment(node: BinaryExpression) {
const lhs = node.left as PropertyAccessEntityNameExpression;
// Fix up parent pointers since we're going to use these nodes before we bind into them
lhs.parent = node;
if (isIdentifier(lhs.expression) && container === file && isNameOfExportsOrModuleExportsAliasDeclaration(file, lhs.expression)) {
// This can be an alias for the 'exports' or 'module.exports' names, e.g.
// var util = module.exports;
// util.property = function ...
bindExportsPropertyAssignment(node);
}
else {
isLegalPosition = propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
bindStaticPropertyAssignment(lhs);
}
if (!isPrototypeProperty && (!targetSymbol || !(targetSymbol.flags & SymbolFlags.Namespace)) && isLegalPosition) {
Debug.assert(isIdentifier(propertyAccess.expression));
const identifier = propertyAccess.expression as Identifier;
}

/**
* For nodes like `x.y = z`, declare a member 'y' on 'x' if x is a function (or IIFE) or class or {}, or not declared.
* Also works for expression statements preceded by JSDoc, like / ** @type number * / x.y;
*/
function bindStaticPropertyAssignment(node: PropertyAccessEntityNameExpression) {
node.expression.parent = node;
bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false);
}

function bindPropertyAssignment(name: EntityNameExpression, propertyAccess: PropertyAccessEntityNameExpression, isPrototypeProperty: boolean) {
let symbol = getJSInitializerSymbol(lookupSymbolForPropertyAccess(name));
const isToplevelNamespaceableInitializer = isBinaryExpression(propertyAccess.parent) ?
propertyAccess.parent.parent.parent.kind === SyntaxKind.SourceFile && getJavascriptInitializer(propertyAccess.parent.right) :
propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
if (!isPrototypeProperty && (!symbol || !(symbol.flags & SymbolFlags.Namespace)) && isToplevelNamespaceableInitializer) {
// make symbols or add declarations for intermediate containers
const flags = SymbolFlags.Module | SymbolFlags.JSContainer;
const excludeFlags = SymbolFlags.ValueModuleExcludes & ~SymbolFlags.JSContainer;
if (targetSymbol) {
addDeclarationToSymbol(symbol, identifier, flags);
}
else {
targetSymbol = declareSymbol(container.locals, /*parent*/ undefined, identifier, flags, excludeFlags);
}
forEachIdentifierInEntityName(propertyAccess.expression, (id, original) => {
if (original) {
// Note: add declaration to original symbol, not the special-syntax's symbol, so that namespaces work for type lookup
addDeclarationToSymbol(original, id, flags);
return original;
}
else if (symbol) {
symbol.exports = symbol.exports || createSymbolTable();
symbol = declareSymbol(symbol.exports, symbol, id, flags, excludeFlags);
}
else {
Debug.assert(!original);
symbol = declareSymbol(container.locals, /*parent*/ undefined, id, flags, excludeFlags);
}
return symbol;
});
}
if (!targetSymbol || !(targetSymbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.NamespaceModule))) {
if (!symbol || !(symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.NamespaceModule | SymbolFlags.ObjectLiteral))) {
return;
}

// Set up the members collection if it doesn't exist already
const symbolTable = isPrototypeProperty ?
(targetSymbol.members || (targetSymbol.members = createSymbolTable())) :
(targetSymbol.exports || (targetSymbol.exports = createSymbolTable()));
(symbol.members || (symbol.members = createSymbolTable())) :
(symbol.exports || (symbol.exports = createSymbolTable()));

// Declare the method/property
declareSymbol(symbolTable, targetSymbol, propertyAccess, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
declareSymbol(symbolTable, symbol, propertyAccess, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
}

function lookupSymbolForPropertyAccess(node: EntityNameExpression): Symbol | undefined {
if (isIdentifier(node)) {
return lookupSymbolForNameWorker(container, node.escapedText);
}
else {
const symbol = getJSInitializerSymbol(lookupSymbolForPropertyAccess(node.expression));
return symbol && symbol.exports && symbol.exports.get(node.name.escapedText);
}
}

function forEachIdentifierInEntityName(e: EntityNameExpression, action: (e: Identifier, symbol: Symbol) => 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

return action(e, lookupSymbolForPropertyAccess(e));
}
else {
const s = getJSInitializerSymbol(forEachIdentifierInEntityName(e.expression, action));
Debug.assert(!!s && !!s.exports);
return action(e.name, s.exports.get(e.name.escapedText));
}
}

function bindCallExpression(node: CallExpression) {
Expand Down
47 changes: 38 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ namespace ts {

function mergeSymbol(target: Symbol, source: Symbol) {
if (!(target.flags & getExcludedSymbolFlags(source.flags)) ||
source.flags & SymbolFlags.JSContainer || target.flags & SymbolFlags.JSContainer) {
(source.flags | target.flags) & SymbolFlags.JSContainer) {
// Javascript static-property-assignment declarations always merge, even though they are also values
if (source.flags & SymbolFlags.ValueModule && target.flags & SymbolFlags.ValueModule && target.constEnumOnlyModule && !source.constEnumOnlyModule) {
// reset flag when merging instantiated module into value module that has only const enums
Expand All @@ -863,6 +863,13 @@ namespace ts {
if (!target.exports) target.exports = createSymbolTable();
mergeSymbolTable(target.exports, source.exports);
}
if ((source.flags | target.flags) & SymbolFlags.JSContainer) {
const sourceInitializer = getJSInitializerSymbol(source);
const targetInitializer = getJSInitializerSymbol(target);
if (sourceInitializer !== source || targetInitializer !== target) {
mergeSymbol(targetInitializer, sourceInitializer);
}
}
recordMergedSymbol(target, source);
}
else if (target.flags & SymbolFlags.NamespaceModule) {
Expand Down Expand Up @@ -4171,6 +4178,11 @@ namespace ts {
}

function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol) {
// function/class/{} assignments are fresh declarations, not property assignments, so return immediately
const specialDeclaration = getAssignedJavascriptInitializer(symbol.valueDeclaration);
if (specialDeclaration) {
return getWidenedLiteralType(checkExpressionCached(specialDeclaration));
}
const types: Type[] = [];
let definedInConstructor = false;
let definedInMethod = false;
Expand Down Expand Up @@ -14208,9 +14220,11 @@ namespace ts {
return node === right && isContextSensitiveAssignment(binaryExpression) ? getTypeOfExpression(left) : undefined;
case SyntaxKind.BarBarToken:
// When an || expression has a contextual type, the operands are contextually typed by that type. When an ||
// expression has no contextual type, the right operand is contextually typed by the type of the left operand.
// expression has no contextual type, the right operand is contextually typed by the type of the left operand,
// except for the special case of Javascript declarations of the form `namespace.prop = namespace.prop || {}`
const type = getContextualType(binaryExpression);
return !type && node === right ? getTypeOfExpression(left, /*cache*/ true) : type;
return !type && node === right && !getDeclaredJavascriptInitializer(binaryExpression.parent) && !getAssignedJavascriptInitializer(binaryExpression) ?
getTypeOfExpression(left, /*cache*/ true) : type;
case SyntaxKind.AmpersandAmpersandToken:
case SyntaxKind.CommaToken:
return node === right ? getContextualType(binaryExpression) : undefined;
Expand Down Expand Up @@ -14850,20 +14864,30 @@ namespace ts {
// Grammar checking
checkGrammarObjectLiteralExpression(node, inDestructuringPattern);

let propertiesTable = createSymbolTable();
let propertiesTable: SymbolTable;
let propertiesArray: Symbol[] = [];
let spread: Type = emptyObjectType;
let propagatedFlags: TypeFlags = TypeFlags.FreshLiteral;

const contextualType = getApparentTypeOfContextualType(node);
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
const isJSObjectLiteral = !contextualType && isInJavaScriptFile(node);
const isInJSFile = isInJavaScriptFile(node);
const isJSObjectLiteral = !contextualType && isInJSFile;
let typeFlags: TypeFlags = 0;
let patternWithComputedProperties = false;
let hasComputedStringProperty = false;
let hasComputedNumberProperty = false;
const isInJSFile = isInJavaScriptFile(node);
if (isInJSFile && node.properties.length === 0) {
// an empty JS object literal that nonetheless has members is a JS namespace
const symbol = getSymbolOfNode(node);
if (symbol.exports) {
propertiesTable = symbol.exports;
symbol.exports.forEach(symbol => propertiesArray.push(getMergedSymbol(symbol)));
return createObjectLiteralType();
}
}
propertiesTable = createSymbolTable();

let offset = 0;
for (let i = 0; i < node.properties.length; i++) {
Expand Down Expand Up @@ -19064,6 +19088,9 @@ namespace ts {
}

function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) {
if (isInJavaScriptFile(node) && getAssignedJavascriptInitializer(node)) {
return checkExpression(node.right, checkMode);
}
return checkBinaryLikeExpression(node.left, node.operatorToken, node.right, checkMode, node);
}

Expand Down Expand Up @@ -19419,10 +19446,11 @@ namespace ts {
}

function checkDeclarationInitializer(declaration: HasExpressionInitializer) {
const type = getTypeOfExpression(declaration.initializer, /*cache*/ true);
const initializer = isInJavaScriptFile(declaration) && getDeclaredJavascriptInitializer(declaration) || declaration.initializer;
const type = getTypeOfExpression(initializer, /*cache*/ true);
return getCombinedNodeFlags(declaration) & NodeFlags.Const ||
(getCombinedModifierFlags(declaration) & ModifierFlags.Readonly && !isParameterPropertyDeclaration(declaration)) ||
isTypeAssertion(declaration.initializer) ? type : getWidenedLiteralType(type);
isTypeAssertion(initializer) ? type : getWidenedLiteralType(type);
}

function isLiteralOfContextualType(candidateType: Type, contextualType: Type): boolean {
Expand Down Expand Up @@ -21950,7 +21978,8 @@ namespace ts {
// Node is the primary declaration of the symbol, just validate the initializer
// Don't validate for-in initializer as it is already an error
if (node.initializer && node.parent.parent.kind !== SyntaxKind.ForInStatement) {
checkTypeAssignableTo(checkExpressionCached(node.initializer), type, node, /*headMessage*/ undefined);
const initializer = isInJavaScriptFile(node) && getDeclaredJavascriptInitializer(node) || node.initializer;
checkTypeAssignableTo(checkExpressionCached(initializer), type, node, /*headMessage*/ undefined);
checkParameterInitializer(node);
}
}
Expand Down
Loading