blob: a80a47bb8622567894546ade67da7bd85dd9f573 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.icu4j.srcgen;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import com.google.currysrc.api.process.Reporter;
import com.google.currysrc.api.process.ast.AstNodes;
import com.google.currysrc.api.process.ast.BodyDeclarationLocator;
import com.google.currysrc.processors.BaseModifyCommentScanner;
import com.google.currysrc.processors.BaseTagElementNodeScanner;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.IDocElement;
import org.eclipse.jdt.core.dom.LineComment;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.currysrc.api.process.ast.BodyDeclarationLocators.findDeclarationNode;
import static com.google.currysrc.api.process.ast.BodyDeclarationLocators.matchesAny;
/**
* Classes for handling {@literal @}.jcite tags used by ICU.
*/
public class TranslateJcite {
/** The string used to escape a jcite tag. */
public static final String ESCAPED_JCITE_TAG = "{@literal @}.jcite";
private TranslateJcite() {}
/**
* Translate JCite "target" tags in comments like
* {@code // ---fooBar}
* to
* {@code // BEGIN_INCLUDE(fooBar)} and {@code // END_INCLUDE(fooBar)}.
*/
public static class BeginEndTagsHandler extends BaseModifyCommentScanner {
private static final Pattern JCITE_TAG_PATTERN = Pattern.compile("//\\s+---(\\S*)\\s*");
private final Set<String> startedJciteTags = Sets.newHashSet();
private final Set<String> endedJciteTags = Sets.newHashSet();
@Override
protected String processComment(Reporter reporter, Comment commentNode, String commentText) {
if (!(commentNode instanceof LineComment)) {
return null;
}
Matcher matcher = JCITE_TAG_PATTERN.matcher(commentText);
if (!matcher.matches()) {
return null;
}
String jciteTag = matcher.group(1);
// Comments are passed in reverse order.
// jcite allows the same tags to be used multiple times. As of ICU56, ICU has up to 2 blocks
// per file.
// @sample does not deal with multiple BEGIN_INCLUDE / END_INCLUDE tags. As a hack we only
// deal with the last instance with a given tag in the file. The first is usually imports and
// we ignore them.
if (startedJciteTags.contains(jciteTag)) {
// Just record the fact in the output file that we've been here with text that will be easy
// to find (in order to find this code).
return "// IGNORED_INCLUDE(" + jciteTag + ")";
}
if (endedJciteTags.contains(jciteTag)) {
startedJciteTags.add(jciteTag);
return "// BEGIN_INCLUDE(" + jciteTag + ")";
} else {
endedJciteTags.add(jciteTag);
return "// END_INCLUDE(" + jciteTag + ")";
}
}
@Override
public String toString() {
return "BeginEndTagsHandler{}";
}
}
/**
* Translates [{@literal@}.jcite [classname]:---[tag name]]
* to
* [{@literal@}sample [source file name] [tag]]
* if the declaration it is associated with appears in a whitelist.
*/
public static class InclusionHandler extends BaseTagElementNodeScanner {
private final String sampleSrcDir;
private final List<BodyDeclarationLocator> whitelist;
public InclusionHandler(String sampleSrcDir, List<BodyDeclarationLocator> whitelist) {
this.sampleSrcDir = sampleSrcDir;
this.whitelist = whitelist;
}
@Override
protected boolean visitTagElement(Reporter reporter, ASTRewrite rewrite, TagElement tagNode) {
String tagName = tagNode.getTagName();
if (tagName == null || !tagName.equalsIgnoreCase("@.jcite")) {
return true;
}
// Determine if this is one of the whitelisted tags and create the appropriate replacement.
BodyDeclaration declarationNode = findDeclarationNode(tagNode);
if (declarationNode == null) {
throw new AssertionError("Unable to find declaration for " + tagNode);
}
boolean matchesWhitelist = matchesAny(whitelist, declarationNode);
TagElement replacementTagNode;
if (matchesWhitelist) {
replacementTagNode = createSampleTagElement(tagNode);
} else {
replacementTagNode = createEscapedJciteTagElement(tagNode);
}
// Hack notice: Replacing a nested TagElement tends to mess up the nesting (e.g. we lose
// enclosing {}'s). Guess: It's because the replacementTagNode is not considered "nested"
// because it doesn't have a TagElement parent until it is in the AST.
// Workaround below: Wrap it in another TagElement with no name.
TagElement fakeWrapper = tagNode.getAST().newTagElement();
fakeWrapper.fragments().add(replacementTagNode);
rewrite.replace(tagNode, fakeWrapper, null /* editGroup */);
return false;
}
private TagElement createSampleTagElement(TagElement tagNode) {
List<IDocElement> fragments = tagNode.fragments();
if (fragments.size() != 1) {
throw new AssertionError("Badly formed .jcite tag: one fragment expected");
}
String fragmentText = fragments.get(0).toString().trim();
int colonIndex = fragmentText.indexOf(':');
if (colonIndex == -1) {
throw new AssertionError("Badly formed .jcite tag: expected ':'");
}
List<String> jciteElements = Splitter.on(":").splitToList(fragmentText);
if (jciteElements.size() != 2) {
throw new AssertionError("Badly formed .jcite tag: expected 2 components");
}
String className = jciteElements.get(0);
String snippetLocator = jciteElements.get(1);
String fileName = sampleSrcDir + '/' + className.replace('.', '/') + ".java";
String snippetLocatorPrefix = "---";
if (!snippetLocator.startsWith(snippetLocatorPrefix)) {
throw new AssertionError("Badly formed .jcite tag: expected --- on snippetLocator");
}
// See the TranslateJciteBeginEndTags transformer.
String newTag = snippetLocator.substring(snippetLocatorPrefix.length());
// Remove any trailing whitespace.
newTag = newTag.trim();
AST ast = tagNode.getAST();
return AstNodes.createTextTagElement(ast, "@sample " + fileName + " " + newTag);
}
private TagElement createEscapedJciteTagElement(TagElement tagNode) {
// Note: This doesn't quite work properly: it introduces an extra space between the escaped
// name and the rest of the tag. e.g. {@literal @}.jcite foo.bar.....
AST ast = tagNode.getAST();
TagElement replacement = ast.newTagElement();
replacement.fragments().add(AstNodes.createTextElement(ast, ESCAPED_JCITE_TAG));
replacement.fragments().addAll(ASTNode.copySubtrees(ast, tagNode.fragments()));
return replacement;
}
@Override
public String toString() {
return "InclusionHandler{" +
"whitelist=" + whitelist +
", sampleSrcDir='" + sampleSrcDir + '\'' +
'}';
}
}
}