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 privatePropNames such + * that outer class private properties are available to inner classes. + */ + private void validatePrivatePropertyUsage(Set privatePropNames, Node classMembers) { + checkState(classMembers.isClassMembers()); + if (!classMembers.hasChildren()) { + return; + } - boolean childIsCtor = validateClassConstructorMember(child); // Has side-effects. - if (childIsCtor) { - if (hasConstructor) { + // Uses a stack object instead of recursion to avoid a stack overflow. + ArrayDeque visitStack = new ArrayDeque<>(); + visitStack.addLast(classMembers.getFirstChild()); + while (!visitStack.isEmpty()) { + Node node = visitStack.removeLast(); + + // Check that these usages reference a private property declared in the class: + // this.#privateProp + // x.#privateProp + // x?.#privateProp + // #privateProp in x + if (node.isPrivateIdentifier() + && (node.isGetProp() + || node.isOptChainGetProp() + || (node.isName() && node.hasParent() && node.getParent().isIn())) + && !privatePropNames.contains(node.getString())) { + String propType = node.getParent().isCall() ? "methods" : "fields"; + errorReporter.error( + "Private " + propType + " must be declared in an enclosing class", + sourceName, + node.getLineno(), + node.getCharno()); + } + + // Checks that private fields are not deleted: + // delete this.#privateProp + // delete x.#privateProp + // delete x?.#privateProp + if (node.isDelProp()) { + Node firstChild = checkNotNull(node.getFirstChild()); + if (firstChild.isPrivateIdentifier() + && (firstChild.isGetProp() || firstChild.isOptChainGetProp())) { errorReporter.error( - "Class may have only one constructor.", // - sourceName, - lineno(child), - charno(child)); + "Private fields cannot be deleted", sourceName, node.getLineno(), node.getCharno()); } - hasConstructor = true; } - body.addChildToBack(transform(child)); + if (node.isClassMembers()) { + // Recurse into the inner class with the outer and inner class private properties. + Set innerClassPrivatePropNames = getPrivatePropsAndValidateUniqueness(node); + innerClassPrivatePropNames.addAll(privatePropNames); + validatePrivatePropertyUsage( + Collections.unmodifiableSet(innerClassPrivatePropNames), node); + } else { + if (node.getNext() != null) { + visitStack.addLast(node.getNext()); + } + if (node.hasChildren()) { + visitStack.addLast(node.getFirstChild()); + } + } } + } - Node classNode = newNode(Token.CLASS, name, superClass, body); - attachPossibleTrailingComment(classNode, tree.getEnd()); + /** + * Returns the set of private properties declared in the class corresponding to + * classMembers. + * + *

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 getPrivatePropsAndValidateUniqueness(Node classMembers) { + // Track all this to ensure we don't have duplicate private getters and setters. + Set privateGetterNames = new TreeSet<>(); + Set privateSetterNames = new TreeSet<>(); + Set privateStaticGetterNames = new TreeSet<>(); + Set privateStaticSetterNames = new TreeSet<>(); + Set privateFieldAndMethodNames = new TreeSet<>(); + + Set privatePropNames = new TreeSet<>(); + for (Node curNode = classMembers.getFirstChild(); + curNode != null; + curNode = curNode.getNext()) { + if (!curNode.isPrivateIdentifier()) { + continue; + } - return classNode; + String propName = curNode.getString(); + checkState( + curNode.isGetterDef() + || curNode.isSetterDef() + || curNode.isMemberFieldDef() + || curNode.isMemberFunctionDef(), + "Private property '%s' has an unsupported token type: %s", + propName, + curNode.getToken()); + + boolean isStatic = curNode.isStaticMember(); + boolean alreadyDeclared = privatePropNames.contains(propName); + switch (curNode.getToken()) { + case GETTER_DEF: + if (alreadyDeclared) { + boolean alreadyDeclaredIsOtherThanItsSetter = + privateFieldAndMethodNames.contains(propName) + || privateGetterNames.contains(propName) + || privateStaticGetterNames.contains(propName) + || (isStatic + ? privateSetterNames.contains(propName) + : privateStaticSetterNames.contains(propName)); + checkState( + alreadyDeclaredIsOtherThanItsSetter + || (isStatic + ? privateStaticSetterNames.contains(propName) + : privateSetterNames.contains(propName))); + alreadyDeclared = alreadyDeclaredIsOtherThanItsSetter; + } + + if (isStatic) { + privateStaticGetterNames.add(propName); + } else { + privateGetterNames.add(propName); + } + break; + case SETTER_DEF: + if (alreadyDeclared) { + boolean alreadyDeclaredIsOtherThanItsGetter = + privateFieldAndMethodNames.contains(propName) + || privateSetterNames.contains(propName) + || privateStaticSetterNames.contains(propName) + || (isStatic + ? privateGetterNames.contains(propName) + : privateStaticGetterNames.contains(propName)); + checkState( + alreadyDeclaredIsOtherThanItsGetter + || (isStatic + ? privateStaticGetterNames.contains(propName) + : privateGetterNames.contains(propName))); + alreadyDeclared = alreadyDeclaredIsOtherThanItsGetter; + } + + if (curNode.isStaticMember()) { + privateStaticSetterNames.add(propName); + } else { + privateSetterNames.add(propName); + } + break; + case MEMBER_FIELD_DEF: + case MEMBER_FUNCTION_DEF: + privateFieldAndMethodNames.add(propName); + break; + default: + break; + } + if (alreadyDeclared) { + errorReporter.error( + "Identifier '" + propName + "' has already been declared", + sourceName, + curNode.getLineno(), + curNode.getCharno()); + } + + privatePropNames.add(propName); + } + return privatePropNames; } /** Returns {@code true} iff this member is a legal class constructor. */ @@ -3005,13 +3328,14 @@ Node processExportDecl(ExportDeclarationTree tree) { } Node processExportSpec(ExportSpecifierTree tree) { - Node importedName = processName(tree.importedName, Token.NAME); + Node importedName = processName(tree.importedName, Token.NAME, IdentifierType.STANDARD); Node exportSpec = newNode(Token.EXPORT_SPEC, importedName); if (tree.destinationName == null) { exportSpec.setShorthandProperty(true); exportSpec.addChildToBack(importedName.cloneTree()); } else { - Node destinationName = processName(tree.destinationName, Token.NAME); + Node destinationName = + processName(tree.destinationName, Token.NAME, IdentifierType.STANDARD); exportSpec.addChildToBack(destinationName); } return exportSpec; @@ -3029,7 +3353,8 @@ Node processImportDecl(ImportDeclarationTree tree) { setSourceInfo(secondChild, tree); } else { secondChild = - newStringNode(Token.IMPORT_STAR, getIdentifierValue(tree.nameSpaceImportIdentifier)); + newStringNodeFromIdentifier( + Token.IMPORT_STAR, IdentifierType.STANDARD, tree.nameSpaceImportIdentifier); setSourceInfo(secondChild, tree.nameSpaceImportIdentifier); } Node thirdChild = processString(tree.moduleSpecifier); @@ -3038,13 +3363,14 @@ Node processImportDecl(ImportDeclarationTree tree) { } Node processImportSpec(ImportSpecifierTree tree) { - Node importedName = processName(tree.importedName, Token.NAME); + Node importedName = processName(tree.importedName, Token.NAME, IdentifierType.STANDARD); Node importSpec = newNode(Token.IMPORT_SPEC, importedName); if (tree.destinationName == null) { importSpec.setShorthandProperty(true); importSpec.addChildToBack(importedName.cloneTree()); } else { - importSpec.addChildToBack(processName(tree.destinationName, Token.NAME)); + importSpec.addChildToBack( + processName(tree.destinationName, Token.NAME, IdentifierType.STANDARD)); } return importSpec; } @@ -3196,7 +3522,11 @@ public Node process(ParseTree node) { case PAREN_EXPRESSION: return processParenthesizedExpression(node.asParenExpression()); case IDENTIFIER_EXPRESSION: - return processName(node.asIdentifierExpression()); + return processName( + node.asIdentifierExpression(), + privateIdLhsOfInScope.inScope() + ? IdentifierType.CAN_BE_PRIVATE + : IdentifierType.STANDARD); case NEW_EXPRESSION: return processNewExpression(node.asNewExpression()); case OBJECT_LITERAL_EXPRESSION: @@ -3965,9 +4295,9 @@ Node newStringNode(Token type, String value) { return Node.newString(type, value).clonePropsFrom(templateNode); } - /** Creates a new string node and attaches any pending JSDoc comments for it. */ - Node newStringNodeWithNonJSDocComment(Token type, String value, SourcePosition start) { - Node node = newStringNode(type, value); + /** Attaches any pending JSDoc comments to the given node. */ + @CanIgnoreReturnValue + Node maybeAddNonJsDocComment(Node node, SourcePosition start) { NonJSDocComment comment = parseNonJSDocCommentAt(start, false); if (comment != null) { node.setNonJSDocComment(comment); diff --git a/src/com/google/javascript/jscomp/parsing/parser/FeatureSet.java b/src/com/google/javascript/jscomp/parsing/parser/FeatureSet.java index b35df639d9f..39d4caecdf1 100644 --- a/src/com/google/javascript/jscomp/parsing/parser/FeatureSet.java +++ b/src/com/google/javascript/jscomp/parsing/parser/FeatureSet.java @@ -268,6 +268,8 @@ public enum Feature { CLASS_STATIC_BLOCK("Class static block", LangVersion.ES_UNSTABLE), // ES_UNSUPPORTED: Features that we can parse, but not yet supported in all checks + // Part of ES2022. Support will improve as implementation progresses. + PRIVATE_CLASS_PROPERTIES("Private class properties", LangVersion.ES_UNSUPPORTED), // TypeScript type syntax that will never be implemented in browsers. Only used as an indicator // to the CodeGenerator that it should handle type syntax. diff --git a/src/com/google/javascript/jscomp/parsing/parser/IdentifierToken.java b/src/com/google/javascript/jscomp/parsing/parser/IdentifierToken.java index a75eb3b8481..ab8063ef732 100644 --- a/src/com/google/javascript/jscomp/parsing/parser/IdentifierToken.java +++ b/src/com/google/javascript/jscomp/parsing/parser/IdentifierToken.java @@ -16,15 +16,19 @@ package com.google.javascript.jscomp.parsing.parser; +import static com.google.common.base.Preconditions.checkState; + import com.google.javascript.jscomp.parsing.parser.util.SourceRange; /** A token representing an identifier. */ public class IdentifierToken extends Token { private final String value; + private final boolean privateIdentifier; public IdentifierToken(SourceRange location, String value) { super(TokenType.IDENTIFIER, location); this.value = value; + privateIdentifier = value.startsWith("#"); } @Override @@ -41,12 +45,26 @@ public boolean valueEquals(String str) { } /** - * Gets the value of the identifier. + * Gets the value of the identifier assuring that it is not a private identifier. + * + *

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