Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 43cf208

Browse files
committedApr 9, 2019
Private Name Support in the Checker (#5)
- [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 mv private name tests together more checker tests for private names update naming and cleanup for check private names for private name support in the checker: - make names more consistent - remove unnecessary checks - add utility function to public API - other small cleanup Move getPropertyNameForUniqueESSymbol to utility for consistency with other calculation of special property names (starting with __), move the calculation of property names for unique es symbols to `utilities.ts`. private name tests strict+es6 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. error message for private names in obj literals Disallow decorating private-named properties because the spec is still in flux. Signed-off-by: Max Heiber <[email protected]>
1 parent ab4cfd5 commit 43cf208

File tree

133 files changed

+3287
-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.

133 files changed

+3287
-57
lines changed
 

‎src/compiler/binder.ts

+27-7
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
@@ -1858,6 +1864,18 @@ namespace ts {
18581864
return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode;
18591865
}
18601866

1867+
// The binder visits every node, so this is a good place to check for
1868+
// the reserved private name (there is only one)
1869+
function checkPrivateName(node: PrivateName) {
1870+
if (node.escapedText === "#constructor") {
1871+
// Report error only if there are no parse errors in file
1872+
if (!file.parseDiagnostics.length) {
1873+
file.bindDiagnostics.push(createDiagnosticForNode(node,
1874+
Diagnostics.constructor_is_a_reserved_word, declarationNameToString(node)));
1875+
}
1876+
}
1877+
}
1878+
18611879
function checkStrictModeBinaryExpression(node: BinaryExpression) {
18621880
if (inStrictMode && isLeftHandSideExpression(node.left) && isAssignmentOperator(node.operatorToken.kind)) {
18631881
// ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an
@@ -2130,6 +2148,8 @@ namespace ts {
21302148
node.flowNode = currentFlow;
21312149
}
21322150
return checkStrictModeIdentifier(<Identifier>node);
2151+
case SyntaxKind.PrivateName:
2152+
return checkPrivateName(node as PrivateName);
21332153
case SyntaxKind.PropertyAccessExpression:
21342154
case SyntaxKind.ElementAccessExpression:
21352155
if (currentFlow && isNarrowableReference(<Expression>node)) {

‎src/compiler/checker.ts

+101-30
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,
@@ -1656,8 +1657,8 @@ namespace ts {
16561657
}
16571658
}
16581659

1659-
function diagnosticName(nameArg: __String | Identifier) {
1660-
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier);
1660+
function diagnosticName(nameArg: __String | Identifier | PrivateName) {
1661+
return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier | PrivateName);
16611662
}
16621663

16631664
function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) {
@@ -2839,15 +2840,16 @@ namespace ts {
28392840
return type;
28402841
}
28412842

2842-
// A reserved member name starts with two underscores, but the third character cannot be an underscore
2843-
// or the @ symbol. A third underscore indicates an escaped form of an identifer that started
2843+
// A reserved member name starts with two underscores, but the third character cannot be an underscore,
2844+
// @, or #. A third underscore indicates an escaped form of an identifer that started
28442845
// with at least two underscores. The @ character indicates that the name is denoted by a well known ES
2845-
// Symbol instance.
2846+
// Symbol instance and the # indicates that the name is a PrivateName.
28462847
function isReservedMemberName(name: __String) {
28472848
return (name as string).charCodeAt(0) === CharacterCodes._ &&
28482849
(name as string).charCodeAt(1) === CharacterCodes._ &&
28492850
(name as string).charCodeAt(2) !== CharacterCodes._ &&
2850-
(name as string).charCodeAt(2) !== CharacterCodes.at;
2851+
(name as string).charCodeAt(2) !== CharacterCodes.at &&
2852+
(name as string).charCodeAt(2) !== CharacterCodes.hash;
28512853
}
28522854

28532855
function getNamedMembers(members: SymbolTable): Symbol[] {
@@ -6554,7 +6556,7 @@ namespace ts {
65546556
*/
65556557
function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
65566558
if (type.flags & TypeFlags.UniqueESSymbol) {
6557-
return (<UniqueESSymbolType>type).escapedName;
6559+
return getPropertyNameForUniqueESSymbol(type.symbol);
65586560
}
65596561
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
65606562
return escapeLeadingUnderscores("" + (<StringLiteralType | NumberLiteralType>type).value);
@@ -9801,6 +9803,9 @@ namespace ts {
98019803
}
98029804

98039805
function getLiteralTypeFromPropertyName(name: PropertyName) {
9806+
if (isPrivateName(name)) {
9807+
return neverType;
9808+
}
98049809
return isIdentifier(name) ? getLiteralType(unescapeLeadingUnderscores(name.escapedText)) :
98059810
getRegularTypeOfLiteralType(isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name));
98069811
}
@@ -13091,23 +13096,44 @@ namespace ts {
1309113096
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false);
1309213097
if (unmatchedProperty) {
1309313098
if (reportErrors) {
13094-
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
13095-
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
13096-
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
13097-
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
13098-
}
13099-
if (props.length === 1) {
13100-
const propName = symbolToString(unmatchedProperty);
13101-
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
13102-
if (length(unmatchedProperty.declarations)) {
13103-
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
13099+
let hasReported = false;
13100+
// give specific error in case where private names have the same description
13101+
if (
13102+
unmatchedProperty.valueDeclaration
13103+
&& isNamedDeclaration(unmatchedProperty.valueDeclaration)
13104+
&& isPrivateName(unmatchedProperty.valueDeclaration.name)
13105+
&& isClassDeclaration(source.symbol.valueDeclaration)
13106+
) {
13107+
const privateNameDescription = unmatchedProperty.valueDeclaration.name.escapedText;
13108+
const symbolTableKey = getPropertyNameForPrivateNameDescription(source.symbol, privateNameDescription);
13109+
if (symbolTableKey && !!getPropertyOfType(source, symbolTableKey)) {
13110+
reportError(
13111+
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,
13112+
diagnosticName(privateNameDescription),
13113+
diagnosticName(source.symbol.valueDeclaration.name || ("(anonymous)" as __String))
13114+
);
13115+
hasReported = true;
1310413116
}
1310513117
}
13106-
else if (props.length > 5) { // arbitrary cutoff for too-long list form
13107-
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);
13108-
}
13109-
else {
13110-
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
13118+
if (!hasReported) {
13119+
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
13120+
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
13121+
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
13122+
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
13123+
}
13124+
if (props.length === 1) {
13125+
const propName = symbolToString(unmatchedProperty);
13126+
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
13127+
if (length(unmatchedProperty.declarations)) {
13128+
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
13129+
}
13130+
}
13131+
else if (props.length > 5) { // arbitrary cutoff for too-long list form
13132+
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);
13133+
}
13134+
else {
13135+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
13136+
}
1311113137
}
1311213138
}
1311313139
return Ternary.False;
@@ -19593,6 +19619,48 @@ namespace ts {
1959319619
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1959419620
}
1959519621

19622+
function getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined {
19623+
let classWithShadowedPrivateName;
19624+
let container = getContainingClass(right);
19625+
while (container) {
19626+
const symbolTableKey = getPropertyNameForPrivateNameDescription(container.symbol, right.escapedText);
19627+
if (symbolTableKey) {
19628+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19629+
if (prop) {
19630+
if (classWithShadowedPrivateName) {
19631+
if (errorNode) {
19632+
error(
19633+
errorNode,
19634+
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,
19635+
diagnosticName(right),
19636+
diagnosticName(classWithShadowedPrivateName.name || ("(anonymous)" as __String))
19637+
);
19638+
}
19639+
return undefined;
19640+
}
19641+
return prop;
19642+
}
19643+
else {
19644+
classWithShadowedPrivateName = container;
19645+
}
19646+
}
19647+
container = getContainingClass(container);
19648+
}
19649+
// If this isn't a case of shadowing, and the lhs has a property with the same
19650+
// private name description, then there is a privacy violation
19651+
if (leftType.symbol.members) {
19652+
const symbolTableKey = getPropertyNameForPrivateNameDescription(leftType.symbol, right.escapedText);
19653+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19654+
if (prop) {
19655+
if (errorNode) {
19656+
error(right, Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_name, symbolToString(prop), typeToString(getDeclaringClass(prop)!));
19657+
}
19658+
}
19659+
}
19660+
// not found
19661+
return undefined;
19662+
}
19663+
1959619664
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier | PrivateName) {
1959719665
let propType: Type;
1959819666
const leftType = checkNonNullExpression(left);
@@ -19605,7 +19673,7 @@ namespace ts {
1960519673
return apparentType;
1960619674
}
1960719675
const assignmentKind = getAssignmentTargetKind(node);
19608-
const prop = getPropertyOfType(apparentType, right.escapedText);
19676+
const prop = isPrivateName(right) ? getPropertyForPrivateName(apparentType, leftType, right, /* errorNode */ right) : getPropertyOfType(apparentType, right.escapedText);
1960919677
if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) {
1961019678
markAliasReferenced(parentSymbol, node);
1961119679
}
@@ -22625,6 +22693,9 @@ namespace ts {
2262522693
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
2262622694
return booleanType;
2262722695
}
22696+
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateName((expr as PropertyAccessExpression).name)) {
22697+
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_name);
22698+
}
2262822699
const links = getNodeLinks(expr);
2262922700
const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol);
2263022701
if (symbol && isReadonlySymbol(symbol)) {
@@ -23936,9 +24007,6 @@ namespace ts {
2393624007
checkGrammarDecoratorsAndModifiers(node);
2393724008

2393824009
checkVariableLikeDeclaration(node);
23939-
if (node.name && isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
23940-
error(node, Diagnostics.Private_names_cannot_be_used_as_parameters);
23941-
}
2394224010
const func = getContainingFunction(node)!;
2394324011
if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) {
2394424012
if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) {
@@ -30695,6 +30763,9 @@ namespace ts {
3069530763
else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (<ParameterDeclaration>node).dotDotDotToken) {
3069630764
return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter);
3069730765
}
30766+
else if (isNamedDeclaration(node) && (flags & ModifierFlags.AccessibilityModifier) && node.name.kind === SyntaxKind.PrivateName) {
30767+
return grammarErrorOnNode(node, Diagnostics.Accessibility_modifiers_cannot_be_used_with_private_names);
30768+
}
3069830769
if (flags & ModifierFlags.Async) {
3069930770
return checkGrammarAsyncModifier(node, lastAsync!);
3070030771
}
@@ -31092,6 +31163,10 @@ namespace ts {
3109231163
return grammarErrorOnNode(prop.equalsToken!, Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment);
3109331164
}
3109431165

31166+
if (name.kind === SyntaxKind.PrivateName) {
31167+
return grammarErrorOnNode(name, Diagnostics.Private_names_are_not_allowed_outside_class_bodies);
31168+
}
31169+
3109531170
// Modifiers are never allowed on properties except for 'async' on a method declaration
3109631171
if (prop.modifiers) {
3109731172
for (const mod of prop.modifiers!) { // TODO: GH#19955
@@ -31533,10 +31608,6 @@ namespace ts {
3153331608
checkESModuleMarker(node.name);
3153431609
}
3153531610

31536-
if (isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
31537-
return grammarErrorOnNode(node.name, Diagnostics.Private_names_are_not_allowed_in_variable_declarations);
31538-
}
31539-
3154031611
const checkLetConstNames = (isLet(node) || isVarConst(node));
3154131612

3154231613
// 1. LexicalDeclaration : LetOrConst BindingList ;

0 commit comments

Comments
 (0)
Please sign in to comment.