/* * 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.devtools.j2objc.translate; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.devtools.j2objc.ast.AbstractTypeDeclaration; import com.google.devtools.j2objc.ast.Block; import com.google.devtools.j2objc.ast.BodyDeclaration; import com.google.devtools.j2objc.ast.Comment; import com.google.devtools.j2objc.ast.CompilationUnit; import com.google.devtools.j2objc.ast.EnumDeclaration; import com.google.devtools.j2objc.ast.MethodDeclaration; import com.google.devtools.j2objc.ast.NativeDeclaration; import com.google.devtools.j2objc.ast.NativeStatement; import com.google.devtools.j2objc.ast.SynchronizedStatement; import com.google.devtools.j2objc.ast.ThisExpression; import com.google.devtools.j2objc.ast.TreeNode; import com.google.devtools.j2objc.ast.TreeUtil; import com.google.devtools.j2objc.ast.TypeDeclaration; import com.google.devtools.j2objc.ast.TypeLiteral; import com.google.devtools.j2objc.ast.UnitTreeVisitor; import com.google.devtools.j2objc.util.CodeReferenceMap; import com.google.devtools.j2objc.util.ElementUtil; import com.google.devtools.j2objc.util.ErrorUtil; import java.lang.reflect.Modifier; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.lang.model.element.TypeElement; /** * Extracts OCNI code blocks into NativeDeclaration and NativeStatement nodes. * Adds native fast enumeration support for Iterable types, when not already * defined. * Adds synchronized blocks to methods with with the synchonized modifier. * * @author Keith Stanger */ public class OcniExtractor extends UnitTreeVisitor { private final ListMultimap<TreeNode, Comment> blockComments; private final CodeReferenceMap deadCodeMap; public OcniExtractor(CompilationUnit unit, CodeReferenceMap deadCodeMap) { super(unit); blockComments = findBlockComments(unit); this.deadCodeMap = deadCodeMap; } /** * Finds all block comments and associates them with their containing type. * This is trickier than you might expect because of inner types. */ private static ListMultimap<TreeNode, Comment> findBlockComments(CompilationUnit unit) { ListMultimap<TreeNode, Comment> blockComments = MultimapBuilder.hashKeys().arrayListValues().build(); for (Comment comment : unit.getCommentList()) { if (!comment.isBlockComment()) { continue; } int commentPos = comment.getStartPosition(); AbstractTypeDeclaration containingType = null; int containingTypePos = -1; for (AbstractTypeDeclaration type : unit.getTypes()) { int typePos = type.getStartPosition(); if (typePos < 0) { continue; } int typeEnd = typePos + type.getLength(); if (commentPos > typePos && commentPos < typeEnd && typePos > containingTypePos) { containingType = type; containingTypePos = typePos; } } blockComments.put(containingType != null ? containingType : unit, comment); } return blockComments; } @Override public void endVisit(CompilationUnit node) { for (Comment comment : blockComments.get(node)) { NativeDeclaration nativeDecl = extractNativeDeclaration(comment); if (nativeDecl != null) { unit.addNativeBlock(nativeDecl); } } } @Override public void endVisit(MethodDeclaration node) { int modifiers = node.getModifiers(); if (Modifier.isNative(modifiers)) { NativeStatement nativeStmt = extractNativeStatement(node); if (nativeStmt != null) { Block body = new Block(); body.addStatement(nativeStmt); node.setBody(body); node.removeModifiers(Modifier.NATIVE); } } if (Modifier.isSynchronized(modifiers)) { TypeElement declaringClass = ElementUtil.getDeclaringClass(node.getExecutableElement()); SynchronizedStatement syncStmt = new SynchronizedStatement( Modifier.isStatic(modifiers) ? new TypeLiteral(declaringClass.asType(), typeUtil) : new ThisExpression(declaringClass.asType())); syncStmt.setBody(TreeUtil.remove(node.getBody())); Block newBody = new Block(); newBody.addStatement(syncStmt); node.setBody(newBody); node.removeModifiers(Modifier.SYNCHRONIZED); } } @Override public void endVisit(EnumDeclaration node) { visitType(node); } @Override public void endVisit(TypeDeclaration node) { if (!node.isInterface()) { visitType(node); } } private void visitType(AbstractTypeDeclaration node) { TypeElement type = node.getTypeElement(); Set<String> methodsPrinted = Sets.newHashSet(); List<BodyDeclaration> bodyDeclarations = node.getBodyDeclarations(); int minPos = 0; int declIdx = 0; for (Comment comment : blockComments.get(node)) { int commentPos = comment.getStartPosition(); while (declIdx < bodyDeclarations.size()) { BodyDeclaration decl = bodyDeclarations.get(declIdx); if (decl.getStartPosition() > commentPos) { break; } minPos = Math.max(minPos, decl.getStartPosition() + decl.getLength()); declIdx++; } if (commentPos > minPos) { NativeDeclaration nativeDecl = extractNativeDeclaration(comment); if (nativeDecl != null) { findMethodSignatures(nativeDecl.getImplementationCode(), methodsPrinted); bodyDeclarations.add(declIdx++, nativeDecl); } } } // If the type implements Iterable and there's no existing implementation // for NSFastEnumeration's protocol method, then add the default // implementation. Don't emit this if the entire class is dead. There is // no need to check if any Iterable methods are dead, since ProGuard is // conservative -- if a class is live and implements Iterable, those // methods are always live. if (typeUtil.findSupertype(type.asType(), "java.lang.Iterable") != null && !methodsPrinted.contains("countByEnumeratingWithState:objects:count:") && (deadCodeMap == null || !deadCodeMap.containsClass(type, elementUtil))) { bodyDeclarations.add(NativeDeclaration.newInnerDeclaration(null, "- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state " + "objects:(__unsafe_unretained id *)stackbuf count:(NSUInteger)len {\n" + " return JreDefaultFastEnumeration(self, state, stackbuf, len);\n}\n")); } } private static final String TYPE_REGEX = "\\([\\w\\s\\*<>\\[\\]]+\\)"; private static final String PARAM_REGEX = "\\s*:\\s*" + TYPE_REGEX + "\\s*\\w+"; private static final String ADDITIONAL_PARAM_REGEX = "\\s+(\\w+)" + PARAM_REGEX; private static final Pattern OBJC_METHOD_DECL_PATTERN = Pattern.compile( "^\\+|-\\s*" + TYPE_REGEX + "\\s*(\\w+)(" + PARAM_REGEX + "((?:" + ADDITIONAL_PARAM_REGEX + ")*))?\\s*\\{"); private static final Pattern ADDITIONAL_PARAM_PATTERN = Pattern.compile(ADDITIONAL_PARAM_REGEX); /** * Finds the signatures of methods defined in the native code. */ private static void findMethodSignatures(String code, Set<String> signatures) { if (code == null) { return; } Matcher matcher = OBJC_METHOD_DECL_PATTERN.matcher(code); while (matcher.find()) { StringBuilder signature = new StringBuilder(); signature.append(matcher.group(1)); if (matcher.group(2) != null) { signature.append(':'); String additionalParams = matcher.group(3); if (additionalParams != null) { Matcher paramsMatcher = ADDITIONAL_PARAM_PATTERN.matcher(additionalParams); while (paramsMatcher.find()) { signature.append(paramsMatcher.group(1)).append(':'); } } } signatures.add(signature.toString()); } } private NativeDeclaration extractNativeDeclaration(TreeNode node) { OcniBlock ocniBlock = extractNativeCode(node); if (ocniBlock != null) { switch (ocniBlock.type) { case HEADER: { NativeDeclaration decl = NativeDeclaration.newInnerDeclaration( ocniBlock.code + '\n', null); decl.addModifiers(Modifier.PUBLIC); return decl; } case SOURCE: return NativeDeclaration.newInnerDeclaration(null, ocniBlock.code + '\n'); } } return null; } private NativeStatement extractNativeStatement(TreeNode node) { OcniBlock ocniBlock = extractNativeCode(node); if (ocniBlock != null) { switch (ocniBlock.type) { case SOURCE: return new NativeStatement(ocniBlock.code); default: ErrorUtil.warning(node, "Unexpected OCNI type: " + ocniBlock.type); } } return null; } /** * Kinds of OCNI comments. Currently there are normal source and header types. */ // TODO(kstanger): move to BlockComment, or a new OCNIComment subclass. public enum OcniType { HEADER("/*-HEADER["), SOURCE("/*-["); private final String delimiter; private OcniType(String delim) { delimiter = delim; } public String delimiter() { return delimiter; } static OcniType fromString(String type) { if (type.isEmpty()) { return SOURCE; } return valueOf(type); } } private static class OcniBlock { private final OcniType type; private final String code; private OcniBlock(OcniType type, String code) { this.type = type; this.code = code; } } private static final Pattern OCNI_PATTERN = Pattern.compile("/\\*-(\\w*)\\[(.*)\\]-\\*/", Pattern.DOTALL); /** * Returns text from within a source code range, where that text is * surrounded by OCNI-like tokens ("/*-[" and "]-*/"), warning * if JSNI delimiters are found instead. * * @param node The node in which to extract the native code. * @return the extracted text between the OCNI delimiters, or null if * a pair of JSNI delimiters aren't in the specified text range */ private OcniBlock extractNativeCode(TreeNode node) { int offset = node.getStartPosition(); String text = unit.getSource().substring(offset, offset + node.getLength()); Matcher m = OCNI_PATTERN.matcher(text); if (m.find()) { String typeStr = m.group(1); try { OcniType type = OcniType.fromString(typeStr); return new OcniBlock(type, m.group(2).trim()); } catch (IllegalArgumentException e) { ErrorUtil.warning(node, "Unknown OCNI type: " + typeStr); return null; } } if (options.jsniWarnings() && hasJsni(text)) { ErrorUtil.warning(node, "JSNI comment found"); } return null; } private boolean hasJsni(String text) { int start = text.indexOf("/*-["); // start after the bracket int end = text.lastIndexOf("]-*/"); if (start == -1 || end <= start) { start = text.indexOf("/*-{"); end = text.lastIndexOf("}-*/"); if (start != -1 && end > start) { return true; } } return false; } }