Skip to content

Commit 9f10880

Browse files
mheiberJoseph Watts
authored and
Joseph Watts
committed
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 <[email protected]>
1 parent 0e964ae commit 9f10880

File tree

128 files changed

+3192
-57
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

+3192
-57
lines changed

src/compiler/binder.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,15 @@ namespace ts {
278278
Debug.assert(isWellKnownSymbolSyntactically(nameExpression));
279279
return getPropertyNameForKnownSymbolName(idText((<PropertyAccessExpression>nameExpression).name));
280280
}
281-
if (isPrivateName(node)) {
282-
return nodePosToString(node) as __String;
281+
if (isPrivateName(name)) {
282+
// containingClass exists because private names only allowed inside classes
283+
const containingClass = getContainingClass(name.parent);
284+
if (!containingClass) {
285+
// we're in a case where there's a private name outside a class (invalid)
286+
return undefined;
287+
}
288+
const containingClassSymbol = containingClass.symbol;
289+
return getPropertyNameForPrivateNameDescription(containingClassSymbol, name.escapedText);
283290
}
284291
return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined;
285292
}
@@ -337,6 +344,10 @@ namespace ts {
337344

338345
const isDefaultExport = hasModifier(node, ModifierFlags.Default);
339346

347+
// need this before getDeclarationName
348+
if (isNamedDeclaration(node)) {
349+
node.name.parent = node;
350+
}
340351
// The exported symbol for an export default function/class node is always named "default"
341352
const name = isDefaultExport && parent ? InternalSymbolName.Default : getDeclarationName(node);
342353

@@ -389,11 +400,6 @@ namespace ts {
389400
symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));
390401
}
391402
else if (!(includes & SymbolFlags.Variable && symbol.flags & SymbolFlags.Assignment)) {
392-
// Assignment declarations are allowed to merge with variables, no matter what other flags they have.
393-
if (isNamedDeclaration(node)) {
394-
node.name.parent = node;
395-
}
396-
397403
// Report errors every position with duplicate declaration
398404
// Report errors on previous encountered declarations
399405
let message = symbol.flags & SymbolFlags.BlockScopedVariable
@@ -1845,6 +1851,18 @@ namespace ts {
18451851
return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode;
18461852
}
18471853

1854+
// The binder visits every node, so this is a good place to check for
1855+
// the reserved private name (there is only one)
1856+
function checkPrivateName(node: PrivateName) {
1857+
if (node.escapedText === "#constructor") {
1858+
// Report error only if there are no parse errors in file
1859+
if (!file.parseDiagnostics.length) {
1860+
file.bindDiagnostics.push(createDiagnosticForNode(node,
1861+
Diagnostics.constructor_is_a_reserved_word, declarationNameToString(node)));
1862+
}
1863+
}
1864+
}
1865+
18481866
function checkStrictModeBinaryExpression(node: BinaryExpression) {
18491867
if (inStrictMode && isLeftHandSideExpression(node.left) && isAssignmentOperator(node.operatorToken.kind)) {
18501868
// ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an
@@ -2117,6 +2135,8 @@ namespace ts {
21172135
node.flowNode = currentFlow;
21182136
}
21192137
return checkStrictModeIdentifier(<Identifier>node);
2138+
case SyntaxKind.PrivateName:
2139+
return checkPrivateName(node as PrivateName);
21202140
case SyntaxKind.PropertyAccessExpression:
21212141
case SyntaxKind.ElementAccessExpression:
21222142
if (currentFlow && isNarrowableReference(<Expression>node)) {

src/compiler/checker.ts

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ namespace ts {
132132
getDeclaredTypeOfSymbol,
133133
getPropertiesOfType,
134134
getPropertyOfType: (type, name) => getPropertyOfType(type, escapeLeadingUnderscores(name)),
135+
getPropertyForPrivateName,
135136
getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, escapeLeadingUnderscores(name)),
136137
getIndexInfoOfType,
137138
getSignaturesOfType,
@@ -1606,8 +1607,8 @@ namespace ts {
16061607
}
16071608
}
16081609

1609-
function diagnosticName(nameArg: __String | Identifier) {
1610-
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
1610+
function diagnosticName(nameArg: __String | Identifier | PrivateName) {
1611+
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier | PrivateName);
16111612
}
16121613

16131614
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {
@@ -2796,15 +2797,16 @@ namespace ts {
27962797
return type;
27972798
}
27982799

2799-
// A reserved member name starts with two underscores, but the third character cannot be an underscore
2800-
// or the @ symbol. A third underscore indicates an escaped form of an identifer that started
2800+
// A reserved member name starts with two underscores, but the third character cannot be an underscore,
2801+
// @, or #. A third underscore indicates an escaped form of an identifer that started
28012802
// with at least two underscores. The @ character indicates that the name is denoted by a well known ES
2802-
// Symbol instance.
2803+
// Symbol instance and the # indicates that the name is a PrivateName.
28032804
function isReservedMemberName(name: __String) {
28042805
return (name as string).charCodeAt(0) === CharacterCodes._ &&
28052806
(name as string).charCodeAt(1) === CharacterCodes._ &&
28062807
(name as string).charCodeAt(2) !== CharacterCodes._ &&
2807-
(name as string).charCodeAt(2) !== CharacterCodes.at;
2808+
(name as string).charCodeAt(2) !== CharacterCodes.at &&
2809+
(name as string).charCodeAt(2) !== CharacterCodes.hash;
28082810
}
28092811

28102812
function getNamedMembers(members: SymbolTable): Symbol[] {
@@ -6511,7 +6513,7 @@ namespace ts {
65116513
*/
65126514
function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
65136515
if (type.flags & TypeFlags.UniqueESSymbol) {
6514-
return (<UniqueESSymbolType>type).escapedName;
6516+
return getPropertyNameForUniqueESSymbol(type.symbol);
65156517
}
65166518
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
65176519
return escapeLeadingUnderscores("" + (<StringLiteralType | NumberLiteralType>type).value);
@@ -9694,6 +9696,9 @@ namespace ts {
96949696
}
96959697

96969698
function getLiteralTypeFromPropertyName(name: PropertyName) {
9699+
if (isPrivateName(name)) {
9700+
return neverType;
9701+
}
96979702
return isIdentifier(name) ? getLiteralType(unescapeLeadingUnderscores(name.escapedText)) :
96989703
getRegularTypeOfLiteralType(isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name));
96999704
}
@@ -12929,23 +12934,44 @@ namespace ts {
1292912934
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false);
1293012935
if (unmatchedProperty) {
1293112936
if (reportErrors) {
12932-
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
12933-
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
12934-
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
12935-
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
12936-
}
12937-
if (props.length === 1) {
12938-
const propName = symbolToString(unmatchedProperty);
12939-
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
12940-
if (length(unmatchedProperty.declarations)) {
12941-
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
12937+
let hasReported = false;
12938+
// give specific error in case where private names have the same description
12939+
if (
12940+
unmatchedProperty.valueDeclaration
12941+
&& isNamedDeclaration(unmatchedProperty.valueDeclaration)
12942+
&& isPrivateName(unmatchedProperty.valueDeclaration.name)
12943+
&& isClassDeclaration(source.symbol.valueDeclaration)
12944+
) {
12945+
const privateNameDescription = unmatchedProperty.valueDeclaration.name.escapedText;
12946+
const symbolTableKey = getPropertyNameForPrivateNameDescription(source.symbol, privateNameDescription);
12947+
if (symbolTableKey && !!getPropertyOfType(source, symbolTableKey)) {
12948+
reportError(
12949+
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,
12950+
diagnosticName(privateNameDescription),
12951+
diagnosticName(source.symbol.valueDeclaration.name || ("(anonymous)" as __String))
12952+
);
12953+
hasReported = true;
1294212954
}
1294312955
}
12944-
else if (props.length > 5) { // arbitrary cutoff for too-long list form
12945-
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4);
12946-
}
12947-
else {
12948-
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
12956+
if (!hasReported) {
12957+
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
12958+
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
12959+
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
12960+
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
12961+
}
12962+
if (props.length === 1) {
12963+
const propName = symbolToString(unmatchedProperty);
12964+
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
12965+
if (length(unmatchedProperty.declarations)) {
12966+
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
12967+
}
12968+
}
12969+
else if (props.length > 5) { // arbitrary cutoff for too-long list form
12970+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4);
12971+
}
12972+
else {
12973+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
12974+
}
1294912975
}
1295012976
}
1295112977
return Ternary.False;
@@ -19383,6 +19409,48 @@ namespace ts {
1938319409
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1938419410
}
1938519411

19412+
function getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined {
19413+
let classWithShadowedPrivateName;
19414+
let container = getContainingClass(right);
19415+
while (container) {
19416+
const symbolTableKey = getPropertyNameForPrivateNameDescription(container.symbol, right.escapedText);
19417+
if (symbolTableKey) {
19418+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19419+
if (prop) {
19420+
if (classWithShadowedPrivateName) {
19421+
if (errorNode) {
19422+
error(
19423+
errorNode,
19424+
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,
19425+
diagnosticName(right),
19426+
diagnosticName(classWithShadowedPrivateName.name || ("(anonymous)" as __String))
19427+
);
19428+
}
19429+
return undefined;
19430+
}
19431+
return prop;
19432+
}
19433+
else {
19434+
classWithShadowedPrivateName = container;
19435+
}
19436+
}
19437+
container = getContainingClass(container);
19438+
}
19439+
// If this isn't a case of shadowing, and the lhs has a property with the same
19440+
// private name description, then there is a privacy violation
19441+
if (leftType.symbol.members) {
19442+
const symbolTableKey = getPropertyNameForPrivateNameDescription(leftType.symbol, right.escapedText);
19443+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19444+
if (prop) {
19445+
if (errorNode) {
19446+
error(right, Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_name, symbolToString(prop), typeToString(getDeclaringClass(prop)!));
19447+
}
19448+
}
19449+
}
19450+
// not found
19451+
return undefined;
19452+
}
19453+
1938619454
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier | PrivateName) {
1938719455
let propType: Type;
1938819456
const leftType = checkNonNullExpression(left);
@@ -19395,7 +19463,7 @@ namespace ts {
1939519463
return apparentType;
1939619464
}
1939719465
const assignmentKind = getAssignmentTargetKind(node);
19398-
const prop = getPropertyOfType(apparentType, right.escapedText);
19466+
const prop = isPrivateName(right) ? getPropertyForPrivateName(apparentType, leftType, right, /* errorNode */ right) : getPropertyOfType(apparentType, right.escapedText);
1939919467
if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) {
1940019468
markAliasReferenced(parentSymbol, node);
1940119469
}
@@ -22384,6 +22452,9 @@ namespace ts {
2238422452
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
2238522453
return booleanType;
2238622454
}
22455+
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateName((expr as PropertyAccessExpression).name)) {
22456+
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_name);
22457+
}
2238722458
const links = getNodeLinks(expr);
2238822459
const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol);
2238922460
if (symbol && isReadonlySymbol(symbol)) {
@@ -23592,9 +23663,6 @@ namespace ts {
2359223663
checkGrammarDecoratorsAndModifiers(node);
2359323664

2359423665
checkVariableLikeDeclaration(node);
23595-
if (node.name && isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
23596-
error(node, Diagnostics.Private_names_cannot_be_used_as_parameters);
23597-
}
2359823666
const func = getContainingFunction(node)!;
2359923667
if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) {
2360023668
if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) {
@@ -30350,6 +30418,9 @@ namespace ts {
3035030418
else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (<ParameterDeclaration>node).dotDotDotToken) {
3035130419
return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter);
3035230420
}
30421+
else if (isNamedDeclaration(node) && (flags & ModifierFlags.AccessibilityModifier) && node.name.kind === SyntaxKind.PrivateName) {
30422+
return grammarErrorOnNode(node, Diagnostics.Accessibility_modifiers_cannot_be_used_with_private_names);
30423+
}
3035330424
if (flags & ModifierFlags.Async) {
3035430425
return checkGrammarAsyncModifier(node, lastAsync!);
3035530426
}
@@ -30747,6 +30818,10 @@ namespace ts {
3074730818
return grammarErrorOnNode(prop.equalsToken!, Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment);
3074830819
}
3074930820

30821+
if (name.kind === SyntaxKind.PrivateName) {
30822+
return grammarErrorOnNode(name, Diagnostics.Private_names_are_not_allowed_outside_class_bodies);
30823+
}
30824+
3075030825
// Modifiers are never allowed on properties except for 'async' on a method declaration
3075130826
if (prop.modifiers) {
3075230827
for (const mod of prop.modifiers!) { // TODO: GH#19955
@@ -31188,10 +31263,6 @@ namespace ts {
3118831263
checkESModuleMarker(node.name);
3118931264
}
3119031265

31191-
if (isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
31192-
return grammarErrorOnNode(node.name, Diagnostics.Private_names_are_not_allowed_in_variable_declarations);
31193-
}
31194-
3119531266
const checkLetConstNames = (isLet(node) || isVarConst(node));
3119631267

3119731268
// 1. LexicalDeclaration : LetOrConst BindingList ;

0 commit comments

Comments
 (0)