Skip to content

Commit a71323b

Browse files
authored
Private Name Support in the Checker (#5)
* begin update checker for private names - [x] treat private names as unique: - case 1: cannot say that a variable is of a class type unless the variable points to an instance of the class - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesUnique.ts) - case 2: private names in class hierarchies do not conflict - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoConflictWhenInheriting.ts) - [x] `#constructor` is reserved - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNameConstructorReserved.ts) - check in `bindWorker`, where every node is visited - [x] Accessibility modifiers can't be used with private names - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoAccessibilityModifiers.ts) - implemented in `checkAccessibilityModifiers`, using `ModifierFlags.AccessibilityModifier` - [x] `delete #foo` not allowed - [x] Private name accesses not allowed outside of the defining class - see test: https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNameNotAccessibleOutsideDefiningClass.ts - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoDelete.ts) - implemented in `checkDeleteExpression` - [x] Do [the right thing](https://gist.github.com/mheiber/b6fc7adb426c2e1cdaceb5d7786fc630) for nested classes for private name support in the checker: - make names more consistent - remove unnecessary checks - add utility function to public API - other small cleanup for consistency with other calculation of special property names (starting with __), move the calculation of property names for unique es symbols to `utilities.ts`. Update private name tests to use 'strict' type checking and to target es6 instead of default. Makes the js output easier to read and tests more surface area with other checker features. Signed-off-by: Max Heiber <max.heiber@gmail.com>
1 parent d33c72c commit a71323b

File tree

128 files changed

+3239
-44
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+3239
-44
lines changed

src/compiler/binder.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,15 @@ namespace ts {
271271
Debug.assert(isWellKnownSymbolSyntactically(nameExpression));
272272
return getPropertyNameForKnownSymbolName(idText((<PropertyAccessExpression>nameExpression).name));
273273
}
274-
if (isPrivateName(node)) {
275-
return nodePosToString(node) as __String;
274+
if (isPrivateName(name)) {
275+
// containingClass exists because private names only allowed inside classes
276+
const containingClass = getContainingClass(name.parent);
277+
if (!containingClass) {
278+
// we're in a case where there's a private name outside a class (invalid)
279+
return undefined;
280+
}
281+
const containingClassSymbol = containingClass.symbol;
282+
return getPropertyNameForPrivateNameDescription(containingClassSymbol, name.escapedText);
276283
}
277284
return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined;
278285
}
@@ -330,6 +337,10 @@ namespace ts {
330337

331338
const isDefaultExport = hasModifier(node, ModifierFlags.Default);
332339

340+
// need this before getDeclarationName
341+
if (isNamedDeclaration(node)) {
342+
node.name.parent = node;
343+
}
333344
// The exported symbol for an export default function/class node is always named "default"
334345
const name = isDefaultExport && parent ? InternalSymbolName.Default : getDeclarationName(node);
335346

@@ -382,11 +393,6 @@ namespace ts {
382393
symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));
383394
}
384395
else if (!(includes & SymbolFlags.Variable && symbol.flags & SymbolFlags.Assignment)) {
385-
// Assignment declarations are allowed to merge with variables, no matter what other flags they have.
386-
if (isNamedDeclaration(node)) {
387-
node.name.parent = node;
388-
}
389-
390396
// Report errors every position with duplicate declaration
391397
// Report errors on previous encountered declarations
392398
let message = symbol.flags & SymbolFlags.BlockScopedVariable
@@ -1802,6 +1808,18 @@ namespace ts {
18021808
return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode;
18031809
}
18041810

1811+
// The binder visits every node, so this is a good place to check for
1812+
// the reserved private name (there is only one)
1813+
function checkPrivateName(node: PrivateName) {
1814+
if (node.escapedText === "#constructor") {
1815+
// Report error only if there are no parse errors in file
1816+
if (!file.parseDiagnostics.length) {
1817+
file.bindDiagnostics.push(createDiagnosticForNode(node,
1818+
Diagnostics.constructor_is_a_reserved_word, declarationNameToString(node)));
1819+
}
1820+
}
1821+
}
1822+
18051823
function checkStrictModeBinaryExpression(node: BinaryExpression) {
18061824
if (inStrictMode && isLeftHandSideExpression(node.left) && isAssignmentOperator(node.operatorToken.kind)) {
18071825
// ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an
@@ -2074,6 +2092,8 @@ namespace ts {
20742092
node.flowNode = currentFlow;
20752093
}
20762094
return checkStrictModeIdentifier(<Identifier>node);
2095+
case SyntaxKind.PrivateName:
2096+
return checkPrivateName(node as PrivateName);
20772097
case SyntaxKind.PropertyAccessExpression:
20782098
case SyntaxKind.ElementAccessExpression:
20792099
if (currentFlow && isNarrowableReference(<Expression>node)) {

src/compiler/checker.ts

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ namespace ts {
122122
getDeclaredTypeOfSymbol,
123123
getPropertiesOfType,
124124
getPropertyOfType: (type, name) => getPropertyOfType(type, escapeLeadingUnderscores(name)),
125+
getPropertyForPrivateName,
125126
getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, escapeLeadingUnderscores(name)),
126127
getIndexInfoOfType,
127128
getSignaturesOfType,
@@ -1572,8 +1573,8 @@ namespace ts {
15721573
}
15731574
}
15741575

1575-
function diagnosticName(nameArg: __String | Identifier) {
1576-
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
1576+
function diagnosticName(nameArg: __String | Identifier | PrivateName) {
1577+
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier | PrivateName);
15771578
}
15781579

15791580
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {
@@ -2679,15 +2680,16 @@ namespace ts {
26792680
return getUnionType(arrayFrom(typeofEQFacts.keys(), getLiteralType));
26802681
}
26812682

2682-
// A reserved member name starts with two underscores, but the third character cannot be an underscore
2683-
// or the @ symbol. A third underscore indicates an escaped form of an identifer that started
2683+
// A reserved member name starts with two underscores, but the third character cannot be an underscore,
2684+
// @, or #. A third underscore indicates an escaped form of an identifer that started
26842685
// with at least two underscores. The @ character indicates that the name is denoted by a well known ES
2685-
// Symbol instance.
2686+
// Symbol instance and the # indicates that the name is a PrivateName.
26862687
function isReservedMemberName(name: __String) {
26872688
return (name as string).charCodeAt(0) === CharacterCodes._ &&
26882689
(name as string).charCodeAt(1) === CharacterCodes._ &&
26892690
(name as string).charCodeAt(2) !== CharacterCodes._ &&
2690-
(name as string).charCodeAt(2) !== CharacterCodes.at;
2691+
(name as string).charCodeAt(2) !== CharacterCodes.at &&
2692+
(name as string).charCodeAt(2) !== CharacterCodes.hash;
26912693
}
26922694

26932695
function getNamedMembers(members: SymbolTable): Symbol[] {
@@ -6250,7 +6252,7 @@ namespace ts {
62506252
*/
62516253
function getLateBoundNameFromType(type: LiteralType | UniqueESSymbolType): __String {
62526254
if (type.flags & TypeFlags.UniqueESSymbol) {
6253-
return `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as __String;
6255+
return getPropertyNameForUniqueESSymbol(type.symbol);
62546256
}
62556257
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
62566258
return escapeLeadingUnderscores("" + (<LiteralType>type).value);
@@ -9258,7 +9260,9 @@ namespace ts {
92589260
}
92599261

92609262
function getLiteralTypeFromPropertyName(prop: Symbol, include: TypeFlags) {
9261-
if (!(getDeclarationModifierFlagsFromSymbol(prop) & ModifierFlags.NonPublicAccessibilityModifier)) {
9263+
const hasNonPublicModifier = !!(getDeclarationModifierFlagsFromSymbol(prop) & ModifierFlags.NonPublicAccessibilityModifier);
9264+
const hasPrivateName = prop.valueDeclaration && isNamedDeclaration(prop.valueDeclaration) && isPrivateName(prop.valueDeclaration.name);
9265+
if (!hasNonPublicModifier && !hasPrivateName) {
92629266
let type = getLateBoundSymbol(prop).nameType;
92639267
if (!type && !isKnownSymbol(prop)) {
92649268
const name = prop.valueDeclaration && getNameOfDeclaration(prop.valueDeclaration);
@@ -12205,7 +12209,28 @@ namespace ts {
1220512209
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties);
1220612210
if (unmatchedProperty) {
1220712211
if (reportErrors) {
12208-
reportError(Diagnostics.Property_0_is_missing_in_type_1, symbolToString(unmatchedProperty), typeToString(source));
12212+
let hasReported = false;
12213+
// give specific error in case where private names have the same description
12214+
if (
12215+
unmatchedProperty.valueDeclaration
12216+
&& isNamedDeclaration(unmatchedProperty.valueDeclaration)
12217+
&& isPrivateName(unmatchedProperty.valueDeclaration.name)
12218+
&& isClassDeclaration(source.symbol.valueDeclaration)
12219+
) {
12220+
const privateNameDescription = unmatchedProperty.valueDeclaration.name.escapedText;
12221+
const symbolTableKey = getPropertyNameForPrivateNameDescription(source.symbol, privateNameDescription);
12222+
if (symbolTableKey && !!getPropertyOfType(source, symbolTableKey)) {
12223+
reportError(
12224+
Diagnostics.Property_0_is_missing_in_type_1_While_type_1_has_a_private_member_with_the_same_spelling_its_declaration_and_accessibility_are_distinct,
12225+
diagnosticName(privateNameDescription),
12226+
diagnosticName(source.symbol.valueDeclaration.name || ("(anonymous)" as __String))
12227+
);
12228+
hasReported = true;
12229+
}
12230+
}
12231+
if (!hasReported) {
12232+
reportError(Diagnostics.Property_0_is_missing_in_type_1, symbolToString(unmatchedProperty), typeToString(source));
12233+
}
1220912234
}
1221012235
return Ternary.False;
1221112236
}
@@ -18423,6 +18448,48 @@ namespace ts {
1842318448
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1842418449
}
1842518450

18451+
function getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined {
18452+
let classWithShadowedPrivateName;
18453+
let container = getContainingClass(right);
18454+
while (container) {
18455+
const symbolTableKey = getPropertyNameForPrivateNameDescription(container.symbol, right.escapedText);
18456+
if (symbolTableKey) {
18457+
const prop = getPropertyOfType(apparentType, symbolTableKey);
18458+
if (prop) {
18459+
if (classWithShadowedPrivateName) {
18460+
if (errorNode) {
18461+
error(
18462+
errorNode,
18463+
Diagnostics.This_usage_of_0_refers_to_the_private_member_declared_in_its_enclosing_class_While_type_1_has_a_private_member_with_the_same_spelling_its_declaration_and_accessibility_are_distinct,
18464+
diagnosticName(right),
18465+
diagnosticName(classWithShadowedPrivateName.name || ("(anonymous)" as __String))
18466+
);
18467+
}
18468+
return undefined;
18469+
}
18470+
return prop;
18471+
}
18472+
else {
18473+
classWithShadowedPrivateName = container;
18474+
}
18475+
}
18476+
container = getContainingClass(container);
18477+
}
18478+
// If this isn't a case of shadowing, and the lhs has a property with the same
18479+
// private name description, then there is a privacy violation
18480+
if (leftType.symbol.members) {
18481+
const symbolTableKey = getPropertyNameForPrivateNameDescription(leftType.symbol, right.escapedText);
18482+
const prop = getPropertyOfType(apparentType, symbolTableKey);
18483+
if (prop) {
18484+
if (errorNode) {
18485+
error(right, Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_name, symbolToString(prop), typeToString(getDeclaringClass(prop)!));
18486+
}
18487+
}
18488+
}
18489+
// not found
18490+
return undefined;
18491+
}
18492+
1842618493
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier | PrivateName) {
1842718494
let propType: Type;
1842818495
const leftType = checkNonNullExpression(left);
@@ -18435,7 +18502,7 @@ namespace ts {
1843518502
return apparentType;
1843618503
}
1843718504
const assignmentKind = getAssignmentTargetKind(node);
18438-
const prop = getPropertyOfType(apparentType, right.escapedText);
18505+
const prop = isPrivateName(right) ? getPropertyForPrivateName(apparentType, leftType, right, /* errorNode */ right) : getPropertyOfType(apparentType, right.escapedText);
1843918506
if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) {
1844018507
markAliasReferenced(parentSymbol, node);
1844118508
}
@@ -21376,6 +21443,9 @@ namespace ts {
2137621443
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
2137721444
return booleanType;
2137821445
}
21446+
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateName((expr as PropertyAccessExpression).name)) {
21447+
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_name);
21448+
}
2137921449
const links = getNodeLinks(expr);
2138021450
const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol);
2138121451
if (symbol && isReadonlySymbol(symbol)) {
@@ -22529,9 +22599,6 @@ namespace ts {
2252922599
checkGrammarDecoratorsAndModifiers(node);
2253022600

2253122601
checkVariableLikeDeclaration(node);
22532-
if (node.name && isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
22533-
error(node, Diagnostics.Private_names_cannot_be_used_as_parameters);
22534-
}
2253522602
const func = getContainingFunction(node)!;
2253622603
if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) {
2253722604
if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) {
@@ -29210,6 +29277,9 @@ namespace ts {
2921029277
else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (<ParameterDeclaration>node).dotDotDotToken) {
2921129278
return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter);
2921229279
}
29280+
else if (isNamedDeclaration(node) && (flags & ModifierFlags.AccessibilityModifier) && node.name.kind === SyntaxKind.PrivateName) {
29281+
return grammarErrorOnNode(node, Diagnostics.Accessibility_modifiers_cannot_be_used_with_private_names);
29282+
}
2921329283
if (flags & ModifierFlags.Async) {
2921429284
return checkGrammarAsyncModifier(node, lastAsync!);
2921529285
}
@@ -29611,6 +29681,10 @@ namespace ts {
2961129681
return grammarErrorOnNode(prop.equalsToken!, Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment);
2961229682
}
2961329683

29684+
if (name.kind === SyntaxKind.PrivateName) {
29685+
return grammarErrorOnNode(name, Diagnostics.Private_names_are_not_allowed_outside_class_bodies);
29686+
}
29687+
2961429688
// Modifiers are never allowed on properties except for 'async' on a method declaration
2961529689
if (prop.modifiers) {
2961629690
for (const mod of prop.modifiers!) { // TODO: GH#19955
@@ -30035,10 +30109,6 @@ namespace ts {
3003530109
checkESModuleMarker(node.name);
3003630110
}
3003730111

30038-
if (isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
30039-
return grammarErrorOnNode(node.name, Diagnostics.Private_names_are_not_allowed_in_variable_declarations);
30040-
}
30041-
3004230112
const checkLetConstNames = (isLet(node) || isVarConst(node));
3004330113

3004430114
// 1. LexicalDeclaration : LetOrConst BindingList ;

src/compiler/diagnosticMessages.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4299,14 +4299,34 @@
42994299
"category": "Error",
43004300
"code": 18003
43014301
},
4302-
"Private names are not allowed in variable declarations.": {
4302+
"Accessibility modifiers cannot be used with private names.": {
43034303
"category": "Error",
43044304
"code": 18004
43054305
},
4306-
"Private names cannot be used as parameters": {
4306+
"The operand of a delete operator cannot be a private name.": {
43074307
"category": "Error",
43084308
"code": 18005
43094309
},
4310+
"'#constructor' is a reserved word.": {
4311+
"category": "Error",
4312+
"code": 18006
4313+
},
4314+
"Property '{0}' is not accessible outside class '{1}' because it has a private name.": {
4315+
"category": "Error",
4316+
"code": 18007
4317+
},
4318+
"This usage of '{0}' refers to the private member declared in its enclosing class. While type '{1}' has a private member with the same spelling, its declaration and accessibility are distinct.": {
4319+
"category": "Error",
4320+
"code": 18008
4321+
},
4322+
"Property '{0}' is missing in type '{1}'. While type '{1}' has a private member with the same spelling, its declaration and accessibility are distinct.": {
4323+
"category": "Error",
4324+
"code": 18009
4325+
},
4326+
"Private names are not allowed outside class bodies.": {
4327+
"category": "Error",
4328+
"code": 18010
4329+
},
43104330

43114331
"File is a CommonJS module; it may be converted to an ES6 module.": {
43124332
"category": "Suggestion",

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,7 @@ namespace ts {
30263026
getDeclaredTypeOfSymbol(symbol: Symbol): Type;
30273027
getPropertiesOfType(type: Type): Symbol[];
30283028
getPropertyOfType(type: Type, propertyName: string): Symbol | undefined;
3029+
getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined;
30293030
/* @internal */ getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined;
30303031
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
30313032
getSignaturesOfType(type: Type, kind: SignatureKind): ReadonlyArray<Signature>;

src/compiler/utilities.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2728,10 +2728,18 @@ namespace ts {
27282728
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateName ? node.escapedText : escapeLeadingUnderscores(node.text);
27292729
}
27302730

2731+
export function getPropertyNameForUniqueESSymbol(symbol: Symbol): __String {
2732+
return `__@${getSymbolId(symbol)}@${symbol.escapedName}` as __String;
2733+
}
2734+
27312735
export function getPropertyNameForKnownSymbolName(symbolName: string): __String {
27322736
return "__@" + symbolName as __String;
27332737
}
27342738

2739+
export function getPropertyNameForPrivateNameDescription(containingClassSymbol: Symbol, description: __String): __String {
2740+
return `__#${getSymbolId(containingClassSymbol)}@${description}` as __String;
2741+
}
2742+
27352743
export function isKnownSymbol(symbol: Symbol): boolean {
27362744
return startsWith(symbol.escapedName as string, "__@");
27372745
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,6 +1876,7 @@ declare namespace ts {
18761876
getDeclaredTypeOfSymbol(symbol: Symbol): Type;
18771877
getPropertiesOfType(type: Type): Symbol[];
18781878
getPropertyOfType(type: Type, propertyName: string): Symbol | undefined;
1879+
getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined;
18791880
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
18801881
getSignaturesOfType(type: Type, kind: SignatureKind): ReadonlyArray<Signature>;
18811882
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;

0 commit comments

Comments
 (0)