xref: /aosp_15_r20/external/icu/tools/srcgen/src/main/java/com/android/icu4j/srcgen/TranslateJcite.java (revision 0e209d3975ff4a8c132096b14b0e9364a753506e)
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 
TranslateJcite()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
processComment(Reporter reporter, Comment commentNode, String commentText)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
toString()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 allowlist.
110    */
111   public static class InclusionHandler extends BaseTagElementNodeScanner {
112 
113     private final String sampleSrcDir;
114 
115     private final List<BodyDeclarationLocator> allowlist;
116 
InclusionHandler(String sampleSrcDir, List<BodyDeclarationLocator> allowlist)117     public InclusionHandler(String sampleSrcDir, List<BodyDeclarationLocator> allowlist) {
118       this.sampleSrcDir = sampleSrcDir;
119       this.allowlist = allowlist;
120     }
121 
122     @Override
visitTagElement(Reporter reporter, ASTRewrite rewrite, TagElement tagNode)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 allowlisted 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 matchesAllowlist = matchesAny(allowlist, declarationNode);
135       TagElement replacementTagNode;
136       if (matchesAllowlist) {
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 
createSampleTagElement(TagElement tagNode)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 
createEscapedJciteTagElement(TagElement tagNode)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
toString()197     public String toString() {
198       return "InclusionHandler{" +
199           "allowlist=" + allowlist +
200           ", sampleSrcDir='" + sampleSrcDir + '\'' +
201           '}';
202     }
203   }
204 }
205