diff --git a/src/com/google/javascript/jscomp/CodeGenerator.java b/src/com/google/javascript/jscomp/CodeGenerator.java index ef952c71fca..ef5e88abfdb 100644 --- a/src/com/google/javascript/jscomp/CodeGenerator.java +++ b/src/com/google/javascript/jscomp/CodeGenerator.java @@ -706,7 +706,7 @@ protected void add(Node node, Context context, boolean printComments) { // Add the property name. if (!node.isQuotedStringKey() - && TokenStream.isJSIdentifier(name) + && (TokenStream.isJSIdentifier(name) || node.isPrivateIdentifier()) && // do not encode literally any non-literal characters that were // Unicode escaped. diff --git a/src/com/google/javascript/jscomp/RewritePrivateClassProperties.java b/src/com/google/javascript/jscomp/RewritePrivateClassProperties.java new file mode 100644 index 00000000000..06e513d5e3a --- /dev/null +++ b/src/com/google/javascript/jscomp/RewritePrivateClassProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Closure Compiler Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.javascript.jscomp; + +import static com.google.javascript.jscomp.TranspilationUtil.cannotConvertYet; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.javascript.jscomp.parsing.parser.FeatureSet; +import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature; +import com.google.javascript.rhino.Node; + +/** Transpiles away usages of private properties in ES6 classes. */ +final class RewritePrivateClassProperties extends AbstractPeepholeTranspilation { + + private final AbstractCompiler compiler; + + RewritePrivateClassProperties(AbstractCompiler compiler) { + this.compiler = compiler; + } + + @Override + FeatureSet getTranspiledAwayFeatures() { + return FeatureSet.BARE_MINIMUM.with(Feature.PRIVATE_CLASS_PROPERTIES); + } + + @Override + @CanIgnoreReturnValue + Node transpileSubtree(Node n) { + if (n.isPrivateIdentifier()) { + cannotConvertYet(compiler, n, "private class properties"); + } + return n; + } +} diff --git a/src/com/google/javascript/jscomp/TranspilationPasses.java b/src/com/google/javascript/jscomp/TranspilationPasses.java index 044cc8b2a75..36bf55e25e4 100644 --- a/src/com/google/javascript/jscomp/TranspilationPasses.java +++ b/src/com/google/javascript/jscomp/TranspilationPasses.java @@ -197,6 +197,9 @@ public static void addTranspilationPasses(PassListBuilder passes, CompilerOption compiler, compiler.getOptions().getBrowserFeaturesetYearObject(), compiler.getOptions().getOutputFeatureSet())); + if (compiler.getOptions().needsTranspilationOf(Feature.PRIVATE_CLASS_PROPERTIES)) { + peepholeTranspilations.add(new RewritePrivateClassProperties(compiler)); + } if (compiler.getOptions().needsTranspilationOf(Feature.OPTIONAL_CATCH_BINDING)) { peepholeTranspilations.add(new RewriteCatchWithNoBinding(compiler)); } diff --git a/src/com/google/javascript/jscomp/parsing/IRFactory.java b/src/com/google/javascript/jscomp/parsing/IRFactory.java index 858f892b63e..420aea78878 100644 --- a/src/com/google/javascript/jscomp/parsing/IRFactory.java +++ b/src/com/google/javascript/jscomp/parsing/IRFactory.java @@ -17,6 +17,7 @@ package com.google.javascript.jscomp.parsing; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.javascript.jscomp.base.JSCompObjects.identical; import static java.lang.Integer.parseInt; @@ -30,6 +31,7 @@ import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.javascript.jscomp.base.format.SimpleFormat; import com.google.javascript.jscomp.parsing.Config.JsDocParsing; import com.google.javascript.jscomp.parsing.Config.LanguageMode; @@ -140,11 +142,13 @@ import com.google.javascript.rhino.dtoa.DToA; import java.math.BigInteger; import java.util.ArrayDeque; +import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; import java.util.function.Predicate; import org.jspecify.annotations.Nullable; @@ -1211,9 +1215,100 @@ void setLengthFrom(Node node, Node ref) { private class TransformDispatcher { - // For now, this is just a pass-through. Soon will distinguish based on an IdentifierType. - String getIdentifierValue(IdentifierToken identifierToken) { - return identifierToken.getValue(); + /** + * Tracks whether a certain language feature is currently in scope. Use in conjunction with + * try-with-resources to auto-handle decrementing. + */ + private static class ScopeTracker { + private int usageCount = 0; + + boolean inScope() { + return usageCount > 0; + } + + AutoDecrement increment() { + return maybeIncrement(true); + } + + AutoDecrement maybeIncrement(boolean condition) { + if (condition) { + checkState(usageCount >= 0); + ++usageCount; + } + return new AutoDecrement() { + private boolean closed = false; + + @Override + public void close() { + checkState(!closed); + if (condition) { + --usageCount; + checkState(usageCount >= 0); + } + closed = true; + } + }; + } + } + + // Variant of AutoCloseable that does not throw. + private interface AutoDecrement extends AutoCloseable { + @Override + void close(); + } + + // Tracks whether a class is currently in scope. Presently used to detect valid usage of + // private fields. + private final ScopeTracker classScope = new ScopeTracker(); + + // Used to detect an IdentifierExpression in the following context: + // Valid: `class A { #x; static isA(o) { return #x in o; } }` + // Invalid: `class A { #x; } function isA(o) { return #x in o; } }` + private final ScopeTracker privateIdLhsOfInScope = new ScopeTracker(); + + /** Indicates the valid values usages for an identifier. */ + private enum IdentifierType { + /** The identifier can never be private. */ + STANDARD, + /** The identifier can be private if it is in a class scope. */ + CAN_BE_PRIVATE + } + + /** + * Creates a StringNode using the value of an IdentifierToken. Reports an error if the + * identifier value is private and the identifier type is STANDARD or the identifier is not + * within the scope of a class. + */ + Node newStringNodeFromIdentifier( + Token type, IdentifierType identifierType, IdentifierToken identifierToken) { + // Private properties can only ever be referenced from within the scope of a class. If we're + // not in a class, we always use STANDARD rules. + if (!classScope.inScope()) { + identifierType = IdentifierType.STANDARD; + } + + String value; + if (identifierToken.isPrivateIdentifier()) { + if (identifierType == IdentifierType.CAN_BE_PRIVATE) { + maybeWarnForFeature(identifierToken, Feature.PRIVATE_CLASS_PROPERTIES); + } else { + errorReporter.error( + "Private identifiers may not be used in this context", + sourceName, + identifierToken.location.start.line, + identifierToken.location.start.column); + } + value = identifierToken.getMaybePrivateValue(); + } else { + value = identifierToken.getValue(); + } + + Node node = newStringNode(type, value); + if (identifierType == IdentifierType.CAN_BE_PRIVATE + && identifierToken.isPrivateIdentifier()) { + node.setPrivateIdentifier(); + } + return node; } /** @@ -1222,15 +1317,21 @@ String getIdentifierValue(IdentifierToken identifierToken) { *
Depending on `input`, this may add quoting, such as for numerical values. For example, in * `{2: null}`, `2` will be transformed into a quoted string. This adjustment loses some * accuracy about the source code, but simplifies the AST. + * + *
Get and set accessors process their name using this method. For classes, this can be a
+ * private property in which case identifierType
will be set to
+ * CAN_BE_PRIVATE
. E.g. class C { get #f() { return value; } }
.
*/
private Node processObjectLitKey(
- com.google.javascript.jscomp.parsing.parser.Token input, Token output) {
+ com.google.javascript.jscomp.parsing.parser.Token input,
+ Token output,
+ IdentifierType identifierType) {
if (input == null) {
return createMissingExpressionNode();
}
if (input.type == TokenType.IDENTIFIER) {
- return processName(input.asIdentifier(), output);
+ return processName(input.asIdentifier(), output, identifierType);
}
LiteralToken literal = input.asLiteral();
@@ -1359,8 +1460,8 @@ private Node processObjectPatternShorthandWithDefault(DefaultParameterTree defau
// {name: /**inlineType */ name = default }
Node nameNode = defaultValueNode.getFirstChild();
Node stringKeyNode =
- newStringNodeWithNonJSDocComment(
- Token.STRING_KEY, nameNode.getString(), defaultParameter.getStart());
+ maybeAddNonJsDocComment(
+ newStringNode(Token.STRING_KEY, nameNode.getString()), defaultParameter.getStart());
setSourceInfo(stringKeyNode, nameNode);
stringKeyNode.setShorthandProperty(true);
stringKeyNode.addChildToBack(defaultValueNode);
@@ -1380,13 +1481,17 @@ private Node processObjectPatternShorthandWithDefault(DefaultParameterTree defau
*/
private Node processObjectPatternPropertyNameAssignment(
PropertyNameAssignmentTree propertyNameAssignment) {
- Node key = processObjectLitKey(propertyNameAssignment.name, Token.STRING_KEY);
+ Node key =
+ processObjectLitKey(
+ propertyNameAssignment.name, Token.STRING_KEY, IdentifierType.STANDARD);
ParseTree targetTree = propertyNameAssignment.value;
final Node valueNode;
if (targetTree == null) {
// `let { /** inlineType */ key } = something;`
// The key is also the target name.
- valueNode = processNameWithInlineComments(propertyNameAssignment.name.asIdentifier());
+ valueNode =
+ processNameWithInlineComments(
+ propertyNameAssignment.name.asIdentifier(), IdentifierType.STANDARD);
key.setShorthandProperty(true);
} else {
valueNode = processDestructuringElementTarget(targetTree);
@@ -1406,7 +1511,9 @@ private Node processDestructuringElementTarget(ParseTree targetTree) {
// let {key: /** inlineType */ name} = something
// let [/** inlineType */ name] = something
// Allow inline JSDoc on the name, since we may well be declaring it here.
- valueNode = processNameWithInlineComments(targetTree.asIdentifierExpression());
+ valueNode =
+ processNameWithInlineComments(
+ targetTree.asIdentifierExpression(), IdentifierType.STANDARD);
} else {
// ({prop: /** string */ ns.a.b} = someObject);
// NOTE: CheckJSDoc will report an error for this case, since we want qualified names to be
@@ -1532,8 +1639,9 @@ Node processBreakStatement(BreakStatementTree statementNode) {
Node transformLabelName(IdentifierToken token) {
Node label =
- newStringNodeWithNonJSDocComment(
- Token.LABEL_NAME, getIdentifierValue(token), token.getStart());
+ maybeAddNonJsDocComment(
+ newStringNodeFromIdentifier(Token.LABEL_NAME, IdentifierType.STANDARD, token),
+ token.getStart());
setSourceInfo(label, token);
return label;
}
@@ -1636,7 +1744,7 @@ Node transformOrEmpty(IdentifierToken token, ParseTree parent) {
setSourceInfo(n, parent);
return n;
}
- return processName(token, Token.NAME);
+ return processName(token, Token.NAME, IdentifierType.STANDARD);
}
Node processFunctionCall(CallExpressionTree callNode) {
@@ -1856,6 +1964,9 @@ Node processFunction(FunctionDeclarationTree functionTree) {
boolean isGenerator = functionTree.isGenerator;
boolean isSignature = (functionTree.functionBody.type == ParseTreeType.EMPTY_STATEMENT);
+ IdentifierType identifierType =
+ functionTree.isClassMember ? IdentifierType.CAN_BE_PRIVATE : IdentifierType.STANDARD;
+
if (isGenerator) {
maybeWarnForFeature(functionTree, Feature.GENERATORS);
}
@@ -1879,7 +1990,7 @@ Node processFunction(FunctionDeclarationTree functionTree) {
IdentifierToken name = functionTree.name;
Node newName;
if (name != null) {
- newName = processNameWithInlineComments(name);
+ newName = processNameWithInlineComments(name, identifierType);
} else {
if (isDeclaration || isMember) {
errorReporter.error(
@@ -1925,7 +2036,7 @@ Node processFunction(FunctionDeclarationTree functionTree) {
if (isMember) {
setSourceInfo(node, functionTree);
- Node member = newStringNode(Token.MEMBER_FUNCTION_DEF, getIdentifierValue(name));
+ Node member = newStringNodeFromIdentifier(Token.MEMBER_FUNCTION_DEF, identifierType, name);
member.addChildToBack(node);
member.setStaticMember(functionTree.isStatic);
// The source info should only include the identifier, not the entire function expression
@@ -1943,8 +2054,10 @@ Node processField(FieldDeclarationTree tree) {
maybeWarnForFeature(tree, Feature.PUBLIC_CLASS_FIELDS);
Node node =
- newStringNodeWithNonJSDocComment(
- Token.MEMBER_FIELD_DEF, getIdentifierValue(tree.name), tree.getStart());
+ maybeAddNonJsDocComment(
+ newStringNodeFromIdentifier(
+ Token.MEMBER_FIELD_DEF, IdentifierType.CAN_BE_PRIVATE, tree.name),
+ tree.getStart());
if (tree.initializer != null) {
Node initializer = transform(tree.initializer);
node.addChildToBack(initializer);
@@ -2025,7 +2138,9 @@ Node processDefaultParameter(DefaultParameterTree tree) {
// allow inline JSDoc on an identifier
// let { /** inlineType */ x = defaultValue } = someObject;
// TODO(bradfordcsmith): Do we need to allow inline JSDoc for qualified names, too?
- targetNode = processNameWithInlineComments(targetTree.asIdentifierExpression());
+ targetNode =
+ processNameWithInlineComments(
+ targetTree.asIdentifierExpression(), IdentifierType.STANDARD);
} else {
// ({prop: /** string */ ns.a.b = 'foo'} = someObject);
// NOTE: CheckJSDoc will report an error for this case, since we want qualified names to be
@@ -2093,7 +2208,7 @@ Node processBinaryExpression(BinaryOperatorTree exprNode) {
markBinaryExpressionFeatures(exprNode);
return newNode(
transformBinaryTokenType(exprNode.operator.type),
- transform(exprNode.left),
+ transformLhsOfBinaryTree(exprNode),
transform(exprNode.right));
} else {
// No pending comments, we can traverse out of order.
@@ -2129,7 +2244,7 @@ private Node processBinaryExpressionHelper(BinaryOperatorTree exprTree) {
exprTree = binaryOperatorTree;
} else {
// Finish things off, add the left operand to the current node.
- Node leftNode = transform(exprTree.left);
+ Node leftNode = transformLhsOfBinaryTree(exprTree);
current.addChildToFront(leftNode);
// Nothing left to do.
exprTree = null;
@@ -2143,6 +2258,17 @@ private Node processBinaryExpressionHelper(BinaryOperatorTree exprTree) {
return root;
}
+ private Node transformLhsOfBinaryTree(BinaryOperatorTree exprTree) {
+ boolean lhsOfInIsPrivateId =
+ exprTree.operator.type == TokenType.IN
+ && exprTree.left instanceof IdentifierExpressionTree
+ && exprTree.left.asIdentifierExpression().identifierToken.isPrivateIdentifier();
+
+ try (AutoDecrement ignored = privateIdLhsOfInScope.maybeIncrement(lhsOfInIsPrivateId)) {
+ return transform(exprTree.left);
+ }
+ }
+
@SuppressWarnings("unused") // for symmetry all the process* methods take a ParseTree
Node processDebuggerStatement(DebuggerStatementTree unused) {
return newNode(Token.DEBUGGER);
@@ -2169,14 +2295,14 @@ Node processLabeledStatement(LabelledStatementTree labelTree) {
return newNode(Token.LABEL, transformLabelName(labelTree.name), statement);
}
- Node processName(IdentifierExpressionTree nameNode) {
- return processName(nameNode.identifierToken, Token.NAME);
+ Node processName(IdentifierExpressionTree nameNode, IdentifierType identifierType) {
+ return processName(nameNode.identifierToken, Token.NAME, identifierType);
}
- Node processName(IdentifierToken identifierToken, Token output) {
+ Node processName(IdentifierToken identifierToken, Token output, IdentifierType identifierType) {
NonJSDocComment comment = parseNonJSDocCommentAt(identifierToken.getStart(), true);
- Node node = newStringNode(output, getIdentifierValue(identifierToken));
+ Node node = newStringNodeFromIdentifier(output, identifierType, identifierToken);
if (output == Token.NAME) {
maybeWarnReservedKeyword(identifierToken);
@@ -2223,16 +2349,18 @@ Node processTemplateLiteralToken(TemplateLiteralToken token) {
return node;
}
- private Node processNameWithInlineComments(IdentifierExpressionTree identifierExpression) {
- return processNameWithInlineComments(identifierExpression.identifierToken);
+ private Node processNameWithInlineComments(
+ IdentifierExpressionTree identifierExpression, IdentifierType identifierType) {
+ return processNameWithInlineComments(identifierExpression.identifierToken, identifierType);
}
- Node processNameWithInlineComments(IdentifierToken identifierToken) {
+ Node processNameWithInlineComments(
+ IdentifierToken identifierToken, IdentifierType identifierType) {
JSDocInfo info = parseInlineJSDocAt(identifierToken.getStart());
NonJSDocComment comment = parseNonJSDocCommentAt(identifierToken.getStart(), false);
maybeWarnReservedKeyword(identifierToken);
- Node node = newStringNode(Token.NAME, getIdentifierValue(identifierToken));
+ Node node = newStringNodeFromIdentifier(Token.NAME, identifierType, identifierToken);
if (info != null) {
node.setJSDocInfo(info);
@@ -2260,7 +2388,9 @@ private void maybeWarnKeywordProperty(Node node) {
}
private void maybeWarnReservedKeyword(IdentifierToken token) {
- String identifier = getIdentifierValue(token);
+ // We're just checking for keywords here, not actually using the identifier so we don't care
+ // about potentially running into a misplaced private identifier.
+ String identifier = token.getMaybePrivateValue();
boolean isIdentifier = false;
if (TokenStream.isKeyword(identifier)) {
features = features.with(Feature.ES3_KEYWORDS_AS_IDENTIFIERS);
@@ -2393,7 +2523,11 @@ Node processComputedPropertySetter(ComputedPropertySetterTree tree) {
}
Node processGetAccessor(GetAccessorTree tree) {
- Node key = processObjectLitKey(tree.propertyName, Token.GETTER_DEF);
+ Node key =
+ processObjectLitKey(
+ tree.propertyName,
+ Token.GETTER_DEF,
+ tree.isClassMember ? IdentifierType.CAN_BE_PRIVATE : IdentifierType.STANDARD);
Node body = transform(tree.body);
Node dummyName = newStringNode(Token.NAME, "");
setSourceInfo(dummyName, tree.body);
@@ -2407,7 +2541,11 @@ Node processGetAccessor(GetAccessorTree tree) {
}
Node processSetAccessor(SetAccessorTree tree) {
- Node key = processObjectLitKey(tree.propertyName, Token.SETTER_DEF);
+ Node key =
+ processObjectLitKey(
+ tree.propertyName,
+ Token.SETTER_DEF,
+ tree.isClassMember ? IdentifierType.CAN_BE_PRIVATE : IdentifierType.STANDARD);
Node paramList = processFormalParameterList(tree.parameter);
setSourceInfo(paramList, tree.parameter);
@@ -2426,12 +2564,13 @@ Node processSetAccessor(SetAccessorTree tree) {
}
Node processPropertyNameAssignment(PropertyNameAssignmentTree tree) {
- Node key = processObjectLitKey(tree.name, Token.STRING_KEY);
+ Node key = processObjectLitKey(tree.name, Token.STRING_KEY, IdentifierType.STANDARD);
if (tree.value != null) {
key.addChildToFront(transform(tree.value));
} else {
Node value =
- newStringNodeWithNonJSDocComment(Token.NAME, key.getString(), tree.name.getStart())
+ maybeAddNonJsDocComment(
+ newStringNode(Token.NAME, key.getString()), tree.name.getStart())
.srcref(key);
key.setShorthandProperty(true);
key.addChildToFront(value);
@@ -2468,8 +2607,9 @@ Node processPropertyGet(MemberExpressionTree getNode) {
}
Node getProp =
- newStringNodeWithNonJSDocComment(
- Token.GETPROP, getIdentifierValue(propName), getNode.memberName.getStart());
+ maybeAddNonJsDocComment(
+ newStringNodeFromIdentifier(Token.GETPROP, IdentifierType.CAN_BE_PRIVATE, propName),
+ getNode.memberName.getStart());
getProp.addChildToBack(leftChild);
setSourceInfo(getProp, propName);
maybeWarnKeywordProperty(getProp);
@@ -2486,8 +2626,10 @@ Node processOptChainPropertyGet(OptionalMemberExpressionTree getNode) {
}
Node getProp =
- newStringNodeWithNonJSDocComment(
- Token.OPTCHAIN_GETPROP, getIdentifierValue(propName), getNode.memberName.getStart());
+ maybeAddNonJsDocComment(
+ newStringNodeFromIdentifier(
+ Token.OPTCHAIN_GETPROP, IdentifierType.CAN_BE_PRIVATE, propName),
+ getNode.memberName.getStart());
getProp.addChildToBack(leftChild);
getProp.setIsOptionalChainStart(getNode.isStartOfOptionalChain);
setSourceInfo(getProp, propName);
@@ -2855,44 +2997,225 @@ Node processClassDeclaration(ClassDeclarationTree tree) {
Node name = transformOrEmpty(tree.name, tree);
Node superClass = transformOrEmpty(tree.superClass, tree);
- Node body = newNode(Token.CLASS_MEMBERS);
- setSourceInfo(body, tree);
+ Node classMembers = newNode(Token.CLASS_MEMBERS);
+ setSourceInfo(classMembers, tree);
+
+ try (AutoDecrement ignored = classScope.increment()) {
+ boolean hasConstructor = false;
+ for (ParseTree child : tree.elements) {
+ switch (child.type) {
+ case COMPUTED_PROPERTY_GETTER:
+ case COMPUTED_PROPERTY_SETTER:
+ case GET_ACCESSOR:
+ case SET_ACCESSOR:
+ features = features.with(Feature.CLASS_GETTER_SETTER);
+ break;
+ case BLOCK:
+ features = features.with(Feature.CLASS_STATIC_BLOCK);
+ break;
+ default:
+ break;
+ }
- boolean hasConstructor = false;
- for (ParseTree child : tree.elements) {
- switch (child.type) {
- case COMPUTED_PROPERTY_GETTER:
- case COMPUTED_PROPERTY_SETTER:
- case GET_ACCESSOR:
- case SET_ACCESSOR:
- features = features.with(Feature.CLASS_GETTER_SETTER);
- break;
- case BLOCK:
- features = features.with(Feature.CLASS_STATIC_BLOCK);
- break;
- default:
- break;
+ boolean childIsCtor = validateClassConstructorMember(child); // Has side-effects.
+ if (childIsCtor) {
+ if (hasConstructor) {
+ errorReporter.error(
+ "Class may have only one constructor.", //
+ sourceName,
+ lineno(child),
+ charno(child));
+ }
+ hasConstructor = true;
+ }
+
+ classMembers.addChildToBack(transform(child));
}
+ }
+
+ // When nested inside of multiple classes, only run the validation once the top-most class
+ // exits scope.
+ if (!classScope.inScope()) {
+ validatePrivatePropertyUsage(
+ Collections.unmodifiableSet(getPrivatePropsAndValidateUniqueness(classMembers)),
+ classMembers);
+ }
+
+ Node classNode = newNode(Token.CLASS, name, superClass, classMembers);
+ attachPossibleTrailingComment(classNode, tree.getEnd());
+
+ return classNode;
+ }
+
+ /**
+ * Validates that all referenced private properties within the scope of classMembers
+ *
are found in privatePropNames
and private fields are not deleted (both
+ * of these cases are syntax errors).
+ *
+ *
Recurses into inner classes and successively adds to While gathering private properties, validates that there are no duplicate property names.
+ * Note that, for private getters and setters, one getter and one setter can have the same name
+ * provided they have the same static or non-static modifier.
+ */
+ private Set You must verify privateIdentifier is false (and presumably error if it is true) before
+ * calling this method.
*
* Prefer calling {@link #isKeyword()} or {@link #valueEquals(String)} if those methods meet
* your needs.
*/
public String getValue() {
+ checkState(!privateIdentifier);
return value;
}
+
+ /** Gets the value of the identifier, allowing it to be a private identifier. */
+ public String getMaybePrivateValue() {
+ return value;
+ }
+
+ /** Whether the value starts with a #. */
+ public boolean isPrivateIdentifier() {
+ return privateIdentifier;
+ }
}
diff --git a/src/com/google/javascript/jscomp/parsing/parser/Scanner.java b/src/com/google/javascript/jscomp/parsing/parser/Scanner.java
index 90668be001a..52919fc1661 100644
--- a/src/com/google/javascript/jscomp/parsing/parser/Scanner.java
+++ b/src/com/google/javascript/jscomp/parsing/parser/Scanner.java
@@ -629,10 +629,10 @@ private Token scanToken() {
// skipComments() call) so its token is an error.
if (peek('!')) {
reportError(getPosition(index), "Shebang comment must be at the start of the file");
- } else {
- reportError(getPosition(index), "Invalid usage of #");
+ return createToken(TokenType.ERROR, beginToken);
}
- return createToken(TokenType.ERROR, beginToken);
+ // Handle private identifiers.
+ return scanIdentifierOrKeyword(beginToken, ch);
// TODO: add NumberToken
// TODO: character following NumericLiteral must not be an IdentifierStart or DecimalDigit
case '0':
@@ -773,6 +773,7 @@ private Token scanIdentifierOrKeyword(int beginToken, char ch) {
boolean containsUnicodeEscape = ch == '\\';
boolean bracedUnicodeEscape = false;
+ boolean isPrivateIdentifier = ch == '#';
int unicodeEscapeLen = containsUnicodeEscape ? 1 : 0;
ch = peekChar();
@@ -804,6 +805,11 @@ private Token scanIdentifierOrKeyword(int beginToken, char ch) {
String value = contents.substring(valueStartIndex, index);
+ if (isPrivateIdentifier && value.equals("#")) {
+ reportError(getPosition(beginToken), "Invalid usage of #");
+ return createToken(TokenType.ERROR, beginToken);
+ }
+
// Process unicode escapes.
if (containsUnicodeEscape) {
value = processUnicodeEscapes(value);
@@ -816,6 +822,10 @@ private Token scanIdentifierOrKeyword(int beginToken, char ch) {
// Check to make sure the first character (or the unicode escape at the
// beginning of the identifier) is a valid identifier start character.
char start = value.charAt(0);
+ if (isPrivateIdentifier) {
+ // Skip the leading # for name validation.
+ start = value.charAt(1);
+ }
if (!Identifiers.isIdentifierStart(start)) {
reportError(
getPosition(beginToken),
diff --git a/src/com/google/javascript/rhino/Node.java b/src/com/google/javascript/rhino/Node.java
index 6d20f40efb1..e3821cf6ed4 100644
--- a/src/com/google/javascript/rhino/Node.java
+++ b/src/com/google/javascript/rhino/Node.java
@@ -203,6 +203,8 @@ enum Prop {
SYNTHESIZED_UNFULFILLED_NAME_DECLARATION,
// This prop holds a reference to a closure-unaware sub-AST.
CLOSURE_UNAWARE_SHADOW,
+ // Indicates that a string node is a private identifier (e.g. `class { #privateProp }`).
+ PRIVATE_IDENTIFIER,
}
// Avoid cloning "values" repeatedly in hot code, we save it off now.
@@ -3211,6 +3213,16 @@ public final void setQuotedStringKey() {
this.putBooleanProp(Prop.QUOTED, true);
}
+ public final boolean isPrivateIdentifier() {
+ return (this instanceof StringNode) && this.getBooleanProp(Prop.PRIVATE_IDENTIFIER);
+ }
+
+ public final void setPrivateIdentifier() {
+ checkState(this instanceof StringNode, this);
+ checkState(this.getString().startsWith("#"));
+ this.putBooleanProp(Prop.PRIVATE_IDENTIFIER, true);
+ }
+
/*** AST type check methods ***/
public final boolean isAdd() {
diff --git a/src/com/google/javascript/rhino/PropTranslator.java b/src/com/google/javascript/rhino/PropTranslator.java
index 0c890f497ed..ea11f767d86 100644
--- a/src/com/google/javascript/rhino/PropTranslator.java
+++ b/src/com/google/javascript/rhino/PropTranslator.java
@@ -174,10 +174,11 @@ private static final void setProps() {
case ACCESS_MODIFIER:
case IS_TYPESCRIPT_ABSTRACT:
case TYPEDEF_TYPE:
+ case PRIVATE_IDENTIFIER:
// These cases cannot be translated to a NodeProperty
return null;
}
- throw new AssertionError();
+ throw new AssertionError("Unhandled prop: " + prop);
}
private static final void checkUnexpectedNullProtoProps() {
diff --git a/test/com/google/javascript/jscomp/CodePrinterTest.java b/test/com/google/javascript/jscomp/CodePrinterTest.java
index 65ca36f3029..911a8d70f1d 100644
--- a/test/com/google/javascript/jscomp/CodePrinterTest.java
+++ b/test/com/google/javascript/jscomp/CodePrinterTest.java
@@ -3150,6 +3150,39 @@ public void testComputedClassFieldStatic() {
""));
}
+ @Test
+ public void testPrivateClassProperties_definition() {
+ assertPrintSame("class C{#f;}");
+ assertPrintSame("class C{#m(){}}");
+ assertPrintSame("class C{*#g(){}}");
+ assertPrintSame("class C{get #g(){}}");
+ assertPrintSame("class C{set #s(x){}}");
+ assertPrintSame("class C{get #p(){}set #p(x){}}");
+ assertPrintSame("class C{async #a(){}}");
+ assertPrintSame("class C{async *#ag(){}}");
+
+ assertPrintSame("class C{static #sf;}");
+ assertPrintSame("class C{static #sm(){}}");
+ assertPrintSame("class C{static *#sg(){}}");
+ assertPrintSame("class C{static get #sg(){}}");
+ assertPrintSame("class C{static set #ss(x){}}");
+ assertPrintSame("class C{static get #sp(){}static set #sp(x){}}");
+ assertPrintSame("class C{static async #sa(){}}");
+ assertPrintSame("class C{static async *#sag(){}}");
+ }
+
+ @Test
+ public void testPrivateClassProperties_usage() {
+ assertPrintSame("class C{#f;#g=this.#f;}");
+ assertPrintSame("class C{#f;#g=this?.#f;}");
+ assertPrintSame("class C{#m(){this.#m()}}");
+ assertPrintSame("class C{#m(){this?.#m()}}");
+
+ assertPrintSame("class C{static #f;static #m(){const t=this;t.#f}}");
+ assertPrintSame("class C{static #f;static #m(){const t=this;t?.#f}}");
+ assertPrintSame("class C{static #f;static #m(){const t=this;#f in t}}");
+ }
+
@Test
public void testSuper() {
assertPrintSame("class C extends foo(){}");
diff --git a/test/com/google/javascript/jscomp/ConvertToDottedPropertiesTest.java b/test/com/google/javascript/jscomp/ConvertToDottedPropertiesTest.java
index eb690989b83..4838f9a8b0c 100644
--- a/test/com/google/javascript/jscomp/ConvertToDottedPropertiesTest.java
+++ b/test/com/google/javascript/jscomp/ConvertToDottedPropertiesTest.java
@@ -208,4 +208,41 @@ public void testContinueOptionalChaining() {
"const chain = window.a.x.y.b.x.y.c.x.y?.d.x.y.e.x.y",
"['f-f'].x.y?.['g-g'].x.y?.h.x.y.i.x.y;"));
}
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_computedProp() {
+ testSame("const x = {['#notAPrivateProp']: 1}");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_computedFieldDef() {
+ testSame("class C {['#notAPrivateProp'] = 1}");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_getterDef() {
+ testSame("const x = {get '#notAPrivateProp'() {}}");
+ testSame("class C {get '#notAPrivateProp'() {}}");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_setterDef() {
+ testSame("const x = {set '#notAPrivateProp'(x) {}}");
+ testSame("class C {set '#notAPrivateProp'(x) {}}");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_stringKey() {
+ testSame("const x = {'#notAPrivateProp': 1}");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_optchainGetelem() {
+ testSame("x?.['#notAPrivateProp']");
+ }
+
+ @Test
+ public void testClassPrivateProp_notPrivateProp_getelem() {
+ testSame("x['#notAPrivateProp']");
+ }
}
diff --git a/test/com/google/javascript/jscomp/parsing/ParserTest.java b/test/com/google/javascript/jscomp/parsing/ParserTest.java
index 1244ec8d59d..1c8ee548676 100644
--- a/test/com/google/javascript/jscomp/parsing/ParserTest.java
+++ b/test/com/google/javascript/jscomp/parsing/ParserTest.java
@@ -91,6 +91,14 @@ public final class ParserTest extends BaseJSTypeTestCase {
private static final String SEMICOLON_EXPECTED = "Semi-colon expected";
+ private static final String INVALID_PRIVATE_ID =
+ "Private identifiers may not be used in this context";
+ private static final String PRIVATE_FIELD_NOT_DEFINED =
+ "Private fields must be declared in an enclosing class";
+ private static final String PRIVATE_METHOD_NOT_DEFINED =
+ "Private methods must be declared in an enclosing class";
+ private static final String PRIVATE_FIELD_DELETED = "Private fields cannot be deleted";
+
private LanguageMode mode;
private JsDocParsing parsingMode;
private Config.StrictMode strictMode;
@@ -5999,6 +6007,527 @@ public void testClassComputedField_es2020_warnsForCodeOutsideClosureUnawareRange
expectFeatures(Feature.CLASSES, Feature.PUBLIC_CLASS_FIELDS);
}
+ @Test
+ public void testPrivateProperty_unstable() {
+ mode = LanguageMode.UNSTABLE;
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+ parseWarning(
+ "class C { #f = 2; }", requiresLanguageModeMessage(Feature.PRIVATE_CLASS_PROPERTIES));
+ }
+
+ @Test
+ public void testPrivateProperty_singleClassMember() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse("class C { #f; }");
+ parse("class C { #m() {} }");
+ parse("class C { *#g() {} }");
+ parse("class C { get #g() {} }");
+ parse("class C { set #s(x) {} }");
+ parse("class C { get #p() {} set #p(x) {} }");
+ parse("class C { async #a() {} }");
+ parse("class C { async *#ag() {} }");
+
+ parse("class C { static #sf; }");
+ parse("class C { static #sm() {} }");
+ parse("class C { static *#sg() {} }");
+ parse("class C { static get #sg() {} }");
+ parse("class C { static set #ss(x) {} }");
+ parse("class C { static get #sp() {} static set #sp(x) {} }");
+ parse("class C { static async #sa() {} }");
+ parse("class C { static async *#sag() {} }");
+ }
+
+ @Test
+ public void testPrivateProperty_definition_linenocharno() {
+ Node n =
+ parse(
+ lines(
+ "class C {", //
+ " #pf = 1;",
+ " #pm() {}",
+ "}"))
+ .getFirstChild();
+
+ Node members = NodeUtil.getClassMembers(n);
+
+ Node privateField = members.getFirstChild();
+ assertThat(privateField.getLineno()).isEqualTo(2);
+ assertThat(privateField.getCharno()).isEqualTo(2);
+ assertThat(privateField.getLength()).isEqualTo(8); // Includes the assignment
+
+ Node privateMethod = members.getLastChild();
+ assertThat(privateMethod.getLineno()).isEqualTo(3);
+ assertThat(privateMethod.getCharno()).isEqualTo(2);
+ assertThat(privateMethod.getLength()).isEqualTo(3); // Just the method name.
+ }
+
+ @Test
+ public void testPrivateProperty_multipleClassMembers() {
+ parse("class C { #f; #g; }");
+ parse("class C { #m() {} #n() {} }");
+ parse("class C { get #g() {} get #h() {} }");
+ parse("class C { set #s(x) {} set #t(x) {} }");
+ parse("class C { get #s() {} set #s(x) {} get #t() {} set #t(x) {} }");
+
+ parse("class C { static #sf; static #sg; }");
+ parse("class C { static #sm() {} static #sn() {} }");
+ parse("class C { static get #sg() {} static get #sh() {} }");
+ parse("class C { static set #ss(x) {} static set #st(x) {} }");
+ parse(
+ "class C { static get #ss() {} static set #ss(x) {} "
+ + "static get #st() {} static set #st(x) {} }");
+
+ parse("class C { #a; #b; c() {} d() {} get #e() {} set #e(x) {} set #f(x) {} }");
+ parse("class C { static #a; #b; static c() {} d() {} static get #e() {} set #f(x) {} }");
+ parse(
+ "class C { static #a; static #b; static c() {} static d() {} "
+ + "static get #e() {} static set #f(x) {} }");
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_identifierAlreadyDeclared() {
+ String expectedError = "Identifier '#p' has already been declared";
+
+ parseError("class C { #p; #p; }", expectedError);
+ parseError("class C { #p() {} #p() {} }", expectedError);
+ parseError("class C { get #p() {} get #p() {} }", expectedError);
+ parseError("class C { set #p(x) {} set #p(x) {} }", expectedError);
+
+ parseError("class C { static #p; static #p; }", expectedError);
+ parseError("class C { static #p() {} static #p() {} }", expectedError);
+ parseError("class C { static get #p() {} static get #p() {} }", expectedError);
+ parseError("class C { static set #p(x) {} static set #p(x) {} }", expectedError);
+
+ parseError("class C { #p; #p() {} }", expectedError);
+ parseError("class C { #p; get #p() {} }", expectedError);
+ parseError("class C { #p; set #p(x) {} }", expectedError);
+
+ parseError("class C { #p() {} #p; }", expectedError);
+ parseError("class C { #p() {} get #p() {} }", expectedError);
+ parseError("class C { #p() {} set #p(x) {} }", expectedError);
+
+ parseError("class C { get #p() {} #p; }", expectedError);
+ parseError("class C { get #p() {} #p() {} }", expectedError);
+ parse(/**/ "class C { get #p() {} set #p(x) {} }"); // OK
+
+ parseError("class C { set #p(x) {} #p; }", expectedError);
+ parseError("class C { set #p(x) {} #p() {} }", expectedError);
+ parse(/**/ "class C { set #p(x) {} get #p() {} }"); // OK
+
+ parseError("class C { #p; static #p; }", expectedError); // Cross-static duplicate field
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_identifierAlreadyDeclared_crossStaticGetterSetter() {
+ String expectedError = "Identifier '#p' has already been declared";
+
+ // Repeating in all orders as the check is unique for each to a degree.
+ parseError("class C { get #p() {} static set #p(x) {} }", expectedError);
+ parseError("class C { set #p(x) {} static get #p() {} }", expectedError);
+ parseError("class C { static get #p() {} set #p(x) {} }", expectedError);
+ parseError("class C { static set #p(x) {} get #p() {} }", expectedError);
+ }
+
+ @Test
+ public void testPrivateProperty_classMembersReferencingOtherPrivateField() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse("class C { #f = 1; f = this.#f; }");
+
+ parse("class C { static #sf = 1; static sf = this.#sf; }");
+ parse("class C { static #sf = 1; static sf = C.#sf; }");
+ parse("class C { static #sf; static { this.#sf = 1; } }");
+ parse("class C { static #sf; static { C.#sf = 1; } }");
+ }
+
+ @Test
+ public void testPrivateProperty_reference_linenocharno() {
+ Node n =
+ parse(
+ lines(
+ "class C {", //
+ " #pf1 = 1;",
+ " #pf2 = this.#pf1;",
+ "}"))
+ .getFirstChild();
+
+ Node members = NodeUtil.getClassMembers(n);
+
+ Node field2 = members.getLastChild();
+ assertNode(field2).hasType(Token.MEMBER_FIELD_DEF);
+
+ Node field2GetProp = field2.getFirstChild();
+ assertNode(field2GetProp).hasType(Token.GETPROP);
+ assertNode(field2GetProp).hasStringThat().isEqualTo("#pf1");
+ assertThat(field2GetProp.getLineno()).isEqualTo(3);
+ assertThat(field2GetProp.getCharno()).isEqualTo(14);
+ assertThat(field2GetProp.getLength()).isEqualTo(4);
+ }
+
+ @Test
+ public void testPrivateProperty_classMethodsReferencingOtherPrivateProp() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse("class C { #f = 1; method() { this.#f = 2; } }");
+ parse("class C { #f = 1; #g = 2; method() { this.#f = this.#g; } }");
+ parse("class C { #f = 1; method() { const t = this; t.#f = 2; } }");
+ parse("class C { #f = 1; method() { const f = () => { this.#f = 2; }; } }");
+ parse("class C { #f = 1; method() { const x = [this.#f]; } }");
+ parse("class C { #f = 1; method() { const x = {y: this.#f}; } }");
+ parse("class C { #pm() { this.#pm(); } }");
+
+ parse("class C { static #f = 1; static method() { this.#f = 2; } }");
+ parse("class C { static #f = 1; static method() { C.#f = 2; } }");
+ parse("class C { static #f = 1; static method() { const x = [this.#f]; } }");
+ parse("class C { static #f = 1; static method() { const x = {y: C.#f}; } }");
+ parse("class C { static #pm() { this.#pm(); } }");
+ parse("class C { static #pm() { C.#pm(); } }");
+ }
+
+ @Test
+ public void testPrivateProperty_nestedClasses() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse(
+ lines(
+ "class Outer {",
+ " #of1;",
+ " #of2;",
+ " om() {",
+ " this.#of1;",
+ " this.#of2;",
+ " class Inner {",
+ " #if1;",
+ " #if2;",
+ " im() {",
+ " this.#of1;",
+ " this.#of2;",
+ " this.#if1;",
+ " this.#if2;",
+ " }",
+ " }",
+ " }",
+ "}"));
+ }
+
+ @Test
+ public void testPrivateProperty_nestedClasses_invalid_referenceInnerFieldFromOuter() {
+ parseError(
+ lines(
+ "class Outer {",
+ " #of1;",
+ " #of2;",
+ " om() {",
+ " this.#of1;",
+ " this.#of2;",
+ " this.#if1;", // Invalid
+ " this.#if2;", // Invalid
+ " class Inner {",
+ " #if1;",
+ " #if2;",
+ " im() {",
+ " this.#of1;",
+ " this.#of2;",
+ " this.#if1;",
+ " this.#if2;",
+ " }",
+ " }",
+ " }",
+ "}"),
+ PRIVATE_FIELD_NOT_DEFINED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ }
+
+ @Test
+ public void testPrivateProperty_classMethodsReferencingOtherPrivatePropViaOptionalChain() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse("class C { #f = 1; method() { const t = this; t?.#f; } }");
+ parse("class C { #pm() { const t = this; t?.#pm(); } }");
+ }
+
+ @Test
+ public void testPrivateProperty_destructuredAssignmentDefaultValue() {
+ parse("class C { #px = 1; method(x) { const { y = this.#px } = x; } }");
+ parse("class C { #px = 1; method(x) { const { y: z = this.#px } = x; } }");
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_nonExistentPrivateProp() {
+ parseError("class C { #f = this.#missing; }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError("class C { method() { this.#missing = 1; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { #f = 1; method() { this.#f = this.#missing; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError("class C { method() { const t = this; t.#missing; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError("class C { method() { const t = this; t?.#missing; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { const f = () => { this.#missing; }; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { class D { method() { this.#missing = 1; } } } }",
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { class D { method() { const x = this.#missing; } } } }",
+ PRIVATE_FIELD_NOT_DEFINED);
+
+ parseError("class C { method() { this.#missing(); } }", PRIVATE_METHOD_NOT_DEFINED);
+
+ parseError("class C { static #f = this.#missing; }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError("class C { static #f = C.#missing; }", PRIVATE_FIELD_NOT_DEFINED);
+
+ parseError("class C { static { this.#missing; } }", PRIVATE_FIELD_NOT_DEFINED);
+ parseError("class C { static { C.#missing; } }", PRIVATE_FIELD_NOT_DEFINED);
+
+ parseError("class C { static { this.#missing(); } }", PRIVATE_METHOD_NOT_DEFINED);
+ parseError("class C { static { C.#missing(); } }", PRIVATE_METHOD_NOT_DEFINED);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_deletePrivateField() {
+ parseError("class C { #f = 1; method() { delete this.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError(
+ "class C { #f = 1; method() { const t = this; delete t.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError(
+ "class C { #f = 1; method() { const t = this; delete t?.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError(
+ "class C { #f = 1; method() { const a = {b: this}; delete ((a.b).#f); } }",
+ PRIVATE_FIELD_DELETED);
+
+ parseError(
+ "class C { static #f = 1; static method() { delete this.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError(
+ "class C { static #f = 1; static method() { delete C.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError("class C { static #f = 1; static { delete this.#f; } }", PRIVATE_FIELD_DELETED);
+ parseError("class C { static #f = 1; static { delete C.#f; } }", PRIVATE_FIELD_DELETED);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_deleteUndeclaredPrivateField() {
+ parseError(
+ "class C { method() { delete this.#f; } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { const t = this; delete t.#f; } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { const t = this; delete t?.#f; } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { method() { const a = {b: this}; delete ((a.b).#f); } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+
+ parseError(
+ "class C { static method() { delete this.#f; } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { static method() { delete C.#f; } }",
+ PRIVATE_FIELD_DELETED,
+ PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { static { delete this.#f; } }", PRIVATE_FIELD_DELETED, PRIVATE_FIELD_NOT_DEFINED);
+ parseError(
+ "class C { static { delete C.#f; } }", PRIVATE_FIELD_DELETED, PRIVATE_FIELD_NOT_DEFINED);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_objectLiteralProperty() {
+ parseError("const x = { #pf: 1 }", INVALID_PRIVATE_ID);
+ parseError("const x = { #pm() {} }", INVALID_PRIVATE_ID);
+ parseError("const x = { get #pp() {} }", INVALID_PRIVATE_ID);
+ parseError("const x = { set #ps(x) {} }", INVALID_PRIVATE_ID);
+
+ parseError("class C { method() { const x = { #pf: 1 }; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const x = { #pm() {} }; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const x = { get #pp() {} }; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const x = { set #ps(x) {} }; } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_destructuredAssignment() {
+ parseError("const { #px } = x;", INVALID_PRIVATE_ID);
+ parseError("const { x: #px } = x;", INVALID_PRIVATE_ID);
+ parseError("const { x = #px } = x;", INVALID_PRIVATE_ID);
+ parseError("const { x: y = #px } = x;", INVALID_PRIVATE_ID);
+
+ parseError("class C { method() { const { #px } = x; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const { x: #px } = x; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const { x = #px } = x; } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { const { x: y = #px } = x; } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_variableName() {
+ parseError("const #pv = 1;", INVALID_PRIVATE_ID);
+
+ parseError("class C { method() { const #pv = 1; } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_functionName() {
+ parseError("function #pf() {}", INVALID_PRIVATE_ID);
+ parseError("function* #pf() {}", INVALID_PRIVATE_ID);
+ parseError("async function #pf() {}", INVALID_PRIVATE_ID);
+ parseError("async function* #pf() {}", INVALID_PRIVATE_ID);
+
+ parseError("class C { method() { function #pf() {} } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { function* #pf() {} } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { async function #pf() {} } }", INVALID_PRIVATE_ID);
+ parseError("class C { method() { async function* #pf() {} } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_paramName() {
+ parseError("function f(#p) {}", INVALID_PRIVATE_ID);
+ parseError("class C { method(#p) {} }", INVALID_PRIVATE_ID);
+ parseError("class C { #p; method(#p) {} }", INVALID_PRIVATE_ID);
+
+ parseError("class C { method() { function f(#p) {} } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_className() {
+ parseError("class #PC {}", INVALID_PRIVATE_ID);
+ parseError("class C extends #PSC {}", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_referencePrivateOutsideClass() {
+ parseError("class C { #f = 1; } const c = new C(); c.#f;", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_referencePrivateOutsideClassViaOptionalChain() {
+ parseError("class C { #f = 1; } const c = new C(); c?.#f;", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_referencePrivatePropFromObjectLiteral() {
+ parseError("const o = {}; o.#f = 1;", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_referencePrivatePropFromObjectLiteralViaOptionalChain() {
+ parseError("const o = {}; o?.#f;", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_importName() {
+ parseError("import #pi from './someModule'", INVALID_PRIVATE_ID);
+ parseError("import {#pi} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("import {x as #pi} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("import * as #pi from './someModule'", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_exportName() {
+ parseError("export const #px = 1", INVALID_PRIVATE_ID);
+ parseError("export var #px = 1", INVALID_PRIVATE_ID);
+ parseError("export function #pf() {}", INVALID_PRIVATE_ID);
+ parseError("export class #pc {}", INVALID_PRIVATE_ID);
+ parseError("export {#px}", INVALID_PRIVATE_ID);
+ parseError("export {x as #px}", INVALID_PRIVATE_ID);
+ parseError("export {#px as default}", INVALID_PRIVATE_ID);
+ parseError("export {#y as class}", INVALID_PRIVATE_ID);
+
+ parseError("export {x as #px} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("export {default as #pd} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("export {#px as default} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("export {#pc as class} from './someModule'", INVALID_PRIVATE_ID);
+ parseError("export {#px} from './someModule'", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_invalid_labeledStatement() {
+ parseError("#pl: while (true) {}", INVALID_PRIVATE_ID);
+ parseError("while (true) { break #pl; }", INVALID_PRIVATE_ID, "undefined label \"#pl\"");
+ parseError("while (true) { continue #pl; }", INVALID_PRIVATE_ID, "undefined label \"#pl\"");
+
+ parseError("class C { method() { #pl: while (true) {} } }", INVALID_PRIVATE_ID);
+ parseError(
+ "class C { method() { while (true) { break #pl; } } }",
+ INVALID_PRIVATE_ID,
+ "undefined label \"#pl\"");
+ parseError(
+ "class C { method() { while (true) { continue #pl; } } }",
+ INVALID_PRIVATE_ID,
+ "undefined label \"#pl\"");
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_valid() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parse("class C { #f = 1; static isC(x) { return #f in x; } }");
+ parse("class C { #f = this; m() { return #f in this.#f; } }");
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_linenocharno() {
+ Node n =
+ parse(
+ lines(
+ "class C {",
+ " #f = 1;",
+ " static isC(x) {",
+ " return #f in x;",
+ " }",
+ "}"))
+ .getFirstChild();
+
+ Node members = NodeUtil.getClassMembers(n);
+
+ Node staticMethod = members.getLastChild();
+ assertNode(staticMethod).hasType(Token.MEMBER_FUNCTION_DEF);
+ Node methodBlock = staticMethod.getFirstChild().getLastChild();
+ assertNode(methodBlock).hasType(Token.BLOCK);
+ Node returnStatement = methodBlock.getFirstChild();
+ assertNode(returnStatement).hasType(Token.RETURN);
+ Node inExpression = returnStatement.getFirstChild();
+ assertNode(inExpression).hasType(Token.IN);
+ Node inExpressionLeft = inExpression.getFirstChild();
+ assertNode(inExpressionLeft).hasType(Token.NAME);
+ assertNode(inExpressionLeft).hasStringThat().isEqualTo("#f");
+ assertThat(inExpressionLeft.getLineno()).isEqualTo(4);
+ assertThat(inExpressionLeft.getCharno()).isEqualTo(11);
+ assertThat(inExpressionLeft.getLength()).isEqualTo(2);
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_invalid_nonExistentPrivateProp() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parseError("class C { static isC(x) { return #missing in x; } }", PRIVATE_FIELD_NOT_DEFINED);
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_invalid_privatePropOnRhs() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parseError("class C { #f = 1; static isC(x) { return #f in #f; } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_invalid_surroundingParenthesis() {
+ expectFeatures(Feature.PRIVATE_CLASS_PROPERTIES);
+
+ parseError("class C { #f = 1; static isC(x) { return (#f) in x; } }", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_inOperatorWithPrivateProp_invalid_notInClass() {
+ parseError("const o = {}; if (#f in o) {}", INVALID_PRIVATE_ID);
+ }
+
+ @Test
+ public void testPrivateProperty_stringKeyThatLooksLikePrivateProp() {
+ parse("const o = {'#notAPrivateProp': 1};");
+ }
+
@Test
public void testEmptyClassStaticBlock() {
parse("class C { static { } }");
diff --git a/test/com/google/javascript/jscomp/parsing/parser/FeatureSetTest.java b/test/com/google/javascript/jscomp/parsing/parser/FeatureSetTest.java
index 466a35ab977..b19b19834b4 100644
--- a/test/com/google/javascript/jscomp/parsing/parser/FeatureSetTest.java
+++ b/test/com/google/javascript/jscomp/parsing/parser/FeatureSetTest.java
@@ -104,8 +104,7 @@ public void testEsNextAndNewer() {
// from these `FeatureSet`s.
assertThat(FeatureSet.ES_NEXT.version()).isEqualTo("es_next");
assertThat(FeatureSet.ES_UNSTABLE.version()).isEqualTo("es_unstable");
- assertThat(FeatureSet.ES_UNSUPPORTED.version())
- .isEqualTo("es_unstable"); // no unsupported features at this time
+ assertThat(FeatureSet.ES_UNSUPPORTED.version()).isEqualTo("es_unsupported");
}
@Test
privatePropNames
such
+ * that outer class private properties are available to inner classes.
+ */
+ private void validatePrivatePropertyUsage(Set
+ * classMembers
.
+ *
+ *