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