Java tutorial
/* * Copyright 2007 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.rhino.jstype.JSTypeNative.NUMBER_STRING_BOOLEAN; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.javascript.jscomp.GlobalNamespace.Name; import com.google.javascript.jscomp.GlobalNamespace.Ref; import com.google.javascript.jscomp.NodeTraversal.Callback; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import com.google.javascript.rhino.TypeI; import com.google.javascript.rhino.TypeIRegistry; import java.text.MessageFormat; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Process variables annotated as {@code @define}. A define is * a special constant that may be overridden by later files and * manipulated by the compiler, much like C preprocessor {@code #define}s. * * @author nicksantos@google.com (Nick Santos) */ class ProcessDefines implements CompilerPass { /** * Defines in this set will not be flagged with "unknown define" warnings. * There are legacy flags that always set these defines, even when they * might not be in the binary. */ private static final Set<String> KNOWN_DEFINES = ImmutableSet.of("COMPILED"); private final AbstractCompiler compiler; private final Map<String, Node> dominantReplacements; private GlobalNamespace namespace = null; // Warnings static final DiagnosticType UNKNOWN_DEFINE_WARNING = DiagnosticType.warning("JSC_UNKNOWN_DEFINE_WARNING", "unknown @define variable {0}"); // Errors static final DiagnosticType INVALID_DEFINE_TYPE_ERROR = DiagnosticType.error("JSC_INVALID_DEFINE_TYPE_ERROR", "@define tag only permits literal types"); static final DiagnosticType INVALID_DEFINE_INIT_ERROR = DiagnosticType.error("JSC_INVALID_DEFINE_INIT_ERROR", "illegal initialization of @define variable {0}"); static final DiagnosticType NON_GLOBAL_DEFINE_INIT_ERROR = DiagnosticType .error("JSC_NON_GLOBAL_DEFINE_INIT_ERROR", "@define variable {0} assignment must be global"); static final DiagnosticType DEFINE_NOT_ASSIGNABLE_ERROR = DiagnosticType.error( "JSC_DEFINE_NOT_ASSIGNABLE_ERROR", "@define variable {0} cannot be reassigned due to code at {1}."); private static final MessageFormat REASON_DEFINE_NOT_ASSIGNABLE = new MessageFormat("line {0} of {1}"); /** * Create a pass that overrides define constants. * * TODO(nicksantos): Write a builder to help JSCompiler induce * {@code replacements} from command-line flags * * @param replacements A hash table of names of defines to their replacements. * All replacements <b>must</b> be literals. */ ProcessDefines(AbstractCompiler compiler, Map<String, Node> replacements) { this.compiler = compiler; dominantReplacements = replacements; } /** * Injects a pre-computed global namespace, so that the same namespace * can be re-used for multiple check passes. Returns {@code this} for * easy chaining. */ ProcessDefines injectNamespace(GlobalNamespace namespace) { this.namespace = namespace; return this; } @Override public void process(Node externs, Node root) { if (namespace == null) { namespace = new GlobalNamespace(compiler, root); } overrideDefines(collectDefines(root, namespace)); } private void overrideDefines(Map<String, DefineInfo> allDefines) { boolean changed = false; for (Map.Entry<String, DefineInfo> def : allDefines.entrySet()) { String defineName = def.getKey(); DefineInfo info = def.getValue(); Node inputValue = dominantReplacements.get(defineName); Node finalValue = inputValue != null ? inputValue : info.getLastValue(); if (finalValue != info.initialValue) { info.initialValueParent.replaceChild(info.initialValue, finalValue.cloneTree()); compiler.addToDebugLog("Overriding @define variable " + defineName); changed = changed || finalValue.getType() != info.initialValue.getType() || !finalValue.isEquivalentTo(info.initialValue); } } if (changed) { compiler.reportCodeChange(); } Set<String> unusedReplacements = Sets.difference(dominantReplacements.keySet(), Sets.union(KNOWN_DEFINES, allDefines.keySet())); for (String unknownDefine : unusedReplacements) { compiler.report(JSError.make(UNKNOWN_DEFINE_WARNING, unknownDefine)); } } private static String format(MessageFormat format, Object... params) { return format.format(params); } /** * Only defines of literal number, string, or boolean are supported. */ private boolean isValidDefineType(JSTypeExpression expression) { TypeIRegistry registry = compiler.getTypeIRegistry(); TypeI type = expression.evaluateInEmptyScope(registry); return !type.isUnknownType() && type.isSubtypeOf(registry.getNativeType(NUMBER_STRING_BOOLEAN)); } /** * Finds all defines, and creates a {@link DefineInfo} data structure for * each one. * @return A map of {@link DefineInfo} structures, keyed by name. */ private Map<String, DefineInfo> collectDefines(Node root, GlobalNamespace namespace) { // Find all the global names with a @define annotation List<Name> allDefines = new ArrayList<>(); for (Name name : namespace.getNameIndex().values()) { Ref decl = name.getDeclaration(); if (name.docInfo != null && name.docInfo.isDefine()) { // Process defines should not depend on check types being enabled, // so we look for the JSDoc instead of the inferred type. if (isValidDefineType(name.docInfo.getType())) { allDefines.add(name); } else { JSError error = JSError.make(decl.node, INVALID_DEFINE_TYPE_ERROR); compiler.report(error); } } else { for (Ref ref : name.getRefs()) { if (ref == decl) { // Declarations were handled above. continue; } Node n = ref.node; Node parent = ref.node.getParent(); JSDocInfo info = n.getJSDocInfo(); if (info == null && parent.isVar() && parent.hasOneChild()) { info = parent.getJSDocInfo(); } if (info != null && info.isDefine()) { allDefines.add(name); break; } } } } CollectDefines pass = new CollectDefines(compiler, allDefines); NodeTraversal.traverse(compiler, root, pass); return pass.getAllDefines(); } /** * Finds all assignments to @defines, and figures out the last value of * the @define. */ private static final class CollectDefines implements Callback { private final AbstractCompiler compiler; private final Map<String, DefineInfo> assignableDefines; private final Map<String, DefineInfo> allDefines; private final Map<Node, RefInfo> allRefInfo; // A hack that allows us to remove ASSIGN/VAR statements when // we're currently visiting one of the children of the assign. private Node lvalueToRemoveLater = null; // A stack tied to the node traversal, to keep track of whether // we're in a conditional block. If 1 is at the top, assignment to // a define is allowed. Otherwise, it's not allowed. private final Deque<Integer> assignAllowed; CollectDefines(AbstractCompiler compiler, List<Name> listOfDefines) { this.compiler = compiler; this.allDefines = new HashMap<>(); assignableDefines = new HashMap<>(); assignAllowed = new ArrayDeque<>(); assignAllowed.push(1); // Create a map of references to defines keyed by node for easy lookup allRefInfo = new HashMap<>(); for (Name name : listOfDefines) { Ref decl = name.getDeclaration(); if (decl != null) { allRefInfo.put(decl.node, new RefInfo(decl, name)); } for (Ref ref : name.getRefs()) { if (ref == decl) { // Declarations were handled above. continue; } // If there's a TWIN def, only put one of the twins in. if (ref.getTwin() == null || !ref.getTwin().isSet()) { allRefInfo.put(ref.node, new RefInfo(ref, name)); } } } } /** * Get a map of {@link DefineInfo} structures, keyed by the name of * the define. */ Map<String, DefineInfo> getAllDefines() { return allDefines; } /** * Keeps track of whether the traversal is in a conditional branch. * We traverse all nodes of the parse tree. */ @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { updateAssignAllowedStack(n, true); return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { RefInfo refInfo = allRefInfo.get(n); if (refInfo != null) { Ref ref = refInfo.ref; Name name = refInfo.name; String fullName = name.getFullName(); switch (ref.type) { case SET_FROM_GLOBAL: case SET_FROM_LOCAL: Node valParent = getValueParent(ref); Node val = valParent.getLastChild(); if (valParent.isAssign() && name.isSimpleName() && name.getDeclaration() == ref) { // For defines, it's an error if a simple name is assigned // before it's declared compiler.report(t.makeError(val, INVALID_DEFINE_INIT_ERROR, fullName)); } else if (processDefineAssignment(t, fullName, val, valParent)) { // remove the assignment so that the variable is still declared, // but no longer assigned to a value, e.g., // DEF_FOO = 5; // becomes "5;" // We can't remove the ASSIGN/VAR when we're still visiting its // children, so we'll have to come back later to remove it. refInfo.name.removeRef(ref); lvalueToRemoveLater = valParent; } break; default: if (t.inGlobalScope()) { // Treat this as a reference to a define in the global scope. // After this point, the define must not be reassigned, // or it's an error. DefineInfo info = assignableDefines.get(fullName); if (info != null) { setDefineInfoNotAssignable(info, t); assignableDefines.remove(fullName); } } break; } } if (!t.inGlobalScope() && n.getJSDocInfo() != null && n.getJSDocInfo().isDefine()) { // warn about @define annotations in local scopes compiler.report(t.makeError(n, NON_GLOBAL_DEFINE_INIT_ERROR, "")); } if (lvalueToRemoveLater == n) { lvalueToRemoveLater = null; if (n.isAssign()) { Node last = n.getLastChild(); n.removeChild(last); parent.replaceChild(n, last); } else { Preconditions.checkState(n.isName()); n.removeChild(n.getFirstChild()); } compiler.reportCodeChange(); } if (n.isCall()) { if (t.inGlobalScope()) { // If there's a function call in the global scope, // we just say it's unsafe and freeze all the defines. // // NOTE(nicksantos): We could be a lot smarter here. For example, // ReplaceOverriddenVars keeps a call graph of all functions and // which functions/variables that they reference, and tries // to statically determine which functions are "safe" and which // are not. But this would be overkill, especially because // the intended use of defines is with config_files, where // all the defines are at the top of the bundle. for (DefineInfo info : assignableDefines.values()) { setDefineInfoNotAssignable(info, t); } assignableDefines.clear(); } } updateAssignAllowedStack(n, false); } /** * Determines whether assignment to a define should be allowed * in the subtree of the given node, and if not, records that fact. * * @param n The node whose subtree we're about to enter or exit. * @param entering True if we're entering the subtree, false otherwise. */ private void updateAssignAllowedStack(Node n, boolean entering) { switch (n.getType()) { case Token.CASE: case Token.FOR: case Token.FUNCTION: case Token.HOOK: case Token.IF: case Token.SWITCH: case Token.WHILE: if (entering) { assignAllowed.push(0); } else { assignAllowed.remove(); } break; } } /** * Determines whether assignment to a define should be allowed * at the current point of the traversal. */ private boolean isAssignAllowed() { return assignAllowed.element() == 1; } /** * Tracks the given define. * * @param t The current traversal, for context. * @param name The full name for this define. * @param value The value assigned to the define. * @param valueParent The parent node of value. * @return Whether we should remove this assignment from the parse tree. */ private boolean processDefineAssignment(NodeTraversal t, String name, Node value, Node valueParent) { if (value == null || !NodeUtil.isValidDefineValue(value, allDefines.keySet())) { compiler.report(t.makeError(value, INVALID_DEFINE_INIT_ERROR, name)); } else if (!isAssignAllowed()) { compiler.report(t.makeError(valueParent, NON_GLOBAL_DEFINE_INIT_ERROR, name)); } else { DefineInfo info = allDefines.get(name); if (info == null) { // First declaration of this define. info = new DefineInfo(value, valueParent); allDefines.put(name, info); assignableDefines.put(name, info); } else if (info.recordAssignment(value)) { // The define was already initialized, but this is a safe // re-assignment. return true; } else { // The define was already initialized, and this is an unsafe // re-assignment. compiler.report(t.makeError(valueParent, DEFINE_NOT_ASSIGNABLE_ERROR, name, info.getReasonWhyNotAssignable())); } } return false; } /** * Gets the parent node of the value for any assignment to a Name. * For example, in the assignment * {@code var x = 3;} * the parent would be the NAME node. */ private static Node getValueParent(Ref ref) { // there are two types of declarations: VARs and ASSIGNs return ref.node.getParent() != null && ref.node.getParent().isVar() ? ref.node : ref.node.getParent(); } /** * Records the fact that because of the current node in the node traversal, * the define can't ever be assigned again. * * @param info Represents the define variable. * @param t The current traversal. */ private static void setDefineInfoNotAssignable(DefineInfo info, NodeTraversal t) { info.setNotAssignable(format(REASON_DEFINE_NOT_ASSIGNABLE, t.getLineNumber(), t.getSourceName())); } /** * A simple data structure for associating a Ref with the name * that it references. */ private static class RefInfo { final Ref ref; final Name name; RefInfo(Ref ref, Name name) { this.ref = ref; this.name = name; } } } /** * A simple class for storing information about a define. * Gathers the initial value, the last assigned value, and whether * the define can be safely assigned a new value. */ private static final class DefineInfo { public final Node initialValueParent; public final Node initialValue; private Node lastValue; private boolean isAssignable; private String reasonNotAssignable; /** * Initializes a define. */ public DefineInfo(Node initialValue, Node initialValueParent) { this.initialValueParent = initialValueParent; this.initialValue = initialValue; lastValue = initialValue; isAssignable = true; } /** * Records the fact that this define can't be assigned a value anymore. * * @param reason A message describing the reason why it can't be assigned. */ public void setNotAssignable(String reason) { isAssignable = false; reasonNotAssignable = reason; } /** * Gets the reason why a define is not assignable. */ public String getReasonWhyNotAssignable() { return reasonNotAssignable; } /** * Records an assigned value. * * @return False if there was an error. */ public boolean recordAssignment(Node value) { lastValue = value; return isAssignable; } /** * Gets the last assigned value. */ public Node getLastValue() { return lastValue; } } }