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