com.google.javascript.jscomp.ProcessTweaks Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter Show documentation
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.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
/*
* Copyright 2011 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.CharMatcher;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Process goog.tweak primitives. Checks that:
*
* - parameters to goog.tweak.register* are literals of the correct type.
*
- the parameter to goog.tweak.get* is a string literal.
*
- parameters to goog.tweak.overrideDefaultValue are literals of the correct
* type.
*
- tweak IDs passed to goog.tweak.get* and goog.tweak.overrideDefaultValue
* correspond to registered tweaks.
*
- all calls to goog.tweak.register* and goog.tweak.overrideDefaultValue are
* within the top-level context.
*
- each tweak is registered only once.
*
- calls to goog.tweak.overrideDefaultValue occur before the call to the
* corresponding goog.tweak.register* function.
*
* @author [email protected] (Andrew Grieve)
*/
class ProcessTweaks implements CompilerPass {
private final AbstractCompiler compiler;
private final boolean stripTweaks;
private final SortedMap compilerDefaultValueOverrides;
private static final CharMatcher ID_MATCHER = CharMatcher.inRange('a', 'z').
or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.anyOf("0123456789_."));
// Warnings and Errors.
static final DiagnosticType UNKNOWN_TWEAK_WARNING =
DiagnosticType.warning(
"JSC_UNKNOWN_TWEAK_WARNING",
"no tweak registered with ID {0}");
static final DiagnosticType TWEAK_MULTIPLY_REGISTERED_ERROR =
DiagnosticType.error(
"JSC_TWEAK_MULTIPLY_REGISTERED_ERROR",
"Tweak {0} has already been registered.");
static final DiagnosticType NON_LITERAL_TWEAK_ID_ERROR =
DiagnosticType.error(
"JSC_NON_LITERAL_TWEAK_ID_ERROR",
"tweak ID must be a string literal");
static final DiagnosticType INVALID_TWEAK_DEFAULT_VALUE_WARNING =
DiagnosticType.warning(
"JSC_INVALID_TWEAK_DEFAULT_VALUE_WARNING",
"tweak {0} registered with {1} must have a default value that is a " +
"literal of type {2}");
static final DiagnosticType NON_GLOBAL_TWEAK_INIT_ERROR =
DiagnosticType.error(
"JSC_NON_GLOBAL_TWEAK_INIT_ERROR",
"tweak declaration {0} must occur in the global scope");
static final DiagnosticType TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR =
DiagnosticType.error(
"JSC_TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR",
"Cannot override the default value of tweak {0} after it has been " +
"registered");
static final DiagnosticType TWEAK_WRONG_GETTER_TYPE_WARNING =
DiagnosticType.warning(
"JSC_TWEAK_WRONG_GETTER_TYPE_WARNING",
"tweak getter function {0} used for tweak registered using {1}");
static final DiagnosticType INVALID_TWEAK_ID_ERROR =
DiagnosticType.error(
"JSC_INVALID_TWEAK_ID_ERROR",
"tweak ID contains illegal characters. Only letters, numbers, _ " +
"and . are allowed");
/**
* An enum of goog.tweak functions.
*/
private static enum TweakFunction {
REGISTER_BOOLEAN("goog.tweak.registerBoolean", "boolean", Token.TRUE,
Token.FALSE),
REGISTER_NUMBER("goog.tweak.registerNumber", "number", Token.NUMBER),
REGISTER_STRING("goog.tweak.registerString", "string", Token.STRING),
OVERRIDE_DEFAULT_VALUE("goog.tweak.overrideDefaultValue"),
GET_COMPILER_OVERRIDES("goog.tweak.getCompilerOverrides_"),
GET_BOOLEAN("goog.tweak.getBoolean", REGISTER_BOOLEAN),
GET_NUMBER("goog.tweak.getNumber", REGISTER_NUMBER),
GET_STRING("goog.tweak.getString", REGISTER_STRING);
final String name;
final String expectedTypeName;
final Token validNodeTypeA;
final Token validNodeTypeB;
final TweakFunction registerFunction;
TweakFunction(String name) {
this(name, null, Token.EMPTY, Token.EMPTY, null);
}
TweakFunction(String name, String expectedTypeName,
Token validNodeTypeA) {
this(name, expectedTypeName, validNodeTypeA, Token.EMPTY, null);
}
TweakFunction(String name, String expectedTypeName,
Token validNodeTypeA, Token validNodeTypeB) {
this(name, expectedTypeName, validNodeTypeA, validNodeTypeB, null);
}
TweakFunction(String name, TweakFunction registerFunction) {
this(name, null, Token.EMPTY, Token.EMPTY, registerFunction);
}
TweakFunction(String name, String expectedTypeName,
Token validNodeTypeA, Token validNodeTypeB,
TweakFunction registerFunction) {
this.name = name;
this.expectedTypeName = expectedTypeName;
this.validNodeTypeA = validNodeTypeA;
this.validNodeTypeB = validNodeTypeB;
this.registerFunction = registerFunction;
}
boolean isValidNodeType(Token type) {
return type == validNodeTypeA || type == validNodeTypeB;
}
boolean isCorrectRegisterFunction(TweakFunction registerFunction) {
checkNotNull(registerFunction);
return this.registerFunction == registerFunction;
}
boolean isGetterFunction() {
return registerFunction != null;
}
String getName() {
return name;
}
String getExpectedTypeName() {
return expectedTypeName;
}
Node createDefaultValueNode() {
switch (this) {
case REGISTER_BOOLEAN:
return IR.falseNode();
case REGISTER_NUMBER:
return IR.number(0);
case REGISTER_STRING:
return IR.string("");
default:
throw new IllegalStateException();
}
}
}
// A map of function name -> TweakFunction.
private static final Map TWEAK_FUNCTIONS_MAP;
static {
TWEAK_FUNCTIONS_MAP = new HashMap<>();
for (TweakFunction func : TweakFunction.values()) {
TWEAK_FUNCTIONS_MAP.put(func.getName(), func);
}
}
ProcessTweaks(AbstractCompiler compiler, boolean stripTweaks,
Map compilerDefaultValueOverrides) {
this.compiler = compiler;
this.stripTweaks = stripTweaks;
// Having the map sorted is required for the unit tests to be deterministic.
this.compilerDefaultValueOverrides = new TreeMap<>();
this.compilerDefaultValueOverrides.putAll(compilerDefaultValueOverrides);
}
@Override
public void process(Node externs, Node root) {
CollectTweaksResult result = collectTweaks(root);
applyCompilerDefaultValueOverrides(result.tweakInfos);
if (stripTweaks) {
stripAllCalls(result.tweakInfos);
} else if (!compilerDefaultValueOverrides.isEmpty()) {
replaceGetCompilerOverridesCalls(result.getOverridesCalls);
}
}
/**
* Passes the compiler default value overrides to the JS by replacing calls
* to goog.tweak.getCompilerOverrids_ with a map of tweak ID->default value;
*/
private void replaceGetCompilerOverridesCalls(
List calls) {
for (TweakFunctionCall call : calls) {
Node callNode = call.callNode;
Node objNode = createCompilerDefaultValueOverridesVarNode(callNode);
callNode.replaceWith(objNode);
compiler.reportChangeToEnclosingScope(objNode);
}
}
/**
* Removes all CALL nodes in the given TweakInfos, replacing calls to getter
* functions with the tweak's default value.
*/
private void stripAllCalls(Map tweakInfos) {
for (TweakInfo tweakInfo : tweakInfos.values()) {
boolean isRegistered = tweakInfo.isRegistered();
for (TweakFunctionCall functionCall : tweakInfo.functionCalls) {
Node callNode = functionCall.callNode;
Node parent = callNode.getParent();
if (functionCall.tweakFunc.isGetterFunction()) {
Node newValue;
if (isRegistered) {
newValue = tweakInfo.getDefaultValueNode().cloneNode();
} else {
// When we find a getter of an unregistered tweak, there has
// already been a warning about it, so now just use a default
// value when stripping.
TweakFunction registerFunction =
functionCall.tweakFunc.registerFunction;
newValue = registerFunction.createDefaultValueNode();
}
parent.replaceChild(callNode, newValue);
compiler.reportChangeToEnclosingScope(parent);
} else {
Node voidZeroNode = IR.voidNode(IR.number(0).srcref(callNode))
.srcref(callNode);
parent.replaceChild(callNode, voidZeroNode);
compiler.reportChangeToEnclosingScope(parent);
}
}
}
}
/**
* Creates a JS object that holds a map of tweakId -> default value override.
*/
private Node createCompilerDefaultValueOverridesVarNode(
Node sourceInformationNode) {
Node objNode = IR.objectlit().srcref(sourceInformationNode);
for (Entry entry : compilerDefaultValueOverrides.entrySet()) {
Node objKeyNode = IR.stringKey(entry.getKey())
.useSourceInfoIfMissingFrom(sourceInformationNode);
Node objValueNode = entry.getValue().cloneNode()
.useSourceInfoIfMissingFrom(sourceInformationNode);
objKeyNode.addChildToBack(objValueNode);
objNode.addChildToBack(objKeyNode);
}
return objNode;
}
/** Sets the default values of tweaks based on compiler options. */
private void applyCompilerDefaultValueOverrides(
Map tweakInfos) {
for (Entry entry : compilerDefaultValueOverrides.entrySet()) {
String tweakId = entry.getKey();
TweakInfo tweakInfo = tweakInfos.get(tweakId);
if (tweakInfo == null) {
compiler.report(JSError.make(UNKNOWN_TWEAK_WARNING, tweakId));
} else {
TweakFunction registerFunc = tweakInfo.registerCall.tweakFunc;
Node value = entry.getValue();
if (!registerFunc.isValidNodeType(value.getToken())) {
compiler.report(JSError.make(INVALID_TWEAK_DEFAULT_VALUE_WARNING,
tweakId, registerFunc.getName(),
registerFunc.getExpectedTypeName()));
} else {
tweakInfo.defaultValueNode = value;
}
}
}
}
/**
* Finds all calls to goog.tweak functions and emits warnings/errors if any
* of the calls have issues.
* @return A map of {@link TweakInfo} structures, keyed by tweak ID.
*/
private CollectTweaksResult collectTweaks(Node root) {
CollectTweaks pass = new CollectTweaks();
NodeTraversal.traverse(compiler, root, pass);
Map tweakInfos = pass.allTweaks;
for (TweakInfo tweakInfo : tweakInfos.values()) {
tweakInfo.emitAllWarnings();
}
return new CollectTweaksResult(tweakInfos, pass.getOverridesCalls);
}
private static final class CollectTweaksResult {
final Map tweakInfos;
final List getOverridesCalls;
CollectTweaksResult(Map tweakInfos,
List getOverridesCalls) {
this.tweakInfos = tweakInfos;
this.getOverridesCalls = getOverridesCalls;
}
}
/**
* Processes all calls to goog.tweak functions.
*/
private final class CollectTweaks extends AbstractPostOrderCallback {
final Map allTweaks = new HashMap<>();
final List getOverridesCalls = new ArrayList<>();
@SuppressWarnings("incomplete-switch")
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isCall()) {
return;
}
String callName = n.getFirstChild().getQualifiedName();
TweakFunction tweakFunc = TWEAK_FUNCTIONS_MAP.get(callName);
if (tweakFunc == null) {
return;
}
if (tweakFunc == TweakFunction.GET_COMPILER_OVERRIDES) {
getOverridesCalls.add(
new TweakFunctionCall(tweakFunc, n));
return;
}
// Ensure the first parameter (the tweak ID) is a string literal.
Node tweakIdNode = n.getSecondChild();
if (!tweakIdNode.isString()) {
compiler.report(JSError.make(tweakIdNode, NON_LITERAL_TWEAK_ID_ERROR));
return;
}
String tweakId = tweakIdNode.getString();
// Make sure there is a TweakInfo structure for it.
TweakInfo tweakInfo = allTweaks.get(tweakId);
if (tweakInfo == null) {
tweakInfo = new TweakInfo(tweakId);
allTweaks.put(tweakId, tweakInfo);
}
switch (tweakFunc) {
case REGISTER_BOOLEAN:
case REGISTER_NUMBER:
case REGISTER_STRING:
// Ensure the ID contains only valid characters.
if (!ID_MATCHER.matchesAllOf(tweakId)) {
compiler.report(JSError.make(tweakIdNode, INVALID_TWEAK_ID_ERROR));
}
// Ensure tweaks are registered in the global scope.
if (!t.inGlobalHoistScope()) {
compiler.report(JSError.make(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId));
break;
}
// Ensure tweaks are registered only once.
if (tweakInfo.isRegistered()) {
compiler.report(JSError.make(n, TWEAK_MULTIPLY_REGISTERED_ERROR, tweakId));
break;
}
Node tweakDefaultValueNode = tweakIdNode.getNext().getNext();
tweakInfo.addRegisterCall(t.getSourceName(), tweakFunc, n,
tweakDefaultValueNode);
break;
case OVERRIDE_DEFAULT_VALUE:
// Ensure tweaks overrides occur in the global scope.
if (!t.inGlobalScope()) {
compiler.report(JSError.make(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId));
break;
}
// Ensure tweak overrides occur before the tweak is registered.
if (tweakInfo.isRegistered()) {
compiler.report(JSError.make(n, TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR, tweakId));
break;
}
tweakDefaultValueNode = tweakIdNode.getNext();
tweakInfo.addOverrideDefaultValueCall(t.getSourceName(), tweakFunc, n,
tweakDefaultValueNode);
break;
case GET_BOOLEAN:
case GET_NUMBER:
case GET_STRING:
tweakInfo.addGetterCall(t.getSourceName(), tweakFunc, n);
break;
default:
break;
}
}
}
/**
* Holds information about a call to a goog.tweak function.
*/
private static final class TweakFunctionCall {
final TweakFunction tweakFunc;
final Node callNode;
final Node valueNode;
TweakFunctionCall(TweakFunction tweakFunc, Node callNode) {
this(tweakFunc, callNode, null);
}
TweakFunctionCall(TweakFunction tweakFunc, Node callNode, Node valueNode) {
this.callNode = callNode;
this.tweakFunc = tweakFunc;
this.valueNode = valueNode;
}
Node getIdNode() {
return callNode.getSecondChild();
}
}
/**
* Stores information about a single tweak.
*/
private final class TweakInfo {
final String tweakId;
final List functionCalls;
TweakFunctionCall registerCall;
Node defaultValueNode;
TweakInfo(String tweakId) {
this.tweakId = tweakId;
functionCalls = new ArrayList<>();
}
/**
* If this tweak is registered, then looks for type warnings in default
* value parameters and getter functions. If it is not registered, emits an
* error for each function call.
*/
void emitAllWarnings() {
if (isRegistered()) {
emitAllTypeWarnings();
} else {
emitUnknownTweakErrors();
}
}
/**
* Emits a warning for each default value parameter that has the wrong type
* and for each getter function that was used for the wrong type of tweak.
*/
void emitAllTypeWarnings() {
for (TweakFunctionCall call : functionCalls) {
Node valueNode = call.valueNode;
TweakFunction tweakFunc = call.tweakFunc;
TweakFunction registerFunc = registerCall.tweakFunc;
if (valueNode != null) {
// For register* and overrideDefaultValue calls, ensure the default
// value is a literal of the correct type.
if (!registerFunc.isValidNodeType(valueNode.getToken())) {
compiler.report(JSError.make(
valueNode, INVALID_TWEAK_DEFAULT_VALUE_WARNING,
tweakId, registerFunc.getName(),
registerFunc.getExpectedTypeName()));
}
} else if (tweakFunc.isGetterFunction()) {
// For getter calls, ensure the correct getter was used.
if (!tweakFunc.isCorrectRegisterFunction(registerFunc)) {
compiler.report(JSError.make(
call.callNode, TWEAK_WRONG_GETTER_TYPE_WARNING,
tweakFunc.getName(), registerFunc.getName()));
}
}
}
}
/**
* Emits an error for each function call that was found.
*/
void emitUnknownTweakErrors() {
for (TweakFunctionCall call : functionCalls) {
compiler.report(JSError.make(
call.getIdNode(), UNKNOWN_TWEAK_WARNING, tweakId));
}
}
void addRegisterCall(String sourceName, TweakFunction tweakFunc,
Node callNode, Node defaultValueNode) {
registerCall = new TweakFunctionCall(tweakFunc, callNode,
defaultValueNode);
functionCalls.add(registerCall);
}
void addOverrideDefaultValueCall(String sourceName,
TweakFunction tweakFunc, Node callNode, Node defaultValueNode) {
functionCalls.add(new TweakFunctionCall(tweakFunc, callNode,
defaultValueNode));
this.defaultValueNode = defaultValueNode;
}
void addGetterCall(String sourceName, TweakFunction tweakFunc,
Node callNode) {
functionCalls.add(new TweakFunctionCall(tweakFunc, callNode));
}
boolean isRegistered() {
return registerCall != null;
}
Node getDefaultValueNode() {
checkState(isRegistered());
// Use calls to goog.tweak.overrideDefaultValue() first.
if (defaultValueNode != null) {
return defaultValueNode;
}
// Use the value passed to the register function next.
if (registerCall.valueNode != null) {
return registerCall.valueNode;
}
// Otherwise, use the default value for the tweak's type.
return registerCall.tweakFunc.createDefaultValueNode();
}
}
}