-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
ES private field check #44648
ES private field check #44648
Changes from 35 commits
f04f22c
30dde52
31fa02b
307247c
61c677b
8292ee2
4723380
9f1c176
511ac22
337f7e8
d059c15
79eeebe
125df93
c6b2a21
320bc00
cc80c7d
ebbb063
ea4fd4b
8b78f01
01c7042
fc2b262
c007a12
40bd336
7982164
1cd313f
97f7d30
e672957
2b7425d
52b9f2a
476bf24
be26d0a
f7ddce5
01e0e60
913d044
7c49552
c6b039f
6f56c6a
c3b6c2a
5fbb2da
c924a51
27d494d
33c3b55
e3c25c9
74e56a6
8447934
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -23846,6 +23846,9 @@ namespace ts { | |||||
case SyntaxKind.InstanceOfKeyword: | ||||||
return narrowTypeByInstanceof(type, expr, assumeTrue); | ||||||
case SyntaxKind.InKeyword: | ||||||
if (isPrivateIdentifier(expr.left)) { | ||||||
return narrowTypeByPrivateIdentifierInInExpression(type, expr, assumeTrue); | ||||||
} | ||||||
const target = getReferenceCandidate(expr.right); | ||||||
const leftType = getTypeOfNode(expr.left); | ||||||
if (leftType.flags & TypeFlags.StringLiteral) { | ||||||
|
@@ -23876,6 +23879,25 @@ namespace ts { | |||||
return type; | ||||||
} | ||||||
|
||||||
function narrowTypeByPrivateIdentifierInInExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { | ||||||
const target = getReferenceCandidate(expr.right); | ||||||
if (!isMatchingReference(reference, target)) { | ||||||
return type; | ||||||
} | ||||||
|
||||||
const privateId = expr.left; | ||||||
Debug.assertNode(privateId, isPrivateIdentifier); | ||||||
const symbol = getNodeLinks(privateId.parent).resolvedSymbol; | ||||||
if (symbol === undefined) { | ||||||
return type; | ||||||
} | ||||||
const classSymbol = symbol.parent!; | ||||||
const targetType = hasStaticModifier(Debug.checkDefined(symbol.valueDeclaration, "should always have a declaration")) | ||||||
? getTypeOfSymbol(classSymbol) as InterfaceType | ||||||
: getDeclaredTypeOfSymbol(classSymbol); | ||||||
return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); | ||||||
} | ||||||
|
||||||
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { | ||||||
// We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows: | ||||||
// When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch. | ||||||
|
@@ -27715,6 +27737,27 @@ namespace ts { | |||||
} | ||||||
} | ||||||
|
||||||
function checkPrivateIdentifierExpression(privId: PrivateIdentifier): Type { | ||||||
// The only valid position for a PrivateIdentifier to appear as an expression is on the left side of | ||||||
// the `#field in expr` BinaryExpression | ||||||
const isPrivateFieldInInExpression = isBinaryExpression(privId.parent) | ||||||
&& privId.parent.left === privId | ||||||
&& privId.parent.operatorToken.kind === SyntaxKind.InKeyword; | ||||||
let symbolResolved = false; | ||||||
if (isPrivateFieldInInExpression) { | ||||||
const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(privId.escapedText, privId); | ||||||
if (lexicallyScopedSymbol) { | ||||||
symbolResolved = true; | ||||||
markPropertyAsReferenced(lexicallyScopedSymbol, /* nodeForCheckWriteOnly: */ undefined, /* isThisAccess: */ false); | ||||||
getNodeLinks(privId.parent).resolvedSymbol = lexicallyScopedSymbol; | ||||||
} | ||||||
} | ||||||
if (!symbolResolved) { | ||||||
error(privId, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error is not 100% correct. We could be inside a class body though this does match the existing errors when handling invalid privateIds. class C {
f() {
return #hello; // Private identifiers are not allowed outside class bodies.
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like something we should be reporting this as a grammar error. Something like:
@DanielRosenwasser any thoughts on wording? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added that new error, and split the check into 3 levels.
|
||||||
} | ||||||
return anyType; | ||||||
sandersn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
function getPrivateIdentifierPropertyOfType(leftType: Type, lexicallyScopedIdentifier: Symbol): Symbol | undefined { | ||||||
return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName); | ||||||
} | ||||||
|
@@ -32023,14 +32066,34 @@ namespace ts { | |||||
} | ||||||
|
||||||
function checkInExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type { | ||||||
const privIdOnLeft = isPrivateIdentifier(left); | ||||||
if (privIdOnLeft) { | ||||||
if (languageVersion < ScriptTarget.ESNext) { | ||||||
checkExternalEmitHelpers(left, ExternalEmitHelpers.ClassPrivateFieldIn); | ||||||
} | ||||||
rbuckton marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
// Unlike in 'checkPrivateIdentifierExpression' we now have access to the RHS type | ||||||
// which provides us with the oppotunity to emit more detailed errors | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regretting that when I disabled all of my extensions to free up some CPU I also disabled the code-spell-checker. I'll reenable, as mistakes like this shouldn't be getting through to the PR. |
||||||
const symbol = getNodeLinks(left.parent).resolvedSymbol; | ||||||
if (!symbol) { | ||||||
const isUncheckedJS = isUncheckedJSSuggestion(left, rightType.symbol, /*excludeClasses*/ true); | ||||||
reportNonexistentProperty(left, rightType, isUncheckedJS); | ||||||
rbuckton marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
rbuckton marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
if (leftType === silentNeverType || rightType === silentNeverType) { | ||||||
rbuckton marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return silentNeverType; | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’ve been meaning to look into the difference between never and silentNever, thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've moved this to the top now. I hadn't done that because I thought we wouldn't get the |
||||||
leftType = checkNonNullType(leftType, left); | ||||||
if (!privIdOnLeft) { | ||||||
leftType = checkNonNullType(leftType, left); | ||||||
// TypeScript 1.0 spec (April 2014): 4.15.5 | ||||||
// Require the left operand to be of type Any, the String primitive type, or the Number primitive type. | ||||||
if (!(allTypesAssignableToKind(leftType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) || | ||||||
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) { | ||||||
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol); | ||||||
} | ||||||
} | ||||||
rightType = checkNonNullType(rightType, right); | ||||||
// TypeScript 1.0 spec (April 2014): 4.15.5 | ||||||
// The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type, | ||||||
// and the right operand to be | ||||||
// The in operator requires the right operand to be | ||||||
// | ||||||
// 1. assignable to the non-primitive type, | ||||||
// 2. an unconstrained type parameter, | ||||||
|
@@ -32048,10 +32111,6 @@ namespace ts { | |||||
// unless *all* instantiations would result in an error. | ||||||
// | ||||||
// The result is always of the Boolean primitive type. | ||||||
if (!(allTypesAssignableToKind(leftType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) || | ||||||
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) { | ||||||
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol); | ||||||
} | ||||||
const rightTypeConstraint = getConstraintOfType(rightType); | ||||||
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) || | ||||||
rightTypeConstraint && ( | ||||||
|
@@ -33433,6 +33492,8 @@ namespace ts { | |||||
switch (kind) { | ||||||
case SyntaxKind.Identifier: | ||||||
return checkIdentifier(node as Identifier, checkMode); | ||||||
case SyntaxKind.PrivateIdentifier: | ||||||
return checkPrivateIdentifierExpression(node as PrivateIdentifier); | ||||||
case SyntaxKind.ThisKeyword: | ||||||
return checkThisExpression(node); | ||||||
case SyntaxKind.SuperKeyword: | ||||||
|
@@ -40219,6 +40280,15 @@ namespace ts { | |||||
return resolveEntityName(name as Identifier, /*meaning*/ SymbolFlags.FunctionScopedVariable); | ||||||
} | ||||||
|
||||||
if (isPrivateIdentifier(name) && isBinaryExpression(name.parent)) { | ||||||
const links = getNodeLinks(name.parent); | ||||||
if (links.resolvedSymbol) { | ||||||
return links.resolvedSymbol; | ||||||
} | ||||||
checkPrivateIdentifierExpression(name); | ||||||
return links.resolvedSymbol; | ||||||
} | ||||||
|
||||||
return undefined; | ||||||
} | ||||||
|
||||||
|
@@ -41605,6 +41675,7 @@ namespace ts { | |||||
case ExternalEmitHelpers.MakeTemplateObject: return "__makeTemplateObject"; | ||||||
case ExternalEmitHelpers.ClassPrivateFieldGet: return "__classPrivateFieldGet"; | ||||||
case ExternalEmitHelpers.ClassPrivateFieldSet: return "__classPrivateFieldSet"; | ||||||
case ExternalEmitHelpers.ClassPrivateFieldIn: return "__classPrivateFieldIn"; | ||||||
case ExternalEmitHelpers.CreateBinding: return "__createBinding"; | ||||||
default: return Debug.fail("Unrecognized helper"); | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be cached on the private identifier? Is there a call that caches? lookupSymbolForPrivateIdentifierDeclaration doesn't, so I guess that there is a wrapper for it that does. This code should call that wrapper instead. (see comment about creating
checkPrivateIdentifier
-- that is probably the right place if it doesn't already exist.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this overnight, it's probably enough to create
checkPrivateIdentifier
in this PR, and fix caching later. The problem predates this PR, but it means that programs with lots of private identifers are likely to have performance problems.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You were right, the symbol was cached. So not having to re-resolve the symbol when narrowing now.
Right now I am caching the symbol on the
BinaryExpression
parent. The only reason I am doing this is because otherwise 'findAllRefs' doesn't see the symbol. everything else seems to work if I store the symbol based on thePrivateIdentifier
. I'd like to address this, I'm thinking there must be a 'set' that PrivateIds need to be added so they are valid places for 'findAllRefs' to search.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be concerned that a code path could reach this line of code before the symbol is set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could put in a call to
checkPrivateIdentifierExpression
if the symbol comes back undefined to be safe.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For find-all-refs, you're possibly being tripped up by the fact
PrivateIdentifier
isn't handled byisExpressionNode
:TypeScript/src/compiler/utilities.ts
Line 1904 in 40bd336
called from here:
https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40232
For an
Identifier
, we would perform a normal name resolution:https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40245
Instead, you're storing the symbol on the parent binary expression and then looking it up at https://github.com/microsoft/TypeScript/blob/7c49552cd516da620b903edccc61af54d1872c3b/src/compiler/checker.ts#L40284, which seems strange. We don't normally store the symbol on
Identifier
references, since they can have multiple meanings, so it seems a bit strange to cache it for PrivateIdentifier (despite @sandersn's comment). However, since a PrivateIdentifier can't have anything other than a value meaning currently, we could probably store it on the NodeLinks for the id itself, rather than its parent expression.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks! Yes it was the
isExpressionNode
that was causing the find-all-refs issue. I had removed the oldPrivateIdentifierInInExpression
syntax kind from that predicate but not putPrivateIdentifier
s in.I rather than
PrivateIdentifier
always returning true forisExpressionNode
, it only returns true if it's in a valid position to be an 'expression'. This means the checker can useisExpressionNode
for the grammer checks.I did think about
utilities.ts
exporting a new more explicitisPrivateIdentifierInValidExpressionPosition
predicate for the checker to use instead but that didn't feel like it added much value.I am now caching the resolved symbol on the privateId itself, not the parent. As private-identifiers can only reference one member of a syntactically outer class, and not come from a different script/module. This seems safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this wasn't happening in the tests. It did happen via the language server, and wasn't getting narrowed types on hover in vscode. I've added a call to
checkPrivateIdentifierExpression
when the symbol isundefined
which fixes the issue.Is there an alternative approach than checking for
undefined
, to see if the type-check has already happened? For the case when gettingundefined
is actually a cache-hit but because of a typo in the privateIdentifier it has no symbol to resolve to - yetcheckPrivateIdentifierExpression
would keep being called.