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

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

/*
 * Copyright 2008 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 com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.AbstractCompiler.LifeCycleStage;
import com.google.javascript.jscomp.MakeDeclaredNamesUnique.BoilerplateRenamer;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 * The goal with this pass is to simplify the other passes, by making less complex statements.
 *
 * 

Starting with statements like: {@code var a = 0, b = foo();} * *

Which become: {@code var a = 0; var b = foo();} * *

The key here is only to break down things that help the other passes and can be put back * together in a form that is at least as small when all is said and done. * *

This pass currently does the following: * *

    *
  1. Simplifies the AST by splitting var/let/const statements, moving initializers out of for * loops, and converting whiles to fors. *
  2. Makes all variable names globally unique (extern or otherwise) so that no value is ever * shadowed (note: "arguments" may require special handling). *
  3. Removes duplicate variable declarations. *
  4. Marks constants with the IS_CONSTANT_NAME annotation. *
  5. Rewrite body of arrow function as a block. *
  6. Take var statements out from for-loop initializer. This: for(var a = 0;a<0;a++) {} becomes: * var a = 0; for(a;a<0;a++) {} *
*/ final class Normalize implements CompilerPass { private final AbstractCompiler compiler; private final AstFactory astFactory; private final boolean assertOnChange; Normalize(Builder builder) { this.compiler = builder.compiler; this.assertOnChange = builder.assertOnChange; this.astFactory = builder.compiler.createAstFactory(); } static Normalize createNormalizeForOptimizations(AbstractCompiler compiler) { // The default option values are the right ones for optimizations return builder(compiler).build(); } static Builder builder(AbstractCompiler compiler) { return new Builder(compiler); } /** Configures and builds a {@link Normalize} object. */ static class Builder { private final AbstractCompiler compiler; private boolean assertOnChange = false; Builder(AbstractCompiler compiler) { this.compiler = compiler; } /** * If the Normalize pass finds work to do, it will throw an exception. * *

This is intended for use in validating that an AST is already normalized. * *

This option is {@code false} by default. */ Builder assertOnChange(boolean assertOnChange) { this.assertOnChange = assertOnChange; return this; } Normalize build() { return new Normalize(this); } } static void normalizeSyntheticCode(AbstractCompiler compiler, Node js, String prefix) { NodeTraversal.traverse(compiler, js, new Normalize.NormalizeStatements(compiler, false)); NodeTraversal.traverse( compiler, js, MakeDeclaredNamesUnique.builder() .withRenamer( new BoilerplateRenamer( compiler.getCodingConvention(), compiler.getUniqueNameIdSupplier(), prefix)) .build()); } static Node parseAndNormalizeTestCode(AbstractCompiler compiler, String code) { Node js = compiler.parseTestCode(code); NodeTraversal.traverse(compiler, js, new Normalize.NormalizeStatements(compiler, false)); return js; } private void reportCodeChange(String changeDescription, Node n) { if (assertOnChange) { throw new IllegalStateException("Normalize constraints violated:\n" + changeDescription); } compiler.reportChangeToEnclosingScope(n); } @Override public void process(Node externs, Node root) { MakeDeclaredNamesUnique renamer = MakeDeclaredNamesUnique.builder().withAssertOnChange(assertOnChange).build(); NodeTraversal.traverseRoots(compiler, renamer, externs, root); NodeTraversal.traverseRoots( compiler, new NormalizeStatements(compiler, assertOnChange), externs, root); removeDuplicateDeclarations(externs, root); new PropagateConstantPropertyOverVars(compiler, assertOnChange).process(externs, root); if (!compiler.getLifeCycleStage().isNormalized()) { compiler.setLifeCycleStage(LifeCycleStage.NORMALIZED); } } /** Propagate constant annotations and IS_CONSTANT_NAME property over the Var graph. */ static class PropagateConstantPropertyOverVars extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final boolean assertOnChange; PropagateConstantPropertyOverVars(AbstractCompiler compiler, boolean forbidChanges) { this.compiler = compiler; this.assertOnChange = forbidChanges; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseRoots(compiler, this, externs, root); } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Note: Constant properties annotations are not propagated. if (!n.isName() || n.getString().isEmpty()) { return; } // Find the JSDocInfo for a top-level variable Var var = t.getScope().getVar(n.getString()); JSDocInfo info = (var != null) ? var.getJSDocInfo() : null; boolean shouldBeConstant = (var != null && var.isConst()) || (info != null && info.isConstant()) || NodeUtil.isConstantByConvention(compiler.getCodingConvention(), n); boolean isMarkedConstant = n.getBooleanProp(Node.IS_CONSTANT_NAME); if (shouldBeConstant && !isMarkedConstant) { if (assertOnChange) { String name = n.getString(); throw new IllegalStateException( "Should be const but not marked as const.\n" + " name: " + name + "\n" + " parent:" + n.getParent().toStringTree()); } n.putBooleanProp(Node.IS_CONSTANT_NAME, true); } } } /** Walk the AST tree and verify that constant names are used consistently. */ static class VerifyConstants extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final boolean checkUserDeclarations; VerifyConstants(AbstractCompiler compiler, boolean checkUserDeclarations) { this.compiler = compiler; this.checkUserDeclarations = checkUserDeclarations; } @Override public void process(Node externs, Node root) { Node externsAndJs = root.getParent(); checkState(externsAndJs != null); checkState(externsAndJs.hasChild(externs)); NodeTraversal.traverseRoots(compiler, this, externs, root); } private final Map constantMap = new HashMap<>(); @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName()) { String name = n.getString(); if (n.getString().isEmpty()) { return; } boolean isConst = n.getBooleanProp(Node.IS_CONSTANT_NAME); if (checkUserDeclarations) { boolean expectedConst = false; CodingConvention convention = compiler.getCodingConvention(); if (NodeUtil.isConstantName(n) || NodeUtil.isConstantByConvention(convention, n)) { expectedConst = true; } else { expectedConst = false; JSDocInfo info = null; Var var = t.getScope().getVar(n.getString()); if (var != null) { info = var.getJSDocInfo(); } if (info != null && info.isConstant()) { expectedConst = true; } else { expectedConst = false; } } if (expectedConst) { Preconditions.checkState( expectedConst == isConst, "The name %s is not annotated as constant.", name); } else { Preconditions.checkState( expectedConst == isConst, "The name %s should not be annotated as constant.", name); } } Boolean value = constantMap.get(name); if (value == null) { constantMap.put(name, isConst); } else if (value.booleanValue() != isConst) { throw new IllegalStateException( "The name " + name + " is not consistently annotated as constant. Expected " + ImmutableMap.copyOf(constantMap)); } } } } /** * Simplify the AST:
* - VAR declarations split, so they represent exactly one child declaration.
* - WHILEs are converted to FORs.
* - FOR loop are initializers are moved out of the FOR structure.
* - LABEL node of children other than LABEL, BLOCK, WHILE, FOR, or DO are moved into a block.
* - Add constant annotations based on coding convention.
*/ static class NormalizeStatements implements NodeTraversal.Callback { private final AbstractCompiler compiler; private final AstFactory astFactory; private final boolean assertOnChange; private final RewriteLogicalAssignmentOperatorsHelper rewriteLogicalAssignmentOperatorsHelper; NormalizeStatements(AbstractCompiler compiler, boolean assertOnChange) { this.compiler = compiler; this.assertOnChange = assertOnChange; this.astFactory = compiler.createAstFactory(); this.rewriteLogicalAssignmentOperatorsHelper = new RewriteLogicalAssignmentOperatorsHelper( compiler, this.astFactory, compiler.getUniqueIdSupplier()); } private void reportCodeChange(String changeDescription, Node n) { if (assertOnChange) { throw new IllegalStateException("Normalize constraints violated:\n" + changeDescription); } compiler.reportChangeToEnclosingScope(n); } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { doStatementNormalizations(n); return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case WHILE: Node expr = n.getFirstChild(); n.setToken(Token.FOR); Node empty = IR.empty(); empty.srcrefIfMissing(n); empty.insertBefore(expr); empty.cloneNode().insertAfter(expr); reportCodeChange("WHILE node", n); break; case FUNCTION: if (visitFunction(n)) { reportCodeChange("Function declaration", n); } break; case EXPORT: splitExportDeclaration(n); break; case ARRAYLIT: case CALL: case PARAM_LIST: case NEW: case OBJECTLIT: case OPTCHAIN_CALL: n.setTrailingComma(false); break; case NAME: annotateConstantsByConvention(n); break; case ASSIGN_OR: case ASSIGN_AND: case ASSIGN_COALESCE: rewriteLogicalAssignmentOperatorsHelper.visitLogicalAssignmentOperator(t, n); break; default: break; } } /** Mark names and properties that are constants by convention. */ private void annotateConstantsByConvention(Node n) { checkState(n.isName()); // Need to check that variables have not been renamed, to determine whether // coding conventions still apply. if (compiler.getLifeCycleStage().isNormalizedObfuscated()) { return; } if (!n.getBooleanProp(Node.IS_CONSTANT_NAME) && NodeUtil.isConstantByConvention(compiler.getCodingConvention(), n)) { checkState(!assertOnChange, "Not marked as constant when it should be: %s", n); n.putBooleanProp(Node.IS_CONSTANT_NAME, true); } } /** Splits ES6 export combined with a variable or function declaration. */ private void splitExportDeclaration(Node n) { if (n.getBooleanProp(Node.EXPORT_DEFAULT)) { return; } Node c = n.getFirstChild(); if (NodeUtil.isDeclaration(c)) { c.detach(); Node exportSpecs = new Node(Token.EXPORT_SPECS).srcref(n); n.addChildToFront(exportSpecs); if (c.isClass() || c.isFunction()) { Node name = c.getFirstChild(); c.insertBefore(n); addNameNodeToExportSpecs(exportSpecs, name); } else { NodeUtil.visitLhsNodesInNode(c, (name) -> addNameNodeToExportSpecs(exportSpecs, name)); // Split up var declarations onto separate lines. for (Node child = c.getFirstChild(); child != null; ) { final Node next = child.getNext(); child.detach(); Node newDeclaration = new Node(c.getToken(), child).srcref(n); newDeclaration.insertBefore(n); child = next; } } reportCodeChange("combined export and declaration", n.getParent()); } } private void addNameNodeToExportSpecs(Node exportSpecs, Node name) { Node exportSpec = new Node(Token.EXPORT_SPEC).srcref(name); exportSpec.addChildToFront(name.cloneNode()); exportSpec.addChildToFront(name.cloneNode()); exportSpecs.addChildToBack(exportSpec); } /** * Rewrite blockless arrow functions to have a block with a single return statement * *

For example: {@code (x) => x} becomes {@code (x) => { return x; }}. * *

This simplifies optimizations as they can now assume all functions have a BLOCK. */ boolean visitFunction(Node n) { checkState(n.isFunction(), n); if (n.isFunction() && !NodeUtil.getFunctionBody(n).isBlock()) { Node returnValue = NodeUtil.getFunctionBody(n); Node body = IR.block(IR.returnNode(returnValue.detach())); body.srcrefTreeIfMissing(returnValue); n.addChildToBack(body); reportCodeChange("blockless arrow function", body); } return false; } /** Do normalizations that introduce new siblings or parents. */ private void doStatementNormalizations(Node n) { if (n.isLabel()) { normalizeLabels(n); } // Only inspect the children of SCRIPTs, BLOCKs and LABELs, as all these // are the only legal place for VARs and FOR statements. if (NodeUtil.isStatementBlock(n) || n.isLabel()) { extractForInitializer(n, null, null); } // Only inspect the children of SCRIPTs, BLOCKs, as all these // are the only legal place for VARs. if (NodeUtil.isStatementBlock(n)) { splitVarDeclarations(n); } if (n.isFunction()) { moveNamedFunctions(n.getLastChild()); } if (NodeUtil.isCompoundAssignmentOp(n) && !NodeUtil.isLogicalAssignmentOp(n)) { // Logical assignments should be handled in visit(), not here normalizeAssignShorthand(n); } } // TODO(johnlenz): Move this to NodeTypeNormalizer once the unit tests are // fixed. /** * Limit the number of special cases where LABELs need to be handled. Only BLOCK and loops are * allowed to be labeled. Loop labels must remain in place as the named continues are not * allowed for labeled blocks. */ private void normalizeLabels(Node n) { checkArgument(n.isLabel()); Node last = n.getLastChild(); // TODO(moz): Avoid adding blocks for cases like "label: let x;" switch (last.getToken()) { case LABEL: case BLOCK: case FOR: case FOR_IN: case FOR_OF: case FOR_AWAIT_OF: case WHILE: case DO: return; default: Node block = IR.block(); block.srcrefIfMissing(last); last.replaceWith(block); block.addChildToFront(last); reportCodeChange("LABEL normalization", n); return; } } /** * Bring the initializers out of FOR loops. These need to be placed before any associated LABEL * nodes. This needs to be done from the top level label first so this is called as a pre-order * callback (from shouldTraverse). * * @param n The node to inspect. * @param before The node to insert the initializer before. * @param beforeParent The parent of the node before which the initializer will be inserted. */ private void extractForInitializer(Node n, @Nullable Node before, @Nullable Node beforeParent) { for (Node next, c = n.getFirstChild(); c != null; c = next) { next = c.getNext(); Node insertBefore = (before == null) ? c : before; Node insertBeforeParent = (before == null) ? n : beforeParent; switch (c.getToken()) { case LABEL: extractForInitializer(c, insertBefore, insertBeforeParent); break; case FOR_IN: case FOR_OF: case FOR_AWAIT_OF: Node first = c.getFirstChild(); if (first.isVar()) { Node lhs = first.getFirstChild(); if (lhs.isDestructuringLhs()) { // Transform: // for (var [a, b = 3] in c) {} // to: // var a; var b; for ([a, b = 3] in c) {} NodeUtil.visitLhsNodesInNode( lhs, (name) -> { // Add a declaration outside the for loop for the given name. checkState( name.isName(), "lhs in destructuring declaration should be a simple name. (%s)", name); Node newName = IR.name(name.getString()).srcref(name); Node newVar = IR.var(newName).srcref(name); newVar.insertBefore(insertBefore); }); // Transform for (var [a, b]... ) to for ([a, b]... Node destructuringPattern = lhs.removeFirstChild(); first.replaceWith(destructuringPattern); } else { // Transform: // for (var a = 1 in b) {} // to: // var a = 1; for (a in b) {}; Node newStatement = first; // Clone just the node, to remove any initialization. Node name = newStatement.getFirstChild().cloneNode(); first.replaceWith(name); newStatement.insertBefore(insertBefore); } reportCodeChange("FOR-IN var declaration", n); } break; case FOR: if (!c.getFirstChild().isEmpty()) { Node init = c.getFirstChild(); if (init.isLet() || init.isConst() || init.isClass() || init.isFunction()) { continue; } Node empty = IR.empty(); empty.srcrefIfMissing(c); init.replaceWith(empty); Node newStatement; // Only VAR statements, and expressions are allowed, // but are handled differently. if (init.isVar()) { newStatement = init; } else { newStatement = NodeUtil.newExpr(init); } newStatement.insertBefore(insertBefore); reportCodeChange("FOR initializer", n); } break; default: break; } } } /** * Split a var (or let or const) node such as: * *

var a, b; * *

into individual statements: * *

var a; var b; * * @param n The whose children we should inspect. */ private void splitVarDeclarations(Node n) { for (Node next, c = n.getFirstChild(); c != null; c = next) { next = c.getNext(); if (NodeUtil.isNameDeclaration(c)) { if (assertOnChange && !c.hasChildren()) { throw new IllegalStateException("Empty VAR node."); } while (c.getFirstChild() != c.getLastChild()) { Node name = c.getFirstChild(); name.detach(); Node newVar = new Node(c.getToken(), name).srcref(n); newVar.insertBefore(c); reportCodeChange("VAR with multiple children", n); } } } } /** * Move all the functions that are valid at the execution of the first statement of the function * to the beginning of the function definition. */ private void moveNamedFunctions(Node functionBody) { checkState(functionBody.getParent().isFunction()); Node insertAfter = null; Node current = functionBody.getFirstChild(); // Skip any declarations at the beginning of the function body, they // are already in the right place. while (current != null && NodeUtil.isFunctionDeclaration(current)) { insertAfter = current; current = current.getNext(); } // Find any remaining declarations and move them. while (current != null) { // Save off the next node as the current node maybe removed. Node next = current.getNext(); if (NodeUtil.isFunctionDeclaration(current)) { // Remove the declaration from the body. current.detach(); // Read the function at the top of the function body (after any // previous declarations). insertAfter = addToFront(functionBody, current, insertAfter); reportCodeChange("Move function declaration not at top of function", functionBody); } current = next; } } private void normalizeAssignShorthand(Node shorthand) { if (!shorthand.getFirstChild().isName()) { return; } Node name = shorthand.getFirstChild(); shorthand.setToken(NodeUtil.getOpFromAssignmentOp(shorthand)); Node insertPoint = IR.empty(); shorthand.replaceWith(insertPoint); Node assign = astFactory.createAssign(name.cloneNode().srcref(name), shorthand).srcref(shorthand); assign.setJSDocInfo(shorthand.getJSDocInfo()); shorthand.setJSDocInfo(null); insertPoint.replaceWith(assign); reportCodeChange("assign shorthand", assign); } /** * @param after The child node to insert the newChild after, or null if newChild should be added * to the front of parent's child list. * @return The inserted child node. */ private static Node addToFront(Node parent, Node newChild, Node after) { if (after == null) { parent.addChildToFront(newChild); } else { newChild.insertAfter(after); } return newChild; } } /** Remove duplicate VAR declarations. */ private void removeDuplicateDeclarations(Node externs, Node root) { NodeTraversal.builder() .setCompiler(compiler) .setCallback(new ScopeTicklingCallback()) .setScopeCreator(new SyntacticScopeCreator(compiler, new DuplicateDeclarationHandler())) .traverseRoots(externs, root); } /** ScopeCreator duplicate declaration handler. */ private final class DuplicateDeclarationHandler implements SyntacticScopeCreator.RedeclarationHandler { private final Set hasOkDuplicateDeclaration = new HashSet<>(); /** Remove duplicate VAR declarations discovered during scope creation. */ @Override public void onRedeclaration(Scope s, String name, Node n, CompilerInput input) { checkState(n.isName()); Node parent = n.getParent(); Var v = s.getVar(name); if (s.isGlobal()) { // We allow variables to be duplicate declared if one // declaration appears in source and the other in externs. // This deals with issues where a browser built-in is declared // in one browser but not in another. if (v.isExtern() && !input.isExtern()) { if (hasOkDuplicateDeclaration.add(v)) { return; } } } if (parent.isFunction()) { if (v.getParentNode().isVar()) { s.undeclare(v); s.declare(name, n, v.getInput()); replaceVarWithAssignment( v.getNameNode(), v.getParentNode(), v.getParentNode().getParent()); } } else if (parent.isVar()) { checkState(parent.hasOneChild()); replaceVarWithAssignment(n, parent, parent.getParent()); } } /** * Remove the parent VAR. There is only one case that need to be handled: "var a = b;" which is * replaced with "a = b" * *

Cases we don't need to handle are VARs with multiple children, which have already been * split into separate declarations, so there is no need to handle that here; "for (var * a;;);"/"for (var a of b)"/"for (var a in b)", which have been moved out of the loop; and * "LABEL: var x;" which has been put in a BLOCK * *

The result of this is that in each case the parent node is replaced which is generally * dangerous in a traversal but is fine here with the scope creator, as the next node of * interest is the parent's next sibling. */ private void replaceVarWithAssignment(Node n, Node parent, Node grandparent) { if (n.hasChildren()) { // The * is being initialize, preserve the new value. n.detach(); // Convert "var name = value" to "name = value" Node value = n.getFirstChild(); value.detach(); Node replacement = astFactory.createAssign(n, value); replacement.setJSDocInfo(parent.getJSDocInfo()); replacement.srcrefIfMissing(parent); Node statement = NodeUtil.newExpr(replacement); parent.replaceWith(statement); reportCodeChange("Duplicate VAR declaration", statement); } else { // It is an empty reference. Remove it. checkState(NodeUtil.isStatementBlock(grandparent), grandparent); parent.detach(); reportCodeChange("Duplicate VAR declaration", grandparent); } } } /** A simple class that causes scope to be created. */ private static final class ScopeTicklingCallback implements NodeTraversal.ScopedCallback { @Override public void enterScope(NodeTraversal t) { // Cause the scope to be created, which will cause duplicate // to be found. t.getScope(); } @Override public void exitScope(NodeTraversal t) { // Nothing to do. } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Nothing to do. } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy