Skip to content

Commit

Permalink
Add new special assignment kinds for recognizing Object.definePropert…
Browse files Browse the repository at this point in the history
…y calls (#27208)

* Add new special assignment kinds for recognizing Object.defineProperty calls

* Add support for prototype assignments, fix nits

* Fix code review comments

* Add test documenting behavior in a few more odd scenarios
  • Loading branch information
weswigham authored Oct 19, 2018
1 parent e379aeb commit 69b1cb5
Show file tree
Hide file tree
Showing 30 changed files with 2,756 additions and 25 deletions.
67 changes: 58 additions & 9 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2112,7 +2112,7 @@ namespace ts {
// Nothing to do
break;
default:
Debug.fail("Unknown special property assignment kind");
Debug.fail("Unknown binary expression special property assignment kind");
}
return checkStrictModeBinaryExpression(<BinaryExpression>node);
case SyntaxKind.CatchClause:
Expand Down Expand Up @@ -2188,6 +2188,19 @@ namespace ts {
return bindFunctionExpression(<FunctionExpression>node);

case SyntaxKind.CallExpression:
const assignmentKind = getAssignmentDeclarationKind(node as CallExpression);
switch (assignmentKind) {
case AssignmentDeclarationKind.ObjectDefinePropertyValue:
return bindObjectDefinePropertyAssignment(node as BindableObjectDefinePropertyCall);
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
return bindObjectDefinePropertyExport(node as BindableObjectDefinePropertyCall);
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
return bindObjectDefinePrototypeProperty(node as BindableObjectDefinePropertyCall);
case AssignmentDeclarationKind.None:
break; // Nothing to do
default:
return Debug.fail("Unknown call expression assignment declaration kind");
}
if (isInJSFile(node)) {
bindCallExpression(<CallExpression>node);
}
Expand Down Expand Up @@ -2351,6 +2364,22 @@ namespace ts {
return true;
}

function bindObjectDefinePropertyExport(node: BindableObjectDefinePropertyCall) {
if (!setCommonJsModuleIndicator(node)) {
return;
}
const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => {
if (symbol) {
addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment);
}
return symbol;
});
if (symbol) {
const flags = SymbolFlags.Property | SymbolFlags.ExportValue;
declareSymbol(symbol.exports!, symbol, node, flags, SymbolFlags.None);
}
}

function bindExportsPropertyAssignment(node: BinaryExpression) {
// When we create a property via 'exports.foo = bar', the 'exports.foo' property access
// expression is the declaration
Expand Down Expand Up @@ -2458,6 +2487,11 @@ namespace ts {
bindPropertyAssignment(lhs.expression, lhs, /*isPrototypeProperty*/ false);
}

function bindObjectDefinePrototypeProperty(node: BindableObjectDefinePropertyCall) {
const namespaceSymbol = lookupSymbolForPropertyAccess((node.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression);
bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ true);
}

/**
* For `x.prototype.y = z`, declare a member `y` on `x` if `x` is a function or class, or not declared.
* Note that jsdoc preceding an ExpressionStatement like `x.prototype.y;` is also treated as a declaration.
Expand All @@ -2476,6 +2510,12 @@ namespace ts {
bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true);
}

function bindObjectDefinePropertyAssignment(node: BindableObjectDefinePropertyCall) {
let namespaceSymbol = lookupSymbolForPropertyAccess(node.arguments[0]);
const isToplevel = node.parent.parent.kind === SyntaxKind.SourceFile;
namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, node.arguments[0], isToplevel, /*isPrototypeProperty*/ false);
bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ false);
}

function bindSpecialPropertyAssignment(node: BinaryExpression) {
const lhs = node.left as PropertyAccessEntityNameExpression;
Expand Down Expand Up @@ -2507,16 +2547,12 @@ namespace ts {
bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false);
}

function bindPropertyAssignment(name: EntityNameExpression, propertyAccess: PropertyAccessEntityNameExpression, isPrototypeProperty: boolean) {
let namespaceSymbol = lookupSymbolForPropertyAccess(name);
const isToplevel = isBinaryExpression(propertyAccess.parent)
? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile
: propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
function bindPotentiallyMissingNamespaces(namespaceSymbol: Symbol | undefined, entityName: EntityNameExpression, isToplevel: boolean, isPrototypeProperty: boolean) {
if (isToplevel && !isPrototypeProperty && (!namespaceSymbol || !(namespaceSymbol.flags & SymbolFlags.Namespace))) {
// make symbols or add declarations for intermediate containers
const flags = SymbolFlags.Module | SymbolFlags.Assignment;
const excludeFlags = SymbolFlags.ValueModuleExcludes & ~SymbolFlags.Assignment;
namespaceSymbol = forEachIdentifierInEntityName(propertyAccess.expression, namespaceSymbol, (id, symbol, parent) => {
namespaceSymbol = forEachIdentifierInEntityName(entityName, namespaceSymbol, (id, symbol, parent) => {
if (symbol) {
addDeclarationToSymbol(symbol, id, flags);
return symbol;
Expand All @@ -2528,6 +2564,10 @@ namespace ts {
}
});
}
return namespaceSymbol;
}

function bindPotentiallyNewExpandoMemberToNamespace(declaration: PropertyAccessEntityNameExpression | CallExpression, namespaceSymbol: Symbol | undefined, isPrototypeProperty: boolean) {
if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) {
return;
}
Expand All @@ -2537,10 +2577,19 @@ namespace ts {
(namespaceSymbol.members || (namespaceSymbol.members = createSymbolTable())) :
(namespaceSymbol.exports || (namespaceSymbol.exports = createSymbolTable()));

const isMethod = isFunctionLikeDeclaration(getAssignedExpandoInitializer(propertyAccess)!);
const isMethod = isFunctionLikeDeclaration(getAssignedExpandoInitializer(declaration)!);
const includes = isMethod ? SymbolFlags.Method : SymbolFlags.Property;
const excludes = isMethod ? SymbolFlags.MethodExcludes : SymbolFlags.PropertyExcludes;
declareSymbol(symbolTable, namespaceSymbol, propertyAccess, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment);
declareSymbol(symbolTable, namespaceSymbol, declaration, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment);
}

function bindPropertyAssignment(name: EntityNameExpression, propertyAccess: PropertyAccessEntityNameExpression, isPrototypeProperty: boolean) {
let namespaceSymbol = lookupSymbolForPropertyAccess(name);
const isToplevel = isBinaryExpression(propertyAccess.parent)
? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile
: propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, propertyAccess.expression, isToplevel, isPrototypeProperty);
bindPotentiallyNewExpandoMemberToNamespace(propertyAccess, namespaceSymbol, isPrototypeProperty);
}

/**
Expand Down
102 changes: 96 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4881,7 +4881,7 @@ namespace ts {
let jsdocType: Type | undefined;
let types: Type[] | undefined;
for (const declaration of symbol.declarations) {
const expression = isBinaryExpression(declaration) ? declaration :
const expression = (isBinaryExpression(declaration) || isCallExpression(declaration)) ? declaration :
isPropertyAccessExpression(declaration) ? isBinaryExpression(declaration.parent) ? declaration.parent : declaration :
undefined;
if (!expression) {
Expand All @@ -4897,9 +4897,11 @@ namespace ts {
definedInMethod = true;
}
}
jsdocType = getJSDocTypeFromAssignmentDeclaration(jsdocType, expression, symbol, declaration);
if (!isCallExpression(expression)) {
jsdocType = getJSDocTypeFromAssignmentDeclaration(jsdocType, expression, symbol, declaration);
}
if (!jsdocType) {
(types || (types = [])).push(isBinaryExpression(expression) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType);
(types || (types = [])).push((isBinaryExpression(expression) || isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType);
}
}
let type = jsdocType;
Expand Down Expand Up @@ -4960,7 +4962,32 @@ namespace ts {
}

/** If we don't have an explicit JSDoc type, get the type from the initializer. */
function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression, kind: AssignmentDeclarationKind) {
function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression | CallExpression, kind: AssignmentDeclarationKind) {
if (isCallExpression(expression)) {
if (resolvedSymbol) {
return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments
}
const objectLitType = checkExpressionCached((expression as BindableObjectDefinePropertyCall).arguments[2]);
const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String);
if (valueType) {
return valueType;
}
const getFunc = getTypeOfPropertyOfType(objectLitType, "get" as __String);
if (getFunc) {
const getSig = getSingleCallSignature(getFunc);
if (getSig) {
return getReturnTypeOfSignature(getSig);
}
}
const setFunc = getTypeOfPropertyOfType(objectLitType, "set" as __String);
if (setFunc) {
const setSig = getSingleCallSignature(setFunc);
if (setSig) {
return getTypeOfFirstParameterOfSignature(setSig);
}
}
return anyType;
}
const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) : getWidenedLiteralType(checkExpressionCached(expression.right));
if (type.flags & TypeFlags.Object &&
kind === AssignmentDeclarationKind.ModuleExports &&
Expand Down Expand Up @@ -5212,7 +5239,7 @@ namespace ts {
}
let type: Type | undefined;
if (isInJSFile(declaration) &&
(isBinaryExpression(declaration) || isPropertyAccessExpression(declaration) && isBinaryExpression(declaration.parent))) {
(isCallExpression(declaration) || isBinaryExpression(declaration) || isPropertyAccessExpression(declaration) && isBinaryExpression(declaration.parent))) {
type = getWidenedTypeFromAssignmentDeclaration(symbol);
}
else if (isJSDocPropertyLikeTag(declaration)
Expand Down Expand Up @@ -16179,6 +16206,31 @@ namespace ts {
getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.Prototype) {
return (container.parent.parent.parent.left as PropertyAccessExpression).expression;
}
// Object.defineProperty(x, "method", { value: function() { } });
// Object.defineProperty(x, "method", { set: (x: () => void) => void });
// Object.defineProperty(x, "method", { get: () => function() { }) });
else if (container.kind === SyntaxKind.FunctionExpression &&
isPropertyAssignment(container.parent) &&
isIdentifier(container.parent.name) &&
(container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") &&
isObjectLiteralExpression(container.parent.parent) &&
isCallExpression(container.parent.parent.parent) &&
container.parent.parent.parent.arguments[2] === container.parent.parent &&
getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) {
return (container.parent.parent.parent.arguments[0] as PropertyAccessExpression).expression;
}
// Object.defineProperty(x, "method", { value() { } });
// Object.defineProperty(x, "method", { set(x: () => void) {} });
// Object.defineProperty(x, "method", { get() { return () => {} } });
else if (isMethodDeclaration(container) &&
isIdentifier(container.name) &&
(container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") &&
isObjectLiteralExpression(container.parent) &&
isCallExpression(container.parent.parent) &&
container.parent.parent.arguments[2] === container.parent &&
getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) {
return (container.parent.parent.arguments[0] as PropertyAccessExpression).expression;
}
}

function getTypeForThisExpressionFromJSDoc(node: Node) {
Expand Down Expand Up @@ -16741,6 +16793,10 @@ namespace ts {
}
const thisType = checkThisExpression(thisAccess.expression);
return thisType && getTypeOfPropertyOfContextualType(thisType, thisAccess.name.escapedText) || false;
case AssignmentDeclarationKind.ObjectDefinePropertyValue:
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
return Debug.fail("Does not apply");
default:
return Debug.assertNever(kind);
}
Expand Down Expand Up @@ -21132,18 +21188,52 @@ namespace ts {
return true;
}

function isReadonlyAssignmentDeclaration(d: Declaration) {
if (!isCallExpression(d)) {
return false;
}
if (!isBindableObjectDefinePropertyCall(d)) {
return false;
}
const objectLitType = checkExpressionCached(d.arguments[2]);
const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String);
if (valueType) {
const writableProp = getPropertyOfType(objectLitType, "writable" as __String);
const writableType = writableProp && getTypeOfSymbol(writableProp);
if (!writableType || writableType === falseType || writableType === regularFalseType) {
return true;
}
// We include this definition whereupon we walk back and check the type at the declaration because
// The usual definition of `Object.defineProperty` will _not_ cause literal types to be preserved in the
// argument types, should the type be contextualized by the call itself.
if (writableProp && writableProp.valueDeclaration && isPropertyAssignment(writableProp.valueDeclaration)) {
const initializer = writableProp.valueDeclaration.initializer;
const rawOriginalType = checkExpression(initializer);
if (rawOriginalType === falseType || rawOriginalType === regularFalseType) {
return true;
}
}
return false;
}
const setProp = getPropertyOfType(objectLitType, "set" as __String);
return !setProp;
}

function isReadonlySymbol(symbol: Symbol): boolean {
// The following symbols are considered read-only:
// Properties with a 'readonly' modifier
// Variables declared with 'const'
// Get accessors without matching set accessors
// Enum members
// Object.defineProperty assignments with writable false or no setter
// Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation)
return !!(getCheckFlags(symbol) & CheckFlags.Readonly ||
symbol.flags & SymbolFlags.Property && getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.Readonly ||
symbol.flags & SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const ||
symbol.flags & SymbolFlags.Accessor && !(symbol.flags & SymbolFlags.SetAccessor) ||
symbol.flags & SymbolFlags.EnumMember);
symbol.flags & SymbolFlags.EnumMember ||
some(symbol.declarations, isReadonlyAssignmentDeclaration)
);
}

function isReferenceToReadonlyEntity(expr: Expression, symbol: Symbol): boolean {
Expand Down
14 changes: 13 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ namespace ts {

export type PropertyName = Identifier | StringLiteral | NumericLiteral | ComputedPropertyName;

export type DeclarationName = Identifier | StringLiteral | NumericLiteral | ComputedPropertyName | BindingPattern;
export type DeclarationName = Identifier | StringLiteralLike | NumericLiteral | ComputedPropertyName | BindingPattern;

export interface Declaration extends Node {
_declarationBrand: any;
Expand Down Expand Up @@ -1774,6 +1774,9 @@ namespace ts {
arguments: NodeArray<Expression>;
}

/** @internal */
export type BindableObjectDefinePropertyCall = CallExpression & { arguments: { 0: EntityNameExpression, 1: StringLiteralLike | NumericLiteral, 2: ObjectLiteralExpression } };

// see: https://tc39.github.io/ecma262/#prod-SuperCall
export interface SuperCall extends CallExpression {
expression: SuperExpression;
Expand Down Expand Up @@ -4347,6 +4350,15 @@ namespace ts {
Property,
// F.prototype = { ... }
Prototype,
// Object.defineProperty(x, 'name', { value: any, writable?: boolean (false by default) });
// Object.defineProperty(x, 'name', { get: Function, set: Function });
// Object.defineProperty(x, 'name', { get: Function });
// Object.defineProperty(x, 'name', { set: Function });
ObjectDefinePropertyValue,
// Object.defineProperty(exports || module.exports, 'name', ...);
ObjectDefinePropertyExports,
// Object.defineProperty(Foo.prototype, 'name', ...);
ObjectDefinePrototypeProperty,
}

/** @deprecated Use FileExtensionInfo instead. */
Expand Down
Loading

0 comments on commit 69b1cb5

Please sign in to comment.