/** * 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.lang.model.element.Modifier; import java.util.ArrayList; import java.util.List; import com.facebook.litho.specmodels.internal.ImmutableList; import com.facebook.litho.annotations.OnUpdateState; import com.facebook.litho.annotations.Param; import com.facebook.litho.annotations.Prop; import com.facebook.litho.annotations.State; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.WildcardTypeName; import static com.facebook.litho.specmodels.model.ClassNames.STATE_VALUE; /** * Class for validating that the state models within a {@link SpecModel} are well-formed. */ public class StateValidation { static List<SpecModelValidationError> validate(SpecModel specModel) { final List<SpecModelValidationError> validationErrors = new ArrayList<>(); validationErrors.addAll(validateStateValues(specModel)); validationErrors.addAll(validateOnUpdateStateMethods(specModel)); return validationErrors; } static List<SpecModelValidationError> validateStateValues(SpecModel specModel) { final List<SpecModelValidationError> validationErrors = new ArrayList<>(); final ImmutableList<StateParamModel> stateValues = specModel.getStateValues(); for (int i = 0, size = stateValues.size(); i < size - 1; i++) { final StateParamModel thisStateValue = stateValues.get(i); for (int j = i + 1; j < size; j++) { final StateParamModel thatStateValue = stateValues.get(j); if (thisStateValue.getName().equals(thatStateValue.getName())) { if (!thisStateValue.getType().equals(thatStateValue.getType())) { validationErrors.add(new SpecModelValidationError( thatStateValue.getRepresentedObject(), "State values with the same name must have the same type.")); } if (thisStateValue.canUpdateLazily() != thatStateValue.canUpdateLazily()) { validationErrors.add(new SpecModelValidationError( thatStateValue.getRepresentedObject(), "State values with the same name must have the same annotated value for " + "canUpdateLazily().")); } } } } return validationErrors; } static List<SpecModelValidationError> validateOnUpdateStateMethods(SpecModel specModel) { final List<SpecModelValidationError> validationErrors = new ArrayList<>(); for (UpdateStateMethodModel updateStateMethodModel : specModel.getUpdateStateMethods()) { validationErrors.addAll(validateOnUpdateStateMethod(specModel, updateStateMethodModel)); } return validationErrors; } /** * Validate that the declaration of a method annotated with {@link OnUpdateState} is correct: * <ul> * <li>1. Method parameters annotated with {@link Param} don't have the same name as parameters * annotated with {@link State} or {@link Prop}.</li> * <li>2. Method parameters not annotated with {@link Param} must be of type * com.facebook.litho.StateValue.</li> * <li>3. Names of method parameters not annotated with {@link Param} must match the name and * type of a parameter annotated with {@link State}.</li> * </ul> * * @return a list of validation errors. If the list is empty, the method is well-formed. */ static List<SpecModelValidationError> validateOnUpdateStateMethod( SpecModel specModel, UpdateStateMethodModel updateStateMethodModel) { final List<SpecModelValidationError> validationErrors = new ArrayList<>(); if (!specModel.hasInjectedDependencies() && !updateStateMethodModel.modifiers.contains(Modifier.STATIC)) { validationErrors.add( new SpecModelValidationError( updateStateMethodModel.representedObject, "Methods in a spec that doesn't have dependency injection must be static.")); } for (MethodParamModel methodParam : updateStateMethodModel.methodParams) { if (MethodParamModelUtils.isAnnotatedWith(methodParam, Param.class)) { // Check #1 for (PropModel prop : specModel.getProps()) { if (methodParam.getName().equals(prop.getName())) { validationErrors.add( new SpecModelValidationError( methodParam.getRepresentedObject(), "Parameters annotated with @Param should not have the same name as a @Prop.")); } } for (StateParamModel stateValue : specModel.getStateValues()) { if (methodParam.getName().equals(stateValue.getName())) { validationErrors.add( new SpecModelValidationError( methodParam.getRepresentedObject(), "Parameters annotated with @Param should not have the same name as a @State " + "value.")); } } } else { // Check #2 if (!(methodParam.getType() instanceof ParameterizedTypeName) || !(((ParameterizedTypeName) methodParam.getType()).rawType.equals(STATE_VALUE))) { validationErrors.add( new SpecModelValidationError( methodParam.getRepresentedObject(), "Only state parameters and parameters annotated with @Param are permitted in " + "@OnUpdateState method, and all state parameters must be of type " + "com.facebook.litho.StateValue, but " + methodParam.getName() + " is of type " + methodParam.getType() + ".")); } else if (((ParameterizedTypeName) methodParam.getType()).typeArguments.size() != 1 || ((ParameterizedTypeName) methodParam.getType()).typeArguments.get(0) instanceof WildcardTypeName) { validationErrors.add( new SpecModelValidationError( methodParam.getRepresentedObject(), "All parameters of type com.facebook.litho.StateValue must define a type " + "argument, " + methodParam.getName() + " in method " + updateStateMethodModel.name + " does not.")); } else if (!definesStateValue( specModel, methodParam.getName(), ((ParameterizedTypeName) methodParam.getType()).typeArguments.get(0))) { // Check #3 validationErrors.add( new SpecModelValidationError( methodParam.getRepresentedObject(), "Names of parameters of type StateValue must match the name and type of a " + "parameter annotated with @State.")); } } } return validationErrors; } private static boolean definesStateValue(SpecModel specModel, String name, TypeName type) { for (StateParamModel stateValue : specModel.getStateValues()) { if (stateValue.getName().equals(name) && stateValue.getType().box().equals(type.box())) { return true; } } return false; } }