/*
* Copyright 2013 Google Inc.
*
* 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.common.css.compiler.passes;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.css.compiler.ast.CssCommentNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDefinitionNode;
import com.google.common.css.compiler.ast.CssMixinNode;
import com.google.common.css.compiler.ast.CssRefinerNode;
import com.google.common.css.compiler.ast.CssSelectorNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.VisitController;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A compiler pass that checks for missing {@code @require} lines for def constant references
* and mixins. This pass is used in conjunction with CollectProvideNamespaces, which provides
* namespaces for constant definitions and mixins.
* Example for def references:
* file foo/gss/button.gss provides namespace {@code @provide 'foo.gss.button';} and has
* the def: {@code @def FONT_SIZE 10px;}.
* File foo/gss/item.gss references the above def as follows:
* {@code @def ITEM_FONT_SIZE FONT_SIZE;}
* This pass enforces that file foo/gss/item.gss contains {@code @require 'foo.gss.button';}
*
*/
public final class CheckMissingRequire extends DefaultTreeVisitor implements CssCompilerPass {
private static final Logger logger = Logger.getLogger(CheckMissingRequire.class.getName());
private static final Pattern OVERRIDE_SELECTOR_REGEX = Pattern.compile(
"/\\*\\*?\\s+@overrideSelector\\s+\\{(.*)\\}\\s+\\*/");
private static final Pattern OVERRIDE_DEF_REGEX = Pattern.compile(
"/\\*\\*?\\s+@overrideDef\\s+\\{(.*)\\}\\s+\\*/");
private final VisitController visitController;
private final ErrorManager errorManager;
// Key: filename; Value: provide namespace
private final Map<String, String> filenameProvideMap;
// Key: filename; Value: require namespace
private final ListMultimap<String, String> filenameRequireMap;
// Multiple namespaces can contain the same defs due to duplicate defs (or mods).
// Key: def name; Value: provide namespace
private final ListMultimap<String, String> defProvideMap;
// Key: defmixin name; Value: provide namespace
private final ListMultimap<String, String> defmixinProvideMap;
public CheckMissingRequire(VisitController visitController,
ErrorManager errorManager,
Map<String, String> filenameProvideMap,
ListMultimap<String, String> filenameRequireMap,
ListMultimap<String, String> defProvideMap,
ListMultimap<String, String> defmixinProvideMap) {
this.visitController = visitController;
this.errorManager = errorManager;
this.filenameProvideMap = filenameProvideMap;
this.filenameRequireMap = filenameRequireMap;
this.defProvideMap = defProvideMap;
this.defmixinProvideMap = defmixinProvideMap;
}
@Override
public boolean enterValueNode(CssValueNode node) {
if (node instanceof CssConstantReferenceNode) {
CssConstantReferenceNode reference = (CssConstantReferenceNode) node;
String filename = reference.getSourceCodeLocation().getSourceCode().getFileName();
List<String> provides = defProvideMap.get(reference.getValue());
// Remove this after switching to the new syntax.
if (provides == null || provides.size() == 0) { // ignore old format @provide
return true;
}
if (hasMissingRequire(provides, filenameProvideMap.get(filename),
filenameRequireMap.get(filename))) {
StringBuilder error = new StringBuilder("Missing @require for constant " +
reference.getValue() + ". Please @require namespace from:\n");
for (String namespace : defProvideMap.get(reference.getValue())) {
error.append("\t");
error.append(namespace);
error.append("\n");
}
errorManager.report(new GssError(error.toString(), reference.getSourceCodeLocation()));
}
}
return true;
}
@Override
public boolean enterMixin(CssMixinNode node) {
String filename = node.getSourceCodeLocation().getSourceCode().getFileName();
List<String> provides = defmixinProvideMap.get(node.getDefinitionName());
// Remove this after switching to the new syntax.
if (provides == null || provides.size() == 0) { // ignore old format @provide
return true;
}
if (hasMissingRequire(provides, filenameProvideMap.get(filename),
filenameRequireMap.get(filename))) {
StringBuilder error = new StringBuilder("Missing @require for mixin " +
node.getDefinitionName() + ". Please @require namespace from:\n");
for (String namespace : defmixinProvideMap.get(node.getDefinitionName())) {
error.append("\t");
error.append(namespace);
error.append("\n");
}
errorManager.report(new GssError(error.toString(), node.getSourceCodeLocation()));
}
return true;
}
private boolean hasMissingRequire(List<String> provides, String currentNamespace,
List<String> requires) {
// Either the namespace should be provided in this very file or it should be @require'd here.
Set<String> defNamespaceSet = Sets.newHashSet(provides);
Set<String> requireNamespaceSet = Sets.newHashSet(requires);
requireNamespaceSet.retainAll(defNamespaceSet);
if (requireNamespaceSet.size() > 0 || defNamespaceSet.contains(currentNamespace)) {
return false;
}
return true;
}
/*
* Check whether @overrideSelector namespaces are @require'd.
*/
@Override
public boolean enterSelector(CssSelectorNode node) {
String filename = node.getSourceCodeLocation().getSourceCode().getFileName();
for (CssRefinerNode refiner : node.getRefiners().getChildren()) {
for (CssCommentNode comment : refiner.getComments()) {
Matcher matcher = OVERRIDE_SELECTOR_REGEX.matcher(comment.getValue());
if (matcher.find()) {
String overrideNamespace = matcher.group(1);
List<String> requires = filenameRequireMap.get(filename);
// Remove this after switching to the new syntax.
if (requires == null || requires.size() == 0) { // ignore old format @require
continue;
}
Set<String> requireNamespaceSet = Sets.newHashSet(requires);
if (!requireNamespaceSet.contains(overrideNamespace)) {
String error = "Missing @require for @overrideSelector {" +
overrideNamespace + "}. Please @require this namespace in file: " +
filename + ".\n";
errorManager.report(new GssError(error, node.getSourceCodeLocation()));
return true;
}
}
}
}
return true;
}
/*
* Check whether @overrideDef namespaces are @require'd.
*/
@Override
public boolean enterDefinition(CssDefinitionNode node) {
String filename = node.getSourceCodeLocation().getSourceCode().getFileName();
for (CssCommentNode comment : node.getComments()) {
Matcher matcher = OVERRIDE_DEF_REGEX.matcher(comment.getValue());
if (matcher.find()) {
String overrideNamespace = matcher.group(1);
List<String> requires = filenameRequireMap.get(filename);
if (requires == null || requires.size() == 0) { // ignore old format @require
continue;
}
Set<String> requireNamespaceSet = Sets.newHashSet(requires);
if (!requireNamespaceSet.contains(overrideNamespace)) {
String error = "Missing @require for @overrideDef {"
+ overrideNamespace + "}. Please @require this namespace in file: "
+ filename + ".\n";
errorManager.report(new GssError(error, node.getSourceCodeLocation()));
return true;
}
}
}
return true;
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}