/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho.specmodels.model; import javax.annotation.Nullable; import javax.lang.model.element.Modifier; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; import java.util.Map; import com.facebook.litho.annotations.OnBind; import com.facebook.litho.specmodels.internal.ImmutableList; import com.facebook.litho.annotations.OnCreateLayout; import com.facebook.litho.annotations.OnCreateLayoutWithSizeSpec; import com.facebook.litho.annotations.OnCreateMountContent; import com.facebook.litho.annotations.OnMount; import com.facebook.litho.annotations.OnUnbind; import com.facebook.litho.annotations.OnUnmount; import com.facebook.litho.annotations.Param; import com.facebook.litho.annotations.Prop; import com.facebook.litho.annotations.State; import com.facebook.litho.annotations.TreeProp; import com.facebook.litho.specmodels.model.DelegateMethodDescription.OptionalParameterType; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; /** * Class for validating that the {@link DelegateMethodModel}s for a {@link SpecModel} are * well-formed. */ public class DelegateMethodValidation { static List<SpecModelValidationError> validateLayoutSpecModel( LayoutSpecModel specModel) { List<SpecModelValidationError> validationErrors = new ArrayList<>(); validationErrors.addAll( validateMethods(specModel, DelegateMethodDescriptions.LAYOUT_SPEC_DELEGATE_METHODS_MAP)); final DelegateMethodModel onCreateLayoutModel = SpecModelUtils.getMethodModelWithAnnotation(specModel, OnCreateLayout.class); final DelegateMethodModel onCreateLayoutWithSizeSpecModel = SpecModelUtils.getMethodModelWithAnnotation(specModel, OnCreateLayoutWithSizeSpec.class); if (onCreateLayoutModel == null && onCreateLayoutWithSizeSpecModel == null) { validationErrors.add( new SpecModelValidationError( specModel.getRepresentedObject(), "You need to have a method annotated with either @OnCreateLayout " + "or @OnCreateLayoutWithSizeSpec in your spec. In most cases, @OnCreateLayout " + "is what you want.")); } else if (onCreateLayoutModel != null && onCreateLayoutWithSizeSpecModel != null) { validationErrors.add( new SpecModelValidationError( specModel.getRepresentedObject(), "Your LayoutSpec should have a method annotated with either @OnCreateLayout " + "or @OnCreateLayoutWithSizeSpec, but not both. In most cases, @OnCreateLayout " + "is what you want.")); } return validationErrors; } static List<SpecModelValidationError> validateMountSpecModel(MountSpecModel specModel) { List<SpecModelValidationError> validationErrors = new ArrayList<>(); validationErrors.addAll( validateMethods(specModel, DelegateMethodDescriptions.MOUNT_SPEC_DELEGATE_METHODS_MAP)); final DelegateMethodModel onCreateMountContentModel = SpecModelUtils.getMethodModelWithAnnotation(specModel, OnCreateMountContent.class); if (onCreateMountContentModel == null) { validationErrors.add( new SpecModelValidationError( specModel.getRepresentedObject(), "All MountSpecs need to have a method annotated with @OnCreateMountContent.")); } else { final TypeName mountType = onCreateMountContentModel.returnType; ImmutableList<Class<? extends Annotation>> methodsAcceptingMountTypeAsSecondParam = ImmutableList.of(OnMount.class, OnBind.class, OnUnbind.class, OnUnmount.class); for (Class<? extends Annotation> annotation : methodsAcceptingMountTypeAsSecondParam) { final DelegateMethodModel method = SpecModelUtils.getMethodModelWithAnnotation(specModel, annotation); if (method != null && (method.methodParams.size() < 2 || !method.methodParams.get(1).getType().equals(mountType))) { validationErrors.add( new SpecModelValidationError( method.representedObject, "The second parameter of a method annotated with " + annotation + " must " + "have the same type as the return type of the method annotated with " + "@OnCreateMountContent (i.e. " + mountType + ").")); } } } return validationErrors; } static List<SpecModelValidationError> validateMethods( SpecModel specModel, Map<Class<? extends Annotation>, DelegateMethodDescription> delegateMethodDescriptions) { List<SpecModelValidationError> validationErrors = new ArrayList<>(); for (DelegateMethodModel delegateMethod : specModel.getDelegateMethods()) { if (!specModel.hasInjectedDependencies() && !delegateMethod.modifiers.contains(Modifier.STATIC)) { validationErrors.add( new SpecModelValidationError( delegateMethod.representedObject, "Methods in a spec that doesn't have dependency injection must be static.")); } } for (Map.Entry<Class<? extends Annotation>, DelegateMethodDescription> entry : delegateMethodDescriptions.entrySet()) { final Class<? extends Annotation> delegateMethodAnnotation = entry.getKey(); final DelegateMethodDescription delegateMethodDescription = entry.getValue(); final DelegateMethodModel delegateMethod = SpecModelUtils.getMethodModelWithAnnotation(specModel, delegateMethodAnnotation); if (delegateMethod == null) { continue; } final ImmutableList<TypeName> definedParameterTypes = delegateMethodDescription.definedParameterTypes; if (delegateMethod.methodParams.size() < definedParameterTypes.size()) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0, size = definedParameterTypes.size(); i < size; i++) { stringBuilder.append(definedParameterTypes.get(i)); if (i < size - 1) { stringBuilder.append(", "); } } validationErrors.add( new SpecModelValidationError( delegateMethod.representedObject, "Methods annotated with " + delegateMethodAnnotation + " must have at least " + definedParameterTypes.size() + " parameters, and they should be of type " + stringBuilder.toString() + ".")); } for (int i = 0, size = delegateMethod.methodParams.size(); i < size; i++) { final MethodParamModel delegateMethodParam = delegateMethod.methodParams.get(i); if (i < definedParameterTypes.size()) { if (!definedParameterTypes.get(i).equals(ClassNames.OBJECT) && !delegateMethodParam.getType().equals(definedParameterTypes.get(i))) { validationErrors.add( new SpecModelValidationError( delegateMethodParam.getRepresentedObject(), "Parameter in position " + i + " of a method annotated with " + delegateMethodAnnotation + " should be of type " + definedParameterTypes.get(i) + ".")); } } else { if (delegateMethodParam instanceof InterStageInputParamModel) { final Annotation annotation = getInterStageInputAnnotation( delegateMethodParam, delegateMethodDescription.interStageInputAnnotations); if (annotation == null) { validationErrors.add( new SpecModelValidationError( delegateMethodParam.getRepresentedObject(), "Inter-stage input annotation is not valid for this method, please use one " + "of the following: " + delegateMethodDescription.interStageInputAnnotations)); } else { final Class<? extends Annotation> interStageOutputMethodAnnotation = DelegateMethodDescriptions.INTER_STAGE_INPUTS_MAP.get( annotation.annotationType()); final DelegateMethodModel interStageOutputMethod = SpecModelUtils.getMethodModelWithAnnotation( specModel, interStageOutputMethodAnnotation); if (interStageOutputMethod == null) { validationErrors.add( new SpecModelValidationError( delegateMethodParam.getRepresentedObject(), "To use " + annotation.annotationType() + " on param " + delegateMethodParam.getName() + " you must have a method annotated " + "with " + interStageOutputMethodAnnotation + " that has a param " + "Output<" + delegateMethodParam.getType().box() + "> " + delegateMethodParam.getName())); } else if ( !hasMatchingInterStageOutput(interStageOutputMethod, delegateMethodParam)) { validationErrors.add( new SpecModelValidationError( delegateMethodParam.getRepresentedObject(), "To use " + annotation.annotationType() + " on param " + delegateMethodParam.getName() + " your method annotated " + "with " + interStageOutputMethodAnnotation + " must have a param " + "Output<" + delegateMethodParam.getType().box() + "> " + delegateMethodParam.getName())); } } } else if (!isOptionalParamValid( specModel, delegateMethodDescription.optionalParameterTypes, delegateMethodParam)) { validationErrors.add( new SpecModelValidationError( delegateMethodParam.getRepresentedObject(), "Not a valid parameter, should be one of the following: " + getStringRepresentationOfParamTypes( delegateMethodDescription.optionalParameterTypes))); } } } } return validationErrors; } @Nullable private static Annotation getInterStageInputAnnotation( MethodParamModel methodParamModel, ImmutableList<Class<? extends Annotation>> validAnnotations) { for (Annotation annotation : methodParamModel.getAnnotations()) { if (validAnnotations.contains(annotation.annotationType())) { return annotation; } } return null; } private static boolean hasMatchingInterStageOutput( DelegateMethodModel method, MethodParamModel interStageInput) { for (MethodParamModel methodParam : method.methodParams) { if (methodParam.getName().equals(interStageInput.getName()) && methodParam.getType() instanceof ParameterizedTypeName && ((ParameterizedTypeName) methodParam.getType()).rawType.equals(ClassNames.OUTPUT) && ((ParameterizedTypeName) methodParam.getType()).typeArguments.size() == 1 && ((ParameterizedTypeName) methodParam.getType()).typeArguments.get(0).equals( interStageInput.getType().box())) { return true; } } return false; } private static boolean isOptionalParamValid( SpecModel specModel, ImmutableList<OptionalParameterType> parameterTypes, MethodParamModel methodParamModel) { for (OptionalParameterType optionalParameterType : parameterTypes) { if (isParamOfType(specModel, optionalParameterType, methodParamModel)) { return true; } } return false; } private static boolean isParamOfType( SpecModel specModel, OptionalParameterType optionalParameterType, MethodParamModel methodParamModel) { switch (optionalParameterType) { case PROP: return MethodParamModelUtils.isAnnotatedWith(methodParamModel, Prop.class); case TREE_PROP: return MethodParamModelUtils.isAnnotatedWith(methodParamModel, TreeProp.class); case STATE: return MethodParamModelUtils.isAnnotatedWith(methodParamModel, State.class); case PARAM: return MethodParamModelUtils.isAnnotatedWith(methodParamModel, Param.class); case INTER_STAGE_OUTPUT: return methodParamModel.getType() instanceof ParameterizedTypeName && ((ParameterizedTypeName) methodParamModel.getType()).rawType.equals(ClassNames.OUTPUT); case PROP_OUTPUT: return SpecModelUtils.isPropOutput(specModel, methodParamModel); case STATE_OUTPUT: return SpecModelUtils.isStateOutput(specModel, methodParamModel); case STATE_VALUE: return SpecModelUtils.isStateValue(specModel, methodParamModel); } return false; } private static String getStringRepresentationOfParamTypes( ImmutableList<OptionalParameterType> optionalParameterTypes) { final StringBuilder stringBuilder = new StringBuilder(); for (OptionalParameterType parameterType : optionalParameterTypes) { stringBuilder .append(getStringRepresentationOfParamType(parameterType)) .append(". "); } return stringBuilder.toString(); } private static String getStringRepresentationOfParamType( OptionalParameterType optionalParameterType) { switch (optionalParameterType) { case PROP: return "@Prop T somePropName"; case TREE_PROP: return "@TreeProp T someTreePropName"; case STATE: return "@State T someStateName"; case PARAM: return "@Param T someParamName"; case INTER_STAGE_OUTPUT: return "Output<T> someOutputName"; case PROP_OUTPUT: return "Output<T> propName, where a prop with type T and name propName is " + "declared elsewhere in the spec"; case STATE_OUTPUT: return "Output<T> stateName, where a state param with type T and name stateName is " + "declared elsewhere in the spec"; case STATE_VALUE: return "StateValue<T> stateName, where a state param with type T and name stateName is " + "declared elsewhere in the spec"; } return "Unexpected parameter type - please report to the Components team"; } }