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