Skip to content

Commit d4f16ba

Browse files
committed
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 27a3ff4 commit d4f16ba

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,
@@ -1605,8 +1606,8 @@ namespace ts {
16051606
}
16061607
}
16071608

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

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

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

28092811
function getNamedMembers(members: SymbolTable): Symbol[] {
@@ -6510,7 +6512,7 @@ namespace ts {
65106512
*/
65116513
function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
65126514
if (type.flags & TypeFlags.UniqueESSymbol) {
6513-
return (<UniqueESSymbolType>type).escapedName;
6515+
return getPropertyNameForUniqueESSymbol(type.symbol);
65146516
}
65156517
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
65166518
return escapeLeadingUnderscores("" + (<StringLiteralType | NumberLiteralType>type).value);
@@ -9733,6 +9735,9 @@ namespace ts {
97339735
}
97349736

97359737
function getLiteralTypeFromPropertyName(name: PropertyName) {
9738+
if (isPrivateName(name)) {
9739+
return neverType;
9740+
}
97369741
return isIdentifier(name) ? getLiteralType(unescapeLeadingUnderscores(name.escapedText)) :
97379742
getRegularTypeOfLiteralType(isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name));
97389743
}
@@ -13008,23 +13013,44 @@ namespace ts {
1300813013
const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false);
1300913014
if (unmatchedProperty) {
1301013015
if (reportErrors) {
13011-
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
13012-
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
13013-
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
13014-
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
13015-
}
13016-
if (props.length === 1) {
13017-
const propName = symbolToString(unmatchedProperty);
13018-
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
13019-
if (length(unmatchedProperty.declarations)) {
13020-
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
13016+
let hasReported = false;
13017+
// give specific error in case where private names have the same description
13018+
if (
13019+
unmatchedProperty.valueDeclaration
13020+
&& isNamedDeclaration(unmatchedProperty.valueDeclaration)
13021+
&& isPrivateName(unmatchedProperty.valueDeclaration.name)
13022+
&& isClassDeclaration(source.symbol.valueDeclaration)
13023+
) {
13024+
const privateNameDescription = unmatchedProperty.valueDeclaration.name.escapedText;
13025+
const symbolTableKey = getPropertyNameForPrivateNameDescription(source.symbol, privateNameDescription);
13026+
if (symbolTableKey && !!getPropertyOfType(source, symbolTableKey)) {
13027+
reportError(
13028+
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,
13029+
diagnosticName(privateNameDescription),
13030+
diagnosticName(source.symbol.valueDeclaration.name || ("(anonymous)" as __String))
13031+
);
13032+
hasReported = true;
1302113033
}
1302213034
}
13023-
else if (props.length > 5) { // arbitrary cutoff for too-long list form
13024-
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);
13025-
}
13026-
else {
13027-
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
13035+
if (!hasReported) {
13036+
const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false));
13037+
if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code &&
13038+
headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) {
13039+
suppressNextError = true; // Retain top-level error for interface implementing issues, otherwise omit it
13040+
}
13041+
if (props.length === 1) {
13042+
const propName = symbolToString(unmatchedProperty);
13043+
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, typeToString(source), typeToString(target));
13044+
if (length(unmatchedProperty.declarations)) {
13045+
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName));
13046+
}
13047+
}
13048+
else if (props.length > 5) { // arbitrary cutoff for too-long list form
13049+
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);
13050+
}
13051+
else {
13052+
reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", "));
13053+
}
1302813054
}
1302913055
}
1303013056
return Ternary.False;
@@ -19493,6 +19519,48 @@ namespace ts {
1949319519
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1949419520
}
1949519521

19522+
function getPropertyForPrivateName(apparentType: Type, leftType: Type, right: PrivateName, errorNode: Node | undefined): Symbol | undefined {
19523+
let classWithShadowedPrivateName;
19524+
let container = getContainingClass(right);
19525+
while (container) {
19526+
const symbolTableKey = getPropertyNameForPrivateNameDescription(container.symbol, right.escapedText);
19527+
if (symbolTableKey) {
19528+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19529+
if (prop) {
19530+
if (classWithShadowedPrivateName) {
19531+
if (errorNode) {
19532+
error(
19533+
errorNode,
19534+
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,
19535+
diagnosticName(right),
19536+
diagnosticName(classWithShadowedPrivateName.name || ("(anonymous)" as __String))
19537+
);
19538+
}
19539+
return undefined;
19540+
}
19541+
return prop;
19542+
}
19543+
else {
19544+
classWithShadowedPrivateName = container;
19545+
}
19546+
}
19547+
container = getContainingClass(container);
19548+
}
19549+
// If this isn't a case of shadowing, and the lhs has a property with the same
19550+
// private name description, then there is a privacy violation
19551+
if (leftType.symbol.members) {
19552+
const symbolTableKey = getPropertyNameForPrivateNameDescription(leftType.symbol, right.escapedText);
19553+
const prop = getPropertyOfType(apparentType, symbolTableKey);
19554+
if (prop) {
19555+
if (errorNode) {
19556+
error(right, Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_name, symbolToString(prop), typeToString(getDeclaringClass(prop)!));
19557+
}
19558+
}
19559+
}
19560+
// not found
19561+
return undefined;
19562+
}
19563+
1949619564
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier | PrivateName) {
1949719565
let propType: Type;
1949819566
const leftType = checkNonNullExpression(left);
@@ -19505,7 +19573,7 @@ namespace ts {
1950519573
return apparentType;
1950619574
}
1950719575
const assignmentKind = getAssignmentTargetKind(node);
19508-
const prop = getPropertyOfType(apparentType, right.escapedText);
19576+
const prop = isPrivateName(right) ? getPropertyForPrivateName(apparentType, leftType, right, /* errorNode */ right) : getPropertyOfType(apparentType, right.escapedText);
1950919577
if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) {
1951019578
markAliasReferenced(parentSymbol, node);
1951119579
}
@@ -22522,6 +22590,9 @@ namespace ts {
2252222590
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
2252322591
return booleanType;
2252422592
}
22593+
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateName((expr as PropertyAccessExpression).name)) {
22594+
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_name);
22595+
}
2252522596
const links = getNodeLinks(expr);
2252622597
const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol);
2252722598
if (symbol && isReadonlySymbol(symbol)) {
@@ -23833,9 +23904,6 @@ namespace ts {
2383323904
checkGrammarDecoratorsAndModifiers(node);
2383423905

2383523906
checkVariableLikeDeclaration(node);
23836-
if (node.name && isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
23837-
error(node, Diagnostics.Private_names_cannot_be_used_as_parameters);
23838-
}
2383923907
const func = getContainingFunction(node)!;
2384023908
if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) {
2384123909
if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) {
@@ -30591,6 +30659,9 @@ namespace ts {
3059130659
else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (<ParameterDeclaration>node).dotDotDotToken) {
3059230660
return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter);
3059330661
}
30662+
else if (isNamedDeclaration(node) && (flags & ModifierFlags.AccessibilityModifier) && node.name.kind === SyntaxKind.PrivateName) {
30663+
return grammarErrorOnNode(node, Diagnostics.Accessibility_modifiers_cannot_be_used_with_private_names);
30664+
}
3059430665
if (flags & ModifierFlags.Async) {
3059530666
return checkGrammarAsyncModifier(node, lastAsync!);
3059630667
}
@@ -30988,6 +31059,10 @@ namespace ts {
3098831059
return grammarErrorOnNode(prop.equalsToken!, Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment);
3098931060
}
3099031061

31062+
if (name.kind === SyntaxKind.PrivateName) {
31063+
return grammarErrorOnNode(name, Diagnostics.Private_names_are_not_allowed_outside_class_bodies);
31064+
}
31065+
3099131066
// Modifiers are never allowed on properties except for 'async' on a method declaration
3099231067
if (prop.modifiers) {
3099331068
for (const mod of prop.modifiers!) { // TODO: GH#19955
@@ -31429,10 +31504,6 @@ namespace ts {
3142931504
checkESModuleMarker(node.name);
3143031505
}
3143131506

31432-
if (isIdentifier(node.name) && node.name.originalKeywordKind === SyntaxKind.PrivateName) {
31433-
return grammarErrorOnNode(node.name, Diagnostics.Private_names_are_not_allowed_in_variable_declarations);
31434-
}
31435-
3143631507
const checkLetConstNames = (isLet(node) || isVarConst(node));
3143731508

3143831509
// 1. LexicalDeclaration : LetOrConst BindingList ;

0 commit comments

Comments
 (0)