/** * 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 java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.facebook.litho.specmodels.internal.ImmutableList; import com.facebook.litho.annotations.ResType; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; /** * Class for validating that the state models within a {@link SpecModel} are well-formed. */ public class PropValidation { // Using these names in props might cause conflicts with the method names in the // component's generated layout builder class so we trigger a more user-friendly // error if the component tries to use them. private static final List<String> RESERVED_PROP_NAMES = Arrays.asList( "withLayout", "key", "loadingEventHandler"); private static final List<TypeName> ILLEGAL_PROP_TYPES = Arrays.<TypeName>asList( ClassNames.COMPONENT_LAYOUT, ClassNames.COMPONENT_LAYOUT_BUILDER, ClassNames.COMPONENT_LAYOUT_CONTAINER_BUILDER, ClassNames.COMPONENT_BUILDER, ClassNames.COMPONENT_BUILDER_WITH_LAYOUT, ClassNames.REFERENCE_BUILDER); static List<SpecModelValidationError> validate(SpecModel specModel) { final List<SpecModelValidationError> validationErrors = new ArrayList<>(); final ImmutableList<PropModel> props = specModel.getProps(); for (int i = 0, size = props.size(); i < size - 1; i++) { for (int j = i + 1; j < size; j++) { if (props.get(i).getName().equals(props.get(j).getName())) { validationErrors.add( new SpecModelValidationError( props.get(i).getRepresentedObject(), "The prop " + props.get(i).getName() + " is defined differently in different " + "methods. Ensure that each instance of this prop is declared in the same " + "way (this means having the same type, resType and value for isOptional).")); } } } for (PropModel prop : props) { if (RESERVED_PROP_NAMES.contains(prop.getName())) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), "'" + prop.getName() + "' is a reserved prop name used by the component's " + "layout builder. Please use another name.")); } if (ILLEGAL_PROP_TYPES.contains(prop.getType())) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), "Props may not be declared with the following types: " + ILLEGAL_PROP_TYPES + ".")); } if (!prop.isOptional() && prop.hasDefault(specModel.getPropDefaults())) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), prop.getName() + " is not optional so it should not be declared with a default " + "value.")); } if (prop.hasVarArgs() && prop.getResType() != ResType.NONE) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), prop.getName() + " is a variable argument, and thus should not set a resType.")); } if (prop.hasVarArgs()) { TypeName typeName = prop.getType(); if (typeName instanceof ParameterizedTypeName) { ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName; if (!parameterizedTypeName.rawType.equals(ClassNames.LIST)) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), prop.getName() + " is a variable argument, and thus should be a List<> type.")); } } else { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), prop.getName() + " is a variable argument, and thus requires a parameterized List type.")); } } validationErrors.addAll(validateResType(prop)); } for (PropDefaultModel propDefault : specModel.getPropDefaults()) { final PropModel prop = SpecModelUtils.getPropWithName(specModel, propDefault.mName); if (prop == null) { validationErrors.add( new SpecModelValidationError( propDefault.mRepresentedObject, "PropDefault " + propDefault.mName + " of type " + propDefault.mType + " does not correspond to any defined prop")); } else if (!(propDefault.mType).equals(prop.getType())) { validationErrors.add( new SpecModelValidationError( propDefault.mRepresentedObject, "PropDefault " + propDefault.mName + " of type " + propDefault.mType + " should be of type " + prop.getType())); } } return validationErrors; } private static List<SpecModelValidationError> validateResType(PropModel prop) { List<SpecModelValidationError> validationErrors = new ArrayList<>(); final ResType resType = prop.getResType(); if (resType.equals(ResType.NONE)) { return validationErrors; } final List<TypeName> validResTypes = new ArrayList<>(); switch (resType) { case STRING: validResTypes.add(TypeName.get(String.class)); validResTypes.add(TypeName.get(CharSequence.class)); break; case STRING_ARRAY: validResTypes.add(TypeName.get(String[].class)); break; case INT: case COLOR: validResTypes.add(TypeName.get(int.class)); validResTypes.add(TypeName.get(Integer.class)); break; case INT_ARRAY: validResTypes.add(TypeName.get(int[].class)); break; case BOOL: validResTypes.add(TypeName.get(boolean.class)); validResTypes.add(TypeName.get(Boolean.class)); break; case DIMEN_SIZE: case DIMEN_TEXT: case DIMEN_OFFSET: validResTypes.add(TypeName.get(int.class)); validResTypes.add(TypeName.get(Integer.class)); validResTypes.add(TypeName.get(float.class)); validResTypes.add(TypeName.get(Float.class)); break; case FLOAT: validResTypes.add(TypeName.get(float.class)); validResTypes.add(TypeName.get(Float.class)); break; case DRAWABLE: validResTypes.add(ClassNames.DRAWABLE); break; } if (!validResTypes.contains(prop.getType())) { validationErrors.add( new SpecModelValidationError( prop.getRepresentedObject(), "A prop declared with resType " + prop.getResType() + " must be one of the " + "following types: " + Arrays.toString(validResTypes.toArray()) + ".")); } return validationErrors; } }