Home | History | Annotate | Download | only in srcgen
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.icu4j.srcgen;
     17 
     18 import com.google.common.base.Splitter;
     19 import com.google.common.collect.Sets;
     20 import com.google.currysrc.api.process.Reporter;
     21 import com.google.currysrc.api.process.ast.AstNodes;
     22 import com.google.currysrc.api.process.ast.BodyDeclarationLocator;
     23 import com.google.currysrc.processors.BaseModifyCommentScanner;
     24 import com.google.currysrc.processors.BaseTagElementNodeScanner;
     25 
     26 import org.eclipse.jdt.core.dom.AST;
     27 import org.eclipse.jdt.core.dom.ASTNode;
     28 import org.eclipse.jdt.core.dom.BodyDeclaration;
     29 import org.eclipse.jdt.core.dom.Comment;
     30 import org.eclipse.jdt.core.dom.IDocElement;
     31 import org.eclipse.jdt.core.dom.LineComment;
     32 import org.eclipse.jdt.core.dom.TagElement;
     33 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
     34 
     35 import java.util.List;
     36 import java.util.Set;
     37 import java.util.regex.Matcher;
     38 import java.util.regex.Pattern;
     39 
     40 import static com.google.currysrc.api.process.ast.BodyDeclarationLocators.findDeclarationNode;
     41 import static com.google.currysrc.api.process.ast.BodyDeclarationLocators.matchesAny;
     42 
     43 /**
     44  * Classes for handling {@literal @}.jcite tags used by ICU.
     45  */
     46 public class TranslateJcite {
     47 
     48   /** The string used to escape a jcite tag. */
     49   public static final String ESCAPED_JCITE_TAG = "{@literal @}.jcite";
     50 
     51   private TranslateJcite() {}
     52 
     53   /**
     54    * Translate JCite "target" tags in comments like
     55    * {@code // ---fooBar}
     56    * to
     57    * {@code // BEGIN_INCLUDE(fooBar)} and {@code // END_INCLUDE(fooBar)}.
     58    */
     59   public static class BeginEndTagsHandler extends BaseModifyCommentScanner {
     60 
     61     private static final Pattern JCITE_TAG_PATTERN = Pattern.compile("//\\s+---(\\S*)\\s*");
     62     private final Set<String> startedJciteTags = Sets.newHashSet();
     63     private final Set<String> endedJciteTags = Sets.newHashSet();
     64 
     65     @Override
     66     protected String processComment(Reporter reporter, Comment commentNode, String commentText) {
     67       if (!(commentNode instanceof LineComment)) {
     68         return null;
     69       }
     70       Matcher matcher = JCITE_TAG_PATTERN.matcher(commentText);
     71       if (!matcher.matches()) {
     72         return null;
     73       }
     74 
     75       String jciteTag = matcher.group(1);
     76 
     77       // Comments are passed in reverse order.
     78 
     79       // jcite allows the same tags to be used multiple times. As of ICU56, ICU has up to 2 blocks
     80       // per file.
     81       // @sample does not deal with multiple BEGIN_INCLUDE / END_INCLUDE tags. As a hack we only
     82       // deal with the last instance with a given tag in the file. The first is usually imports and
     83       // we ignore them.
     84       if (startedJciteTags.contains(jciteTag)) {
     85         // Just record the fact in the output file that we've been here with text that will be easy
     86         // to find (in order to find this code).
     87         return "// IGNORED_INCLUDE(" + jciteTag + ")";
     88       }
     89 
     90       if (endedJciteTags.contains(jciteTag)) {
     91         startedJciteTags.add(jciteTag);
     92         return "// BEGIN_INCLUDE(" + jciteTag + ")";
     93       } else {
     94         endedJciteTags.add(jciteTag);
     95         return "// END_INCLUDE(" + jciteTag + ")";
     96       }
     97     }
     98 
     99     @Override
    100     public String toString() {
    101       return "BeginEndTagsHandler{}";
    102     }
    103   }
    104 
    105   /**
    106    * Translates [{@literal@}.jcite [classname]:---[tag name]]
    107    * to
    108    * [{@literal@}sample [source file name] [tag]]
    109    * if the declaration it is associated with appears in a whitelist.
    110    */
    111   public static class InclusionHandler extends BaseTagElementNodeScanner {
    112 
    113     private final String sampleSrcDir;
    114 
    115     private final List<BodyDeclarationLocator> whitelist;
    116 
    117     public InclusionHandler(String sampleSrcDir, List<BodyDeclarationLocator> whitelist) {
    118       this.sampleSrcDir = sampleSrcDir;
    119       this.whitelist = whitelist;
    120     }
    121 
    122     @Override
    123     protected boolean visitTagElement(Reporter reporter, ASTRewrite rewrite, TagElement tagNode) {
    124       String tagName = tagNode.getTagName();
    125       if (tagName == null || !tagName.equalsIgnoreCase("@.jcite")) {
    126         return true;
    127       }
    128 
    129       // Determine if this is one of the whitelisted tags and create the appropriate replacement.
    130       BodyDeclaration declarationNode = findDeclarationNode(tagNode);
    131       if (declarationNode == null) {
    132         throw new AssertionError("Unable to find declaration for " + tagNode);
    133       }
    134       boolean matchesWhitelist = matchesAny(whitelist, declarationNode);
    135       TagElement replacementTagNode;
    136       if (matchesWhitelist) {
    137         replacementTagNode = createSampleTagElement(tagNode);
    138       } else {
    139         replacementTagNode = createEscapedJciteTagElement(tagNode);
    140       }
    141 
    142       // Hack notice: Replacing a nested TagElement tends to mess up the nesting (e.g. we lose
    143       // enclosing {}'s). Guess: It's because the replacementTagNode is not considered "nested"
    144       // because it doesn't have a TagElement parent until it is in the AST.
    145       // Workaround below: Wrap it in another TagElement with no name.
    146       TagElement fakeWrapper = tagNode.getAST().newTagElement();
    147       fakeWrapper.fragments().add(replacementTagNode);
    148 
    149       rewrite.replace(tagNode, fakeWrapper, null /* editGroup */);
    150       return false;
    151     }
    152 
    153     private TagElement createSampleTagElement(TagElement tagNode) {
    154       List<IDocElement> fragments = tagNode.fragments();
    155       if (fragments.size() != 1) {
    156         throw new AssertionError("Badly formed .jcite tag: one fragment expected");
    157       }
    158       String fragmentText = fragments.get(0).toString().trim();
    159       int colonIndex = fragmentText.indexOf(':');
    160       if (colonIndex == -1) {
    161         throw new AssertionError("Badly formed .jcite tag: expected ':'");
    162       }
    163       List<String> jciteElements = Splitter.on(":").splitToList(fragmentText);
    164       if (jciteElements.size() != 2) {
    165         throw new AssertionError("Badly formed .jcite tag: expected 2 components");
    166       }
    167 
    168       String className = jciteElements.get(0);
    169       String snippetLocator = jciteElements.get(1);
    170 
    171       String fileName = sampleSrcDir + '/' + className.replace('.', '/') + ".java";
    172 
    173       String snippetLocatorPrefix = "---";
    174       if (!snippetLocator.startsWith(snippetLocatorPrefix)) {
    175         throw new AssertionError("Badly formed .jcite tag: expected --- on snippetLocator");
    176       }
    177       // See the TranslateJciteBeginEndTags transformer.
    178       String newTag = snippetLocator.substring(snippetLocatorPrefix.length());
    179       // Remove any trailing whitespace.
    180       newTag = newTag.trim();
    181 
    182       AST ast = tagNode.getAST();
    183       return AstNodes.createTextTagElement(ast, "@sample " + fileName + " " + newTag);
    184     }
    185 
    186     private TagElement createEscapedJciteTagElement(TagElement tagNode) {
    187       // Note: This doesn't quite work properly: it introduces an extra space between the escaped
    188       // name and the rest of the tag. e.g. {@literal @}.jcite  foo.bar.....
    189       AST ast = tagNode.getAST();
    190       TagElement replacement = ast.newTagElement();
    191       replacement.fragments().add(AstNodes.createTextElement(ast, ESCAPED_JCITE_TAG));
    192       replacement.fragments().addAll(ASTNode.copySubtrees(ast, tagNode.fragments()));
    193       return replacement;
    194     }
    195 
    196     @Override
    197     public String toString() {
    198       return "InclusionHandler{" +
    199           "whitelist=" + whitelist +
    200           ", sampleSrcDir='" + sampleSrcDir + '\'' +
    201           '}';
    202     }
    203   }
    204 }
    205