// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.processing;
import javax.annotation.Nullable;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.facebook.infer.annotation.SuppressFieldNotInitialized;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactPropertyHolder;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import static javax.lang.model.element.Modifier.*;
import static javax.tools.Diagnostic.Kind.ERROR;
import static javax.tools.Diagnostic.Kind.WARNING;
/**
* This annotation processor crawls subclasses of ReactShadowNode and ViewManager and finds their
* exported properties with the @ReactProp or @ReactGroupProp annotation. It generates a class
* per shadow node/view manager that is named {@code <classname>$$PropSetter}. This class contains methods
* to retrieve the name and type of all methods and a way to set these properties without
* reflection.
*/
@SupportedAnnotationTypes("com.facebook.react.uimanager.annotations.ReactPropertyHolder")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ReactPropertyProcessor extends AbstractProcessor {
private static final Map<TypeName, String> DEFAULT_TYPES;
private static final Set<TypeName> BOXED_PRIMITIVES;
private static final TypeName PROPS_TYPE =
ClassName.get("com.facebook.react.uimanager", "ReactStylesDiffMap");
private static final TypeName STRING_TYPE = TypeName.get(String.class);
private static final TypeName READABLE_MAP_TYPE = TypeName.get(ReadableMap.class);
private static final TypeName READABLE_ARRAY_TYPE = TypeName.get(ReadableArray.class);
private static final TypeName VIEW_MANAGER_TYPE =
ClassName.get("com.facebook.react.uimanager", "ViewManager");
private static final TypeName SHADOW_NODE_TYPE =
ClassName.get("com.facebook.react.uimanager", "ReactShadowNode");
private static final ClassName VIEW_MANAGER_SETTER_TYPE =
ClassName.get(
"com.facebook.react.uimanager",
"ViewManagerPropertyUpdater",
"ViewManagerSetter");
private static final ClassName SHADOW_NODE_SETTER_TYPE =
ClassName.get(
"com.facebook.react.uimanager",
"ViewManagerPropertyUpdater",
"ShadowNodeSetter");
private static final TypeName PROPERTY_MAP_TYPE =
ParameterizedTypeName.get(Map.class, String.class, String.class);
private static final TypeName CONCRETE_PROPERTY_MAP_TYPE =
ParameterizedTypeName.get(HashMap.class, String.class, String.class);
private final Map<ClassName, ClassInfo> mClasses;
@SuppressFieldNotInitialized
private Filer mFiler;
@SuppressFieldNotInitialized
private Messager mMessager;
@SuppressFieldNotInitialized
private Elements mElements;
@SuppressFieldNotInitialized
private Types mTypes;
static {
DEFAULT_TYPES = new HashMap<>();
// Primitives
DEFAULT_TYPES.put(TypeName.BOOLEAN, "boolean");
DEFAULT_TYPES.put(TypeName.DOUBLE, "number");
DEFAULT_TYPES.put(TypeName.FLOAT, "number");
DEFAULT_TYPES.put(TypeName.INT, "number");
// Boxed primitives
DEFAULT_TYPES.put(TypeName.BOOLEAN.box(), "boolean");
DEFAULT_TYPES.put(TypeName.INT.box(), "number");
// Class types
DEFAULT_TYPES.put(STRING_TYPE, "String");
DEFAULT_TYPES.put(READABLE_ARRAY_TYPE, "Array");
DEFAULT_TYPES.put(READABLE_MAP_TYPE, "Map");
BOXED_PRIMITIVES = new HashSet<>();
BOXED_PRIMITIVES.add(TypeName.BOOLEAN.box());
BOXED_PRIMITIVES.add(TypeName.FLOAT.box());
BOXED_PRIMITIVES.add(TypeName.INT.box());
}
public ReactPropertyProcessor() {
mClasses = new HashMap<>();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mElements = processingEnv.getElementUtils();
mTypes = processingEnv.getTypeUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// Clear properties from previous rounds
mClasses.clear();
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ReactPropertyHolder.class);
for (Element element : elements) {
try {
TypeElement classType = (TypeElement) element;
ClassName className = ClassName.get(classType);
mClasses.put(className, parseClass(className, classType));
} catch (Exception e) {
error(element, e.getMessage());
}
}
for (ClassInfo classInfo : mClasses.values()) {
try {
if (!shouldIgnoreClass(classInfo)) {
// Sort by name
Collections.sort(
classInfo.mProperties, new Comparator<PropertyInfo>() {
@Override
public int compare(PropertyInfo a, PropertyInfo b) {
return a.mProperty.name().compareTo(b.mProperty.name());
}
});
generateCode(classInfo, classInfo.mProperties);
} else if (shouldWarnClass(classInfo)) {
warning(classInfo.mElement, "Class was skipped. Classes need to be non-private.");
}
} catch (IOException e) {
error(e.getMessage());
} catch (ReactPropertyException e) {
error(e.element, e.getMessage());
} catch (Exception e) {
error(classInfo.mElement, e.getMessage());
}
}
return true;
}
private ClassInfo parseClass(ClassName className, TypeElement typeElement) {
TypeName targetType = getTargetType(typeElement.asType());
TypeName viewType = targetType.equals(SHADOW_NODE_TYPE) ? null : targetType;
ClassInfo classInfo = new ClassInfo(className, typeElement, viewType);
findProperties(classInfo, typeElement);
return classInfo;
}
private void findProperties(ClassInfo classInfo, TypeElement typeElement) {
PropertyInfo.Builder propertyBuilder = new PropertyInfo.Builder(mTypes, mElements, classInfo);
// Recursively search class hierarchy
while (typeElement != null) {
for (Element element : typeElement.getEnclosedElements()) {
ReactProp prop = element.getAnnotation(ReactProp.class);
ReactPropGroup propGroup = element.getAnnotation(ReactPropGroup.class);
try {
if (prop != null || propGroup != null) {
checkElement(element);
}
if (prop != null) {
classInfo.addProperty(propertyBuilder.build(element, new RegularProperty(prop)));
} else if (propGroup != null) {
for (int i = 0, size = propGroup.names().length; i < size; i++) {
classInfo
.addProperty(propertyBuilder.build(element, new GroupProperty(propGroup, i)));
}
}
} catch (ReactPropertyException e) {
error(e.element, e.getMessage());
}
}
typeElement = (TypeElement) mTypes.asElement(typeElement.getSuperclass());
}
}
private TypeName getTargetType(TypeMirror mirror) {
TypeName typeName = TypeName.get(mirror);
if (typeName instanceof ParameterizedTypeName) {
ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName;
if (parameterizedTypeName.rawType.equals(VIEW_MANAGER_TYPE)) {
return parameterizedTypeName.typeArguments.get(0);
}
} else if (typeName.equals(SHADOW_NODE_TYPE)) {
return SHADOW_NODE_TYPE;
} else if (typeName.equals(TypeName.OBJECT)) {
throw new IllegalArgumentException("Could not find target type");
}
List<? extends TypeMirror> types = mTypes.directSupertypes(mirror);
return getTargetType(types.get(0));
}
private void generateCode(ClassInfo classInfo, List<PropertyInfo> properties)
throws IOException, ReactPropertyException {
MethodSpec getMethods = MethodSpec.methodBuilder("getProperties")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.addParameter(PROPERTY_MAP_TYPE, "props")
.returns(TypeName.VOID)
.addCode(generateGetProperties(properties))
.build();
TypeName superType = getSuperType(classInfo);
ClassName className = classInfo.mClassName;
String holderClassName =
getClassName((TypeElement) classInfo.mElement, className.packageName()) + "$$PropsSetter";
TypeSpec holderClass = TypeSpec.classBuilder(holderClassName)
.addSuperinterface(superType)
.addModifiers(PUBLIC)
.addMethod(generateSetPropertySpec(classInfo, properties))
.addMethod(getMethods)
.build();
JavaFile javaFile = JavaFile.builder(className.packageName(), holderClass)
.addFileComment("Generated by " + getClass().getName())
.build();
javaFile.writeTo(mFiler);
}
private String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
private static TypeName getSuperType(ClassInfo classInfo) {
switch (classInfo.getType()) {
case VIEW_MANAGER:
return ParameterizedTypeName.get(
VIEW_MANAGER_SETTER_TYPE,
classInfo.mClassName,
classInfo.mViewType);
case SHADOW_NODE:
return ParameterizedTypeName.get(SHADOW_NODE_SETTER_TYPE, classInfo.mClassName);
default:
throw new IllegalArgumentException();
}
}
private static MethodSpec generateSetPropertySpec(
ClassInfo classInfo,
List<PropertyInfo> properties) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("setProperty")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.VOID);
switch (classInfo.getType()) {
case VIEW_MANAGER:
builder
.addParameter(classInfo.mClassName, "manager")
.addParameter(classInfo.mViewType, "view");
break;
case SHADOW_NODE:
builder
.addParameter(classInfo.mClassName, "node");
break;
}
return builder
.addParameter(STRING_TYPE, "name")
.addParameter(PROPS_TYPE, "props")
.addCode(generateSetProperty(classInfo, properties))
.build();
}
private static CodeBlock generateSetProperty(ClassInfo info, List<PropertyInfo> properties) {
if (properties.isEmpty()) {
return CodeBlock.builder().build();
}
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("switch (name) {\n").indent();
for (int i = 0, size = properties.size(); i < size; i++) {
PropertyInfo propertyInfo = properties.get(i);
builder
.add("case \"$L\":\n", propertyInfo.mProperty.name())
.indent();
switch (info.getType()) {
case VIEW_MANAGER:
builder.add("manager.$L(view, ", propertyInfo.methodName);
break;
case SHADOW_NODE:
builder.add("node.$L(", propertyInfo.methodName);
break;
}
if (propertyInfo.mProperty instanceof GroupProperty) {
builder.add("$L, ", ((GroupProperty) propertyInfo.mProperty).mGroupIndex);
}
if (BOXED_PRIMITIVES.contains(propertyInfo.propertyType)) {
builder.add("props.isNull(name) ? null : ");
}
getPropertyExtractor(propertyInfo, builder);
builder.addStatement(")");
builder
.addStatement("break")
.unindent();
}
builder.unindent().add("}\n");
return builder.build();
}
private static CodeBlock.Builder getPropertyExtractor(
PropertyInfo info,
CodeBlock.Builder builder) {
TypeName propertyType = info.propertyType;
if (propertyType.equals(STRING_TYPE)) {
return builder.add("props.getString(name)");
} else if (propertyType.equals(READABLE_ARRAY_TYPE)) {
return builder.add("props.getArray(name)");
} else if (propertyType.equals(READABLE_MAP_TYPE)) {
return builder.add("props.getMap(name)");
}
if (BOXED_PRIMITIVES.contains(propertyType)) {
propertyType = propertyType.unbox();
}
if (propertyType.equals(TypeName.BOOLEAN)) {
return builder.add("props.getBoolean(name, $L)", info.mProperty.defaultBoolean());
} if (propertyType.equals(TypeName.DOUBLE)) {
double defaultDouble = info.mProperty.defaultDouble();
if (Double.isNaN(defaultDouble)) {
return builder.add("props.getDouble(name, $T.NaN)", Double.class);
} else {
return builder.add("props.getDouble(name, $Lf)", defaultDouble);
}
}
if (propertyType.equals(TypeName.FLOAT)) {
float defaultFloat = info.mProperty.defaultFloat();
if (Float.isNaN(defaultFloat)) {
return builder.add("props.getFloat(name, $T.NaN)", Float.class);
} else {
return builder.add("props.getFloat(name, $Lf)", defaultFloat);
}
}
if (propertyType.equals(TypeName.INT)) {
return builder.add("props.getInt(name, $L)", info.mProperty.defaultInt());
}
throw new IllegalArgumentException();
}
private static CodeBlock generateGetProperties(List<PropertyInfo> properties)
throws ReactPropertyException {
CodeBlock.Builder builder = CodeBlock.builder();
for (PropertyInfo propertyInfo : properties) {
try {
String typeName = getPropertypTypeName(propertyInfo.mProperty, propertyInfo.propertyType);
builder.addStatement("props.put($S, $S)", propertyInfo.mProperty.name(), typeName);
} catch (IllegalArgumentException e) {
throw new ReactPropertyException(e.getMessage(), propertyInfo);
}
}
return builder.build();
}
private static String getPropertypTypeName(Property property, TypeName propertyType) {
String defaultType = DEFAULT_TYPES.get(propertyType);
String useDefaultType = property instanceof RegularProperty ?
ReactProp.USE_DEFAULT_TYPE : ReactPropGroup.USE_DEFAULT_TYPE;
return useDefaultType.equals(property.customType()) ? defaultType : property.customType();
}
private static void checkElement(Element element) throws ReactPropertyException {
if (element.getKind() == ElementKind.METHOD
&& element.getModifiers().contains(PUBLIC)) {
return;
}
throw new ReactPropertyException(
"@ReactProp and @ReachPropGroup annotation must be on a public method",
element);
}
private static boolean shouldIgnoreClass(ClassInfo classInfo) {
return classInfo.mElement.getModifiers().contains(PRIVATE)
|| classInfo.mElement.getModifiers().contains(ABSTRACT)
|| classInfo.mViewType instanceof TypeVariableName;
}
private static boolean shouldWarnClass(ClassInfo classInfo) {
return classInfo.mElement.getModifiers().contains(PRIVATE);
}
private void error(Element element, String message) {
mMessager.printMessage(ERROR, message, element);
}
private void error(String message) {
mMessager.printMessage(ERROR, message);
}
private void warning(Element element, String message) {
mMessager.printMessage(WARNING, message, element);
}
private interface Property {
String name();
String customType();
double defaultDouble();
float defaultFloat();
int defaultInt();
boolean defaultBoolean();
}
private static class RegularProperty implements Property {
private final ReactProp mProp;
public RegularProperty(ReactProp prop) {
mProp = prop;
}
@Override
public String name() {
return mProp.name();
}
@Override
public String customType() {
return mProp.customType();
}
@Override
public double defaultDouble() {
return mProp.defaultDouble();
}
@Override
public float defaultFloat() {
return mProp.defaultFloat();
}
@Override
public int defaultInt() {
return mProp.defaultInt();
}
@Override
public boolean defaultBoolean() {
return mProp.defaultBoolean();
}
}
private static class GroupProperty implements Property {
private final ReactPropGroup mProp;
private final int mGroupIndex;
public GroupProperty(ReactPropGroup prop, int groupIndex) {
mProp = prop;
mGroupIndex = groupIndex;
}
@Override
public String name() {
return mProp.names()[mGroupIndex];
}
@Override
public String customType() {
return mProp.customType();
}
@Override
public double defaultDouble() {
return mProp.defaultDouble();
}
@Override
public float defaultFloat() {
return mProp.defaultFloat();
}
@Override
public int defaultInt() {
return mProp.defaultInt();
}
@Override
public boolean defaultBoolean() {
throw new UnsupportedOperationException();
}
}
private enum SettableType {
VIEW_MANAGER,
SHADOW_NODE
}
private static class ClassInfo {
public final ClassName mClassName;
public final Element mElement;
public final @Nullable TypeName mViewType;
public final List<PropertyInfo> mProperties;
public ClassInfo(ClassName className, TypeElement element, @Nullable TypeName viewType) {
mClassName = className;
mElement = element;
mViewType = viewType;
mProperties = new ArrayList<>();
}
public SettableType getType() {
return mViewType == null ? SettableType.SHADOW_NODE : SettableType.VIEW_MANAGER;
}
public void addProperty(PropertyInfo propertyInfo) throws ReactPropertyException {
String name = propertyInfo.mProperty.name();
if (checkPropertyExists(name)) {
throw new ReactPropertyException(
"Module " + mClassName + " has already registered a property named \"" +
name + "\". If you want to override a property, don't add" +
"the @ReactProp annotation to the property in the subclass", propertyInfo);
}
mProperties.add(propertyInfo);
}
private boolean checkPropertyExists(String name) {
for (PropertyInfo propertyInfo : mProperties) {
if (propertyInfo.mProperty.name().equals(name)) {
return true;
}
}
return false;
}
}
private static class PropertyInfo {
public final String methodName;
public final TypeName propertyType;
public final Element element;
public final Property mProperty;
private PropertyInfo(
String methodName,
TypeName propertyType,
Element element,
Property property) {
this.methodName = methodName;
this.propertyType = propertyType;
this.element = element;
mProperty = property;
}
public static class Builder {
private final Types mTypes;
private final Elements mElements;
private final ClassInfo mClassInfo;
public Builder(Types types, Elements elements, ClassInfo classInfo) {
mTypes = types;
mElements = elements;
mClassInfo = classInfo;
}
public PropertyInfo build(Element element, Property property)
throws ReactPropertyException {
String methodName = element.getSimpleName().toString();
ExecutableElement method = (ExecutableElement) element;
List<? extends VariableElement> parameters = method.getParameters();
if (parameters.size() != getArgCount(mClassInfo.getType(), property)) {
throw new ReactPropertyException("Wrong number of args", element);
}
int index = 0;
if (mClassInfo.getType() == SettableType.VIEW_MANAGER) {
TypeMirror mirror = parameters.get(index++).asType();
if (!mTypes.isSubtype(mirror, mElements.getTypeElement("android.view.View").asType())) {
throw new ReactPropertyException("First argument must be a subclass of View", element);
}
}
if (property instanceof GroupProperty) {
TypeName indexType = TypeName.get(parameters.get(index++).asType());
if (!indexType.equals(TypeName.INT)) {
throw new ReactPropertyException(
"Argument " + index + " must be an int for @ReactPropGroup",
element);
}
}
TypeName propertyType = TypeName.get(parameters.get(index++).asType());
if (!DEFAULT_TYPES.containsKey(propertyType)) {
throw new ReactPropertyException(
"Argument " + index + " must be of a supported type",
element);
}
return new PropertyInfo(methodName, propertyType, element, property);
}
private static int getArgCount(SettableType type, Property property) {
int baseCount = type == SettableType.SHADOW_NODE ? 1 : 2;
return property instanceof GroupProperty ? baseCount + 1 : baseCount;
}
}
}
private static class ReactPropertyException extends Exception {
public final Element element;
public ReactPropertyException(String message, PropertyInfo propertyInfo) {
super(message);
this.element = propertyInfo.element;
}
public ReactPropertyException(String message, Element element) {
super(message);
this.element = element;
}
}
}