Skip to content

Commit 45bb73d

Browse files
committed
basic type check and narrow
Signed-off-by: Ashley Claymore <[email protected]>
1 parent 1a71fc2 commit 45bb73d

File tree

8 files changed

+377
-221
lines changed

8 files changed

+377
-221
lines changed

src/compiler/binder.ts

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

src/compiler/checker.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22750,6 +22750,22 @@ namespace ts {
2275022750
return type;
2275122751
}
2275222752

22753+
function narrowTypeByPrivateIdentifierInInExpression(type: Type, expr: PrivateIdentifierInInExpression, assumeTrue: boolean): Type {
22754+
const target = getReferenceCandidate(expr.expression);
22755+
if (!isMatchingReference(reference, target)) {
22756+
return type;
22757+
}
22758+
22759+
const privateId = expr.name;
22760+
const klass = lookupClassForPrivateIdentifierDeclaration(privateId);
22761+
if (klass === undefined) {
22762+
return type;
22763+
}
22764+
22765+
const classTypeForPrivateField = getTypeOfSymbolAtLocation(getSymbolOfNode(klass), klass);
22766+
return getNarrowedType(type, classTypeForPrivateField, assumeTrue, isTypeDerivedFrom);
22767+
}
22768+
2275322769
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
2275422770
// We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows:
2275522771
// When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch.
@@ -23164,6 +23180,8 @@ namespace ts {
2316423180
return narrowType(type, (<ParenthesizedExpression | NonNullExpression>expr).expression, assumeTrue);
2316523181
case SyntaxKind.BinaryExpression:
2316623182
return narrowTypeByBinaryExpression(type, <BinaryExpression>expr, assumeTrue);
23183+
case SyntaxKind.PrivateIdentifierInInExpression:
23184+
return narrowTypeByPrivateIdentifierInInExpression(type, <PrivateIdentifierInInExpression>expr, assumeTrue);
2316723185
case SyntaxKind.PrefixUnaryExpression:
2316823186
if ((<PrefixUnaryExpression>expr).operator === SyntaxKind.ExclamationToken) {
2316923187
return narrowType(type, (<PrefixUnaryExpression>expr).operand, !assumeTrue);
@@ -26265,6 +26283,17 @@ namespace ts {
2626526283
}
2626626284
}
2626726285

26286+
function lookupClassForPrivateIdentifierDeclaration(id: PrivateIdentifier): ClassLikeDeclaration | undefined {
26287+
for (let containingClass = getContainingClass(id); !!containingClass; containingClass = getContainingClass(containingClass)) {
26288+
const { symbol } = containingClass;
26289+
const name = getSymbolNameForPrivateIdentifier(symbol, id.escapedText);
26290+
const prop = (symbol.members && symbol.members.get(name)) || (symbol.exports && symbol.exports.get(name));
26291+
if (prop) {
26292+
return containingClass;
26293+
}
26294+
}
26295+
}
26296+
2626826297
function getPrivateIdentifierPropertyOfType(leftType: Type, lexicallyScopedIdentifier: Symbol): Symbol | undefined {
2626926298
return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName);
2627026299
}
@@ -30445,6 +30474,11 @@ namespace ts {
3044530474
isTypeAssignableToKind(leftType, TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.TypeParameter))) {
3044630475
error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol);
3044730476
}
30477+
checkInExpressionRHS(right, rightType);
30478+
return booleanType;
30479+
}
30480+
30481+
function checkInExpressionRHS(right: Expression, rightType: Type) {
3044830482
const rightTypeConstraint = getConstraintOfType(rightType);
3044930483
if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive) ||
3045030484
rightTypeConstraint && (
@@ -30454,7 +30488,6 @@ namespace ts {
3045430488
) {
3045530489
error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_not_be_a_primitive);
3045630490
}
30457-
return booleanType;
3045830491
}
3045930492

3046030493
function checkObjectLiteralAssignment(node: ObjectLiteralExpression, sourceType: Type, rightIsThis?: boolean): Type {
@@ -30691,6 +30724,26 @@ namespace ts {
3069130724
return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target);
3069230725
}
3069330726

30727+
function checkPrivateIdentifierInInExpression(node: PrivateIdentifierInInExpression, checkMode?: CheckMode) {
30728+
const privateId = node.name;
30729+
const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(privateId.escapedText, privateId);
30730+
if (lexicallyScopedSymbol === undefined) {
30731+
// TODO(aclaymore): use better error message - we might be in a class but with no matching privateField
30732+
error(privateId, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies);
30733+
return anyType;
30734+
}
30735+
30736+
const exp = node.expression;
30737+
let rightType = checkExpression(exp, checkMode);
30738+
if (rightType === silentNeverType) {
30739+
return silentNeverType;
30740+
}
30741+
rightType = checkNonNullType(rightType, exp);
30742+
// TODO(aclaymore): Do RHS rules matching 'in' rules? (e.g. throw on null)
30743+
checkInExpressionRHS(exp, rightType);
30744+
return booleanType;
30745+
}
30746+
3069430747
const enum CheckBinaryExpressionState {
3069530748
MaybeCheckLeft,
3069630749
CheckRight,
@@ -31817,6 +31870,8 @@ namespace ts {
3181731870
return checkPostfixUnaryExpression(<PostfixUnaryExpression>node);
3181831871
case SyntaxKind.BinaryExpression:
3181931872
return checkBinaryExpression(<BinaryExpression>node, checkMode);
31873+
case SyntaxKind.PrivateIdentifierInInExpression:
31874+
return checkPrivateIdentifierInInExpression(<PrivateIdentifierInInExpression>node, checkMode);
3182031875
case SyntaxKind.ConditionalExpression:
3182131876
return checkConditionalExpression(<ConditionalExpression>node, checkMode);
3182231877
case SyntaxKind.SpreadElement:

src/compiler/parser.ts

Lines changed: 4 additions & 2 deletions
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, (<CommaListExpression>node).elements);
445+
case SyntaxKind.PrivateIdentifierInInExpression:
446+
return visitNode(cbNode, (<PrivateIdentifierInInExpression>node).name) ||
447+
visitNode(cbNode, (<PrivateIdentifierInInExpression>node).expression);
445448

446449
case SyntaxKind.JsxElement:
447450
return visitNode(cbNode, (<JsxElement>node).openingElement) ||
@@ -4393,8 +4396,7 @@ namespace ts {
43934396
return createMissingNode(SyntaxKind.InKeyword, /*reportAtCurrentPosition*/ true, Diagnostics._0_expected, tokenToString(SyntaxKind.InKeyword));
43944397
}
43954398
nextToken();
4396-
// TODO(aclaymore): LHS can be a binary expression of higher precedence
4397-
// const exp = parseUnaryExpressionOrHigher();
4399+
// TODO(aclaymore) verify precedence is correct
43984400
const exp = parseBinaryExpressionOrHigher(OperatorPrecedence.Relational);
43994401
return finishNode(factory.createPrivateIdentifierInInExpression(id, exp), pos);
44004402
}
Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,97 @@
1-
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(18,13): error TS1005: 'in' expected.
2-
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(21,14): error TS2304: Cannot find name '#p1'.
3-
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(21,14): error TS18016: Private identifiers are not allowed outside class bodies.
1+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(14,26): error TS2571: Object is of type 'unknown'.
2+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(16,19): error TS18016: Private identifiers are not allowed outside class bodies.
3+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(18,23): error TS1005: 'in' expected.
4+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(20,14): error TS2304: Cannot find name '#p1'.
5+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(20,14): error TS18016: Private identifiers are not allowed outside class bodies.
6+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(22,23): error TS2407: The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type 'boolean'.
7+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(34,14): error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
8+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(34,21): error TS2361: The right-hand side of an 'in' expression must not be a primitive.
9+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(36,14): error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
10+
tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts(62,12): error TS18016: Private identifiers are not allowed outside class bodies.
411

512

6-
==== tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts (3 errors) ====
13+
==== tests/cases/conformance/classes/members/privateNames/privateNameInInExpression.ts (10 errors) ====
714
// TODO(aclaymore) split up into seperate cases
815

916
class Foo {
1017
#p1 = 1;
11-
m1(v: {}) {
12-
#p1 in v; // Good
13-
}
14-
m2(v: any) {
15-
#p1 in v.p1.p2; // Good
16-
}
17-
m3(v: unknown) {
18-
#p1 in v; // Bad - RHS of in must be object type or any
19-
}
20-
m4(v: any) {
21-
#p2 in v; // Bad - Invalid private id
22-
}
23-
m5(v: any) {
24-
(#p1) in v; // Bad - private id is not an expression on it's own
25-
18+
basics(v: any) {
19+
const a = #p1 in v; // Good - a is boolean
20+
21+
const b = #p1 in v.p1.p2; // Good - b is boolean
22+
23+
const c = #p1 in (v as {}); // Good - c is boolean
24+
25+
const d = #p1 in (v as Foo); // Good d is boolean (not true)
26+
27+
const e = #p1 in (v as unknown); // Bad - RHS of in must be object type or any
28+
~~~~~~~~~~~~~~
29+
!!! error TS2571: Object is of type 'unknown'.
30+
31+
const f = #p2 in v; // Bad - Invalid privateID
32+
~~~
33+
!!! error TS18016: Private identifiers are not allowed outside class bodies.
34+
35+
const g = (#p1) in v; // Bad - private id is not an expression on it's own
36+
2637
!!! error TS1005: 'in' expected.
27-
}
28-
m6(v: any) {
38+
2939
for (#p1 in v) { /* no-op */ } // Bad - 'in' not allowed
3040
~~~
3141
!!! error TS2304: Cannot find name '#p1'.
3242
~~~
3343
!!! error TS18016: Private identifiers are not allowed outside class bodies.
34-
}
35-
m7(v: any) {
36-
for (let x in #p1 in v as any) { /* no-op */ } // Good - weird but valid
37-
}
38-
m8(v: any) {
44+
3945
for (let x in #p1 in v) { /* no-op */ } // Bad - rhs of in should be a object/any
46+
~~~~~~~~
47+
!!! error TS2407: The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type 'boolean'.
48+
49+
for (let x in #p1 in v as any) { /* no-op */ } // Good - weird but valid
50+
4051
}
41-
m9(v: any) {
52+
precedence(v: any) {
4253
// '==' has lower precedence than 'in'
4354
// '<' has same prededence than 'in'
4455
// '<<' has higher prededence than 'in'
4556

4657
v == #p1 in v == v; // Good precidence: ((v == (#p1 in v)) == v)
4758

4859
v << #p1 in v << v; // Good precidence: (v << (#p1 in (v << v)))
60+
~~~~~~~~~~~~~
61+
!!! error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
62+
~~~~~~
63+
!!! error TS2361: The right-hand side of an 'in' expression must not be a primitive.
4964

5065
v << #p1 in v == v; // Good precidence: ((v << (#p1 in v)) == v)
66+
~~~~~~~~
67+
!!! error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
5168

5269
v == #p1 in v < v; // Good precidence: (v == ((#p1 in v) < v))
5370

5471
#p1 in v && #p1 in v; // Good precidence: ((#p1 in v) && (#p1 in v))
5572
}
56-
m10() {
57-
class Bar {
58-
m10(v: any) {
59-
#p1 in v; // Good: access parent class
73+
flow(v: unknown) {
74+
if (typeof v === 'object' && v !== null) {
75+
if (#p1 in v) {
76+
const y1 = v; // good y1 is typeof Foo
77+
} else {
78+
const y2 = v; // y2 is not typeof Foo
79+
}
80+
}
81+
82+
class Nested {
83+
m(v: any) {
84+
if (#p1 in v) {
85+
const y1 = v; // Good y1 if typeof Foo
86+
}
6087
}
6188
}
6289
}
6390
}
6491

6592
function error(v: Foo) {
66-
return #p1 in v; // Bad: outside of class
93+
return #p1 in v; // Bad - outside of class
94+
~~~
95+
!!! error TS18016: Private identifiers are not allowed outside class bodies.
6796
}
6897

0 commit comments

Comments
 (0)