All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.CheckSuper Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2016 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.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableList;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import org.jspecify.nullness.Nullable;

/**
 * Check for errors related to the `super` keyword.
 *
 * 

NOTE: One might be tempted to make one or both of these refactoring improvements to this * class. * *

    *
  1. Use an inner class as the {@code Callback} instead of making the whole class implement it. *
  2. Use a {@code ScopedCallback} since we need to track function scopes here. *
* *

Unfortunately, this class is used with {@code CombinedCompilerPass}, which will pass instances * of it directly to {@code NodeTraversal.traverse()}. */ final class CheckSuper implements CompilerPass, NodeTraversal.Callback { static final DiagnosticType MISSING_CALL_TO_SUPER = DiagnosticType.error("JSC_MISSING_CALL_TO_SUPER", "constructor is missing a call to super()"); static final DiagnosticType THIS_BEFORE_SUPER = DiagnosticType.error("JSC_THIS_BEFORE_SUPER", "cannot access this before calling super()"); static final DiagnosticType SUPER_ACCESS_BEFORE_SUPER_CONSTRUCTOR = DiagnosticType.error( "JSC_SUPER_ACCESS_BEFORE_SUPER_CONSTRUCTOR", "cannot access super properties before calling super()"); static final DiagnosticType INVALID_SUPER_CALL = DiagnosticType.error( "JSC_INVALID_SUPER_CALL", "super() not allowed except in the constructor of a subclass"); // The JS spec allows calls to `super()` in an arrow function within a constructor, // as long as `super()` executes exactly once before any references to `this` or `super.prop`. // However, doing that makes it very hard to statically determine whether `super()` is being // called when it should be. // There's really no good reason to call `super()` in an arrow function. // It indicates that your code is overly complicated and you should refactor, so we will not // allow it. static final DiagnosticType SUPER_CALL_IN_ARROW = DiagnosticType.error( "JSC_SUPER_CALL_IN_ARROW", "closure-compiler does not allow calls to `super()` in arrow functions"); static final DiagnosticType INVALID_SUPER_USAGE = DiagnosticType.error( "JSC_INVALID_SUPER_USAGE", "''super'' may only be used in a call or property access"); static final DiagnosticType INVALID_SUPER_ACCESS = DiagnosticType.error( "JSC_INVALID_SUPER_ACCESS", "''super'' may only be accessed within a method"); static final DiagnosticType INVALID_SUPER_CALL_WITH_SUGGESTION = DiagnosticType.error( "JSC_INVALID_SUPER_CALL_WITH_SUGGESTION", "super() not allowed here. Did you mean super.{0}?"); private final AbstractCompiler compiler; public CheckSuper(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); checkState(this.contextStack.isEmpty(), ImmutableList.of(this.contextStack)); } private final ArrayDeque contextStack = new ArrayDeque<>(); @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case ROOT: checkState(this.contextStack.isEmpty()); this.contextStack.push(new SuperNotAllowedContext(n)); break; case FUNCTION: { Context currentContext = contextStack.peek(); Context newContext = getContextForFunctionNode(currentContext, n); if (newContext != currentContext) { this.contextStack.push(newContext); } } break; case BLOCK: // For class static blocks if (NodeUtil.isClassStaticBlock(n)) { Context currentContext = contextStack.peek(); Context newContext = new StaticBlockContext(n); if (newContext != currentContext) { this.contextStack.push(newContext); } } break; case CLASS: // TODO (user): For class fields default: break; } return true; } private Context getContextForFunctionNode(Context currentContext, Node fn) { if (NodeUtil.isMethodDeclaration(fn)) { if (NodeUtil.isEs6Constructor(fn)) { return new ConstructorContext(fn); } else { return new MethodContext(fn); } } else { // Arrow function context varies depending on the actual context, but // super is never allowed in normal functions. return fn.isArrowFunction() ? currentContext.getContextForArrowFunctionNode(fn) : new SuperNotAllowedContext(fn); } } @Override public void visit(NodeTraversal t, Node n, Node parent) { Context currentContext = checkNotNull(contextStack.peek()); switch (n.getToken()) { case SUPER: if (isSuperConstructorCall(n)) { // Note: defer recording the call in `currentContext.visitSuperConstructorCall` until // visitng the parent CALL node. This ensures any this/super references in the // call arguments are treated as invalid, pre-super() call, references. } else if (isSuperPropertyAccess(n)) { currentContext.visitSuperPropertyAccess(t, n); } else { // super used some way other than `super()`, `super.prop`, or `super[expr]`. t.report(n, INVALID_SUPER_USAGE); } break; case CALL: Node callee = n.getFirstChild(); if (callee.isSuper()) { currentContext.visitSuperConstructorCall(t, callee); } break; case THIS: currentContext.visitThis(t, n); break; case RETURN: currentContext.visitReturn(t, n); break; case ROOT: checkState(this.contextStack.size() == 1); break; default: break; } if (n == currentContext.getContextNode()) { currentContext.visitContextNode(t); this.contextStack.pop(); } } private boolean isSuperConstructorCall(Node superNode) { checkState(superNode.isSuper(), superNode); Node parent = superNode.getParent(); return parent.isCall() && superNode.isFirstChildOf(parent); } private boolean isSuperPropertyAccess(Node superNode) { checkState(superNode.isSuper(), superNode); Node parent = superNode.getParent(); return NodeUtil.isNormalGet(parent) && superNode.isFirstChildOf(parent); } /** Tracks lexical context and determines correct traversal behavior based on it. */ private abstract static class Context { private final Node contextNode; Context(Node contextNode) { this.contextNode = checkNotNull(contextNode); } Node getContextNode() { return contextNode; } /** The correct context for an arrow function depends on the enclosing context. */ abstract Context getContextForArrowFunctionNode(Node arrowFn); /** Handle a super constructor call. */ abstract void visitSuperConstructorCall(NodeTraversal t, Node superNode); /** Handle a super property reference. (`super.prop` or `super[expr]`) */ abstract void visitSuperPropertyAccess(NodeTraversal t, Node superNode); void visitThis(NodeTraversal t, Node thisNode) { // Ignored except in constructor methods. } void visitReturn(NodeTraversal t, Node returnNode) { // Ignored except in constructor methods. } void visitContextNode(NodeTraversal t) { // Called when visiting the root node of this context after all of its children have been // visited. } } /** Lexical context when not within a method, class, or object literal. */ private static class SuperNotAllowedContext extends Context { SuperNotAllowedContext(Node contextNode) { super(contextNode); } @Override Context getContextForArrowFunctionNode(Node arrowFn) { // Since super isn't allowed in this context, it isn't allowed in any of its arrow functions // either. return this; } @Override void visitSuperConstructorCall(NodeTraversal t, Node superNode) { // We're not within a constructor method. t.report(superNode, INVALID_SUPER_CALL); } @Override void visitSuperPropertyAccess(NodeTraversal t, Node superNode) { // We're not within a method. t.report(superNode, INVALID_SUPER_ACCESS); } } /** Lexicial context within a non-constructor method of either a class or an object literal. */ private static class MethodContext extends Context { private final boolean isClassMethod; MethodContext(Node functionNode) { super(functionNode); checkArgument( functionNode.isFunction() && NodeUtil.isMethodDeclaration(functionNode), functionNode); Node objLitOrClassMembers = checkNotNull(functionNode.getGrandparent()); if (objLitOrClassMembers.isObjectLit()) { isClassMethod = false; } else { checkState(objLitOrClassMembers.isClassMembers(), objLitOrClassMembers); isClassMethod = true; } } @Override Context getContextForArrowFunctionNode(Node arrowFn) { // In a method an arrow function keeps the same context as the method, but a non-arrow // function creates a new non-method context. return this; } @Override void visitSuperPropertyAccess(NodeTraversal t, Node superNode) { // super property access in a method is perfectly valid, so do not report an error. } @Override void visitSuperConstructorCall(NodeTraversal t, Node superNode) { if (isClassMethod) { // super() - not allowed in non-constructor methods. String propName = getPropertyName(); if (propName != null) { // Maybe the user was confused by other languages where super() invokes the // same method in the parent class? t.report(superNode, INVALID_SUPER_CALL_WITH_SUGGESTION, propName); } else { t.report(superNode, INVALID_SUPER_CALL); } } else { // object literal methods cannot contain super() calls t.report(superNode, INVALID_SUPER_CALL); } } /** Return the property name associated with the method, if any, otherwise NULL. */ @Nullable String getPropertyName() { Node parent = checkNotNull(getContextNode().getParent()); // ``` // class X { // propertyName() {} // get propertyName() {} // set propertyName(value) {} // [expression]() {} // no name for this one // } // ``` if (parent.isMemberFunctionDef() || parent.isGetterDef() || parent.isSetterDef()) { return parent.getString(); } else { return null; } } } /** Lexical context within a class constructor method. */ private static class ConstructorContext extends Context { final boolean hasParentClass; // Will be set to the first `super()` call that appears lexically, if any. @Nullable Node firstSuperCall = null; // Call to super() isn't required if the constructor returns a value. boolean returnsAValue = false; @Nullable Node thisAccessedBeforeSuper = null; @Nullable Node superPropertyAccessedBeforeSuperCall = null; ConstructorContext(Node contextNode) { super(contextNode); checkArgument(NodeUtil.isEs6Constructor(contextNode), contextNode); Node classNode = NodeUtil.getEnclosingClass(contextNode); hasParentClass = !classNode.getSecondChild().isEmpty(); } @Override Context getContextForArrowFunctionNode(Node arrowFn) { // Unlike normal methods, we need to create a separate context for arrow functions in // constructors. We need to distinguish between return statements and `super()` calls // that appear directly in the constructor and those that appear in arrow functions. return new ConstructorArrowContext(this, arrowFn); } @Override void visitSuperConstructorCall(NodeTraversal t, Node superNode) { if (firstSuperCall == null) { firstSuperCall = superNode; } } @Override void visitSuperPropertyAccess(NodeTraversal t, Node superNode) { if (firstSuperCall == null) { superPropertyAccessedBeforeSuperCall = superNode; } } @Override void visitThis(NodeTraversal t, Node thisNode) { if (firstSuperCall == null) { thisAccessedBeforeSuper = thisNode; } } @Override void visitReturn(NodeTraversal t, Node returnNode) { if (returnNode.hasChildren()) { returnsAValue = true; } } @Override void visitContextNode(NodeTraversal t) { // We've now visited the entire constructor, so we can decide whether to report errors. if (!hasParentClass) { if (firstSuperCall != null) { // calling `super()` only makes sense when there is a super class. t.report(firstSuperCall, INVALID_SUPER_CALL); } } else { // There is a parent class, so a call to `super()` is required unless the constructor // returns a value. if (firstSuperCall == null && !returnsAValue) { // The context node is the function itself , but ErrorToFixMapper expects the error to // be reported on the MEMBER_FUNCTION_DEF that is its parent. t.report(getContextNode().getParent(), MISSING_CALL_TO_SUPER); } if (thisAccessedBeforeSuper != null) { t.report(thisAccessedBeforeSuper, THIS_BEFORE_SUPER); } if (superPropertyAccessedBeforeSuperCall != null) { t.report(superPropertyAccessedBeforeSuperCall, SUPER_ACCESS_BEFORE_SUPER_CONSTRUCTOR); } } } } /** Lexical context within an arrow function enclosed by a class constructor method. */ private static class ConstructorArrowContext extends Context { private final ConstructorContext constructorContext; ConstructorArrowContext(ConstructorContext constructorContext, Node arrowFn) { super(arrowFn); this.constructorContext = checkNotNull(constructorContext); } @Override Context getContextForArrowFunctionNode(Node arrowFn) { // Arrow functions within arrow functions share the same context behavior. return this; } @Override void visitSuperConstructorCall(NodeTraversal t, Node superNode) { t.report(superNode, SUPER_CALL_IN_ARROW); // Tell the constructor context about this `super()` call in order to avoid confusing and // likely redundant error messages such as "missing super call" or "`this` accessed before // super()". constructorContext.visitSuperConstructorCall(t, superNode); } @Override void visitSuperPropertyAccess(NodeTraversal t, Node superNode) { // We will pretend the arrow function is immediately called, so it's as if the super // property reference appeared directly in the constructor. // The result is that you get an error for code like the following, even though it isn't // technically a JS error. // ``` // class Sub extends Base { // constructor() { // let arrow = () => super.prop; // ERROR: super.prop comes before super() // super(); // arrow(); // not really executed until here, though // } // } // ``` // This behavior is consistent with TypeScript. // In general there's no good reason to declare such arrow functions before calling `super()`. constructorContext.visitSuperPropertyAccess(t, superNode); } } /** Lexical context within a static initialization block in a class. */ private static class StaticBlockContext extends Context { StaticBlockContext(Node contextNode) { super(contextNode); checkArgument(NodeUtil.isClassStaticBlock(contextNode), contextNode); } @Override Context getContextForArrowFunctionNode(Node arrowFn) { // Arrow functions still use the same context as the class static block return this; } @Override void visitSuperConstructorCall(NodeTraversal t, Node superNode) { // We're not inside a constructor. t.report(superNode, INVALID_SUPER_CALL); } @Override void visitSuperPropertyAccess(NodeTraversal t, Node superNode) { // perfectly valid, nothing to do here } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy