Skip to content

Commit 69b1cb5

Browse files
authored
Add new special assignment kinds for recognizing Object.defineProperty 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
1 parent e379aeb commit 69b1cb5

30 files changed

+2756
-25
lines changed

src/compiler/binder.ts

+58-9
Original file line numberDiff line numberDiff line change
@@ -2112,7 +2112,7 @@ namespace ts {
21122112
// Nothing to do
21132113
break;
21142114
default:
2115-
Debug.fail("Unknown special property assignment kind");
2115+
Debug.fail("Unknown binary expression special property assignment kind");
21162116
}
21172117
return checkStrictModeBinaryExpression(<BinaryExpression>node);
21182118
case SyntaxKind.CatchClause:
@@ -2188,6 +2188,19 @@ namespace ts {
21882188
return bindFunctionExpression(<FunctionExpression>node);
21892189

21902190
case SyntaxKind.CallExpression:
2191+
const assignmentKind = getAssignmentDeclarationKind(node as CallExpression);
2192+
switch (assignmentKind) {
2193+
case AssignmentDeclarationKind.ObjectDefinePropertyValue:
2194+
return bindObjectDefinePropertyAssignment(node as BindableObjectDefinePropertyCall);
2195+
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
2196+
return bindObjectDefinePropertyExport(node as BindableObjectDefinePropertyCall);
2197+
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
2198+
return bindObjectDefinePrototypeProperty(node as BindableObjectDefinePropertyCall);
2199+
case AssignmentDeclarationKind.None:
2200+
break; // Nothing to do
2201+
default:
2202+
return Debug.fail("Unknown call expression assignment declaration kind");
2203+
}
21912204
if (isInJSFile(node)) {
21922205
bindCallExpression(<CallExpression>node);
21932206
}
@@ -2351,6 +2364,22 @@ namespace ts {
23512364
return true;
23522365
}
23532366

2367+
function bindObjectDefinePropertyExport(node: BindableObjectDefinePropertyCall) {
2368+
if (!setCommonJsModuleIndicator(node)) {
2369+
return;
2370+
}
2371+
const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => {
2372+
if (symbol) {
2373+
addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment);
2374+
}
2375+
return symbol;
2376+
});
2377+
if (symbol) {
2378+
const flags = SymbolFlags.Property | SymbolFlags.ExportValue;
2379+
declareSymbol(symbol.exports!, symbol, node, flags, SymbolFlags.None);
2380+
}
2381+
}
2382+
23542383
function bindExportsPropertyAssignment(node: BinaryExpression) {
23552384
// When we create a property via 'exports.foo = bar', the 'exports.foo' property access
23562385
// expression is the declaration
@@ -2458,6 +2487,11 @@ namespace ts {
24582487
bindPropertyAssignment(lhs.expression, lhs, /*isPrototypeProperty*/ false);
24592488
}
24602489

2490+
function bindObjectDefinePrototypeProperty(node: BindableObjectDefinePropertyCall) {
2491+
const namespaceSymbol = lookupSymbolForPropertyAccess((node.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression);
2492+
bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ true);
2493+
}
2494+
24612495
/**
24622496
* For `x.prototype.y = z`, declare a member `y` on `x` if `x` is a function or class, or not declared.
24632497
* Note that jsdoc preceding an ExpressionStatement like `x.prototype.y;` is also treated as a declaration.
@@ -2476,6 +2510,12 @@ namespace ts {
24762510
bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true);
24772511
}
24782512

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

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

2510-
function bindPropertyAssignment(name: EntityNameExpression, propertyAccess: PropertyAccessEntityNameExpression, isPrototypeProperty: boolean) {
2511-
let namespaceSymbol = lookupSymbolForPropertyAccess(name);
2512-
const isToplevel = isBinaryExpression(propertyAccess.parent)
2513-
? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile
2514-
: propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
2550+
function bindPotentiallyMissingNamespaces(namespaceSymbol: Symbol | undefined, entityName: EntityNameExpression, isToplevel: boolean, isPrototypeProperty: boolean) {
25152551
if (isToplevel && !isPrototypeProperty && (!namespaceSymbol || !(namespaceSymbol.flags & SymbolFlags.Namespace))) {
25162552
// make symbols or add declarations for intermediate containers
25172553
const flags = SymbolFlags.Module | SymbolFlags.Assignment;
25182554
const excludeFlags = SymbolFlags.ValueModuleExcludes & ~SymbolFlags.Assignment;
2519-
namespaceSymbol = forEachIdentifierInEntityName(propertyAccess.expression, namespaceSymbol, (id, symbol, parent) => {
2555+
namespaceSymbol = forEachIdentifierInEntityName(entityName, namespaceSymbol, (id, symbol, parent) => {
25202556
if (symbol) {
25212557
addDeclarationToSymbol(symbol, id, flags);
25222558
return symbol;
@@ -2528,6 +2564,10 @@ namespace ts {
25282564
}
25292565
});
25302566
}
2567+
return namespaceSymbol;
2568+
}
2569+
2570+
function bindPotentiallyNewExpandoMemberToNamespace(declaration: PropertyAccessEntityNameExpression | CallExpression, namespaceSymbol: Symbol | undefined, isPrototypeProperty: boolean) {
25312571
if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) {
25322572
return;
25332573
}
@@ -2537,10 +2577,19 @@ namespace ts {
25372577
(namespaceSymbol.members || (namespaceSymbol.members = createSymbolTable())) :
25382578
(namespaceSymbol.exports || (namespaceSymbol.exports = createSymbolTable()));
25392579

2540-
const isMethod = isFunctionLikeDeclaration(getAssignedExpandoInitializer(propertyAccess)!);
2580+
const isMethod = isFunctionLikeDeclaration(getAssignedExpandoInitializer(declaration)!);
25412581
const includes = isMethod ? SymbolFlags.Method : SymbolFlags.Property;
25422582
const excludes = isMethod ? SymbolFlags.MethodExcludes : SymbolFlags.PropertyExcludes;
2543-
declareSymbol(symbolTable, namespaceSymbol, propertyAccess, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment);
2583+
declareSymbol(symbolTable, namespaceSymbol, declaration, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment);
2584+
}
2585+
2586+
function bindPropertyAssignment(name: EntityNameExpression, propertyAccess: PropertyAccessEntityNameExpression, isPrototypeProperty: boolean) {
2587+
let namespaceSymbol = lookupSymbolForPropertyAccess(name);
2588+
const isToplevel = isBinaryExpression(propertyAccess.parent)
2589+
? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile
2590+
: propertyAccess.parent.parent.kind === SyntaxKind.SourceFile;
2591+
namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, propertyAccess.expression, isToplevel, isPrototypeProperty);
2592+
bindPotentiallyNewExpandoMemberToNamespace(propertyAccess, namespaceSymbol, isPrototypeProperty);
25442593
}
25452594

25462595
/**

src/compiler/checker.ts

+96-6
Original file line numberDiff line numberDiff line change
@@ -4881,7 +4881,7 @@ namespace ts {
48814881
let jsdocType: Type | undefined;
48824882
let types: Type[] | undefined;
48834883
for (const declaration of symbol.declarations) {
4884-
const expression = isBinaryExpression(declaration) ? declaration :
4884+
const expression = (isBinaryExpression(declaration) || isCallExpression(declaration)) ? declaration :
48854885
isPropertyAccessExpression(declaration) ? isBinaryExpression(declaration.parent) ? declaration.parent : declaration :
48864886
undefined;
48874887
if (!expression) {
@@ -4897,9 +4897,11 @@ namespace ts {
48974897
definedInMethod = true;
48984898
}
48994899
}
4900-
jsdocType = getJSDocTypeFromAssignmentDeclaration(jsdocType, expression, symbol, declaration);
4900+
if (!isCallExpression(expression)) {
4901+
jsdocType = getJSDocTypeFromAssignmentDeclaration(jsdocType, expression, symbol, declaration);
4902+
}
49014903
if (!jsdocType) {
4902-
(types || (types = [])).push(isBinaryExpression(expression) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType);
4904+
(types || (types = [])).push((isBinaryExpression(expression) || isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType);
49034905
}
49044906
}
49054907
let type = jsdocType;
@@ -4960,7 +4962,32 @@ namespace ts {
49604962
}
49614963

49624964
/** If we don't have an explicit JSDoc type, get the type from the initializer. */
4963-
function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression, kind: AssignmentDeclarationKind) {
4965+
function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression | CallExpression, kind: AssignmentDeclarationKind) {
4966+
if (isCallExpression(expression)) {
4967+
if (resolvedSymbol) {
4968+
return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments
4969+
}
4970+
const objectLitType = checkExpressionCached((expression as BindableObjectDefinePropertyCall).arguments[2]);
4971+
const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String);
4972+
if (valueType) {
4973+
return valueType;
4974+
}
4975+
const getFunc = getTypeOfPropertyOfType(objectLitType, "get" as __String);
4976+
if (getFunc) {
4977+
const getSig = getSingleCallSignature(getFunc);
4978+
if (getSig) {
4979+
return getReturnTypeOfSignature(getSig);
4980+
}
4981+
}
4982+
const setFunc = getTypeOfPropertyOfType(objectLitType, "set" as __String);
4983+
if (setFunc) {
4984+
const setSig = getSingleCallSignature(setFunc);
4985+
if (setSig) {
4986+
return getTypeOfFirstParameterOfSignature(setSig);
4987+
}
4988+
}
4989+
return anyType;
4990+
}
49644991
const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) : getWidenedLiteralType(checkExpressionCached(expression.right));
49654992
if (type.flags & TypeFlags.Object &&
49664993
kind === AssignmentDeclarationKind.ModuleExports &&
@@ -5212,7 +5239,7 @@ namespace ts {
52125239
}
52135240
let type: Type | undefined;
52145241
if (isInJSFile(declaration) &&
5215-
(isBinaryExpression(declaration) || isPropertyAccessExpression(declaration) && isBinaryExpression(declaration.parent))) {
5242+
(isCallExpression(declaration) || isBinaryExpression(declaration) || isPropertyAccessExpression(declaration) && isBinaryExpression(declaration.parent))) {
52165243
type = getWidenedTypeFromAssignmentDeclaration(symbol);
52175244
}
52185245
else if (isJSDocPropertyLikeTag(declaration)
@@ -16179,6 +16206,31 @@ namespace ts {
1617916206
getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.Prototype) {
1618016207
return (container.parent.parent.parent.left as PropertyAccessExpression).expression;
1618116208
}
16209+
// Object.defineProperty(x, "method", { value: function() { } });
16210+
// Object.defineProperty(x, "method", { set: (x: () => void) => void });
16211+
// Object.defineProperty(x, "method", { get: () => function() { }) });
16212+
else if (container.kind === SyntaxKind.FunctionExpression &&
16213+
isPropertyAssignment(container.parent) &&
16214+
isIdentifier(container.parent.name) &&
16215+
(container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") &&
16216+
isObjectLiteralExpression(container.parent.parent) &&
16217+
isCallExpression(container.parent.parent.parent) &&
16218+
container.parent.parent.parent.arguments[2] === container.parent.parent &&
16219+
getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) {
16220+
return (container.parent.parent.parent.arguments[0] as PropertyAccessExpression).expression;
16221+
}
16222+
// Object.defineProperty(x, "method", { value() { } });
16223+
// Object.defineProperty(x, "method", { set(x: () => void) {} });
16224+
// Object.defineProperty(x, "method", { get() { return () => {} } });
16225+
else if (isMethodDeclaration(container) &&
16226+
isIdentifier(container.name) &&
16227+
(container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") &&
16228+
isObjectLiteralExpression(container.parent) &&
16229+
isCallExpression(container.parent.parent) &&
16230+
container.parent.parent.arguments[2] === container.parent &&
16231+
getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) {
16232+
return (container.parent.parent.arguments[0] as PropertyAccessExpression).expression;
16233+
}
1618216234
}
1618316235

1618416236
function getTypeForThisExpressionFromJSDoc(node: Node) {
@@ -16741,6 +16793,10 @@ namespace ts {
1674116793
}
1674216794
const thisType = checkThisExpression(thisAccess.expression);
1674316795
return thisType && getTypeOfPropertyOfContextualType(thisType, thisAccess.name.escapedText) || false;
16796+
case AssignmentDeclarationKind.ObjectDefinePropertyValue:
16797+
case AssignmentDeclarationKind.ObjectDefinePropertyExports:
16798+
case AssignmentDeclarationKind.ObjectDefinePrototypeProperty:
16799+
return Debug.fail("Does not apply");
1674416800
default:
1674516801
return Debug.assertNever(kind);
1674616802
}
@@ -21132,18 +21188,52 @@ namespace ts {
2113221188
return true;
2113321189
}
2113421190

21191+
function isReadonlyAssignmentDeclaration(d: Declaration) {
21192+
if (!isCallExpression(d)) {
21193+
return false;
21194+
}
21195+
if (!isBindableObjectDefinePropertyCall(d)) {
21196+
return false;
21197+
}
21198+
const objectLitType = checkExpressionCached(d.arguments[2]);
21199+
const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String);
21200+
if (valueType) {
21201+
const writableProp = getPropertyOfType(objectLitType, "writable" as __String);
21202+
const writableType = writableProp && getTypeOfSymbol(writableProp);
21203+
if (!writableType || writableType === falseType || writableType === regularFalseType) {
21204+
return true;
21205+
}
21206+
// We include this definition whereupon we walk back and check the type at the declaration because
21207+
// The usual definition of `Object.defineProperty` will _not_ cause literal types to be preserved in the
21208+
// argument types, should the type be contextualized by the call itself.
21209+
if (writableProp && writableProp.valueDeclaration && isPropertyAssignment(writableProp.valueDeclaration)) {
21210+
const initializer = writableProp.valueDeclaration.initializer;
21211+
const rawOriginalType = checkExpression(initializer);
21212+
if (rawOriginalType === falseType || rawOriginalType === regularFalseType) {
21213+
return true;
21214+
}
21215+
}
21216+
return false;
21217+
}
21218+
const setProp = getPropertyOfType(objectLitType, "set" as __String);
21219+
return !setProp;
21220+
}
21221+
2113521222
function isReadonlySymbol(symbol: Symbol): boolean {
2113621223
// The following symbols are considered read-only:
2113721224
// Properties with a 'readonly' modifier
2113821225
// Variables declared with 'const'
2113921226
// Get accessors without matching set accessors
2114021227
// Enum members
21228+
// Object.defineProperty assignments with writable false or no setter
2114121229
// Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation)
2114221230
return !!(getCheckFlags(symbol) & CheckFlags.Readonly ||
2114321231
symbol.flags & SymbolFlags.Property && getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.Readonly ||
2114421232
symbol.flags & SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const ||
2114521233
symbol.flags & SymbolFlags.Accessor && !(symbol.flags & SymbolFlags.SetAccessor) ||
21146-
symbol.flags & SymbolFlags.EnumMember);
21234+
symbol.flags & SymbolFlags.EnumMember ||
21235+
some(symbol.declarations, isReadonlyAssignmentDeclaration)
21236+
);
2114721237
}
2114821238

2114921239
function isReferenceToReadonlyEntity(expr: Expression, symbol: Symbol): boolean {

src/compiler/types.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ namespace ts {
790790

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

793-
export type DeclarationName = Identifier | StringLiteral | NumericLiteral | ComputedPropertyName | BindingPattern;
793+
export type DeclarationName = Identifier | StringLiteralLike | NumericLiteral | ComputedPropertyName | BindingPattern;
794794

795795
export interface Declaration extends Node {
796796
_declarationBrand: any;
@@ -1774,6 +1774,9 @@ namespace ts {
17741774
arguments: NodeArray<Expression>;
17751775
}
17761776

1777+
/** @internal */
1778+
export type BindableObjectDefinePropertyCall = CallExpression & { arguments: { 0: EntityNameExpression, 1: StringLiteralLike | NumericLiteral, 2: ObjectLiteralExpression } };
1779+
17771780
// see: https://tc39.github.io/ecma262/#prod-SuperCall
17781781
export interface SuperCall extends CallExpression {
17791782
expression: SuperExpression;
@@ -4347,6 +4350,15 @@ namespace ts {
43474350
Property,
43484351
// F.prototype = { ... }
43494352
Prototype,
4353+
// Object.defineProperty(x, 'name', { value: any, writable?: boolean (false by default) });
4354+
// Object.defineProperty(x, 'name', { get: Function, set: Function });
4355+
// Object.defineProperty(x, 'name', { get: Function });
4356+
// Object.defineProperty(x, 'name', { set: Function });
4357+
ObjectDefinePropertyValue,
4358+
// Object.defineProperty(exports || module.exports, 'name', ...);
4359+
ObjectDefinePropertyExports,
4360+
// Object.defineProperty(Foo.prototype, 'name', ...);
4361+
ObjectDefinePrototypeProperty,
43504362
}
43514363

43524364
/** @deprecated Use FileExtensionInfo instead. */

0 commit comments

Comments
 (0)