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

com.google.javascript.jscomp.RewriteClassMembers 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 2021 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.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.Deque;
import org.jspecify.nullness.Nullable;

/** Replaces the ES2022 class fields and class static blocks with constructor declaration. */
public final class RewriteClassMembers implements NodeTraversal.ScopedCallback, CompilerPass {

  private final AbstractCompiler compiler;
  private final AstFactory astFactory;
  private final SynthesizeExplicitConstructors ctorCreator;
  private final Deque classStack;

  public RewriteClassMembers(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.astFactory = compiler.createAstFactory();
    this.ctorCreator = new SynthesizeExplicitConstructors(compiler);
    this.classStack = new ArrayDeque<>();
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
    TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(
        compiler, Feature.PUBLIC_CLASS_FIELDS, Feature.CLASS_STATIC_BLOCK);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case SCRIPT:
        FeatureSet scriptFeatures = NodeUtil.getFeatureSetOfScript(n);
        return scriptFeatures == null
            || scriptFeatures.contains(Feature.PUBLIC_CLASS_FIELDS)
            || scriptFeatures.contains(Feature.CLASS_STATIC_BLOCK);
      case CLASS:
        Node classNameNode = NodeUtil.getNameNode(n);

        if (classNameNode == null) {
          t.report(
              n, TranspilationUtil.CANNOT_CONVERT_YET, "Anonymous classes with ES2022 features");
          return false;
        }

        @Nullable Node classInsertionPoint = getStatementDeclaringClass(n, classNameNode);

        if (classInsertionPoint == null) {
          t.report(
              n,
              TranspilationUtil.CANNOT_CONVERT_YET,
              "Class in a non-extractable location with ES2022 features");
          return false;
        }

        if (!n.getFirstChild().isEmpty()
            && !classNameNode.matchesQualifiedName(n.getFirstChild())) {
          // we do not allow `let x = class C {}` where the names inside the class can be shadowed
          // at this time
          t.report(n, TranspilationUtil.CANNOT_CONVERT_YET, "Classes with possible name shadowing");
          return false;
        }

        classStack.push(new ClassRecord(n, classNameNode.getQualifiedName(), classInsertionPoint));
        break;
      case COMPUTED_FIELD_DEF:
        checkState(!classStack.isEmpty());
        t.report(n, TranspilationUtil.CANNOT_CONVERT_YET, "Computed fields");
        classStack.peek().cannotConvert = true;
        return false;
      case MEMBER_FIELD_DEF:
        checkState(!classStack.isEmpty());
        if (NodeUtil.referencesEnclosingReceiver(n)) {
          t.report(n, TranspilationUtil.CANNOT_CONVERT_YET, "Member references this or super");
          classStack.peek().cannotConvert = true;
          break;
        }
        classStack.peek().enterField(n);
        break;
      case BLOCK:
        if (!NodeUtil.isClassStaticBlock(n)) {
          break;
        }
        if (NodeUtil.referencesEnclosingReceiver(n)) {
          t.report(n, TranspilationUtil.CANNOT_CONVERT_YET, "Member references this or super");
          classStack.peek().cannotConvert = true;
          break;
        }
        checkState(!classStack.isEmpty());
        classStack.peek().recordStaticBlock(n);
        break;
      case NAME:
        for (ClassRecord record : classStack) {
          // For now, we are just processing these names as strings, and so we will also give
          // CANNOT_CONVERT_YET errors for patterns that technically can be simply inlined, such as:
          // class C {
          //    y = (x) => x;
          //    constructor(x) {}
          // }
          // Either using scopes to be more precise or just doing renaming for all conflicting
          // constructor declarations would addresss this issue.
          record.potentiallyRecordNameInRhs(n);
        }
        break;
      default:
        break;
    }
    return true;
  }

  @Override
  public void enterScope(NodeTraversal t) {
    Node scopeRoot = t.getScopeRoot();
    if (NodeUtil.isFunctionBlock(scopeRoot) && NodeUtil.isEs6Constructor(scopeRoot.getParent())) {
      classStack.peek().recordConstructorScope(t.getScope());
    }
  }

  @Override
  public void exitScope(NodeTraversal t) {}

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getToken()) {
      case CLASS:
        visitClass(t);
        return;
      case MEMBER_FIELD_DEF:
        classStack.peek().exitField();
        return;
      default:
        return;
    }
  }

  /** Transpile the actual class members themselves */
  private void visitClass(NodeTraversal t) {
    ClassRecord currClassRecord = classStack.pop();
    if (currClassRecord.cannotConvert) {
      return;
    }

    rewriteInstanceMembers(t, currClassRecord);
    rewriteStaticMembers(t, currClassRecord);
  }

  /** Rewrites and moves all instance fields */
  private void rewriteInstanceMembers(NodeTraversal t, ClassRecord record) {
    Deque instanceMembers = record.instanceMembers;

    if (instanceMembers.isEmpty()) {
      return;
    }
    ctorCreator.synthesizeClassConstructorIfMissing(t, record.classNode);
    Node ctor = NodeUtil.getEs6ClassConstructorMemberFunctionDef(record.classNode);
    Node ctorBlock = ctor.getFirstChild().getLastChild();
    Node insertionPoint = findInitialInstanceInsertionPoint(ctorBlock);
    ImmutableSet ctorDefinedNames = record.getConstructorDefinedNames();

    while (!instanceMembers.isEmpty()) {
      Node instanceMember = instanceMembers.pop();
      checkState(instanceMember.isMemberFieldDef());

      for (Node nameInRhs : record.referencedNamesByMember.get(instanceMember)) {
        String name = nameInRhs.getString();
        if (ctorDefinedNames.contains(name)) {
          t.report(
              nameInRhs,
              TranspilationUtil.CANNOT_CONVERT_YET,
              "Initializer referencing identifier '" + name + "' declared in constructor");
          return;
        }
      }

      Node thisNode = astFactory.createThisForEs6ClassMember(instanceMember);

      Node transpiledNode = convNonCompFieldToGetProp(thisNode, instanceMember.detach());
      if (insertionPoint == ctorBlock) { // insert the field at the beginning of the block, no super
        ctorBlock.addChildToFront(transpiledNode);
      } else {
        transpiledNode.insertAfter(insertionPoint);
      }
      t.reportCodeChange(); // we moved the field from the class body
      t.reportCodeChange(ctorBlock); // to the constructor, so we need both
    }
  }

  /** Rewrites and moves all static blocks and fields */
  private void rewriteStaticMembers(NodeTraversal t, ClassRecord record) {
    Deque staticMembers = record.staticMembers;

    while (!staticMembers.isEmpty()) {
      Node staticMember = staticMembers.pop();
      // if the name is a property access, we want the whole chain of accesses, while for other
      // cases we only want the name node
      Node nameToUse =
          astFactory.createQNameWithUnknownType(record.classNameString).srcrefTree(staticMember);

      Node transpiledNode;

      switch (staticMember.getToken()) {
        case BLOCK:
          if (!NodeUtil.getVarsDeclaredInBranch(staticMember).isEmpty()) {
            t.report(staticMember, TranspilationUtil.CANNOT_CONVERT_YET, "Var in static block");
          }
          transpiledNode = staticMember.detach();
          break;
        case MEMBER_FIELD_DEF:
          transpiledNode = convNonCompFieldToGetProp(nameToUse, staticMember.detach());
          break;
        default:
          throw new IllegalStateException(String.valueOf(staticMember));
      }
      transpiledNode.insertAfter(record.classInsertionPoint);
      t.reportCodeChange();
    }
  }

  /**
   * Creates a node that represents receiver.key = value; where the key and value comes from the
   * non-computed field
   */
  private Node convNonCompFieldToGetProp(Node receiver, Node noncomputedField) {
    checkArgument(noncomputedField.isMemberFieldDef());
    checkArgument(noncomputedField.getParent() == null, noncomputedField);
    checkArgument(receiver.getParent() == null, receiver);
    Node getProp =
        astFactory.createGetProp(
            receiver, noncomputedField.getString(), AstFactory.type(noncomputedField));
    Node fieldValue = noncomputedField.getFirstChild();
    Node result =
        (fieldValue != null)
            ? astFactory.createAssignStatement(getProp, fieldValue.detach())
            : astFactory.exprResult(getProp);
    result.srcrefTreeIfMissing(noncomputedField);
    return result;
  }

  /**
   * Finds the location in the constructor to put the transpiled instance fields
   *
   * 

Returns the constructor body if there is no super() call so the field can be put at the * beginning of the class * *

Returns the super() call otherwise so the field can be put after the super() call */ private Node findInitialInstanceInsertionPoint(Node ctorBlock) { if (NodeUtil.referencesSuper(ctorBlock)) { // will use the fact that if there is super in the constructor, the first appearance of // super // must be the super call for (Node stmt = ctorBlock.getFirstChild(); stmt != null; stmt = stmt.getNext()) { if (NodeUtil.isExprCall(stmt) && stmt.getFirstFirstChild().isSuper()) { return stmt; } } } return ctorBlock; // in case the super loop doesn't work, insert at beginning of block } /** * Gets the location of the statement declaring the class * * @return null if the class cannot be extracted */ private @Nullable Node getStatementDeclaringClass(Node classNode, Node classNameNode) { if (NodeUtil.isClassDeclaration(classNode)) { // `class C {}` -> can use `C.staticMember` to extract static fields checkState(NodeUtil.isStatement(classNode)); return classNode; } final Node parent = classNode.getParent(); if (parent.isName()) { // `let C = class {};` // We can use `C.staticMemberName = ...` to extract static fields checkState(parent == classNameNode); checkState(NodeUtil.isStatement(classNameNode.getParent())); return classNameNode.getParent(); } if (parent.isAssign() && parent.getFirstChild() == classNameNode && parent.getParent().isExprResult()) { // `something.C = class {}` // we can use `something.C.staticMemberName = ...` to extract static fields checkState(NodeUtil.isStatement(classNameNode.getGrandparent())); return classNameNode.getGrandparent(); } return null; } /** * Accumulates information about different classes while going down the AST in shouldTraverse() */ private static final class ClassRecord { // During traversal, contains the current member being traversed. After traversal, always null @Nullable Node currentMember; boolean cannotConvert; // Instance fields final Deque instanceMembers = new ArrayDeque<>(); // Static fields + static blocks final Deque staticMembers = new ArrayDeque<>(); // Mapping from MEMBER_FIELD_DEF (& COMPUTED_FIELD_DEF) nodes to all name nodes in that RHS final SetMultimap referencedNamesByMember = MultimapBuilder.linkedHashKeys().hashSetValues().build(); // Set of all the Vars defined in the constructor arguments scope and constructor body scope ImmutableSet constructorVars = ImmutableSet.of(); final Node classNode; final String classNameString; final Node classInsertionPoint; ClassRecord(Node classNode, String classNameString, Node classInsertionPoint) { this.classNode = classNode; this.classNameString = classNameString; this.classInsertionPoint = classInsertionPoint; } void enterField(Node field) { checkArgument(field.isComputedFieldDef() || field.isMemberFieldDef()); if (field.isStaticMember()) { staticMembers.push(field); } else { instanceMembers.push(field); } currentMember = field; } void exitField() { currentMember = null; } void recordStaticBlock(Node block) { checkArgument(NodeUtil.isClassStaticBlock(block)); staticMembers.push(block); } void potentiallyRecordNameInRhs(Node nameNode) { checkArgument(nameNode.isName()); if (currentMember == null) { return; } checkState(currentMember.isMemberFieldDef()); referencedNamesByMember.put(currentMember, nameNode); } void recordConstructorScope(Scope s) { checkArgument(s.isFunctionBlockScope(), s); checkState(constructorVars.isEmpty(), constructorVars); ImmutableSet.Builder builder = ImmutableSet.builder(); builder.addAll(s.getAllSymbols()); Scope argsScope = s.getParent(); builder.addAll(argsScope.getAllSymbols()); constructorVars = builder.build(); } ImmutableSet getConstructorDefinedNames() { return constructorVars.stream().map(Var::getName).collect(toImmutableSet()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy