Skip to content

Commit f04f22c

Browse files
authored
es private fields in in (#52)
add support for the 'private-fields-in-in' TC39 proposal Signed-off-by: Ashley Claymore <[email protected]>
1 parent 4903c64 commit f04f22c

32 files changed

+2060
-258
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,8 @@ namespace ts {
879879
return (expr as PrefixUnaryExpression).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr as PrefixUnaryExpression).operand);
880880
case SyntaxKind.TypeOfExpression:
881881
return isNarrowingExpression((expr as TypeOfExpression).expression);
882+
case SyntaxKind.PrivateIdentifierInInExpression:
883+
return isNarrowingExpression((expr as PrivateIdentifierInInExpression).expression);
882884
}
883885
return false;
884886
}

src/compiler/checker.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23590,6 +23590,32 @@ namespace ts {
2359023590
return type;
2359123591
}
2359223592

23593+
function narrowTypeByPrivateIdentifierInInExpression(type: Type, expr: PrivateIdentifierInInExpression, assumeTrue: boolean): Type {
23594+
const target = getReferenceCandidate(expr.expression);
23595+
if (!isMatchingReference(reference, target)) {
23596+
return type;
23597+
}
23598+
23599+
const privateId = expr.name;
23600+
const symbol = lookupSymbolForPrivateIdentifierDeclaration(privateId.escapedText, privateId);
23601+
if (symbol === undefined) {
23602+
return type;
23603+
}
23604+
const classSymbol = symbol.parent!;
23605+
const classType = getTypeOfSymbol(classSymbol) as InterfaceType;
23606+
const classDecl = symbol.valueDeclaration;
23607+
Debug.assert(classDecl, "should always have a declaration");
23608+
let targetType: Type;
23609+
if (hasStaticModifier(classDecl)) {
23610+
targetType = classType;
23611+
}
23612+
else {
23613+
const classInstanceType = getDeclaredTypeOfSymbol(classSymbol);
23614+
targetType = classInstanceType;
23615+
}
23616+
return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom);
23617+
}
23618+
2359323619
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
2359423620
// We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows:
2359523621
// When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch.
@@ -24022,6 +24048,8 @@ namespace ts {
2402224048
return narrowType(type, (expr as ParenthesizedExpression | NonNullExpression).expression, assumeTrue);
2402324049
case SyntaxKind.BinaryExpression:
2402424050
return narrowTypeByBinaryExpression(type, expr as BinaryExpression, assumeTrue);
24051+
case SyntaxKind.PrivateIdentifierInInExpression:
24052+
return narrowTypeByPrivateIdentifierInInExpression(type, expr as PrivateIdentifierInInExpression, assumeTrue);
2402524053
case SyntaxKind.PrefixUnaryExpression:
2402624054
if ((expr as PrefixUnaryExpression).operator === SyntaxKind.ExclamationToken) {
2402724055
return narrowType(type, (expr as PrefixUnaryExpression).operand, !assumeTrue);
@@ -27726,6 +27754,7 @@ namespace ts {
2772627754
}
2772727755

2772827756
function getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined {
27757+
const originalName = name;
2772927758
let props = getPropertiesOfType(containingType);
2773027759
if (typeof name !== "string") {
2773127760
const parent = name.parent;
@@ -27734,7 +27763,23 @@ namespace ts {
2773427763
}
2773527764
name = idText(name);
2773627765
}
27737-
return getSpellingSuggestionForName(name, props, SymbolFlags.Value);
27766+
const suggestion = getSpellingSuggestionForName(name, props, SymbolFlags.Value);
27767+
if (suggestion) {
27768+
return suggestion;
27769+
}
27770+
// If we have `#typo in expr` then we can still look up potential privateIdentifiers from the surrounding classes
27771+
if (typeof originalName !== "string" && isPrivateIdentifierInInExpression(originalName.parent)) {
27772+
const privateIdentifiers: Symbol[] = [];
27773+
forEachEnclosingClass(originalName, (klass: ClassLikeDeclaration) => {
27774+
forEach(klass.members, member => {
27775+
if (isPrivateIdentifierClassElementDeclaration(member)) {
27776+
privateIdentifiers.push(member.symbol);
27777+
}
27778+
});
27779+
});
27780+
return getSpellingSuggestionForName(name, privateIdentifiers, SymbolFlags.Value);
27781+
}
27782+
return undefined;
2773827783
}
2773927784

2774027785
function getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined {
@@ -31494,6 +31539,11 @@ namespace ts {
3149431539
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) {
3149531540
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol);
3149631541
}
31542+
checkInExpressionRHS(right, rightType);
31543+
return booleanType;
31544+
}
31545+
31546+
function checkInExpressionRHS(right: Expression, rightType: Type) {
3149731547
const rightTypeConstraint = getConstraintOfType(rightType);
3149831548
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
3149931549
rightTypeConstraint && (
@@ -31503,7 +31553,6 @@ namespace ts {
3150331553
) {
3150431554
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive);
3150531555
}
31506-
return booleanType;
3150731556
}
3150831557

3150931558
function checkObjectLiteralAssignment(node: ObjectLiteralExpression, sourceType: Type, rightIsThis?: boolean): Type {
@@ -31740,6 +31789,40 @@ namespace ts {
3174031789
return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target);
3174131790
}
3174231791

31792+
function checkPrivateIdentifierInInExpression(node: PrivateIdentifierInInExpression, checkMode?: CheckMode) {
31793+
const privateId = node.name;
31794+
const exp = node.expression;
31795+
let rightType = checkExpression(exp, checkMode);
31796+
31797+
const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(privateId.escapedText, privateId);
31798+
if (lexicallyScopedSymbol === undefined) {
31799+
if (!getContainingClass(node)) {
31800+
error(privateId, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies);
31801+
}
31802+
else {
31803+
const suggestion = getSuggestedSymbolForNonexistentProperty(privateId, rightType);
31804+
if (suggestion) {
31805+
const suggestedName = symbolName(suggestion);
31806+
error(privateId, Diagnostics.Cannot_find_name_0_Did_you_mean_1, diagnosticName(privateId), suggestedName);
31807+
}
31808+
else {
31809+
error(privateId, Diagnostics.Cannot_find_name_0, diagnosticName(privateId));
31810+
}
31811+
}
31812+
return anyType;
31813+
}
31814+
31815+
markPropertyAsReferenced(lexicallyScopedSymbol, /* nodeForCheckWriteOnly: */ undefined, /* isThisAccess: */ false);
31816+
getNodeLinks(node).resolvedSymbol = lexicallyScopedSymbol;
31817+
31818+
if (rightType === silentNeverType) {
31819+
return silentNeverType;
31820+
}
31821+
rightType = checkNonNullType(rightType, exp);
31822+
checkInExpressionRHS(exp, rightType);
31823+
return booleanType;
31824+
}
31825+
3174331826
function createCheckBinaryExpression() {
3174431827
interface WorkArea {
3174531828
readonly checkMode: CheckMode | undefined;
@@ -32921,6 +33004,8 @@ namespace ts {
3292133004
return checkPostfixUnaryExpression(node as PostfixUnaryExpression);
3292233005
case SyntaxKind.BinaryExpression:
3292333006
return checkBinaryExpression(node as BinaryExpression, checkMode);
33007+
case SyntaxKind.PrivateIdentifierInInExpression:
33008+
return checkPrivateIdentifierInInExpression(node as PrivateIdentifierInInExpression, checkMode);
3292433009
case SyntaxKind.ConditionalExpression:
3292533010
return checkConditionalExpression(node as ConditionalExpression, checkMode);
3292633011
case SyntaxKind.SpreadElement:
@@ -39416,6 +39501,15 @@ namespace ts {
3941639501
return resolveEntityName(name as Identifier, /*meaning*/ SymbolFlags.FunctionScopedVariable);
3941739502
}
3941839503

39504+
if (isPrivateIdentifier(name) && isPrivateIdentifierInInExpression(name.parent)) {
39505+
const links = getNodeLinks(name.parent);
39506+
if (links.resolvedSymbol) {
39507+
return links.resolvedSymbol;
39508+
}
39509+
checkPrivateIdentifierInInExpression(name.parent);
39510+
return links.resolvedSymbol;
39511+
}
39512+
3941939513
return undefined;
3942039514
}
3942139515

src/compiler/emitter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,6 +1716,8 @@ namespace ts {
17161716
return emitPostfixUnaryExpression(node as PostfixUnaryExpression);
17171717
case SyntaxKind.BinaryExpression:
17181718
return emitBinaryExpression(node as BinaryExpression);
1719+
case SyntaxKind.PrivateIdentifierInInExpression:
1720+
return emitPrivateIdentifierInInExpression(node as PrivateIdentifierInInExpression);
17191721
case SyntaxKind.ConditionalExpression:
17201722
return emitConditionalExpression(node as ConditionalExpression);
17211723
case SyntaxKind.TemplateExpression:
@@ -2690,6 +2692,24 @@ namespace ts {
26902692
}
26912693
}
26922694

2695+
function emitPrivateIdentifierInInExpression(node: PrivateIdentifierInInExpression) {
2696+
const linesBeforeIn = getLinesBetweenNodes(node, node.name, node.inToken);
2697+
const linesAfterIn = getLinesBetweenNodes(node, node.inToken, node.expression);
2698+
2699+
emitLeadingCommentsOfPosition(node.name.pos);
2700+
emitPrivateIdentifier(node.name);
2701+
emitTrailingCommentsOfPosition(node.name.end);
2702+
2703+
writeLinesAndIndent(linesBeforeIn, /*writeSpaceIfNotIndenting*/ true);
2704+
emitLeadingCommentsOfPosition(node.inToken.pos);
2705+
writeTokenNode(node.inToken, writeKeyword);
2706+
emitTrailingCommentsOfPosition(node.inToken.end, /*prefixSpace*/ true);
2707+
writeLinesAndIndent(linesAfterIn, /*writeSpaceIfNotIndenting*/ true);
2708+
2709+
emit(node.expression);
2710+
decreaseIndentIf(linesBeforeIn, linesAfterIn);
2711+
}
2712+
26932713
function emitConditionalExpression(node: ConditionalExpression) {
26942714
const linesBeforeQuestion = getLinesBetweenNodes(node, node.condition, node.questionToken);
26952715
const linesAfterQuestion = getLinesBetweenNodes(node, node.questionToken, node.whenTrue);

src/compiler/factory/emitHelpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ namespace ts {
3434
// Class Fields Helpers
3535
createClassPrivateFieldGetHelper(receiver: Expression, state: Identifier, kind: PrivateIdentifierKind, f: Identifier | undefined): Expression;
3636
createClassPrivateFieldSetHelper(receiver: Expression, state: Identifier, value: Expression, kind: PrivateIdentifierKind, f: Identifier | undefined): Expression;
37+
createClassPrivateFieldInHelper(receiver: Expression, state: Identifier): Expression;
3738
}
3839

3940
export function createEmitHelperFactory(context: TransformationContext): EmitHelperFactory {
@@ -72,6 +73,7 @@ namespace ts {
7273
// Class Fields Helpers
7374
createClassPrivateFieldGetHelper,
7475
createClassPrivateFieldSetHelper,
76+
createClassPrivateFieldInHelper
7577
};
7678

7779
/**
@@ -392,6 +394,10 @@ namespace ts {
392394
return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldSet"), /*typeArguments*/ undefined, args);
393395
}
394396

397+
function createClassPrivateFieldInHelper(receiver: Expression, state: Identifier) {
398+
context.requestEmitHelper(classPrivateFieldInHelper);
399+
return factory.createCallExpression(getUnscopedHelperName("__classPrivateFieldIn"), /* typeArguments*/ undefined, [receiver, state]);
400+
}
395401
}
396402

397403
/* @internal */
@@ -954,6 +960,17 @@ namespace ts {
954960
};`
955961
};
956962

963+
export const classPrivateFieldInHelper: UnscopedEmitHelper = {
964+
name: "typescript:classPrivateFieldIn",
965+
importName: "__classPrivateFieldIn",
966+
scoped: false,
967+
text: `
968+
var __classPrivateFieldIn = (this && this.__classPrivateFieldIn) || function(receiver, state) {
969+
if (receiver === null || (typeof receiver !== "object" && typeof receiver !== "function")) throw new TypeError("Cannot use 'in' operator on non-object");
970+
return typeof state === "function" ? receiver === state : state.has(receiver);
971+
};`
972+
};
973+
957974
let allUnscopedEmitHelpers: ReadonlyESMap<string, UnscopedEmitHelper> | undefined;
958975

959976
export function getAllUnscopedEmitHelpers() {
@@ -979,6 +996,7 @@ namespace ts {
979996
exportStarHelper,
980997
classPrivateFieldGetHelper,
981998
classPrivateFieldSetHelper,
999+
classPrivateFieldInHelper,
9821000
createBindingHelper,
9831001
setModuleDefaultHelper
9841002
], helper => helper.name));

src/compiler/factory/nodeFactory.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,8 @@ namespace ts {
443443
createMergeDeclarationMarker,
444444
createSyntheticReferenceExpression,
445445
updateSyntheticReferenceExpression,
446+
createPrivateIdentifierInInExpression,
447+
updatePrivateIdentifierInInExpression,
446448
cloneNode,
447449

448450
// Lazily load factory methods for common operator factories and utilities
@@ -3066,6 +3068,34 @@ namespace ts {
30663068
: node;
30673069
}
30683070

3071+
// @api
3072+
function createPrivateIdentifierInInExpression(name: PrivateIdentifier, inToken: Token<SyntaxKind.InKeyword>, expression: Expression) {
3073+
const node = createBaseExpression<PrivateIdentifierInInExpression>(SyntaxKind.PrivateIdentifierInInExpression);
3074+
node.name = name;
3075+
node.inToken = inToken;
3076+
node.expression = expression;
3077+
node.transformFlags |=
3078+
propagateChildFlags(node.name) |
3079+
propagateChildFlags(node.inToken) |
3080+
propagateChildFlags(node.expression) |
3081+
TransformFlags.ContainsESNext;
3082+
return node;
3083+
}
3084+
3085+
// @api
3086+
function updatePrivateIdentifierInInExpression(
3087+
node: PrivateIdentifierInInExpression,
3088+
name: PrivateIdentifier,
3089+
inToken: Token<SyntaxKind.InKeyword>,
3090+
expression: Expression
3091+
): PrivateIdentifierInInExpression {
3092+
return node.name !== name
3093+
|| node.inToken !== inToken
3094+
|| node.expression !== expression
3095+
? update(createPrivateIdentifierInInExpression(name, inToken, expression), node)
3096+
: node;
3097+
}
3098+
30693099
//
30703100
// Misc
30713101
//

src/compiler/factory/nodeTests.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ namespace ts {
8787
return node.kind === SyntaxKind.EqualsGreaterThanToken;
8888
}
8989

90+
/*@internal*/
91+
export function isInKeyword(node: Node): node is InKeyword {
92+
return node.kind === SyntaxKind.InKeyword;
93+
}
94+
9095
// Identifiers
9196

9297
export function isIdentifier(node: Node): node is Identifier {
@@ -444,6 +449,10 @@ namespace ts {
444449
return node.kind === SyntaxKind.CommaListExpression;
445450
}
446451

452+
export function isPrivateIdentifierInInExpression(node: Node): node is PrivateIdentifierInInExpression {
453+
return node.kind === SyntaxKind.PrivateIdentifierInInExpression;
454+
}
455+
447456
// Misc
448457

449458
export function isTemplateSpan(node: Node): node is TemplateSpan {

src/compiler/parser.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,9 @@ namespace ts {
442442
return visitNodes(cbNode, cbNodes, node.decorators);
443443
case SyntaxKind.CommaListExpression:
444444
return visitNodes(cbNode, cbNodes, (node as CommaListExpression).elements);
445+
case SyntaxKind.PrivateIdentifierInInExpression:
446+
return visitNode(cbNode, (node as PrivateIdentifierInInExpression).name) ||
447+
visitNode(cbNode, (node as PrivateIdentifierInInExpression).expression);
445448

446449
case SyntaxKind.JsxElement:
447450
return visitNode(cbNode, (node as JsxElement).openingElement) ||
@@ -4446,9 +4449,32 @@ namespace ts {
44464449
);
44474450
}
44484451

4452+
function parsePrivateIdentifierInInExpression(pos: number): Expression {
4453+
// PrivateIdentifierInInExpression[in]:
4454+
// [+in] PrivateIdentifier in RelationalExpression[?in]
4455+
4456+
Debug.assert(token() === SyntaxKind.PrivateIdentifier, "parsePrivateIdentifierInInExpression should only have been called if we had a privateIdentifier");
4457+
Debug.assert(inDisallowInContext() === false, "parsePrivateIdentifierInInExpression should only have been called if 'in' is allowed");
4458+
const id = parsePrivateIdentifier();
4459+
if (token() !== SyntaxKind.InKeyword) {
4460+
return createMissingNode(SyntaxKind.InKeyword, /*reportAtCurrentPosition*/ true, Diagnostics._0_expected, tokenToString(SyntaxKind.InKeyword));
4461+
}
4462+
4463+
const inToken = parseTokenNode<Token<SyntaxKind.InKeyword>>();
4464+
const exp = parseBinaryExpressionOrHigher(OperatorPrecedence.Relational);
4465+
return finishNode(factory.createPrivateIdentifierInInExpression(id, inToken, exp), pos);
4466+
}
4467+
44494468
function parseBinaryExpressionOrHigher(precedence: OperatorPrecedence): Expression {
4469+
// parse a BinaryExpression the LHS is either:
4470+
// 1) a PrivateIdentifierInInExpression when 'in' flag allowed and lookahead matches 'PrivateIdentifier in'
4471+
// 2) a UnaryExpression
4472+
44504473
const pos = getNodePos();
4451-
const leftOperand = parseUnaryExpressionOrHigher();
4474+
const tryPrivateIdentifierInIn = token() === SyntaxKind.PrivateIdentifier && !inDisallowInContext() && lookAhead(nextTokenIsInKeyword);
4475+
const leftOperand = tryPrivateIdentifierInIn
4476+
? parsePrivateIdentifierInInExpression(pos)
4477+
: parseUnaryExpressionOrHigher();
44524478
return parseBinaryExpressionRest(precedence, leftOperand, pos);
44534479
}
44544480

@@ -5977,6 +6003,11 @@ namespace ts {
59776003
return (tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.NumericLiteral || token() === SyntaxKind.BigIntLiteral || token() === SyntaxKind.StringLiteral) && !scanner.hasPrecedingLineBreak();
59786004
}
59796005

6006+
function nextTokenIsInKeyword() {
6007+
nextToken();
6008+
return token() === SyntaxKind.InKeyword;
6009+
}
6010+
59806011
function isDeclaration(): boolean {
59816012
while (true) {
59826013
switch (token()) {

0 commit comments

Comments
 (0)