com.google.javascript.jscomp.ReplaceCssNames Maven / Gradle / Ivy
/*
* Copyright 2009 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.STRING_TYPE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* ReplaceCssNames replaces occurrences of goog.getCssName('foo') with
* a shorter version from the passed in renaming map. There are two
* styles of operation: for 'BY_WHOLE' we look up the whole string in the
* renaming map. For 'BY_PART', all the class name's components,
* separated by '-', are renamed individually and then recombined.
*
* Given the renaming map:
* {
* once: 'a',
* upon: 'b',
* atime: 'c',
* long: 'd',
* time: 'e',
* ago: 'f'
* }
*
* The following outputs are expected with the 'BY_PART' renaming style:
*
* goog.getCssName('once') -> 'a'
* goog.getCssName('once-upon-atime') -> 'a-b-c'
*
* var baseClass = goog.getCssName('long-time');
* el.className = goog.getCssName(baseClass, 'ago');
* ->
* var baseClass = 'd-e';
* el.className = baseClass + '-f';
*
* However if we have the following renaming map with the 'BY_WHOLE' renaming style:
* {
* once: 'a',
* upon-atime: 'b',
* long-time: 'c',
* ago: 'd'
* }
*
* Then we would expect:
*
* goog.getCssName('once') -> 'a'
*
* var baseClass = goog.getCssName('long-time');
* el.className = goog.getCssName(baseClass, 'ago');
* ->
* var baseClass = 'c';
* el.className = baseClass + '-d';
*
* In addition, the CSS names before replacement can optionally be gathered.
*/
class ReplaceCssNames implements CompilerPass {
static final Node GET_CSS_NAME_FUNCTION = IR.getprop(IR.name("goog"), IR.string("getCssName"));
static final DiagnosticType INVALID_NUM_ARGUMENTS_ERROR =
DiagnosticType.error("JSC_GETCSSNAME_NUM_ARGS",
"goog.getCssName called with \"{0}\" arguments, expected 1 or 2.");
static final DiagnosticType STRING_LITERAL_EXPECTED_ERROR =
DiagnosticType.error("JSC_GETCSSNAME_STRING_LITERAL_EXPECTED",
"goog.getCssName called with invalid argument, string literal " +
"expected. Was \"{0}\".");
static final DiagnosticType UNEXPECTED_STRING_LITERAL_ERROR =
DiagnosticType.error("JSC_GETCSSNAME_UNEXPECTED_STRING_LITERAL",
"goog.getCssName called with invalid arguments, string literal " +
"passed as first of two arguments. Did you mean " +
"goog.getCssName(\"{0}-{1}\")?");
static final DiagnosticType UNKNOWN_SYMBOL_WARNING =
DiagnosticType.warning("JSC_GETCSSNAME_UNKNOWN_CSS_SYMBOL",
"goog.getCssName called with unrecognized symbol \"{0}\" in class " +
"\"{1}\".");
private final AbstractCompiler compiler;
private final Map cssNames;
private CssRenamingMap symbolMap;
private final Set skiplist;
private JSType nativeStringType;
ReplaceCssNames(
AbstractCompiler compiler,
@Nullable Map cssNames,
@Nullable Set skiplist) {
this.compiler = compiler;
this.cssNames = cssNames;
this.skiplist = skiplist;
}
private JSType getNativeStringType() {
if (nativeStringType == null) {
nativeStringType =
compiler.getTypeRegistry().getNativeType(STRING_TYPE);
}
return nativeStringType;
}
@Override
public void process(Node externs, Node root) {
// The CssRenamingMap may not have been available from the compiler when
// this ReplaceCssNames pass was constructed, so getCssRenamingMap() should
// only be called before this pass is actually run.
symbolMap = getCssRenamingMap();
NodeTraversal.traverse(compiler, root, new Traversal());
}
@VisibleForTesting
protected CssRenamingMap getCssRenamingMap() {
return compiler.getCssRenamingMap();
}
private class Traversal extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isCall() && n.getFirstChild().matchesQualifiedName(GET_CSS_NAME_FUNCTION)) {
int count = n.getChildCount();
Node first = n.getSecondChild();
switch (count) {
case 2:
// Replace the function call with the processed argument.
if (first.isString()) {
processStringNode(first);
n.removeChild(first);
parent.replaceChild(n, first);
t.reportCodeChange();
} else {
compiler.report(
JSError.make(n, STRING_LITERAL_EXPECTED_ERROR, first.getToken().toString()));
}
break;
case 3:
// Replace function call with concatenation of two args. It's
// assumed the first arg has already been processed.
Node second = first.getNext();
if (!second.isString()) {
compiler.report(
JSError.make(n, STRING_LITERAL_EXPECTED_ERROR, second.getToken().toString()));
} else if (first.isString()) {
compiler.report(
JSError.make(
n, UNEXPECTED_STRING_LITERAL_ERROR, first.getString(), second.getString()));
} else {
processStringNode(second);
n.removeChild(first);
Node replacement = IR.add(first,
IR.string("-" + second.getString())
.useSourceInfoIfMissingFrom(second))
.useSourceInfoIfMissingFrom(n);
replacement.setJSType(getNativeStringType());
parent.replaceChild(n, replacement);
t.reportCodeChange();
}
break;
default:
compiler.report(JSError.make(n, INVALID_NUM_ARGUMENTS_ERROR, String.valueOf(count)));
}
}
}
/**
* Processes a string argument to goog.getCssName(). The string will be renamed based off the
* symbol map. If there is no map or any part of the name can't be renamed, a warning is
* reported to the compiler and the node is left unchanged.
*
* If the type is unexpected then an error is reported to the compiler.
*
* @param n The string node to process.
*/
private void processStringNode(Node n) {
String name = n.getString();
if (skiplist != null && skiplist.contains(name)) {
// We apply the skiplist before splitting on dashes, and not after.
// External substitution maps should do the same.
return;
}
String[] parts = name.split("-");
if (symbolMap != null) {
String replacement = null;
switch (symbolMap.getStyle()) {
case BY_WHOLE:
replacement = symbolMap.get(name);
if (replacement == null) {
compiler.report(JSError.make(n, UNKNOWN_SYMBOL_WARNING, name, name));
return;
}
break;
case BY_PART:
String[] replaced = new String[parts.length];
for (int i = 0; i < parts.length; i++) {
String part = symbolMap.get(parts[i]);
if (part == null) {
// If we can't encode all parts, don't encode any of it.
compiler.report(JSError.make(n, UNKNOWN_SYMBOL_WARNING, parts[i], name));
return;
}
replaced[i] = part;
}
replacement = Joiner.on("-").join(replaced);
break;
default:
throw new IllegalStateException(
"Unknown replacement style: " + symbolMap.getStyle());
}
n.setString(replacement);
}
if (cssNames != null) {
// We still want to collect statistics even if we've already
// done the full replace. The statistics are collected on a
// per-part basis.
for (String element : parts) {
int count = cssNames.getOrDefault(element, 0);
cssNames.put(element, count + 1);
}
}
}
}
}