001package react4j.processor; 002 003import com.palantir.javapoet.TypeName; 004import java.io.IOException; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.Comparator; 010import java.util.LinkedHashMap; 011import java.util.List; 012import java.util.Set; 013import java.util.StringJoiner; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016import javax.annotation.Nonnull; 017import javax.annotation.Nullable; 018import javax.annotation.processing.RoundEnvironment; 019import javax.annotation.processing.SupportedAnnotationTypes; 020import javax.annotation.processing.SupportedSourceVersion; 021import javax.lang.model.SourceVersion; 022import javax.lang.model.element.AnnotationMirror; 023import javax.lang.model.element.AnnotationValue; 024import javax.lang.model.element.Element; 025import javax.lang.model.element.ElementKind; 026import javax.lang.model.element.ExecutableElement; 027import javax.lang.model.element.Modifier; 028import javax.lang.model.element.TypeElement; 029import javax.lang.model.element.TypeParameterElement; 030import javax.lang.model.element.VariableElement; 031import javax.lang.model.type.ExecutableType; 032import javax.lang.model.type.TypeKind; 033import javax.lang.model.type.TypeMirror; 034import javax.lang.model.type.TypeVariable; 035import javax.lang.model.util.Elements; 036import javax.lang.model.util.Types; 037import org.realityforge.proton.AbstractStandardProcessor; 038import org.realityforge.proton.AnnotationsUtil; 039import org.realityforge.proton.DeferredElementSet; 040import org.realityforge.proton.ElementsUtil; 041import org.realityforge.proton.MemberChecks; 042import org.realityforge.proton.ProcessorException; 043import org.realityforge.proton.StopWatch; 044 045/** 046 * Annotation processor that analyzes React4j annotated source code and generates models from the annotations. 047 */ 048@SuppressWarnings( "Duplicates" ) 049@SupportedAnnotationTypes( Constants.VIEW_CLASSNAME ) 050@SupportedSourceVersion( SourceVersion.RELEASE_17 ) 051public final class React4jProcessor 052 extends AbstractStandardProcessor 053{ 054 private static final String SENTINEL_NAME = "<default>"; 055 private static final Pattern DEFAULT_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Default$" ); 056 private static final Pattern VALIDATE_INPUT_PATTERN = Pattern.compile( "^validate([A-Z].*)$" ); 057 private static final Pattern LAST_INPUT_PATTERN = Pattern.compile( "^last([A-Z].*)$" ); 058 private static final Pattern PREV_INPUT_PATTERN = Pattern.compile( "^prev([A-Z].*)$" ); 059 private static final Pattern INPUT_PATTERN = Pattern.compile( "^([a-z].*)$" ); 060 private static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" ); 061 private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" ); 062 @Nonnull 063 private final DeferredElementSet _deferredTypes = new DeferredElementSet(); 064 @Nonnull 065 private final StopWatch _analyzeViewStopWatch = new StopWatch( "Analyze View" ); 066 067 @Override 068 protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches ) 069 { 070 stopWatches.add( _analyzeViewStopWatch ); 071 } 072 073 @Override 074 public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env ) 075 { 076 debugAnnotationProcessingRootElements( env ); 077 collectRootTypeNames( env ); 078 processTypeElements( annotations, 079 env, 080 Constants.VIEW_CLASSNAME, 081 _deferredTypes, 082 _analyzeViewStopWatch.getName(), 083 this::process, 084 _analyzeViewStopWatch ); 085 errorIfProcessingOverAndInvalidTypesDetected( env ); 086 clearRootTypeNamesIfProcessingOver( env ); 087 return true; 088 } 089 090 @Override 091 @Nonnull 092 protected String getIssueTrackerURL() 093 { 094 return "https://github.com/react4j/react4j/issues"; 095 } 096 097 @Nonnull 098 @Override 099 protected String getOptionPrefix() 100 { 101 return "react4j"; 102 } 103 104 private void process( @Nonnull final TypeElement element ) 105 throws IOException, ProcessorException 106 { 107 final ViewDescriptor descriptor = parse( element ); 108 final String packageName = descriptor.getPackageName(); 109 emitTypeSpec( packageName, ViewGenerator.buildType( processingEnv, descriptor ) ); 110 emitTypeSpec( packageName, BuilderGenerator.buildType( processingEnv, descriptor ) ); 111 if ( descriptor.needsInjection() ) 112 { 113 emitTypeSpec( packageName, FactoryGenerator.buildType( processingEnv, descriptor ) ); 114 } 115 } 116 117 /** 118 * Return true if there is any method annotated with @PostConstruct. 119 */ 120 private boolean hasPostConstruct( @Nonnull final List<ExecutableElement> methods ) 121 { 122 return 123 methods.stream().anyMatch( e -> AnnotationsUtil.hasAnnotationOfType( e, Constants.POST_CONSTRUCT_CLASSNAME ) ); 124 } 125 126 @Nonnull 127 private ViewDescriptor parse( @Nonnull final TypeElement typeElement ) 128 { 129 final String name = deriveViewName( typeElement ); 130 final ViewType type = extractViewType( typeElement ); 131 final boolean exportBuilder = extractExportBuilder( typeElement ); 132 final List<ExecutableElement> methods = 133 ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ); 134 135 final boolean hasPostConstruct = hasPostConstruct( methods ); 136 final boolean shouldSetDefaultPriority = shouldSetDefaultPriority( methods ); 137 138 MemberChecks.mustNotBeFinal( Constants.VIEW_CLASSNAME, typeElement ); 139 MemberChecks.mustBeAbstract( Constants.VIEW_CLASSNAME, typeElement ); 140 MemberChecks.mustBeClass( Constants.VIEW_CLASSNAME, typeElement ); 141 MemberChecks.mustNotBeNonStaticNestedType( Constants.VIEW_CLASSNAME, typeElement ); 142 final List<ExecutableElement> constructors = ElementsUtil.getConstructors( typeElement ); 143 if ( 1 != constructors.size() || !isConstructorValid( constructors.get( 0 ) ) ) 144 { 145 throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME, 146 "have a single, package-access constructor or the default constructor" ), 147 typeElement ); 148 } 149 final ExecutableElement constructor = constructors.get( 0 ); 150 verifyConstructorParameterOrder( constructor ); 151 verifyPostConstructMethodName( methods ); 152 153 final boolean sting = deriveSting( constructor ); 154 final boolean notSyntheticConstructor = 155 Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ); 156 157 final ViewDescriptor descriptor = 158 new ViewDescriptor( name, 159 typeElement, 160 constructor, 161 type, 162 exportBuilder, 163 sting, 164 notSyntheticConstructor, 165 hasPostConstruct, 166 shouldSetDefaultPriority ); 167 168 if ( typeElement.getModifiers().contains( Modifier.PUBLIC ) && 169 ElementsUtil.isWarningNotSuppressed( typeElement, 170 Constants.WARNING_PUBLIC_VIEW, 171 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 172 { 173 final String message = 174 MemberChecks.shouldNot( Constants.VIEW_CLASSNAME, 175 "be public. " + 176 MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_VIEW, 177 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 178 warning( message, typeElement ); 179 } 180 181 for ( final Element element : descriptor.getElement().getEnclosedElements() ) 182 { 183 if ( ElementKind.METHOD == element.getKind() ) 184 { 185 final ExecutableElement method = (ExecutableElement) element; 186 if ( method.getModifiers().contains( Modifier.PUBLIC ) && 187 MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, typeElement, method ) && 188 ElementsUtil.isWarningNotSuppressed( method, 189 Constants.WARNING_PUBLIC_METHOD, 190 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 191 { 192 final String message = 193 MemberChecks.shouldNot( Constants.VIEW_CLASSNAME, 194 "declare a public method. " + 195 MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_METHOD, 196 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 197 warning( message, method ); 198 } 199 if ( method.getModifiers().contains( Modifier.FINAL ) && 200 ElementsUtil.isWarningNotSuppressed( method, 201 Constants.WARNING_FINAL_METHOD, 202 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 203 { 204 final String message = 205 MemberChecks.shouldNot( Constants.VIEW_CLASSNAME, 206 "declare a final method. " + 207 MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD, 208 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 209 warning( message, method ); 210 } 211 if ( method.getModifiers().contains( Modifier.PROTECTED ) && 212 ElementsUtil.isWarningNotSuppressed( method, 213 Constants.WARNING_PROTECTED_METHOD, 214 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) && 215 !isMethodAProtectedOverride( typeElement, method ) ) 216 { 217 final String message = 218 MemberChecks.shouldNot( Constants.VIEW_CLASSNAME, 219 "declare a protected method. " + 220 MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD, 221 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 222 warning( message, method ); 223 } 224 } 225 } 226 227 determineViewCapabilities( descriptor, typeElement ); 228 determineInputs( descriptor, methods ); 229 determinePreludeCheckCandidates( descriptor, typeElement, methods ); 230 determineInputValidatesMethods( descriptor, methods ); 231 determineOnInputChangeMethods( descriptor, methods ); 232 determineDefaultInputsMethods( descriptor, methods ); 233 determineDefaultInputsFields( descriptor ); 234 determinePreUpdateMethod( typeElement, descriptor, methods ); 235 determinePostMountOrUpdateMethod( typeElement, descriptor, methods ); 236 determinePostUpdateMethod( typeElement, descriptor, methods ); 237 determinePostMountMethod( typeElement, descriptor, methods ); 238 determineOnErrorMethod( typeElement, descriptor, methods ); 239 determineScheduleRenderMethods( typeElement, descriptor, methods ); 240 determinePublishMethods( typeElement, descriptor, methods ); 241 determinePreRenderMethods( typeElement, descriptor, methods ); 242 determinePostRenderMethods( typeElement, descriptor, methods ); 243 determineRenderMethod( typeElement, descriptor, methods ); 244 245 for ( final InputDescriptor input : descriptor.getInputs() ) 246 { 247 if ( !isInputRequired( input ) ) 248 { 249 input.markAsOptional(); 250 } 251 else 252 { 253 if ( input.isFromTreeContext() ) 254 { 255 throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME, 256 "specify require=ENABLE when fromTreeContext=true" ), 257 input.getElement() ); 258 } 259 } 260 } 261 262 /* 263 * Sorting must occur after @InputDefault has been processed to ensure the sorting 264 * correctly sorts optional inputs after required inputs. 265 */ 266 descriptor.sortInputs(); 267 268 verifyInputsNotAnnotatedWithArezAnnotations( descriptor ); 269 270 return descriptor; 271 } 272 273 private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement, 274 @Nonnull final ExecutableElement method ) 275 { 276 final ExecutableElement overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method ); 277 return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED ); 278 } 279 280 private boolean deriveSting( @Nonnull final ExecutableElement constructor ) 281 { 282 return !getInjectableConstructorParameters( constructor ).isEmpty() && 283 null != processingEnv.getElementUtils().getTypeElement( Constants.STING_INJECTABLE_CLASSNAME ); 284 } 285 286 private void verifyConstructorParameterOrder( @Nonnull final ExecutableElement constructor ) 287 { 288 if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) && 289 constructor.getParameters().size() > 1 && 290 ElementsUtil.isWarningNotSuppressed( constructor, 291 Constants.WARNING_CONSTRUCTOR_PARAMETER_ORDER, 292 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 293 { 294 final StringJoiner actualOrder = new StringJoiner( ", " ); 295 int maxObservedGroup = -1; 296 boolean invalidOrder = false; 297 for ( final VariableElement parameter : constructor.getParameters() ) 298 { 299 final int group = classifyConstructorParameterGroup( parameter ); 300 actualOrder.add( getConstructorParameterGroupLabel( group ) ); 301 if ( group < maxObservedGroup ) 302 { 303 invalidOrder = true; 304 } 305 else 306 { 307 maxObservedGroup = group; 308 } 309 } 310 311 if ( invalidOrder ) 312 { 313 final String message = 314 MemberChecks.should( Constants.VIEW_CLASSNAME, 315 "declare constructor parameters in the order inject, tree, input. Actual order: " + 316 actualOrder + ". " + 317 MemberChecks.suppressedBy( Constants.WARNING_CONSTRUCTOR_PARAMETER_ORDER, 318 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 319 warning( message, constructor ); 320 } 321 } 322 } 323 324 private void verifyPostConstructMethodName( @Nonnull final List<ExecutableElement> methods ) 325 { 326 final List<ExecutableElement> postConstructMethods = 327 methods.stream() 328 .filter( e -> AnnotationsUtil.hasAnnotationOfType( e, Constants.POST_CONSTRUCT_CLASSNAME ) ) 329 .toList(); 330 331 if ( 1 == postConstructMethods.size() ) 332 { 333 final ExecutableElement method = postConstructMethods.get( 0 ); 334 if ( !"postConstruct".contentEquals( method.getSimpleName() ) && 335 ElementsUtil.isWarningNotSuppressed( method, 336 Constants.WARNING_POST_CONSTRUCT_NAME, 337 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 338 { 339 final String message = 340 MemberChecks.should( Constants.POST_CONSTRUCT_CLASSNAME, 341 "be named 'postConstruct' when it is the only @PostConstruct method in the @View. " + 342 MemberChecks.suppressedBy( Constants.WARNING_POST_CONSTRUCT_NAME, 343 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 344 warning( message, method ); 345 } 346 } 347 } 348 349 private int classifyConstructorParameterGroup( @Nonnull final VariableElement parameter ) 350 { 351 return isInputParameter( parameter ) ? isFromTreeContextInput( parameter ) ? 1 : 2 : 0; 352 } 353 354 @Nonnull 355 private String getConstructorParameterGroupLabel( final int group ) 356 { 357 return switch ( group ) 358 { 359 case 0 -> "inject"; 360 case 1 -> "tree"; 361 case 2 -> "input"; 362 default -> throw new IllegalArgumentException( "Unexpected constructor parameter group: " + group ); 363 }; 364 } 365 366 @Nonnull 367 private List<VariableElement> getInjectableConstructorParameters( @Nonnull final ExecutableElement constructor ) 368 { 369 return constructor.getParameters().stream() 370 .map( parameter -> (VariableElement) parameter ) 371 .filter( parameter -> !isInputParameter( parameter ) ) 372 .toList(); 373 } 374 375 private boolean isInputParameter( @Nonnull final VariableElement parameter ) 376 { 377 return AnnotationsUtil.hasAnnotationOfType( parameter, Constants.INPUT_CLASSNAME ); 378 } 379 380 private boolean isConstructorValid( @Nonnull final ExecutableElement ctor ) 381 { 382 if ( Elements.Origin.EXPLICIT != processingEnv.getElementUtils().getOrigin( ctor ) ) 383 { 384 return true; 385 } 386 else 387 { 388 return ElementsUtil.isPackageAccess( ctor ); 389 } 390 } 391 392 private void verifyInputsNotAnnotatedWithArezAnnotations( @Nonnull final ViewDescriptor descriptor ) 393 { 394 for ( final InputDescriptor input : descriptor.getInputs() ) 395 { 396 final Element element = input.getElement(); 397 for ( final AnnotationMirror mirror : element.getAnnotationMirrors() ) 398 { 399 final String classname = mirror.getAnnotationType().toString(); 400 if ( classname.startsWith( "arez.annotations." ) ) 401 { 402 throw new ProcessorException( "@Input target must not be annotated with any arez annotations but " + 403 "is annotated by '" + classname + "'.", element ); 404 } 405 } 406 } 407 } 408 409 private void determineOnInputChangeMethods( @Nonnull final ViewDescriptor descriptor, 410 @Nonnull final List<ExecutableElement> methods ) 411 { 412 final List<ExecutableElement> onInputChangeMethods = 413 methods 414 .stream() 415 .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.ON_INPUT_CHANGE_CLASSNAME ) ) 416 .toList(); 417 418 final ArrayList<OnInputChangeDescriptor> onInputChangeDescriptors = new ArrayList<>(); 419 for ( final ExecutableElement method : onInputChangeMethods ) 420 { 421 final String phase = 422 AnnotationsUtil.getEnumAnnotationParameter( method, Constants.ON_INPUT_CHANGE_CLASSNAME, "phase" ); 423 final boolean preUpdate = phase.equals( "PRE" ); 424 425 final List<? extends VariableElement> parameters = method.getParameters(); 426 final ExecutableType methodType = resolveMethodType( descriptor, method ); 427 final List<? extends TypeMirror> parameterTypes = methodType.getParameterTypes(); 428 429 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 430 Constants.VIEW_CLASSNAME, 431 Constants.ON_INPUT_CHANGE_CLASSNAME, 432 method ); 433 MemberChecks.mustNotThrowAnyExceptions( Constants.ON_INPUT_CHANGE_CLASSNAME, method ); 434 MemberChecks.mustNotReturnAnyValue( Constants.ON_INPUT_CHANGE_CLASSNAME, method ); 435 436 final int parameterCount = parameters.size(); 437 if ( 0 == parameterCount ) 438 { 439 throw new ProcessorException( "@OnInputChange target must have at least 1 parameter.", method ); 440 } 441 final List<InputDescriptor> inputDescriptors = new ArrayList<>( parameterCount ); 442 for ( int i = 0; i < parameterCount; i++ ) 443 { 444 final VariableElement parameter = parameters.get( i ); 445 final String name = deriveOnInputChangeName( parameter ); 446 final InputDescriptor input = descriptor.findInputNamed( name ); 447 if ( null == input ) 448 { 449 throw new ProcessorException( "@OnInputChange target has a parameter named '" + 450 parameter.getSimpleName() + "' and the parameter is associated with a " + 451 "@Input named '" + name + "' but there is no corresponding @Input " + 452 "annotated method.", parameter ); 453 } 454 final Types typeUtils = processingEnv.getTypeUtils(); 455 if ( !typeUtils.isAssignable( parameterTypes.get( i ), input.getType() ) ) 456 { 457 throw new ProcessorException( "@OnInputChange target has a parameter named '" + 458 parameter.getSimpleName() + "' and the parameter type is not " + 459 "assignable to the return type of the associated @Input annotated method.", 460 method ); 461 } 462 final boolean mismatchedNullability = 463 ( 464 AnnotationsUtil.hasNonnullAnnotation( parameter ) && 465 AnnotationsUtil.hasNullableAnnotation( input.getElement() ) 466 ) || 467 ( 468 AnnotationsUtil.hasNullableAnnotation( parameter ) && 469 input.isNonNull() ); 470 471 if ( mismatchedNullability ) 472 { 473 throw new ProcessorException( "@OnInputChange target has a parameter named '" + 474 parameter.getSimpleName() + "' that has a nullability annotation " + 475 "incompatible with the associated @Input method named " + 476 method.getSimpleName(), method ); 477 } 478 if ( input.isImmutable() ) 479 { 480 throw new ProcessorException( "@OnInputChange target has a parameter named '" + 481 parameter.getSimpleName() + "' that is associated with an immutable @Input.", 482 method ); 483 } 484 inputDescriptors.add( input ); 485 } 486 onInputChangeDescriptors.add( new OnInputChangeDescriptor( method, inputDescriptors, preUpdate ) ); 487 } 488 descriptor.setOnInputChangeDescriptors( onInputChangeDescriptors ); 489 } 490 491 @Nonnull 492 private String deriveOnInputChangeName( @Nonnull final VariableElement parameter ) 493 { 494 final AnnotationValue value = 495 AnnotationsUtil.findAnnotationValue( parameter, Constants.INPUT_REF_CLASSNAME, "value" ); 496 497 if ( null != value ) 498 { 499 return (String) value.getValue(); 500 } 501 else 502 { 503 final String parameterName = parameter.getSimpleName().toString(); 504 if ( LAST_INPUT_PATTERN.matcher( parameterName ).matches() || 505 PREV_INPUT_PATTERN.matcher( parameterName ).matches() ) 506 { 507 return Character.toLowerCase( parameterName.charAt( 4 ) ) + parameterName.substring( 5 ); 508 } 509 else if ( INPUT_PATTERN.matcher( parameterName ).matches() ) 510 { 511 return parameterName; 512 } 513 else 514 { 515 throw new ProcessorException( "@OnInputChange target has a parameter named '" + parameterName + 516 "' is not explicitly associated with a input using @InputRef nor does it " + 517 "follow required naming conventions 'prev[MyInput]', 'last[MyInput]' or " + 518 "'[myInput]'.", parameter ); 519 } 520 } 521 } 522 523 private void determineInputValidatesMethods( @Nonnull final ViewDescriptor descriptor, 524 @Nonnull final List<ExecutableElement> methods ) 525 { 526 final List<ExecutableElement> inputValidateMethods = 527 methods 528 .stream() 529 .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_VALIDATE_CLASSNAME ) ) 530 .toList(); 531 532 for ( final ExecutableElement method : inputValidateMethods ) 533 { 534 final String name = deriveInputValidateName( method ); 535 final InputDescriptor input = descriptor.findInputNamed( name ); 536 if ( null == input ) 537 { 538 throw new ProcessorException( "@InputValidate target for input named '" + name + "' has no corresponding " + 539 "@Input annotated method.", method ); 540 } 541 if ( 1 != method.getParameters().size() ) 542 { 543 throw new ProcessorException( "@InputValidate target must have exactly 1 parameter", method ); 544 } 545 final ExecutableType methodType = resolveMethodType( descriptor, method ); 546 if ( !processingEnv.getTypeUtils().isAssignable( methodType.getParameterTypes().get( 0 ), input.getType() ) ) 547 { 548 throw new ProcessorException( "@InputValidate target has a parameter type that is not assignable to the " + 549 "return type of the associated @Input annotated method.", method ); 550 } 551 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 552 Constants.VIEW_CLASSNAME, 553 Constants.INPUT_VALIDATE_CLASSNAME, 554 method ); 555 MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_VALIDATE_CLASSNAME, method ); 556 MemberChecks.mustNotReturnAnyValue( Constants.INPUT_VALIDATE_CLASSNAME, method ); 557 558 final VariableElement param = method.getParameters().get( 0 ); 559 final boolean mismatchedNullability = 560 ( 561 AnnotationsUtil.hasNonnullAnnotation( param ) && 562 AnnotationsUtil.hasNullableAnnotation( input.getElement() ) 563 ) || 564 ( 565 AnnotationsUtil.hasNullableAnnotation( param ) && 566 input.isNonNull() ); 567 568 if ( mismatchedNullability ) 569 { 570 throw new ProcessorException( "@InputValidate target has a parameter that has a nullability annotation " + 571 "incompatible with the associated @Input method named " + 572 input.getElement().getSimpleName(), method ); 573 } 574 input.setValidateMethod( method ); 575 } 576 } 577 578 @Nonnull 579 private String deriveInputValidateName( @Nonnull final Element element ) 580 throws ProcessorException 581 { 582 final String name = 583 (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_VALIDATE_CLASSNAME, "name" ) 584 .getValue(); 585 586 if ( isSentinelName( name ) ) 587 { 588 final String deriveName = deriveName( element, VALIDATE_INPUT_PATTERN, name ); 589 if ( null == deriveName ) 590 { 591 throw new ProcessorException( "@InputValidate target has not specified name nor is it named according " + 592 "to the convention 'validate[Name]Input'.", element ); 593 } 594 return deriveName; 595 } 596 else 597 { 598 if ( !SourceVersion.isIdentifier( name ) ) 599 { 600 throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " + 601 "name must be a valid java identifier.", element ); 602 } 603 else if ( SourceVersion.isKeyword( name ) ) 604 { 605 throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " + 606 "name must not be a java keyword.", element ); 607 } 608 return name; 609 } 610 } 611 612 private void determineDefaultInputsMethods( @Nonnull final ViewDescriptor descriptor, 613 @Nonnull final List<ExecutableElement> methods ) 614 { 615 final List<ExecutableElement> defaultInputsMethods = 616 methods 617 .stream() 618 .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) ) 619 .toList(); 620 621 for ( final ExecutableElement method : defaultInputsMethods ) 622 { 623 final String name = deriveInputDefaultName( method ); 624 final InputDescriptor input = descriptor.findInputNamed( name ); 625 if ( null == input ) 626 { 627 throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " + 628 "@Input annotated method.", method ); 629 } 630 final ExecutableType methodType = resolveMethodType( descriptor, method ); 631 if ( !processingEnv.getTypeUtils().isAssignable( methodType.getReturnType(), input.getType() ) ) 632 { 633 throw new ProcessorException( "@InputDefault target has a return type that is not assignable to the " + 634 "return type of the associated @Input annotated method.", method ); 635 } 636 MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(), 637 Constants.VIEW_CLASSNAME, 638 Constants.INPUT_DEFAULT_CLASSNAME, 639 method ); 640 MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_DEFAULT_CLASSNAME, method ); 641 MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_DEFAULT_CLASSNAME, method ); 642 MemberChecks.mustReturnAValue( Constants.INPUT_DEFAULT_CLASSNAME, method ); 643 644 input.setDefaultMethod( method ); 645 } 646 } 647 648 private void determineDefaultInputsFields( @Nonnull final ViewDescriptor descriptor ) 649 { 650 final List<VariableElement> defaultInputsFields = 651 ElementsUtil.getFields( descriptor.getElement() ).stream() 652 .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) ) 653 .toList(); 654 655 for ( final VariableElement field : defaultInputsFields ) 656 { 657 final String name = deriveInputDefaultName( field ); 658 final InputDescriptor input = descriptor.findInputNamed( name ); 659 if ( null == input ) 660 { 661 throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " + 662 "@Input annotated method.", field ); 663 } 664 if ( !processingEnv.getTypeUtils().isAssignable( field.asType(), input.getType() ) ) 665 { 666 throw new ProcessorException( "@InputDefault target has a type that is not assignable to the " + 667 "return type of the associated @Input annotated method.", field ); 668 } 669 MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(), 670 Constants.VIEW_CLASSNAME, 671 Constants.INPUT_DEFAULT_CLASSNAME, 672 field ); 673 MemberChecks.mustBeFinal( Constants.INPUT_DEFAULT_CLASSNAME, field ); 674 input.setDefaultField( field ); 675 } 676 } 677 678 @Nonnull 679 private String deriveInputDefaultName( @Nonnull final Element element ) 680 throws ProcessorException 681 { 682 final String name = 683 (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_DEFAULT_CLASSNAME, "name" ) 684 .getValue(); 685 686 if ( isSentinelName( name ) ) 687 { 688 if ( element instanceof ExecutableElement ) 689 { 690 final String deriveName = deriveName( element, DEFAULT_GETTER_PATTERN, name ); 691 if ( null == deriveName ) 692 { 693 throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " + 694 "to the convention 'get[Name]Default'.", element ); 695 } 696 return deriveName; 697 } 698 else 699 { 700 final String fieldName = element.getSimpleName().toString(); 701 boolean matched = true; 702 final int lengthPrefix = "DEFAULT_".length(); 703 final int length = fieldName.length(); 704 if ( fieldName.startsWith( "DEFAULT_" ) && length > lengthPrefix ) 705 { 706 for ( int i = lengthPrefix; i < length; i++ ) 707 { 708 final char ch = fieldName.charAt( i ); 709 if ( Character.isLowerCase( ch ) || 710 ( 711 ( i != lengthPrefix || !Character.isJavaIdentifierStart( ch ) ) && 712 ( i == lengthPrefix || !Character.isJavaIdentifierPart( ch ) ) 713 ) ) 714 { 715 matched = false; 716 break; 717 } 718 } 719 } 720 else 721 { 722 matched = false; 723 } 724 if ( matched ) 725 { 726 return uppercaseConstantToPascalCase( fieldName.substring( lengthPrefix ) ); 727 } 728 else 729 { 730 throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " + 731 "to the convention 'DEFAULT_[NAME]'.", element ); 732 } 733 } 734 } 735 else 736 { 737 if ( !SourceVersion.isIdentifier( name ) ) 738 { 739 throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " + 740 "name must be a valid java identifier.", element ); 741 } 742 else if ( SourceVersion.isKeyword( name ) ) 743 { 744 throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " + 745 "name must not be a java keyword.", element ); 746 } 747 return name; 748 } 749 } 750 751 @Nonnull 752 private String uppercaseConstantToPascalCase( @Nonnull final String candidate ) 753 { 754 final String s = candidate.toLowerCase(); 755 final StringBuilder sb = new StringBuilder(); 756 boolean uppercase = false; 757 for ( int i = 0; i < s.length(); i++ ) 758 { 759 final char ch = s.charAt( i ); 760 if ( '_' == ch ) 761 { 762 uppercase = true; 763 } 764 else if ( uppercase ) 765 { 766 sb.append( Character.toUpperCase( ch ) ); 767 uppercase = false; 768 } 769 else 770 { 771 sb.append( ch ); 772 } 773 } 774 return sb.toString(); 775 } 776 777 private void determineInputs( @Nonnull final ViewDescriptor descriptor, 778 @Nonnull final List<ExecutableElement> methods ) 779 { 780 final List<InputDescriptor> inputs = new ArrayList<>(); 781 methods 782 .stream() 783 .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_CLASSNAME ) ) 784 .map( m -> createMethodInputDescriptor( descriptor, methods, m ) ) 785 .forEach( input -> addInputDescriptor( inputs, input ) ); 786 descriptor 787 .getConstructor() 788 .getParameters() 789 .stream() 790 .filter( this::isInputParameter ) 791 .map( p -> createConstructorInputDescriptor( descriptor, p ) ) 792 .forEach( input -> addInputDescriptor( inputs, input ) ); 793 794 final var childrenInput = inputs.stream().filter( p -> p.getName().equals( "children" ) ).findAny().orElse( null ); 795 final var childInput = inputs.stream().filter( p -> p.getName().equals( "child" ) ).findAny().orElse( null ); 796 if ( null != childrenInput && null != childInput ) 797 { 798 throw new ProcessorException( "Multiple candidate children @Input annotated methods: " + 799 childrenInput.getElement().getSimpleName() + " and " + 800 childInput.getElement().getSimpleName(), 801 childrenInput.getElement() ); 802 } 803 804 descriptor.setInputs( inputs ); 805 } 806 807 private boolean isDisposableDerivableAtCompileTime( @Nonnull final Element type ) 808 { 809 final var kind = type.getKind(); 810 if ( ElementKind.CLASS == kind && 811 AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_CLASSNAME ) ) 812 { 813 return true; 814 } 815 else if ( ElementKind.CLASS == kind || ElementKind.INTERFACE == kind ) 816 { 817 if ( AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) ) 818 { 819 return true; 820 } 821 else 822 { 823 return ElementsUtil.isAssignableTo( processingEnv, type, Constants.DISPOSABLE_CLASSNAME ); 824 } 825 } 826 else 827 { 828 return false; 829 } 830 } 831 832 private void determinePreludeCheckCandidates( @Nonnull final ViewDescriptor descriptor, 833 @Nonnull final TypeElement typeElement, 834 @Nonnull final List<ExecutableElement> methods ) 835 { 836 final var candidates = new ArrayList<PreludeChecksDescriptor>(); 837 838 final var fields = new LinkedHashMap<String, VariableElement>(); 839 for ( final var member : processingEnv.getElementUtils().getAllMembers( typeElement ) ) 840 { 841 if ( ElementKind.FIELD == member.getKind() ) 842 { 843 fields.putIfAbsent( member.getSimpleName().toString(), (VariableElement) member ); 844 } 845 } 846 847 for ( final var field : fields.values() ) 848 { 849 for ( final var annotation : new String[]{ Constants.COMPONENT_DEPENDENCY_CLASSNAME, 850 Constants.AUTO_OBSERVE_CLASSNAME } ) 851 { 852 if ( AnnotationsUtil.hasAnnotationOfType( field, annotation ) ) 853 { 854 MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(), 855 Constants.VIEW_CLASSNAME, 856 annotation, 857 field ); 858 final var fieldType = processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), field ); 859 final var observationMode = determinePreludeCheckObservationMode( field, fieldType ); 860 candidates.add( new PreludeChecksDescriptor( field, fieldType, observationMode ) ); 861 } 862 } 863 } 864 for ( final var method : methods ) 865 { 866 for ( final var annotation : new String[]{ Constants.COMPONENT_DEPENDENCY_CLASSNAME, 867 Constants.AUTO_OBSERVE_CLASSNAME } ) 868 { 869 if ( AnnotationsUtil.hasAnnotationOfType( method, annotation ) ) 870 { 871 MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(), 872 Constants.VIEW_CLASSNAME, 873 annotation, 874 method ); 875 final var returnType = resolveMethodType( descriptor, method ).getReturnType(); 876 final var observationMode = determinePreludeCheckObservationMode( method, returnType ); 877 candidates.add( new PreludeChecksDescriptor( method, returnType, observationMode ) ); 878 } 879 } 880 } 881 882 descriptor.setPreludeCheckCandidates( candidates ); 883 } 884 885 private void addInputDescriptor( @Nonnull final List<InputDescriptor> inputs, @Nonnull final InputDescriptor input ) 886 { 887 final var existing = inputs.stream().filter( p -> p.getName().equals( input.getName() ) ).findAny().orElse( null ); 888 if ( null != existing ) 889 { 890 throw new ProcessorException( "Multiple @Input declarations for input named '" + input.getName() + 891 "': " + existing.getElement().getSimpleName() + " and " + 892 input.getElement().getSimpleName(), 893 input.getElement() ); 894 } 895 inputs.add( input ); 896 } 897 898 private boolean isInputRequired( @Nonnull final InputDescriptor input ) 899 { 900 final String requiredValue = input.getRequiredValue(); 901 if ( "ENABLE".equals( requiredValue ) ) 902 { 903 return true; 904 } 905 else if ( "DISABLE".equals( requiredValue ) ) 906 { 907 return false; 908 } 909 else if ( input.isFromTreeContext() ) 910 { 911 return false; 912 } 913 else 914 { 915 return !input.hasDefaultMethod() && 916 !input.hasDefaultField() && 917 !AnnotationsUtil.hasNullableAnnotation( input.getElement() ); 918 } 919 } 920 921 @Nonnull 922 private InputDescriptor createMethodInputDescriptor( @Nonnull final ViewDescriptor descriptor, 923 @Nonnull final List<ExecutableElement> methods, 924 @Nonnull final ExecutableElement method ) 925 { 926 final String name = deriveInputName( method ); 927 final ExecutableType methodType = resolveMethodType( descriptor, method ); 928 929 verifyNoDuplicateAnnotations( method ); 930 MemberChecks.mustBeAbstract( Constants.INPUT_CLASSNAME, method ); 931 MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_CLASSNAME, method ); 932 MemberChecks.mustReturnAValue( Constants.INPUT_CLASSNAME, method ); 933 MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_CLASSNAME, method ); 934 MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(), 935 Constants.VIEW_CLASSNAME, 936 Constants.INPUT_CLASSNAME, 937 method ); 938 final TypeMirror returnType = method.getReturnType(); 939 if ( !returnType.getKind().isPrimitive() && 940 !AnnotationsUtil.hasNonnullAnnotation( method ) && 941 !AnnotationsUtil.hasNullableAnnotation( method ) && 942 ElementsUtil.isWarningNotSuppressed( method, 943 Constants.WARNING_MISSING_INPUT_NULLABILITY, 944 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 945 { 946 final String message = 947 MemberChecks.shouldNot( Constants.INPUT_CLASSNAME, 948 "return a non-primitive type without a @Nonnull or @Nullable annotation. " + 949 MemberChecks.suppressedBy( Constants.WARNING_MISSING_INPUT_NULLABILITY, 950 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 951 warning( message, method ); 952 } 953 validateInputNameAndType( name, returnType, method ); 954 955 if ( returnType instanceof final TypeVariable typeVariable ) 956 { 957 final String typeVariableName = typeVariable.asElement().getSimpleName().toString(); 958 List<? extends TypeParameterElement> typeParameters = method.getTypeParameters(); 959 if ( typeParameters.stream().anyMatch( p -> p.getSimpleName().toString().equals( typeVariableName ) ) ) 960 { 961 throw new ProcessorException( "@Input named '" + name + "' is has a type variable as a return type " + 962 "that is declared on the method.", method ); 963 } 964 } 965 final String qualifier = (String) AnnotationsUtil 966 .getAnnotationValue( method, Constants.INPUT_CLASSNAME, "qualifier" ).getValue(); 967 final boolean fromTreeContextInput = isFromTreeContextInput( method ); 968 final Element inputType = ElementsUtil.asTypeElement( processingEnv, returnType ); 969 final boolean observable = isInputObservable( methods, method ); 970 final boolean disposable = null != inputType && isDisposableDerivableAtCompileTime( inputType ); 971 final TypeName typeName = TypeName.get( returnType ); 972 if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( method ) ) 973 { 974 throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " + 975 "@Nonnull annotation. The return type should be the primitive type.", 976 method ); 977 } 978 if ( !"".equals( qualifier ) && !fromTreeContextInput ) 979 { 980 throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME, 981 "specify qualifier unless fromTreeContext=true" ), 982 method ); 983 } 984 final String requiredValue = 985 AnnotationsUtil.getEnumAnnotationParameter( method, Constants.INPUT_CLASSNAME, "require" ); 986 987 final InputDescriptor inputDescriptor = 988 new InputDescriptor( descriptor, 989 name, 990 qualifier, 991 method, 992 returnType, 993 method, 994 methodType, 995 null, 996 fromTreeContextInput, 997 true, 998 observable, 999 disposable, 1000 null, 1001 requiredValue ); 1002 if ( inputDescriptor.mayNeedMutableInputAccessedInPostConstructInvariant() ) 1003 { 1004 if ( ElementsUtil.isWarningSuppressed( method, 1005 Constants.WARNING_MUTABLE_INPUT_ACCESSED_IN_POST_CONSTRUCT, 1006 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 1007 { 1008 inputDescriptor.suppressMutableInputAccessedInPostConstruct(); 1009 } 1010 } 1011 return inputDescriptor; 1012 } 1013 1014 @Nonnull 1015 private InputDescriptor createConstructorInputDescriptor( @Nonnull final ViewDescriptor descriptor, 1016 @Nonnull final VariableElement parameter ) 1017 { 1018 final String name = deriveInputName( parameter ); 1019 final TypeMirror type = parameter.asType(); 1020 if ( !type.getKind().isPrimitive() && 1021 !AnnotationsUtil.hasNonnullAnnotation( parameter ) && 1022 !AnnotationsUtil.hasNullableAnnotation( parameter ) && 1023 ElementsUtil.isWarningNotSuppressed( parameter, 1024 Constants.WARNING_MISSING_INPUT_NULLABILITY, 1025 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 1026 { 1027 final String message = 1028 MemberChecks.shouldNot( Constants.INPUT_CLASSNAME, 1029 "return a non-primitive type without a @Nonnull or @Nullable annotation. " + 1030 MemberChecks.suppressedBy( Constants.WARNING_MISSING_INPUT_NULLABILITY, 1031 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 1032 warning( message, parameter ); 1033 } 1034 validateInputNameAndType( name, type, parameter ); 1035 1036 final String qualifier = (String) AnnotationsUtil 1037 .getAnnotationValue( parameter, Constants.INPUT_CLASSNAME, "qualifier" ).getValue(); 1038 final boolean fromTreeContextInput = isFromTreeContextInput( parameter ); 1039 final Element inputType = ElementsUtil.asTypeElement( processingEnv, type ); 1040 //final boolean observable = isInputObservable( methods, method ); 1041 final var observable = 1042 AnnotationsUtil.getEnumAnnotationParameter( parameter, Constants.INPUT_CLASSNAME, "observable" ); 1043 if ( "ENABLE".equals( observable ) ) 1044 { 1045 throw new ProcessorException( "@Input target must not specify observable=ENABLE " + 1046 "for an immutable input.", parameter ); 1047 } 1048 final boolean disposable = null != inputType && isDisposableDerivableAtCompileTime( inputType ); 1049 final TypeName typeName = TypeName.get( type ); 1050 if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( parameter ) ) 1051 { 1052 throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " + 1053 "@Nonnull annotation. The return type should be the primitive type.", 1054 parameter ); 1055 } 1056 final ImmutableInputKeyStrategy strategy = getImmutableInputKeyStrategy( typeName, inputType ); 1057 if ( !"".equals( qualifier ) && !fromTreeContextInput ) 1058 { 1059 throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME, 1060 "specify qualifier unless fromTreeContext=true" ), 1061 parameter ); 1062 } 1063 final String requiredValue = 1064 AnnotationsUtil.getEnumAnnotationParameter( parameter, Constants.INPUT_CLASSNAME, "require" ); 1065 1066 return new InputDescriptor( descriptor, 1067 name, 1068 qualifier, 1069 parameter, 1070 type, 1071 null, 1072 null, 1073 parameter, 1074 fromTreeContextInput, 1075 false, 1076 false, 1077 disposable, 1078 strategy, 1079 requiredValue ); 1080 } 1081 1082 @Nonnull 1083 private ImmutableInputKeyStrategy getImmutableInputKeyStrategy( @Nonnull final TypeName typeName, 1084 @Nullable final Element element ) 1085 { 1086 if ( typeName.toString().equals( "java.lang.String" ) ) 1087 { 1088 return ImmutableInputKeyStrategy.IS_STRING; 1089 } 1090 else if ( typeName.isBoxedPrimitive() || typeName.isPrimitive() ) 1091 { 1092 return ImmutableInputKeyStrategy.TO_STRING; 1093 } 1094 else if ( null != element ) 1095 { 1096 if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) && 1097 isAssignableToKeyed( element ) ) 1098 { 1099 return ImmutableInputKeyStrategy.KEYED; 1100 } 1101 else if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) && 1102 ( 1103 isAssignableToIdentifiable( element ) || 1104 AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) || 1105 ( AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) && 1106 isIdRequired( (TypeElement) element ) ) 1107 ) ) 1108 { 1109 return ImmutableInputKeyStrategy.AREZ_IDENTIFIABLE; 1110 } 1111 else if ( ElementKind.ENUM == element.getKind() ) 1112 { 1113 return ImmutableInputKeyStrategy.ENUM; 1114 } 1115 } 1116 return ImmutableInputKeyStrategy.DYNAMIC; 1117 } 1118 1119 private boolean isAssignableToKeyed( @Nonnull final Element element ) 1120 { 1121 return ElementsUtil.isAssignableTo( processingEnv, element, Constants.KEYED_CLASSNAME ); 1122 } 1123 1124 private boolean isAssignableToIdentifiable( @Nonnull final Element element ) 1125 { 1126 final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.IDENTIFIABLE_CLASSNAME ); 1127 final TypeMirror identifiableErasure = processingEnv.getTypeUtils().erasure( typeElement.asType() ); 1128 return processingEnv.getTypeUtils().isAssignable( element.asType(), identifiableErasure ); 1129 } 1130 1131 /** 1132 * The logic from this method has been cloned from Arez. 1133 * One day we should consider improving Arez so that this is not required somehow? 1134 */ 1135 private boolean isIdRequired( @Nonnull final TypeElement element ) 1136 { 1137 final String requireIdParameter = 1138 AnnotationsUtil.getEnumAnnotationParameter( element, Constants.AREZ_COMPONENT_CLASSNAME, "requireId" ); 1139 return !"DISABLE".equals( requireIdParameter ); 1140 } 1141 1142 @Nonnull 1143 private String deriveInputName( @Nonnull final Element element ) 1144 throws ProcessorException 1145 { 1146 final String specifiedName = 1147 (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_CLASSNAME, "name" ).getValue(); 1148 1149 final String name; 1150 if ( element instanceof ExecutableElement method ) 1151 { 1152 name = getPropertyAccessorName( method, specifiedName ); 1153 } 1154 else 1155 { 1156 name = isSentinelName( specifiedName ) ? element.getSimpleName().toString() : specifiedName; 1157 } 1158 if ( !SourceVersion.isIdentifier( name ) ) 1159 { 1160 throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " + 1161 "name must be a valid java identifier.", element ); 1162 } 1163 else if ( SourceVersion.isKeyword( name ) ) 1164 { 1165 throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " + 1166 "name must not be a java keyword.", element ); 1167 } 1168 else 1169 { 1170 return name; 1171 } 1172 } 1173 1174 private void validateInputNameAndType( @Nonnull final String name, 1175 @Nonnull final TypeMirror type, 1176 @Nonnull final Element element ) 1177 { 1178 if ( "build".equals( name ) ) 1179 { 1180 throw new ProcessorException( "@Input named 'build' is invalid as it conflicts with the method named " + 1181 "build() that is used in the generated Builder classes", 1182 element ); 1183 } 1184 else if ( "child".equals( name ) && 1185 ( type.getKind() != TypeKind.DECLARED && !"react4j.ReactNode".equals( type.toString() ) ) ) 1186 { 1187 throw new ProcessorException( "@Input named 'child' should be of type react4j.ReactNode", element ); 1188 } 1189 else if ( "children".equals( name ) && 1190 ( type.getKind() != TypeKind.DECLARED && !"react4j.ReactNode[]".equals( type.toString() ) ) ) 1191 { 1192 throw new ProcessorException( "@Input named 'children' should be of type react4j.ReactNode[]", element ); 1193 } 1194 } 1195 1196 private void determineOnErrorMethod( @Nonnull final TypeElement typeElement, 1197 @Nonnull final ViewDescriptor descriptor, 1198 @Nonnull final List<ExecutableElement> methods ) 1199 { 1200 for ( final ExecutableElement method : methods ) 1201 { 1202 if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.ON_ERROR_CLASSNAME ) ) 1203 { 1204 MemberChecks.mustNotBeAbstract( Constants.ON_ERROR_CLASSNAME, method ); 1205 MemberChecks.mustBeSubclassCallable( typeElement, 1206 Constants.VIEW_CLASSNAME, 1207 Constants.ON_ERROR_CLASSNAME, 1208 method ); 1209 MemberChecks.mustNotReturnAnyValue( Constants.ON_ERROR_CLASSNAME, method ); 1210 MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ERROR_CLASSNAME, method ); 1211 1212 boolean infoFound = false; 1213 boolean errorFound = false; 1214 for ( final VariableElement parameter : method.getParameters() ) 1215 { 1216 final TypeName typeName = TypeName.get( parameter.asType() ); 1217 if ( typeName.toString().equals( Constants.ERROR_INFO_CLASSNAME ) ) 1218 { 1219 if ( infoFound ) 1220 { 1221 throw new ProcessorException( "@OnError target has multiple parameters of type " + 1222 Constants.ERROR_INFO_CLASSNAME, 1223 method ); 1224 } 1225 infoFound = true; 1226 } 1227 else if ( typeName.toString().equals( Constants.JS_ERROR_CLASSNAME ) ) 1228 { 1229 if ( errorFound ) 1230 { 1231 throw new ProcessorException( "@OnError target has multiple parameters of type " + 1232 Constants.JS_ERROR_CLASSNAME, 1233 method ); 1234 } 1235 errorFound = true; 1236 } 1237 else 1238 { 1239 throw new ProcessorException( "@OnError target has parameter of invalid type named " + 1240 parameter.getSimpleName(), 1241 parameter ); 1242 } 1243 } 1244 descriptor.setOnError( method ); 1245 } 1246 } 1247 } 1248 1249 private void determineScheduleRenderMethods( @Nonnull final TypeElement typeElement, 1250 @Nonnull final ViewDescriptor descriptor, 1251 @Nonnull final List<ExecutableElement> methods ) 1252 { 1253 final List<ScheduleRenderDescriptor> scheduleRenderDescriptors = new ArrayList<>(); 1254 for ( final ExecutableElement method : methods ) 1255 { 1256 final AnnotationMirror annotation = 1257 AnnotationsUtil.findAnnotationByType( method, Constants.SCHEDULE_RENDER_CLASSNAME ); 1258 if ( null != annotation ) 1259 { 1260 MemberChecks.mustBeAbstract( Constants.SCHEDULE_RENDER_CLASSNAME, method ); 1261 MemberChecks.mustBeSubclassCallable( typeElement, 1262 Constants.VIEW_CLASSNAME, 1263 Constants.SCHEDULE_RENDER_CLASSNAME, 1264 method ); 1265 MemberChecks.mustNotReturnAnyValue( Constants.SCHEDULE_RENDER_CLASSNAME, method ); 1266 MemberChecks.mustNotThrowAnyExceptions( Constants.SCHEDULE_RENDER_CLASSNAME, method ); 1267 1268 final ViewType viewType = descriptor.getType(); 1269 if ( ViewType.STATEFUL != viewType ) 1270 { 1271 final String message = 1272 MemberChecks.mustNot( Constants.SCHEDULE_RENDER_CLASSNAME, 1273 "be enclosed in a type if it is annotated by @View(type=" + viewType + 1274 "). The type must be STATEFUL" ); 1275 throw new ProcessorException( message, method ); 1276 } 1277 1278 final boolean skipShouldViewUpdate = 1279 AnnotationsUtil.getAnnotationValueValue( annotation, "skipShouldViewUpdate" ); 1280 1281 scheduleRenderDescriptors.add( new ScheduleRenderDescriptor( method, skipShouldViewUpdate ) ); 1282 } 1283 } 1284 descriptor.setScheduleRenderDescriptors( scheduleRenderDescriptors ); 1285 } 1286 1287 private void determinePublishMethods( @Nonnull final TypeElement typeElement, 1288 @Nonnull final ViewDescriptor descriptor, 1289 @Nonnull final List<ExecutableElement> methods ) 1290 { 1291 final List<PublishDescriptor> descriptors = new ArrayList<>(); 1292 for ( final ExecutableElement method : methods ) 1293 { 1294 final AnnotationMirror annotation = AnnotationsUtil.findAnnotationByType( method, Constants.PUBLISH_CLASSNAME ); 1295 if ( null != annotation ) 1296 { 1297 MemberChecks.mustBeSubclassCallable( typeElement, 1298 Constants.VIEW_CLASSNAME, 1299 Constants.PUBLISH_CLASSNAME, 1300 method ); 1301 MemberChecks.mustNotHaveAnyParameters( Constants.PUBLISH_CLASSNAME, method ); 1302 MemberChecks.mustNotHaveAnyTypeParameters( Constants.PUBLISH_CLASSNAME, method ); 1303 MemberChecks.mustReturnAValue( Constants.PUBLISH_CLASSNAME, method ); 1304 MemberChecks.mustNotThrowAnyExceptions( Constants.PUBLISH_CLASSNAME, method ); 1305 1306 final String qualifier = AnnotationsUtil.getAnnotationValueValue( annotation, "qualifier" ); 1307 final ExecutableType methodType = resolveMethodType( descriptor, method ); 1308 1309 if ( TypeKind.TYPEVAR == methodType.getReturnType().getKind() ) 1310 { 1311 throw new ProcessorException( MemberChecks.mustNot( Constants.PUBLISH_CLASSNAME, "return a type variable" ), 1312 method ); 1313 } 1314 1315 descriptors.add( new PublishDescriptor( qualifier, method, methodType ) ); 1316 } 1317 } 1318 descriptor.setPublishDescriptors( descriptors ); 1319 } 1320 1321 private void determinePreRenderMethods( @Nonnull final TypeElement typeElement, 1322 @Nonnull final ViewDescriptor descriptor, 1323 @Nonnull final List<ExecutableElement> methods ) 1324 { 1325 final List<RenderHookDescriptor> descriptors = new ArrayList<>(); 1326 for ( final ExecutableElement method : methods ) 1327 { 1328 final AnnotationMirror annotation = 1329 AnnotationsUtil.findAnnotationByType( method, Constants.PRE_RENDER_CLASSNAME ); 1330 if ( null != annotation ) 1331 { 1332 MemberChecks.mustBeSubclassCallable( typeElement, 1333 Constants.VIEW_CLASSNAME, 1334 Constants.PRE_RENDER_CLASSNAME, 1335 method ); 1336 MemberChecks.mustNotBeAbstract( Constants.PRE_RENDER_CLASSNAME, method ); 1337 MemberChecks.mustNotHaveAnyParameters( Constants.PRE_RENDER_CLASSNAME, method ); 1338 MemberChecks.mustNotHaveAnyTypeParameters( Constants.PRE_RENDER_CLASSNAME, method ); 1339 MemberChecks.mustNotReturnAnyValue( Constants.PRE_RENDER_CLASSNAME, method ); 1340 MemberChecks.mustNotThrowAnyExceptions( Constants.PRE_RENDER_CLASSNAME, method ); 1341 1342 final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" ); 1343 final ExecutableType methodType = resolveMethodType( descriptor, method ); 1344 1345 descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) ); 1346 } 1347 } 1348 descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) ); 1349 descriptor.setPreRenderDescriptors( descriptors ); 1350 } 1351 1352 private void determinePostRenderMethods( @Nonnull final TypeElement typeElement, 1353 @Nonnull final ViewDescriptor descriptor, 1354 @Nonnull final List<ExecutableElement> methods ) 1355 { 1356 final List<RenderHookDescriptor> descriptors = new ArrayList<>(); 1357 for ( final ExecutableElement method : methods ) 1358 { 1359 final AnnotationMirror annotation = 1360 AnnotationsUtil.findAnnotationByType( method, Constants.POST_RENDER_CLASSNAME ); 1361 if ( null != annotation ) 1362 { 1363 MemberChecks.mustBeSubclassCallable( typeElement, 1364 Constants.VIEW_CLASSNAME, 1365 Constants.POST_RENDER_CLASSNAME, 1366 method ); 1367 MemberChecks.mustNotBeAbstract( Constants.POST_RENDER_CLASSNAME, method ); 1368 MemberChecks.mustNotHaveAnyParameters( Constants.POST_RENDER_CLASSNAME, method ); 1369 MemberChecks.mustNotHaveAnyTypeParameters( Constants.POST_RENDER_CLASSNAME, method ); 1370 MemberChecks.mustNotReturnAnyValue( Constants.POST_RENDER_CLASSNAME, method ); 1371 MemberChecks.mustNotThrowAnyExceptions( Constants.POST_RENDER_CLASSNAME, method ); 1372 1373 final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" ); 1374 final ExecutableType methodType = resolveMethodType( descriptor, method ); 1375 1376 descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) ); 1377 } 1378 } 1379 descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) ); 1380 descriptor.setPostRenderDescriptors( descriptors ); 1381 } 1382 1383 private void determineRenderMethod( @Nonnull final TypeElement typeElement, 1384 @Nonnull final ViewDescriptor descriptor, 1385 @Nonnull final List<ExecutableElement> methods ) 1386 { 1387 boolean foundRender = false; 1388 for ( final ExecutableElement method : methods ) 1389 { 1390 final AnnotationMirror annotation = 1391 AnnotationsUtil.findAnnotationByType( method, Constants.RENDER_CLASSNAME ); 1392 if ( null != annotation ) 1393 { 1394 MemberChecks.mustNotBeAbstract( Constants.RENDER_CLASSNAME, method ); 1395 MemberChecks.mustBeSubclassCallable( typeElement, 1396 Constants.VIEW_CLASSNAME, 1397 Constants.RENDER_CLASSNAME, 1398 method ); 1399 MemberChecks.mustNotHaveAnyParameters( Constants.RENDER_CLASSNAME, method ); 1400 MemberChecks.mustReturnAnInstanceOf( processingEnv, 1401 method, 1402 Constants.RENDER_CLASSNAME, 1403 Constants.VNODE_CLASSNAME ); 1404 MemberChecks.mustNotThrowAnyExceptions( Constants.RENDER_CLASSNAME, method ); 1405 MemberChecks.mustNotHaveAnyTypeParameters( Constants.RENDER_CLASSNAME, method ); 1406 if ( !method.getReturnType().getKind().isPrimitive() && 1407 !AnnotationsUtil.hasNonnullAnnotation( method ) && 1408 !AnnotationsUtil.hasNullableAnnotation( method ) && 1409 ElementsUtil.isWarningNotSuppressed( method, 1410 Constants.WARNING_MISSING_RENDER_NULLABILITY, 1411 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ) 1412 { 1413 final String message = 1414 MemberChecks.should( Constants.RENDER_CLASSNAME, 1415 "be annotated by a @Nonnull or a @Nullable annotation. " + 1416 MemberChecks.suppressedBy( Constants.WARNING_MISSING_RENDER_NULLABILITY, 1417 Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) ); 1418 warning( message, method ); 1419 } 1420 1421 descriptor.setRender( method ); 1422 foundRender = true; 1423 } 1424 } 1425 final boolean requireRender = descriptor.requireRender(); 1426 if ( requireRender && !foundRender ) 1427 { 1428 throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME, 1429 "contain a method annotated with the " + 1430 MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) + 1431 " annotation or must specify type=NO_RENDER" ), 1432 typeElement ); 1433 } 1434 else if ( !requireRender ) 1435 { 1436 if ( foundRender ) 1437 { 1438 throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME, 1439 "contain a method annotated with the " + 1440 MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) + 1441 " annotation or must not specify type=NO_RENDER" ), 1442 typeElement ); 1443 } 1444 else if ( !descriptor.hasConstructor() && 1445 !descriptor.hasPostConstruct() && 1446 null == descriptor.getPostMount() && 1447 null == descriptor.getPostRender() && 1448 null == descriptor.getPreUpdate() && 1449 null == descriptor.getPostUpdate() && 1450 descriptor.getPreRenderDescriptors().isEmpty() && 1451 descriptor.getPostRenderDescriptors().isEmpty() && 1452 !descriptor.hasPreUpdateOnInputChange() && 1453 !descriptor.hasPostUpdateOnInputChange() ) 1454 { 1455 throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME, 1456 "contain lifecycle methods if the the @View(type=NO_RENDER) parameter is specified" ), 1457 typeElement ); 1458 } 1459 } 1460 } 1461 1462 private void determinePostMountMethod( @Nonnull final TypeElement typeElement, 1463 @Nonnull final ViewDescriptor descriptor, 1464 @Nonnull final List<ExecutableElement> methods ) 1465 { 1466 for ( final ExecutableElement method : methods ) 1467 { 1468 if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_CLASSNAME ) ) 1469 { 1470 MemberChecks.mustBeLifecycleHook( typeElement, 1471 Constants.VIEW_CLASSNAME, 1472 Constants.POST_MOUNT_CLASSNAME, 1473 method ); 1474 descriptor.setPostMount( method ); 1475 } 1476 } 1477 } 1478 1479 private void determinePostMountOrUpdateMethod( @Nonnull final TypeElement typeElement, 1480 @Nonnull final ViewDescriptor descriptor, 1481 @Nonnull final List<ExecutableElement> methods ) 1482 { 1483 for ( final ExecutableElement method : methods ) 1484 { 1485 if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_OR_UPDATE_CLASSNAME ) ) 1486 { 1487 MemberChecks.mustBeLifecycleHook( typeElement, 1488 Constants.VIEW_CLASSNAME, 1489 Constants.POST_MOUNT_OR_UPDATE_CLASSNAME, 1490 method ); 1491 descriptor.setPostRender( method ); 1492 } 1493 } 1494 } 1495 1496 private void determinePostUpdateMethod( @Nonnull final TypeElement typeElement, 1497 @Nonnull final ViewDescriptor descriptor, 1498 @Nonnull final List<ExecutableElement> methods ) 1499 { 1500 for ( final ExecutableElement method : methods ) 1501 { 1502 if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_UPDATE_CLASSNAME ) ) 1503 { 1504 MemberChecks.mustBeLifecycleHook( typeElement, 1505 Constants.VIEW_CLASSNAME, 1506 Constants.POST_UPDATE_CLASSNAME, 1507 method ); 1508 descriptor.setPostUpdate( method ); 1509 } 1510 } 1511 } 1512 1513 private void determinePreUpdateMethod( @Nonnull final TypeElement typeElement, 1514 @Nonnull final ViewDescriptor descriptor, 1515 @Nonnull final List<ExecutableElement> methods ) 1516 { 1517 for ( final ExecutableElement method : methods ) 1518 { 1519 if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.PRE_UPDATE_CLASSNAME ) ) 1520 { 1521 MemberChecks.mustBeLifecycleHook( typeElement, 1522 Constants.VIEW_CLASSNAME, 1523 Constants.PRE_UPDATE_CLASSNAME, 1524 method ); 1525 descriptor.setPreUpdate( method ); 1526 } 1527 } 1528 } 1529 1530 private ExecutableType resolveMethodType( @Nonnull final ViewDescriptor descriptor, 1531 @Nonnull final ExecutableElement method ) 1532 { 1533 return (ExecutableType) processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), method ); 1534 } 1535 1536 @Nonnull 1537 private String deriveViewName( @Nonnull final TypeElement typeElement ) 1538 { 1539 final String name = 1540 (String) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "name" ) 1541 .getValue(); 1542 1543 if ( isSentinelName( name ) ) 1544 { 1545 return typeElement.getSimpleName().toString(); 1546 } 1547 else 1548 { 1549 if ( !SourceVersion.isIdentifier( name ) ) 1550 { 1551 throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) + 1552 " target specified an invalid name '" + name + "'. The " + 1553 "name must be a valid java identifier.", typeElement ); 1554 } 1555 else if ( SourceVersion.isKeyword( name ) ) 1556 { 1557 throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) + 1558 " target specified an invalid name '" + name + "'. The " + 1559 "name must not be a java keyword.", typeElement ); 1560 } 1561 return name; 1562 } 1563 } 1564 1565 private void determineViewCapabilities( @Nonnull final ViewDescriptor descriptor, 1566 @Nonnull final TypeElement typeElement ) 1567 { 1568 if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_CLASSNAME ) ) 1569 { 1570 throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME, 1571 "be annotated with the " + 1572 MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) + 1573 " as React4j will add the annotation." ), 1574 typeElement ); 1575 } 1576 1577 if ( descriptor.needsInjection() && !descriptor.getDeclaredType().getTypeArguments().isEmpty() ) 1578 { 1579 throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) + 1580 " target has enabled injection integration but the class " + 1581 "has type arguments which is incompatible with injection integration.", 1582 typeElement ); 1583 } 1584 } 1585 1586 @Nonnull 1587 private ViewType extractViewType( @Nonnull final TypeElement typeElement ) 1588 { 1589 final String declaredType = 1590 AnnotationsUtil.getEnumAnnotationParameter( typeElement, Constants.VIEW_CLASSNAME, "type" ); 1591 return ViewType.valueOf( declaredType ); 1592 } 1593 1594 private boolean extractExportBuilder( @Nonnull final TypeElement typeElement ) 1595 { 1596 return (Boolean) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "exportBuilder" ) 1597 .getValue(); 1598 } 1599 1600 private boolean isInputObservable( @Nonnull final List<ExecutableElement> methods, 1601 @Nonnull final Element element ) 1602 { 1603 final var parameter = 1604 AnnotationsUtil.getEnumAnnotationParameter( element, Constants.INPUT_CLASSNAME, "observable" ); 1605 return switch ( parameter ) 1606 { 1607 case "ENABLE" -> true; 1608 case "DISABLE" -> false; 1609 default -> hasAnyArezObserverMethods( methods ); 1610 }; 1611 } 1612 1613 private boolean hasAnyArezObserverMethods( @Nonnull final List<ExecutableElement> methods ) 1614 { 1615 return 1616 methods 1617 .stream() 1618 .anyMatch( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.MEMOIZE_CLASSNAME ) || 1619 ( AnnotationsUtil.hasAnnotationOfType( m, Constants.OBSERVE_CLASSNAME ) && 1620 ( !m.getParameters().isEmpty() || !m.getSimpleName().toString().equals( "trackRender" ) ) ) ); 1621 } 1622 1623 @Nonnull 1624 private ObserveMode determinePreludeCheckObservationMode( @Nonnull final Element element, 1625 @Nonnull final TypeMirror type ) 1626 { 1627 if ( type.getKind().isPrimitive() ) 1628 { 1629 return ObserveMode.NO_OBSERVE; 1630 } 1631 else 1632 { 1633 final TypeElement typeElement = ElementsUtil.asTypeElement( processingEnv, type ); 1634 if ( null != typeElement ) 1635 { 1636 final var resolution = resolveArezComponentObservable( typeElement ); 1637 if ( ArezComponentObservableResolution.DISABLED == resolution ) 1638 { 1639 return ObserveMode.NO_OBSERVE; 1640 } 1641 else if ( ArezComponentObservableResolution.ENABLED == resolution || isAssignableToComponentObservable( type ) ) 1642 { 1643 return AnnotationsUtil.hasNonnullAnnotation( element ) ? 1644 ObserveMode.OBSERVE_NONNULL : 1645 ObserveMode.OBSERVE_NULLABLE; 1646 } 1647 else if ( canTypeUseRuntimeComponentObservableCheck( type, typeElement ) ) 1648 { 1649 // Type does not implement `arez.component.ComponentObservable` but it is not final so try at runtime 1650 return ObserveMode.RUNTIME_CHECK; 1651 } 1652 else 1653 { 1654 // Can never implement arez.component.ComponentObservable 1655 return ObserveMode.NO_OBSERVE; 1656 } 1657 } 1658 else 1659 { 1660 return ObserveMode.NO_OBSERVE; 1661 } 1662 } 1663 } 1664 1665 private boolean canTypeUseRuntimeComponentObservableCheck( @Nonnull final TypeMirror type, 1666 @Nullable final Element typeElement ) 1667 { 1668 return TypeKind.TYPEVAR == type.getKind() || 1669 null != typeElement && 1670 ( ElementKind.INTERFACE == typeElement.getKind() || 1671 ( ElementKind.CLASS == typeElement.getKind() && 1672 !typeElement.getModifiers().contains( Modifier.FINAL ) ) ); 1673 } 1674 1675 private boolean isAssignableToComponentObservable( @Nonnull final TypeMirror type ) 1676 { 1677 return ElementsUtil.isAssignableTo( processingEnv, type, Constants.COMPONENT_OBSERVABLE_CLASSNAME ); 1678 } 1679 1680 @Nonnull 1681 private ArezComponentObservableResolution resolveArezComponentObservable( @Nonnull final TypeElement element ) 1682 { 1683 if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) ) 1684 { 1685 return ArezComponentObservableResolution.NOT_AREZ_COMPONENT; 1686 } 1687 1688 final String observableParameter = 1689 AnnotationsUtil.getEnumAnnotationParameter( element, Constants.AREZ_COMPONENT_CLASSNAME, "observable" ); 1690 return switch ( observableParameter ) 1691 { 1692 case "ENABLE" -> ArezComponentObservableResolution.ENABLED; 1693 case "DISABLE" -> ArezComponentObservableResolution.DISABLED; 1694 default -> 1695 { 1696 final boolean disposeOnDeactivate = (Boolean) 1697 AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "disposeOnDeactivate" ) 1698 .getValue(); 1699 yield disposeOnDeactivate ? 1700 ArezComponentObservableResolution.ENABLED : 1701 ArezComponentObservableResolution.DISABLED; 1702 } 1703 }; 1704 } 1705 1706 private enum ArezComponentObservableResolution 1707 { 1708 ENABLED, 1709 DISABLED, 1710 NOT_AREZ_COMPONENT 1711 } 1712 1713 private boolean isFromTreeContextInput( @Nonnull final Element element ) 1714 { 1715 return (Boolean) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_CLASSNAME, "fromTreeContext" ) 1716 .getValue(); 1717 } 1718 1719 private boolean shouldSetDefaultPriority( @Nonnull final List<ExecutableElement> methods ) 1720 { 1721 return 1722 methods 1723 .stream() 1724 .filter( method -> !method.getModifiers().contains( Modifier.PRIVATE ) ) 1725 .anyMatch( method -> AnnotationsUtil.hasAnnotationOfType( method, Constants.MEMOIZE_CLASSNAME ) || 1726 AnnotationsUtil.hasAnnotationOfType( method, Constants.OBSERVE_CLASSNAME ) ); 1727 } 1728 1729 private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method ) 1730 throws ProcessorException 1731 { 1732 final List<String> annotations = 1733 Arrays.asList( Constants.INPUT_DEFAULT_CLASSNAME, 1734 Constants.INPUT_VALIDATE_CLASSNAME, 1735 Constants.ON_INPUT_CHANGE_CLASSNAME, 1736 Constants.INPUT_CLASSNAME ); 1737 MemberChecks.verifyNoOverlappingAnnotations( method, annotations, Collections.emptyMap() ); 1738 } 1739 1740 private boolean isSentinelName( @Nonnull final String name ) 1741 { 1742 return SENTINEL_NAME.equals( name ); 1743 } 1744 1745 @Nonnull 1746 private String getPropertyAccessorName( @Nonnull final ExecutableElement method, 1747 @Nonnull final String specifiedName ) 1748 throws ProcessorException 1749 { 1750 String name = deriveName( method, GETTER_PATTERN, specifiedName ); 1751 if ( null != name ) 1752 { 1753 return name; 1754 } 1755 else if ( method.getReturnType().getKind() == TypeKind.BOOLEAN ) 1756 { 1757 name = deriveName( method, ISSER_PATTERN, specifiedName ); 1758 if ( null != name ) 1759 { 1760 return name; 1761 } 1762 } 1763 return method.getSimpleName().toString(); 1764 } 1765 1766 @Nullable 1767 private String deriveName( @Nonnull final Element method, @Nonnull final Pattern pattern, @Nonnull final String name ) 1768 throws ProcessorException 1769 { 1770 if ( isSentinelName( name ) ) 1771 { 1772 final String methodName = method.getSimpleName().toString(); 1773 final Matcher matcher = pattern.matcher( methodName ); 1774 if ( matcher.find() ) 1775 { 1776 final String candidate = matcher.group( 1 ); 1777 return Character.toLowerCase( candidate.charAt( 0 ) ) + candidate.substring( 1 ); 1778 } 1779 else 1780 { 1781 return null; 1782 } 1783 } 1784 else 1785 { 1786 return name; 1787 } 1788 } 1789}