001package arez.processor; 002 003import com.palantir.javapoet.ParameterizedTypeName; 004import com.palantir.javapoet.TypeName; 005import java.io.IOException; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.HashMap; 011import java.util.HashSet; 012import java.util.LinkedHashMap; 013import java.util.List; 014import java.util.Locale; 015import java.util.Map; 016import java.util.Objects; 017import java.util.Set; 018import java.util.function.Function; 019import java.util.regex.Matcher; 020import java.util.regex.Pattern; 021import java.util.regex.PatternSyntaxException; 022import javax.annotation.Nonnull; 023import javax.annotation.Nullable; 024import javax.annotation.processing.ProcessingEnvironment; 025import javax.annotation.processing.RoundEnvironment; 026import javax.annotation.processing.SupportedAnnotationTypes; 027import javax.annotation.processing.SupportedSourceVersion; 028import javax.lang.model.AnnotatedConstruct; 029import javax.lang.model.SourceVersion; 030import javax.lang.model.element.AnnotationMirror; 031import javax.lang.model.element.AnnotationValue; 032import javax.lang.model.element.Element; 033import javax.lang.model.element.ElementKind; 034import javax.lang.model.element.ExecutableElement; 035import javax.lang.model.element.Modifier; 036import javax.lang.model.element.TypeElement; 037import javax.lang.model.element.VariableElement; 038import javax.lang.model.type.DeclaredType; 039import javax.lang.model.type.ExecutableType; 040import javax.lang.model.type.TypeKind; 041import javax.lang.model.type.TypeMirror; 042import javax.lang.model.util.Elements; 043import javax.lang.model.util.Types; 044import org.realityforge.proton.AbstractStandardProcessor; 045import org.realityforge.proton.AnnotationsUtil; 046import org.realityforge.proton.DeferredElementSet; 047import org.realityforge.proton.ElementsUtil; 048import org.realityforge.proton.MemberChecks; 049import org.realityforge.proton.NamesUtil; 050import org.realityforge.proton.ProcessorException; 051import org.realityforge.proton.StopWatch; 052import org.realityforge.proton.SuperficialValidation; 053import org.realityforge.proton.TypesUtil; 054import static javax.tools.Diagnostic.Kind.*; 055 056/** 057 * Annotation processor that analyzes Arez annotated source and generates models from the annotations. 058 */ 059@SupportedAnnotationTypes( "arez.annotations.*" ) 060@SupportedSourceVersion( SourceVersion.RELEASE_17 ) 061public final class ArezProcessor 062 extends AbstractStandardProcessor 063{ 064 @Nonnull 065 static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" ); 066 @Nonnull 067 private static final Pattern ON_ACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Activate$" ); 068 @Nonnull 069 private static final Pattern ON_DEACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Deactivate$" ); 070 @Nonnull 071 private static final Pattern SETTER_PATTERN = Pattern.compile( "^set([A-Z].*)$" ); 072 @Nonnull 073 private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" ); 074 @Nonnull 075 private static final Pattern OBSERVABLE_INITIAL_METHOD_PATTERN = Pattern.compile( "^getInitial([A-Z].*)$" ); 076 @Nonnull 077 private static final Pattern OBSERVABLE_INITIAL_FIELD_PATTERN = Pattern.compile( "^INITIAL_([A-Z].*)$" ); 078 @Nonnull 079 private static final List<String> OBJECT_METHODS = 080 Arrays.asList( "hashCode", "equals", "clone", "toString", "finalize", "getClass", "wait", "notifyAll", "notify" ); 081 @Nonnull 082 private static final List<String> AREZ_SPECIAL_METHODS = 083 Arrays.asList( "observe", "dispose", "isDisposed", "getArezId" ); 084 @Nonnull 085 private static final String AREZ_COMPONENT_LIKE_DESCRIPTION = 086 "@ArezComponentLike or an annotation annotated by @ActAsArezComponent"; 087 @Nonnull 088 private static final String AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION = "an Arez component-like type"; 089 @Nonnull 090 private static final List<String> MISPLACED_USAGE_ANNOTATION_CLASSNAMES = 091 Arrays.asList( Constants.ACTION_CLASSNAME, 092 Constants.REQUIRES_TRANSACTION_CLASSNAME, 093 Constants.OBSERVE_CLASSNAME, 094 Constants.OBSERVABLE_CLASSNAME, 095 Constants.MEMOIZE_CLASSNAME, 096 Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME, 097 Constants.COMPONENT_ID_CLASSNAME, 098 Constants.COMPONENT_ID_REF_CLASSNAME, 099 Constants.COMPONENT_REF_CLASSNAME, 100 Constants.COMPONENT_NAME_REF_CLASSNAME, 101 Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME, 102 Constants.COMPONENT_STATE_REF_CLASSNAME, 103 Constants.CONTEXT_REF_CLASSNAME, 104 Constants.OBSERVABLE_VALUE_REF_CLASSNAME, 105 Constants.COMPUTABLE_VALUE_REF_CLASSNAME, 106 Constants.OBSERVER_REF_CLASSNAME, 107 Constants.POST_CONSTRUCT_CLASSNAME, 108 Constants.PRE_DISPOSE_CLASSNAME, 109 Constants.POST_DISPOSE_CLASSNAME, 110 Constants.ON_ACTIVATE_CLASSNAME, 111 Constants.ON_DEACTIVATE_CLASSNAME, 112 Constants.ON_DEPS_CHANGE_CLASSNAME, 113 Constants.REFERENCE_CLASSNAME, 114 Constants.REFERENCE_ID_CLASSNAME, 115 Constants.INVERSE_CLASSNAME, 116 Constants.PRE_INVERSE_REMOVE_CLASSNAME, 117 Constants.POST_INVERSE_ADD_CLASSNAME, 118 Constants.AUTO_OBSERVE_CLASSNAME, 119 Constants.CASCADE_DISPOSE_CLASSNAME, 120 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 121 Constants.OBSERVABLE_INITIAL_CLASSNAME, 122 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 123 @Nonnull 124 private static final List<String> METHOD_ANNOTATIONS = 125 Arrays.asList( Constants.ACTION_CLASSNAME, 126 Constants.REQUIRES_TRANSACTION_CLASSNAME, 127 Constants.AUTO_OBSERVE_CLASSNAME, 128 Constants.OBSERVE_CLASSNAME, 129 Constants.ON_DEPS_CHANGE_CLASSNAME, 130 Constants.OBSERVER_REF_CLASSNAME, 131 Constants.OBSERVABLE_CLASSNAME, 132 Constants.OBSERVABLE_INITIAL_CLASSNAME, 133 Constants.OBSERVABLE_VALUE_REF_CLASSNAME, 134 Constants.MEMOIZE_CLASSNAME, 135 Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME, 136 Constants.COMPUTABLE_VALUE_REF_CLASSNAME, 137 Constants.COMPONENT_REF_CLASSNAME, 138 Constants.COMPONENT_ID_CLASSNAME, 139 Constants.COMPONENT_ID_REF_CLASSNAME, 140 Constants.COMPONENT_NAME_REF_CLASSNAME, 141 Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME, 142 Constants.COMPONENT_STATE_REF_CLASSNAME, 143 Constants.CASCADE_DISPOSE_CLASSNAME, 144 Constants.CONTEXT_REF_CLASSNAME, 145 Constants.POST_CONSTRUCT_CLASSNAME, 146 Constants.PRE_DISPOSE_CLASSNAME, 147 Constants.POST_DISPOSE_CLASSNAME, 148 Constants.REFERENCE_CLASSNAME, 149 Constants.REFERENCE_ID_CLASSNAME, 150 Constants.INVERSE_CLASSNAME, 151 Constants.PRE_INVERSE_REMOVE_CLASSNAME, 152 Constants.POST_INVERSE_ADD_CLASSNAME, 153 Constants.ON_ACTIVATE_CLASSNAME, 154 Constants.ON_DEACTIVATE_CLASSNAME, 155 Constants.COMPONENT_DEPENDENCY_CLASSNAME ); 156 @Nonnull 157 private static final Pattern ID_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Id$" ); 158 @Nonnull 159 private static final Pattern RAW_ID_GETTER_PATTERN = Pattern.compile( "^(.*)Id$" ); 160 @Nonnull 161 private static final Pattern OBSERVABLE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ObservableValue$" ); 162 @Nonnull 163 private static final Pattern COMPUTABLE_VALUE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ComputableValue$" ); 164 @Nonnull 165 private static final Pattern OBSERVER_REF_PATTERN = Pattern.compile( "^get([A-Z].*)Observer$" ); 166 @Nonnull 167 private static final Pattern ON_DEPS_CHANGE_PATTERN = Pattern.compile( "^on([A-Z].*)DepsChange" ); 168 @Nonnull 169 private static final Pattern PRE_INVERSE_REMOVE_PATTERN = Pattern.compile( "^pre([A-Z].*)Remove" ); 170 @Nonnull 171 private static final Pattern POST_INVERSE_ADD_PATTERN = Pattern.compile( "^post([A-Z].*)Add" ); 172 @Nonnull 173 private static final Pattern CAPTURE_PATTERN = Pattern.compile( "^capture([A-Z].*)" ); 174 @Nonnull 175 private static final Pattern POP_PATTERN = Pattern.compile( "^pop([A-Z].*)" ); 176 @Nonnull 177 private static final Pattern PUSH_PATTERN = Pattern.compile( "^push([A-Z].*)" ); 178 @Nonnull 179 private final DeferredElementSet _deferredTypes = new DeferredElementSet(); 180 @Nonnull 181 private final StopWatch _analyzeComponentStopWatch = new StopWatch( "Analyze Component" ); 182 183 @Override 184 @Nonnull 185 protected String getIssueTrackerURL() 186 { 187 return "https://github.com/arez/arez/issues"; 188 } 189 190 @Nonnull 191 @Override 192 protected String getOptionPrefix() 193 { 194 return "arez"; 195 } 196 197 @Override 198 protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches ) 199 { 200 stopWatches.add( _analyzeComponentStopWatch ); 201 } 202 203 @Override 204 public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env ) 205 { 206 debugAnnotationProcessingRootElements( env ); 207 collectRootTypeNames( env ); 208 if ( !env.processingOver() ) 209 { 210 detectMisplacedArezAnnotations( env ); 211 warnOnPublicConstructorsInArezComponentLikeTypes( env ); 212 } 213 processTypeElements( annotations, 214 env, 215 Constants.COMPONENT_CLASSNAME, 216 _deferredTypes, 217 _analyzeComponentStopWatch.getName(), 218 this::process, 219 _analyzeComponentStopWatch ); 220 errorIfProcessingOverAndInvalidTypesDetected( env ); 221 clearRootTypeNamesIfProcessingOver( env ); 222 return true; 223 } 224 225 private void process( @Nonnull final TypeElement element ) 226 throws IOException, ProcessorException 227 { 228 final ComponentDescriptor descriptor = parse( element ); 229 emitTypeSpec( descriptor.getPackageName(), ComponentGenerator.buildType( processingEnv, descriptor ) ); 230 } 231 232 private void warnOnPublicConstructorsInArezComponentLikeTypes( @Nonnull final RoundEnvironment env ) 233 { 234 final var visited = new HashSet<TypeElement>(); 235 for ( final var rootElement : env.getRootElements() ) 236 { 237 warnOnPublicConstructorsInArezComponentLikeTypes( rootElement, visited ); 238 } 239 } 240 241 private void warnOnPublicConstructorsInArezComponentLikeTypes( @Nonnull final Element element, 242 @Nonnull final Set<TypeElement> visited ) 243 { 244 if ( element instanceof final TypeElement typeElement ) 245 { 246 if ( visited.add( typeElement ) ) 247 { 248 warnOnPublicConstructorsInArezComponentLikeType( typeElement ); 249 for ( final var enclosedElement : typeElement.getEnclosedElements() ) 250 { 251 if ( enclosedElement instanceof TypeElement ) 252 { 253 warnOnPublicConstructorsInArezComponentLikeTypes( enclosedElement, visited ); 254 } 255 } 256 } 257 } 258 } 259 260 private void warnOnPublicConstructorsInArezComponentLikeType( @Nonnull final TypeElement typeElement ) 261 { 262 if ( !isArezComponentAnnotated( typeElement ) && isArezComponentLikeAnnotated( typeElement ) ) 263 { 264 for ( final var constructor : ElementsUtil.getConstructors( typeElement ) ) 265 { 266 if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) && 267 constructor.getModifiers().contains( Modifier.PUBLIC ) && 268 isWarningNotSuppressed( constructor, Constants.WARNING_PUBLIC_CONSTRUCTOR ) ) 269 { 270 final var message = 271 "Arez component-like target should not have a public constructor. The type should have a package-access " + 272 "constructor so instantiation is controlled by the framework. " + 273 suppressedBy( Constants.WARNING_PUBLIC_CONSTRUCTOR ); 274 warning( message, constructor ); 275 } 276 } 277 } 278 } 279 280 private void detectMisplacedArezAnnotations( @Nonnull final RoundEnvironment env ) 281 { 282 final var componentTypes = findComponentTypes( env ); 283 for ( final var annotationClassname : MISPLACED_USAGE_ANNOTATION_CLASSNAMES ) 284 { 285 final var annotationType = findTypeElement( annotationClassname ); 286 if ( null == annotationType ) 287 { 288 continue; 289 } 290 291 for ( final var element : env.getElementsAnnotatedWith( annotationType ) ) 292 { 293 final var type = findNearestEnclosingType( element ); 294 if ( null == type || !isValidArezAnnotationContainer( element, type, componentTypes ) ) 295 { 296 final var message = 297 "@" + annotationType.getSimpleName() + " is only supported within a type annotated by " + 298 "@ArezComponent or " + AREZ_COMPONENT_LIKE_DESCRIPTION; 299 processingEnv.getMessager().printMessage( ERROR, message, element ); 300 } 301 } 302 } 303 } 304 305 @Nullable 306 private TypeElement findNearestEnclosingType( @Nonnull final Element element ) 307 { 308 var current = element; 309 while ( null != current && !( current instanceof TypeElement ) ) 310 { 311 current = current.getEnclosingElement(); 312 } 313 return (TypeElement) current; 314 } 315 316 @Nonnull 317 private Set<TypeElement> findComponentTypes( @Nonnull final RoundEnvironment env ) 318 { 319 final var annotationType = findTypeElement( Constants.COMPONENT_CLASSNAME ); 320 if ( null == annotationType ) 321 { 322 return Collections.emptySet(); 323 } 324 325 final var componentTypes = new HashSet<TypeElement>(); 326 for ( final var element : env.getElementsAnnotatedWith( annotationType ) ) 327 { 328 if ( element instanceof TypeElement ) 329 { 330 componentTypes.add( (TypeElement) element ); 331 } 332 } 333 return componentTypes; 334 } 335 336 private boolean isValidArezAnnotationContainer( @Nonnull final Element element, 337 @Nonnull final TypeElement type, 338 @Nonnull final Set<TypeElement> componentTypes ) 339 { 340 if ( isArezComponentAnnotated( type ) || isArezComponentLikeAnnotated( type ) ) 341 { 342 return true; 343 } 344 else if ( element instanceof TypeElement ) 345 { 346 return false; 347 } 348 else 349 { 350 final var containerType = processingEnv.getTypeUtils().erasure( type.asType() ); 351 return 352 componentTypes 353 .stream() 354 .anyMatch( componentType -> 355 !type.equals( componentType ) && 356 processingEnv.getTypeUtils() 357 .isSubtype( processingEnv.getTypeUtils().erasure( componentType.asType() ), containerType ) ); 358 } 359 } 360 361 @Nonnull 362 private ObservableDescriptor addObservable( @Nonnull final ComponentDescriptor component, 363 @Nonnull final AnnotationMirror annotation, 364 @Nonnull final ExecutableElement method, 365 @Nonnull final ExecutableType methodType ) 366 throws ProcessorException 367 { 368 MemberChecks.mustBeOverridable( component.getElement(), 369 Constants.COMPONENT_CLASSNAME, 370 Constants.OBSERVABLE_CLASSNAME, 371 method ); 372 373 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 374 final boolean expectSetter = AnnotationsUtil.getAnnotationValueValue( annotation, "expectSetter" ); 375 final VariableElement readOutsideTransaction = 376 AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" ); 377 final VariableElement writeOutsideTransaction = 378 AnnotationsUtil.getAnnotationValueValue( annotation, "writeOutsideTransaction" ); 379 final boolean setterAlwaysMutates = AnnotationsUtil.getAnnotationValueValue( annotation, "setterAlwaysMutates" ); 380 final TypeMirror equalityComparator = 381 AnnotationsUtil.getAnnotationValueValue( annotation, "equalityComparator" ); 382 final Boolean requireInitializer = isInitializerRequired( method ); 383 384 final TypeMirror returnType = method.getReturnType(); 385 final String methodName = method.getSimpleName().toString(); 386 String name; 387 final boolean setter; 388 if ( TypeKind.VOID == returnType.getKind() ) 389 { 390 setter = true; 391 //Should be a setter 392 if ( 1 != method.getParameters().size() ) 393 { 394 throw new ProcessorException( "@Observable target should be a setter or getter", method ); 395 } 396 397 name = deriveName( method, SETTER_PATTERN, declaredName ); 398 if ( null == name ) 399 { 400 name = methodName; 401 } 402 } 403 else 404 { 405 setter = false; 406 //Must be a getter 407 if ( !method.getParameters().isEmpty() ) 408 { 409 throw new ProcessorException( "@Observable target should be a setter or getter", method ); 410 } 411 name = 412 NamesUtil.getPropertyAccessorName( method, 413 GETTER_PATTERN, 414 ISSER_PATTERN, 415 declaredName, 416 Constants.SENTINEL ); 417 } 418 // Override name if supplied by user 419 if ( !Constants.SENTINEL.equals( declaredName ) ) 420 { 421 name = declaredName; 422 if ( !SourceVersion.isIdentifier( name ) ) 423 { 424 throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " + 425 "name must be a valid java identifier.", method ); 426 } 427 else if ( SourceVersion.isKeyword( name ) ) 428 { 429 throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " + 430 "name must not be a java keyword.", method ); 431 } 432 } 433 checkNameUnique( component, name, method, Constants.OBSERVABLE_CLASSNAME ); 434 435 if ( setter && !expectSetter ) 436 { 437 throw new ProcessorException( "Method annotated with @Observable is a setter but defines " + 438 "expectSetter = false for observable named " + name, method ); 439 } 440 441 final ObservableDescriptor observable = component.findOrCreateObservable( name ); 442 final String equalityComparatorClassName = equalityComparator.toString(); 443 444 observable.setReadOutsideTransaction( readOutsideTransaction.getSimpleName().toString() ); 445 observable.setWriteOutsideTransaction( writeOutsideTransaction.getSimpleName().toString() ); 446 if ( !setterAlwaysMutates ) 447 { 448 observable.setSetterAlwaysMutates( false ); 449 } 450 if ( !expectSetter ) 451 { 452 observable.setExpectSetter( false ); 453 } 454 if ( !observable.expectSetter() ) 455 { 456 if ( observable.hasSetter() ) 457 { 458 throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " + 459 "setter exists named " + observable.getSetter().getSimpleName() + 460 "for observable named " + name, method ); 461 } 462 } 463 if ( setter ) 464 { 465 observable.setSetterDeclaredEqualityComparator( equalityComparatorClassName ); 466 if ( observable.hasSetter() ) 467 { 468 throw new ProcessorException( "Method annotated with @Observable defines duplicate setter for " + 469 "observable named " + name, method ); 470 } 471 if ( !observable.expectSetter() ) 472 { 473 throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " + 474 "setter exists for observable named " + name, method ); 475 } 476 observable.setSetter( method, methodType ); 477 } 478 else 479 { 480 observable.setGetterDeclaredEqualityComparator( equalityComparatorClassName ); 481 if ( observable.hasGetter() ) 482 { 483 throw new ProcessorException( "Method annotated with @Observable defines duplicate getter for " + 484 "observable named " + name, method ); 485 } 486 observable.setGetter( method, methodType ); 487 } 488 if ( null != requireInitializer ) 489 { 490 if ( !method.getModifiers().contains( Modifier.ABSTRACT ) ) 491 { 492 throw new ProcessorException( "@Observable target set initializer parameter to ENABLED but " + 493 "method is not abstract.", method ); 494 } 495 final Boolean existing = observable.getInitializer(); 496 if ( null == existing ) 497 { 498 observable.setInitializer( requireInitializer ); 499 } 500 else if ( existing != requireInitializer ) 501 { 502 throw new ProcessorException( "@Observable target set initializer parameter to value that differs from " + 503 "the paired observable method.", method ); 504 } 505 } 506 return observable; 507 } 508 509 private void addObservableValueRef( @Nonnull final ComponentDescriptor component, 510 @Nonnull final AnnotationMirror annotation, 511 @Nonnull final ExecutableElement method, 512 @Nonnull final ExecutableType methodType ) 513 throws ProcessorException 514 { 515 mustBeStandardRefMethod( processingEnv, 516 component, 517 method, 518 Constants.OBSERVABLE_VALUE_REF_CLASSNAME ); 519 520 final TypeMirror returnType = methodType.getReturnType(); 521 if ( TypeKind.DECLARED != returnType.getKind() || 522 !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ObservableValue" ) ) 523 { 524 throw new ProcessorException( "Method annotated with @ObservableValueRef must return an instance of " + 525 "arez.ObservableValue", method ); 526 } 527 528 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 529 final String name; 530 if ( Constants.SENTINEL.equals( declaredName ) ) 531 { 532 name = deriveName( method, OBSERVABLE_REF_PATTERN, declaredName ); 533 if ( null == name ) 534 { 535 throw new ProcessorException( "Method annotated with @ObservableValueRef should specify name or be " + 536 "named according to the convention get[Name]ObservableValue", method ); 537 } 538 } 539 else 540 { 541 name = declaredName; 542 if ( !SourceVersion.isIdentifier( name ) ) 543 { 544 throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " + 545 "name must be a valid java identifier.", method ); 546 } 547 else if ( SourceVersion.isKeyword( name ) ) 548 { 549 throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " + 550 "name must not be a java keyword.", method ); 551 } 552 } 553 554 component.findOrCreateObservable( name ).addRefMethod( method, methodType ); 555 } 556 557 private void addComputableValueRef( @Nonnull final ComponentDescriptor component, 558 @Nonnull final AnnotationMirror annotation, 559 @Nonnull final ExecutableElement method, 560 @Nonnull final ExecutableType methodType ) 561 throws ProcessorException 562 { 563 mustBeRefMethod( component, method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME ); 564 shouldBeInternalRefMethod( processingEnv, 565 component, 566 method, 567 Constants.COMPUTABLE_VALUE_REF_CLASSNAME ); 568 569 final TypeMirror returnType = methodType.getReturnType(); 570 if ( TypeKind.DECLARED != returnType.getKind() || 571 !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ComputableValue" ) ) 572 { 573 throw new ProcessorException( "Method annotated with @ComputableValueRef must return an instance of " + 574 "arez.ComputableValue", method ); 575 } 576 577 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 578 final String name; 579 if ( Constants.SENTINEL.equals( declaredName ) ) 580 { 581 name = deriveName( method, COMPUTABLE_VALUE_REF_PATTERN, declaredName ); 582 if ( null == name ) 583 { 584 throw new ProcessorException( "Method annotated with @ComputableValueRef should specify name or be " + 585 "named according to the convention get[Name]ComputableValue", method ); 586 } 587 } 588 else 589 { 590 name = declaredName; 591 if ( !SourceVersion.isIdentifier( name ) ) 592 { 593 throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " + 594 "name must be a valid java identifier.", method ); 595 } 596 else if ( SourceVersion.isKeyword( name ) ) 597 { 598 throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " + 599 "name must not be a java keyword.", method ); 600 } 601 } 602 603 MemberChecks.mustBeSubclassCallable( component.getElement(), 604 Constants.COMPONENT_CLASSNAME, 605 Constants.COMPUTABLE_VALUE_REF_CLASSNAME, 606 method ); 607 MemberChecks.mustNotThrowAnyExceptions( Constants.COMPUTABLE_VALUE_REF_CLASSNAME, method ); 608 component.findOrCreateMemoize( name ).addRefMethod( method, methodType ); 609 } 610 611 @Nonnull 612 private String deriveMemoizeName( @Nonnull final ExecutableElement method, 613 @Nonnull final AnnotationMirror annotation ) 614 throws ProcessorException 615 { 616 final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 617 if ( Constants.SENTINEL.equals( name ) ) 618 { 619 return NamesUtil.getPropertyAccessorName( method, 620 GETTER_PATTERN, 621 ISSER_PATTERN, 622 name, 623 Constants.SENTINEL ); 624 } 625 else 626 { 627 if ( !SourceVersion.isIdentifier( name ) ) 628 { 629 throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " + 630 "name must be a valid java identifier.", method ); 631 } 632 else if ( SourceVersion.isKeyword( name ) ) 633 { 634 throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " + 635 "name must not be a java keyword.", method ); 636 } 637 return name; 638 } 639 } 640 641 private void addOnActivate( @Nonnull final ComponentDescriptor component, 642 @Nonnull final AnnotationMirror annotation, 643 @Nonnull final ExecutableElement method ) 644 throws ProcessorException 645 { 646 final String name = 647 deriveHookName( component, 648 method, 649 ON_ACTIVATE_PATTERN, 650 "Activate", 651 AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) ); 652 setOnActivate( component, component.findOrCreateMemoize( name ), method ); 653 } 654 655 private void addOnDeactivate( @Nonnull final ComponentDescriptor component, 656 @Nonnull final AnnotationMirror annotation, 657 @Nonnull final ExecutableElement method ) 658 throws ProcessorException 659 { 660 final String name = 661 deriveHookName( component, 662 method, 663 ON_DEACTIVATE_PATTERN, 664 "Deactivate", 665 AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) ); 666 MemberChecks.mustBeLifecycleHook( component.getElement(), 667 Constants.COMPONENT_CLASSNAME, 668 Constants.ON_DEACTIVATE_CLASSNAME, 669 method ); 670 shouldBeInternalHookMethod( processingEnv, 671 component, 672 method, 673 Constants.ON_DEACTIVATE_CLASSNAME ); 674 component.findOrCreateMemoize( name ).setOnDeactivate( method ); 675 } 676 677 @Nonnull 678 private String deriveHookName( @Nonnull final ComponentDescriptor component, 679 @Nonnull final ExecutableElement method, 680 @Nonnull final Pattern pattern, 681 @Nonnull final String type, 682 @Nonnull final String name ) 683 throws ProcessorException 684 { 685 final String value = deriveName( method, pattern, name ); 686 if ( null == value ) 687 { 688 throw new ProcessorException( "Unable to derive name for @On" + type + " as does not match " + 689 "on[Name]" + type + " pattern. Please specify name.", method ); 690 } 691 else if ( !SourceVersion.isIdentifier( value ) ) 692 { 693 throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " + 694 "name must be a valid java identifier.", component.getElement() ); 695 } 696 else if ( SourceVersion.isKeyword( value ) ) 697 { 698 throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " + 699 "name must not be a java keyword.", component.getElement() ); 700 } 701 else 702 { 703 return value; 704 } 705 } 706 707 private void addComponentStateRef( @Nonnull final ComponentDescriptor component, 708 @Nonnull final AnnotationMirror annotation, 709 @Nonnull final ExecutableElement method ) 710 throws ProcessorException 711 { 712 mustBeStandardRefMethod( processingEnv, 713 component, 714 method, 715 Constants.COMPONENT_STATE_REF_CLASSNAME ); 716 717 final TypeMirror returnType = method.getReturnType(); 718 if ( TypeKind.BOOLEAN != returnType.getKind() ) 719 { 720 throw new ProcessorException( "@ComponentStateRef target must return a boolean", method ); 721 } 722 final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "value" ); 723 final ComponentStateRefDescriptor.State state = 724 ComponentStateRefDescriptor.State.valueOf( variableElement.getSimpleName().toString() ); 725 726 component.getComponentStateRefs().add( new ComponentStateRefDescriptor( method, state ) ); 727 } 728 729 private void addContextRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method ) 730 throws ProcessorException 731 { 732 mustBeStandardRefMethod( processingEnv, 733 component, 734 method, 735 Constants.CONTEXT_REF_CLASSNAME ); 736 MemberChecks.mustReturnAnInstanceOf( processingEnv, 737 method, 738 Constants.OBSERVER_REF_CLASSNAME, 739 "arez.ArezContext" ); 740 component.getContextRefs().add( method ); 741 } 742 743 private void addComponentIdRef( @Nonnull final ComponentDescriptor component, 744 @Nonnull final ExecutableElement method ) 745 { 746 mustBeRefMethod( component, method, Constants.COMPONENT_ID_REF_CLASSNAME ); 747 MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_REF_CLASSNAME, method ); 748 component.getComponentIdRefs().add( method ); 749 } 750 751 private void addComponentRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method ) 752 throws ProcessorException 753 { 754 mustBeStandardRefMethod( processingEnv, 755 component, 756 method, 757 Constants.COMPONENT_REF_CLASSNAME ); 758 MemberChecks.mustReturnAnInstanceOf( processingEnv, 759 method, 760 Constants.COMPONENT_REF_CLASSNAME, 761 "arez.Component" ); 762 component.getComponentRefs().add( method ); 763 } 764 765 private void setComponentId( @Nonnull final ComponentDescriptor component, 766 @Nonnull final ExecutableElement componentId, 767 @Nonnull final ExecutableType componentIdMethodType ) 768 throws ProcessorException 769 { 770 MemberChecks.mustNotBeAbstract( Constants.COMPONENT_ID_CLASSNAME, componentId ); 771 MemberChecks.mustBeSubclassCallable( component.getElement(), 772 Constants.COMPONENT_CLASSNAME, 773 Constants.COMPONENT_ID_CLASSNAME, 774 componentId ); 775 MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_CLASSNAME, componentId ); 776 MemberChecks.mustReturnAValue( Constants.COMPONENT_ID_CLASSNAME, componentId ); 777 MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_ID_CLASSNAME, componentId ); 778 779 if ( null != component.getComponentId() ) 780 { 781 throw new ProcessorException( "@ComponentId target duplicates existing method named " + 782 component.getComponentId().getSimpleName(), componentId ); 783 } 784 else 785 { 786 component.setComponentId( Objects.requireNonNull( componentId ) ); 787 component.setComponentIdMethodType( componentIdMethodType ); 788 } 789 } 790 791 private void setComponentTypeNameRef( @Nonnull final ComponentDescriptor component, 792 @Nonnull final ExecutableElement method ) 793 throws ProcessorException 794 { 795 mustBeStandardRefMethod( processingEnv, 796 component, 797 method, 798 Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME ); 799 MemberChecks.mustReturnAnInstanceOf( processingEnv, 800 method, 801 Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME, 802 String.class.getName() ); 803 component.getComponentTypeNameRefs().add( method ); 804 } 805 806 private void addComponentNameRef( @Nonnull final ComponentDescriptor component, 807 @Nonnull final ExecutableElement method ) 808 throws ProcessorException 809 { 810 mustBeStandardRefMethod( processingEnv, 811 component, 812 method, 813 Constants.COMPONENT_NAME_REF_CLASSNAME ); 814 MemberChecks.mustReturnAnInstanceOf( processingEnv, 815 method, 816 Constants.COMPONENT_NAME_REF_CLASSNAME, 817 String.class.getName() ); 818 component.getComponentNameRefs().add( method ); 819 } 820 821 private void addPostConstruct( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method ) 822 throws ProcessorException 823 { 824 MemberChecks.mustBeLifecycleHook( component.getElement(), 825 Constants.COMPONENT_CLASSNAME, 826 Constants.POST_CONSTRUCT_CLASSNAME, 827 method ); 828 shouldBeInternalLifecycleMethod( processingEnv, 829 component, 830 method, 831 Constants.POST_CONSTRUCT_CLASSNAME ); 832 component.getPostConstructs().add( method ); 833 } 834 835 private void addPreDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method ) 836 throws ProcessorException 837 { 838 MemberChecks.mustBeLifecycleHook( component.getElement(), 839 Constants.COMPONENT_CLASSNAME, 840 Constants.PRE_DISPOSE_CLASSNAME, 841 method ); 842 shouldBeInternalLifecycleMethod( processingEnv, 843 component, 844 method, 845 Constants.PRE_DISPOSE_CLASSNAME ); 846 component.getPreDisposes().add( method ); 847 } 848 849 private void addPostDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method ) 850 throws ProcessorException 851 { 852 MemberChecks.mustBeLifecycleHook( component.getElement(), 853 Constants.COMPONENT_CLASSNAME, 854 Constants.POST_DISPOSE_CLASSNAME, 855 method ); 856 shouldBeInternalLifecycleMethod( processingEnv, 857 component, 858 method, 859 Constants.POST_DISPOSE_CLASSNAME ); 860 component.getPostDisposes().add( method ); 861 } 862 863 private void linkUnAnnotatedObservables( @Nonnull final ComponentDescriptor component, 864 @Nonnull final Map<String, CandidateMethod> getters, 865 @Nonnull final Map<String, CandidateMethod> setters ) 866 throws ProcessorException 867 { 868 for ( final ObservableDescriptor observable : component.getObservables().values() ) 869 { 870 if ( !observable.hasSetter() && !observable.hasGetter() ) 871 { 872 throw new ProcessorException( "@ObservableValueRef target unable to be associated with an " + 873 "Observable property", observable.getRefMethods().get( 0 ).getMethod() ); 874 } 875 else if ( !observable.hasSetter() && observable.expectSetter() ) 876 { 877 final CandidateMethod candidate = setters.remove( observable.getName() ); 878 if ( null != candidate ) 879 { 880 MemberChecks.mustBeOverridable( component.getElement(), 881 Constants.COMPONENT_CLASSNAME, 882 Constants.OBSERVABLE_CLASSNAME, 883 candidate.getMethod() ); 884 observable.setSetter( candidate.getMethod(), candidate.getMethodType() ); 885 } 886 else if ( observable.hasGetter() ) 887 { 888 throw new ProcessorException( "@Observable target defined getter but no setter was defined and no " + 889 "setter could be automatically determined", observable.getGetter() ); 890 } 891 } 892 else if ( !observable.hasGetter() ) 893 { 894 final CandidateMethod candidate = getters.remove( observable.getName() ); 895 if ( null != candidate ) 896 { 897 MemberChecks.mustBeOverridable( component.getElement(), 898 Constants.COMPONENT_CLASSNAME, 899 Constants.OBSERVABLE_CLASSNAME, 900 candidate.getMethod() ); 901 observable.setGetter( candidate.getMethod(), candidate.getMethodType() ); 902 } 903 else 904 { 905 throw new ProcessorException( "@Observable target defined setter but no getter was defined and no " + 906 "getter could be automatically determined", observable.getSetter() ); 907 } 908 } 909 } 910 911 // Find pairs of un-annotated abstract setter/getter pairs and treat them as if they 912 // are annotated with @Observable 913 for ( final Map.Entry<String, CandidateMethod> entry : new ArrayList<>( getters.entrySet() ) ) 914 { 915 final CandidateMethod getter = entry.getValue(); 916 if ( getter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) ) 917 { 918 final String name = entry.getKey(); 919 final CandidateMethod setter = setters.remove( name ); 920 if ( null != setter && setter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) ) 921 { 922 final ObservableDescriptor observable = component.findOrCreateObservable( name ); 923 observable.setGetter( getter.getMethod(), getter.getMethodType() ); 924 observable.setSetter( setter.getMethod(), setter.getMethodType() ); 925 getters.remove( name ); 926 } 927 } 928 } 929 } 930 931 private void linkUnAnnotatedObserves( @Nonnull final ComponentDescriptor component, 932 @Nonnull final Map<String, CandidateMethod> observes, 933 @Nonnull final Map<String, List<CandidateMethod>> onDepsChanges ) 934 throws ProcessorException 935 { 936 for ( final ObserveDescriptor observe : component.getObserves().values() ) 937 { 938 if ( !observe.hasObserve() ) 939 { 940 final CandidateMethod candidate = observes.remove( observe.getName() ); 941 if ( null != candidate ) 942 { 943 observe.setObserveMethod( false, 944 Priority.NORMAL, 945 true, 946 true, 947 true, 948 "AREZ", 949 false, 950 false, 951 candidate.getMethod(), 952 candidate.getMethodType() ); 953 } 954 else 955 { 956 throw new ProcessorException( "@OnDepsChange target has no corresponding @Observe that could " + 957 "be automatically determined", observe.getFirstOnDepsChange() ); 958 } 959 } 960 final var candidates = onDepsChanges.remove( observe.getName() ); 961 if ( null != candidates ) 962 { 963 for ( final var candidate : candidates ) 964 { 965 setOnDepsChange( component, observe, candidate.getMethod() ); 966 } 967 } 968 } 969 } 970 971 private void setOnDepsChange( @Nonnull final ComponentDescriptor component, 972 @Nonnull final ObserveDescriptor observe, 973 @Nonnull final ExecutableElement method ) 974 { 975 MemberChecks.mustNotBeAbstract( Constants.ON_DEPS_CHANGE_CLASSNAME, method ); 976 MemberChecks.mustBeSubclassCallable( component.getElement(), 977 Constants.COMPONENT_CLASSNAME, 978 Constants.ON_DEPS_CHANGE_CLASSNAME, 979 method ); 980 final var parameters = method.getParameters(); 981 if ( 982 !( 983 parameters.isEmpty() || 984 ( 1 == parameters.size() && Constants.OBSERVER_CLASSNAME.equals( parameters.get( 0 ).asType().toString() ) ) 985 ) 986 ) 987 { 988 throw new ProcessorException( "@OnDepsChange target must not have any parameters or must have a single " + 989 "parameter of type arez.Observer", method ); 990 } 991 992 MemberChecks.mustNotReturnAnyValue( Constants.ON_DEPS_CHANGE_CLASSNAME, method ); 993 MemberChecks.mustNotThrowAnyExceptions( Constants.ON_DEPS_CHANGE_CLASSNAME, method ); 994 shouldBeInternalHookMethod( processingEnv, 995 component, 996 method, 997 Constants.ON_DEPS_CHANGE_CLASSNAME ); 998 observe.setOnDepsChange( method ); 999 } 1000 1001 private void setOnActivate( @Nonnull final ComponentDescriptor component, 1002 @Nonnull final MemoizeDescriptor memoize, 1003 @Nonnull final ExecutableElement method ) 1004 throws ProcessorException 1005 { 1006 MemberChecks.mustNotBeAbstract( Constants.ON_ACTIVATE_CLASSNAME, method ); 1007 MemberChecks.mustBeSubclassCallable( component.getElement(), 1008 Constants.COMPONENT_CLASSNAME, 1009 Constants.ON_ACTIVATE_CLASSNAME, 1010 method ); 1011 1012 final var parameters = method.getParameters(); 1013 if ( !parameters.isEmpty() && 1014 !( 1 == parameters.size() && 1015 parameters.get( 0 ).asType().toString().startsWith( Constants.COMPUTABLE_VALUE_CLASSNAME ) ) ) 1016 { 1017 MemberChecks.mustNotHaveAnyParameters( Constants.ON_ACTIVATE_CLASSNAME, method ); 1018 } 1019 1020 MemberChecks.mustNotReturnAnyValue( Constants.ON_ACTIVATE_CLASSNAME, method ); 1021 MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ACTIVATE_CLASSNAME, method ); 1022 shouldBeInternalHookMethod( processingEnv, 1023 component, 1024 method, 1025 Constants.ON_ACTIVATE_CLASSNAME ); 1026 1027 memoize.setOnActivate( method ); 1028 } 1029 1030 private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method ) 1031 throws ProcessorException 1032 { 1033 final Map<String, Collection<String>> exceptions = new HashMap<>(); 1034 exceptions.put( Constants.OBSERVABLE_CLASSNAME, 1035 Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME, 1036 Constants.CASCADE_DISPOSE_CLASSNAME, 1037 Constants.AUTO_OBSERVE_CLASSNAME, 1038 Constants.REFERENCE_ID_CLASSNAME, 1039 Constants.INVERSE_CLASSNAME ) ); 1040 exceptions.put( Constants.REFERENCE_CLASSNAME, 1041 Arrays.asList( Constants.CASCADE_DISPOSE_CLASSNAME, 1042 Constants.AUTO_OBSERVE_CLASSNAME ) ); 1043 exceptions.put( Constants.POST_CONSTRUCT_CLASSNAME, 1044 Collections.singletonList( Constants.ACTION_CLASSNAME ) ); 1045 1046 MemberChecks.verifyNoOverlappingAnnotations( method, METHOD_ANNOTATIONS, exceptions ); 1047 } 1048 1049 private void verifyNoDuplicateAnnotations( @Nonnull final VariableElement field ) 1050 throws ProcessorException 1051 { 1052 MemberChecks.verifyNoOverlappingAnnotations( field, 1053 Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME, 1054 Constants.CASCADE_DISPOSE_CLASSNAME, 1055 Constants.AUTO_OBSERVE_CLASSNAME, 1056 Constants.OBSERVABLE_INITIAL_CLASSNAME ), 1057 Collections.emptyMap() ); 1058 } 1059 1060 private void validate( final boolean allowEmpty, @Nonnull final ComponentDescriptor component ) 1061 throws ProcessorException 1062 { 1063 component.getCascadeDisposes().values().forEach( CascadeDisposeDescriptor::validate ); 1064 component.getAutoObserves().values().forEach( AutoObserveDescriptor::validate ); 1065 component.getObservables().values().forEach( ObservableDescriptor::validate ); 1066 component.getMemoizes().values().forEach( e -> e.validate( processingEnv ) ); 1067 component.getMemoizeContextParameters().values().forEach( p -> p.validate( processingEnv ) ); 1068 component.getObserves().values().forEach( ObserveDescriptor::validate ); 1069 component.getDependencies().values().forEach( DependencyDescriptor::validate ); 1070 component.getReferences().values().forEach( ReferenceDescriptor::validate ); 1071 component.getInverses().values().forEach( e -> e.validate( processingEnv ) ); 1072 1073 final boolean hasZeroReactiveElements = 1074 component.getObservables().isEmpty() && 1075 component.getActions().isEmpty() && 1076 component.getMemoizes().isEmpty() && 1077 component.getDependencies().isEmpty() && 1078 component.getAutoObserves().isEmpty() && 1079 component.getCascadeDisposes().isEmpty() && 1080 component.getReferences().isEmpty() && 1081 component.getInverses().isEmpty() && 1082 component.getObserves().isEmpty(); 1083 1084 final TypeElement element = component.getElement(); 1085 if ( null != component.getDefaultPriority() && 1086 component.getMemoizes().isEmpty() && 1087 component.getObserves().isEmpty() && 1088 isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY ) ) 1089 { 1090 final String message = 1091 MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) + " target should not specify " + 1092 "the defaultPriority parameter unless it contains methods annotated with either the " + 1093 MemberChecks.toSimpleName( Constants.MEMOIZE_CLASSNAME ) + " annotation or the " + 1094 MemberChecks.toSimpleName( Constants.OBSERVE_CLASSNAME ) + " annotation. " + 1095 suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY ); 1096 warning( message, element ); 1097 } 1098 if ( !allowEmpty && hasZeroReactiveElements ) 1099 { 1100 throw new ProcessorException( "@ArezComponent target has no methods annotated with @Action, " + 1101 "@AutoObserve, @CascadeDispose, @Memoize, @Observable, @Inverse, " + 1102 "@Reference, @ComponentDependency or @Observe", element ); 1103 } 1104 else if ( allowEmpty && 1105 !hasZeroReactiveElements && 1106 isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_ALLOW_EMPTY ) ) 1107 { 1108 final String message = 1109 "@ArezComponent target has specified allowEmpty = true but has methods " + 1110 "annotated with @Action, @AutoObserve, @CascadeDispose, @Memoize, @Observable, @Inverse, " + 1111 "@Reference, @ComponentDependency or @Observe. " + 1112 suppressedBy( Constants.WARNING_UNNECESSARY_ALLOW_EMPTY ); 1113 warning( message, element ); 1114 } 1115 1116 for ( final ExecutableElement componentIdRef : component.getComponentIdRefs() ) 1117 { 1118 if ( null != component.getComponentId() && 1119 !processingEnv.getTypeUtils() 1120 .isSameType( component.getComponentId().getReturnType(), componentIdRef.getReturnType() ) ) 1121 { 1122 throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() + 1123 " and a @ComponentId annotated method with a return type " + 1124 componentIdRef.getReturnType() + ". The types must match.", 1125 element ); 1126 } 1127 else if ( null == component.getComponentId() && 1128 !processingEnv.getTypeUtils() 1129 .isSameType( processingEnv.getTypeUtils().getPrimitiveType( TypeKind.INT ), 1130 componentIdRef.getReturnType() ) ) 1131 { 1132 throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() + 1133 " but no @ComponentId annotated method. The type is expected to be of " + 1134 "type int.", element ); 1135 } 1136 } 1137 for ( final ExecutableElement constructor : ElementsUtil.getConstructors( element ) ) 1138 { 1139 if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) && 1140 constructor.getModifiers().contains( Modifier.PUBLIC ) && 1141 ElementsUtil.isWarningNotSuppressed( constructor, Constants.WARNING_PUBLIC_CONSTRUCTOR ) ) 1142 { 1143 final String instruction = 1144 component.isStingEnabled() ? 1145 "The type is instantiated by the sting injection framework and should have a package-access constructor. " : 1146 "It is recommended that a static create method be added to the component that is responsible " + 1147 "for instantiating the arez implementation class. "; 1148 1149 final String message = 1150 MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME, 1151 "have a public constructor. " + instruction + 1152 MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_CONSTRUCTOR, 1153 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ); 1154 warning( message, constructor ); 1155 } 1156 } 1157 if ( null != component.getDeclaredDefaultReadOutsideTransaction() && 1158 component.getObservables().isEmpty() && 1159 component.getMemoizes().isEmpty() && 1160 isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) ) 1161 { 1162 final String message = 1163 "@ArezComponent target has specified a value for the defaultReadOutsideTransaction parameter but does not " + 1164 "contain any methods annotated with either @Memoize or @Observable. " + 1165 suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT ); 1166 warning( message, element ); 1167 } 1168 if ( null != component.getDeclaredDefaultWriteOutsideTransaction() && 1169 component.getObservables().isEmpty() && 1170 isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) ) 1171 { 1172 final String message = 1173 "@ArezComponent target has specified a value for the defaultWriteOutsideTransaction parameter but does not " + 1174 "contain any methods annotated with @Observable. " + 1175 suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT ); 1176 warning( message, element ); 1177 } 1178 if ( null != component.getDeclaredDefaultSkipIfDisposed() && 1179 component.getActions().isEmpty() && 1180 isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) ) 1181 { 1182 final String message = 1183 "@ArezComponent target has specified a value for the defaultSkipIfDisposed parameter but does not " + 1184 "contain any methods annotated with @Action. " + 1185 suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT ); 1186 warning( message, element ); 1187 } 1188 } 1189 1190 private void processCascadeDisposeFields( @Nonnull final ComponentDescriptor component ) 1191 { 1192 ElementsUtil.getFields( component.getElement() ) 1193 .stream() 1194 .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.CASCADE_DISPOSE_CLASSNAME ) ) 1195 .forEach( field -> processCascadeDisposeField( component, field ) ); 1196 } 1197 1198 private void processAutoObserveFields( @Nonnull final ComponentDescriptor component ) 1199 { 1200 ElementsUtil.getFields( component.getElement() ) 1201 .stream() 1202 .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.AUTO_OBSERVE_CLASSNAME ) ) 1203 .forEach( field -> processAutoObserveField( component, field ) ); 1204 } 1205 1206 private void processCascadeDisposeField( @Nonnull final ComponentDescriptor component, 1207 @Nonnull final VariableElement field ) 1208 { 1209 verifyNoDuplicateAnnotations( field ); 1210 MemberChecks.mustBeSubclassCallable( component.getElement(), 1211 Constants.COMPONENT_CLASSNAME, 1212 Constants.CASCADE_DISPOSE_CLASSNAME, 1213 field ); 1214 emitWarningForManagedFieldAccess( component, field, Constants.CASCADE_DISPOSE_CLASSNAME ); 1215 mustBeCascadeDisposeTypeCompatible( field ); 1216 emitWarningForConflictingDisposeModel( field ); 1217 if ( field.getModifiers().contains( Modifier.FINAL ) ) 1218 { 1219 verifyFieldHasExplicitNullabilityAnnotation( field, Constants.CASCADE_DISPOSE_CLASSNAME ); 1220 } 1221 component.addCascadeDispose( new CascadeDisposeDescriptor( field ) ); 1222 } 1223 1224 private void processAutoObserveField( @Nonnull final ComponentDescriptor component, 1225 @Nonnull final VariableElement field ) 1226 { 1227 verifyNoDuplicateAnnotations( field ); 1228 MemberChecks.mustBeSubclassCallable( component.getElement(), 1229 Constants.COMPONENT_CLASSNAME, 1230 Constants.AUTO_OBSERVE_CLASSNAME, 1231 field ); 1232 emitWarningForManagedFieldAccess( component, field, Constants.AUTO_OBSERVE_CLASSNAME ); 1233 MemberChecks.mustBeFinal( Constants.AUTO_OBSERVE_CLASSNAME, field ); 1234 final boolean validateTypeAtRuntime = isAutoObserveValidateTypeAtRuntime( field ); 1235 mustBeAutoObserveTypeCompatible( component, validateTypeAtRuntime, field ); 1236 verifyFieldHasExplicitNullabilityAnnotation( field, Constants.AUTO_OBSERVE_CLASSNAME ); 1237 component.addAutoObserve( new AutoObserveDescriptor( validateTypeAtRuntime, field ) ); 1238 } 1239 1240 private void verifyFieldHasExplicitNullabilityAnnotation( @Nonnull final VariableElement field, 1241 @Nonnull final String annotationClassname ) 1242 { 1243 final boolean hasNonnullAnnotation = isElementAnnotatedBy( field, AnnotationsUtil.NONNULL_CLASSNAME ); 1244 final boolean hasNullableAnnotation = isElementAnnotatedBy( field, AnnotationsUtil.NULLABLE_CLASSNAME ); 1245 final String annotationName = annotationClassname.substring( annotationClassname.lastIndexOf( '.' ) + 1 ); 1246 if ( hasNonnullAnnotation && hasNullableAnnotation ) 1247 { 1248 throw new ProcessorException( "@" + annotationName + " target must not be annotated with both " + 1249 AnnotationsUtil.NULLABLE_CLASSNAME + " and " + 1250 AnnotationsUtil.NONNULL_CLASSNAME, 1251 field ); 1252 } 1253 if ( !hasNonnullAnnotation && !hasNullableAnnotation ) 1254 { 1255 throw new ProcessorException( "@" + annotationName + " target must be annotated with either " + 1256 AnnotationsUtil.NULLABLE_CLASSNAME + " or " + 1257 AnnotationsUtil.NONNULL_CLASSNAME, 1258 field ); 1259 } 1260 } 1261 1262 @Nonnull 1263 private String suppressedBy( @Nonnull final String warning ) 1264 { 1265 return MemberChecks.suppressedBy( warning, Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 1266 } 1267 1268 private boolean isWarningNotSuppressed( @Nonnull final Element element, @Nonnull final String warning ) 1269 { 1270 return !ElementsUtil.isWarningSuppressed( element, 1271 warning, 1272 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 1273 } 1274 1275 @SuppressWarnings( "SameParameterValue" ) 1276 @Nonnull 1277 private String extractName( @Nonnull final ExecutableElement method, 1278 @Nonnull final Function<ExecutableElement, String> function, 1279 @Nonnull final String annotationClassname ) 1280 { 1281 return AnnotationsUtil.extractName( method, function, annotationClassname, "name", Constants.SENTINEL ); 1282 } 1283 1284 private void mustBeCascadeDisposeTypeCompatible( @Nonnull final VariableElement field ) 1285 { 1286 final TypeMirror typeMirror = field.asType(); 1287 if ( !ElementsUtil.isAssignableTo( processingEnv, typeMirror, getDisposableTypeElement() ) ) 1288 { 1289 final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror ); 1290 final AnnotationMirror value = 1291 null != typeElement ? 1292 AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) : 1293 null; 1294 if ( null == value ) 1295 { 1296 throw new ProcessorException( "@CascadeDispose target must be assignable to " + 1297 Constants.DISPOSABLE_CLASSNAME + " or a type annotated with @ArezComponent", 1298 field ); 1299 } 1300 } 1301 } 1302 1303 private void mustBeAutoObserveTypeCompatible( @Nonnull final ComponentDescriptor component, 1304 final boolean validateTypeAtRuntime, 1305 @Nonnull final VariableElement field ) 1306 { 1307 final TypeMirror type = processingEnv.getTypeUtils().asMemberOf( component.asDeclaredType(), field ); 1308 if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() ) 1309 { 1310 throw new ProcessorException( "@AutoObserve target must be a non-primitive value", field ); 1311 } 1312 if ( validateTypeAtRuntime ) 1313 { 1314 final Element element = processingEnv.getTypeUtils().asElement( type ); 1315 if ( !( element instanceof TypeElement ) || !isArezComponentLikeAnnotated( (TypeElement) element ) ) 1316 { 1317 throw new ProcessorException( "@AutoObserve target specified validateTypeAtRuntime = true but the " + 1318 "declared type is not annotated with " + AREZ_COMPONENT_LIKE_DESCRIPTION, field ); 1319 } 1320 } 1321 else if ( !isAutoObserveCompileTimeCompatible( type ) ) 1322 { 1323 throw new ProcessorException( "@AutoObserve target must be an instance compatible with " + 1324 Constants.COMPONENT_OBSERVABLE_CLASSNAME, field ); 1325 } 1326 } 1327 1328 private void addCascadeDisposeMethod( @Nonnull final ComponentDescriptor component, 1329 @Nonnull final ExecutableElement method, 1330 @Nullable final ObservableDescriptor observable ) 1331 { 1332 MemberChecks.mustNotHaveAnyParameters( Constants.CASCADE_DISPOSE_CLASSNAME, method ); 1333 MemberChecks.mustNotThrowAnyExceptions( Constants.CASCADE_DISPOSE_CLASSNAME, method ); 1334 MemberChecks.mustBeSubclassCallable( component.getElement(), 1335 Constants.COMPONENT_CLASSNAME, 1336 Constants.CASCADE_DISPOSE_CLASSNAME, 1337 method ); 1338 mustBeCascadeDisposeTypeCompatible( method ); 1339 emitWarningForConflictingDisposeModel( method ); 1340 component.addCascadeDispose( new CascadeDisposeDescriptor( method, observable ) ); 1341 } 1342 1343 private void addAutoObserveMethod( @Nonnull final ComponentDescriptor component, 1344 @Nonnull final ExecutableElement method, 1345 @Nullable final ObservableDescriptor observable ) 1346 { 1347 MemberChecks.mustNotHaveAnyParameters( Constants.AUTO_OBSERVE_CLASSNAME, method ); 1348 MemberChecks.mustNotThrowAnyExceptions( Constants.AUTO_OBSERVE_CLASSNAME, method ); 1349 MemberChecks.mustBeSubclassCallable( component.getElement(), 1350 Constants.COMPONENT_CLASSNAME, 1351 Constants.AUTO_OBSERVE_CLASSNAME, 1352 method ); 1353 MemberChecks.mustReturnAValue( Constants.AUTO_OBSERVE_CLASSNAME, method ); 1354 final boolean validateTypeAtRuntime = isAutoObserveValidateTypeAtRuntime( method ); 1355 mustBeAutoObserveTypeCompatible( validateTypeAtRuntime, method ); 1356 component.addAutoObserve( new AutoObserveDescriptor( validateTypeAtRuntime, method, observable ) ); 1357 } 1358 1359 private static boolean isAutoObserveValidateTypeAtRuntime( @Nonnull final AnnotatedConstruct annotatedConstruct ) 1360 { 1361 return Boolean.TRUE.equals( AnnotationsUtil 1362 .getAnnotationValue( annotatedConstruct, 1363 Constants.AUTO_OBSERVE_CLASSNAME, 1364 "validateTypeAtRuntime" ) 1365 .getValue() ); 1366 } 1367 1368 private void emitWarningForConflictingDisposeModel( @Nonnull final VariableElement field ) 1369 { 1370 if ( isWarningNotSuppressed( field, Constants.WARNING_CONFLICTING_DISPOSE_MODEL ) && 1371 isLivenessDisposedArezComponent( field.asType() ) ) 1372 { 1373 final String message = 1374 "Field named '" + field.getSimpleName() + "' is annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + 1375 " but has a type that is an Arez component configured with disposeOnDeactivate = true. " + 1376 "Disposal should be managed either by liveness (i.e. disposeOnDeactivate = true) or explicitly " + 1377 "(via @CascadeDispose or manual disposal), but not both. Please choose a single disposal model " + 1378 "or suppress the warning by annotating the field with @SuppressWarnings( \"" + 1379 Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" ) or @SuppressArezWarnings( \"" + 1380 Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" )"; 1381 warning( message, field ); 1382 } 1383 } 1384 1385 private void emitWarningForConflictingDisposeModel( @Nonnull final ExecutableElement method ) 1386 { 1387 if ( isWarningNotSuppressed( method, Constants.WARNING_CONFLICTING_DISPOSE_MODEL ) && 1388 isLivenessDisposedArezComponent( method.getReturnType() ) ) 1389 { 1390 final String message = 1391 "Method named '" + method.getSimpleName() + "' is annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + 1392 " but returns an Arez component configured with disposeOnDeactivate = true. Disposal should be managed " + 1393 "either by liveness (i.e. disposeOnDeactivate = true) or explicitly (via @CascadeDispose or manual " + 1394 "disposal), but not both. Please choose a single disposal model or suppress the warning by annotating " + 1395 "the method with @SuppressWarnings( \"" + Constants.WARNING_CONFLICTING_DISPOSE_MODEL + 1396 "\" ) or @SuppressArezWarnings( \"" + Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" )"; 1397 warning( message, method ); 1398 } 1399 } 1400 1401 private void mustBeCascadeDisposeTypeCompatible( @Nonnull final ExecutableElement method ) 1402 { 1403 final TypeMirror typeMirror = method.getReturnType(); 1404 if ( !ElementsUtil.isAssignableTo( processingEnv, typeMirror, getDisposableTypeElement() ) ) 1405 { 1406 final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror ); 1407 final AnnotationMirror value = 1408 null != typeElement ? 1409 AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) : 1410 null; 1411 if ( null == value ) 1412 { 1413 //The type of the field must implement {@link arez.Disposable} or must be annotated by {@link ArezComponent} 1414 throw new ProcessorException( "@CascadeDispose target must return a type assignable to " + 1415 Constants.DISPOSABLE_CLASSNAME + " or a type annotated with @ArezComponent", 1416 method ); 1417 } 1418 } 1419 } 1420 1421 private void mustBeAutoObserveTypeCompatible( final boolean validateTypeAtRuntime, 1422 @Nonnull final ExecutableElement method ) 1423 { 1424 final TypeMirror type = method.getReturnType(); 1425 if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() ) 1426 { 1427 throw new ProcessorException( "@AutoObserve target must return a non-primitive value", method ); 1428 } 1429 if ( validateTypeAtRuntime ) 1430 { 1431 final Element element = processingEnv.getTypeUtils().asElement( type ); 1432 if ( !( element instanceof TypeElement ) || !isArezComponentLikeAnnotated( (TypeElement) element ) ) 1433 { 1434 throw new ProcessorException( "@AutoObserve target specified validateTypeAtRuntime = true but the " + 1435 "declared return type is not annotated with " + AREZ_COMPONENT_LIKE_DESCRIPTION, 1436 method ); 1437 } 1438 } 1439 else if ( !isAutoObserveCompileTimeCompatible( type ) ) 1440 { 1441 throw new ProcessorException( "@AutoObserve target must return an instance compatible with " + 1442 Constants.COMPONENT_OBSERVABLE_CLASSNAME, method ); 1443 } 1444 } 1445 1446 @SuppressWarnings( "BooleanMethodIsAlwaysInverted" ) 1447 private boolean isAutoObserveCompileTimeCompatible( @Nonnull final TypeMirror type ) 1448 { 1449 if ( ElementsUtil.isAssignableTo( processingEnv, 1450 type, 1451 getTypeElement( Constants.COMPONENT_OBSERVABLE_CLASSNAME ) ) ) 1452 { 1453 return true; 1454 } 1455 1456 final Element element = processingEnv.getTypeUtils().asElement( type ); 1457 if ( element instanceof TypeElement typeElement ) 1458 { 1459 final AnnotationMirror arezComponent = 1460 AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ); 1461 if ( null != arezComponent ) 1462 { 1463 final boolean disposeOnDeactivate = getAnnotationParameter( arezComponent, "disposeOnDeactivate" ); 1464 return isComponentObservableRequired( arezComponent, disposeOnDeactivate ); 1465 } 1466 } 1467 return false; 1468 } 1469 1470 private void addOrUpdateDependency( @Nonnull final ComponentDescriptor component, 1471 @Nonnull final ExecutableElement method, 1472 @Nonnull final ObservableDescriptor observable ) 1473 { 1474 final DependencyDescriptor dependencyDescriptor = 1475 component.getDependencies().computeIfAbsent( method, m -> createMethodDependencyDescriptor( component, method ) ); 1476 dependencyDescriptor.setObservable( observable ); 1477 } 1478 1479 private void addAction( @Nonnull final ComponentDescriptor component, 1480 @Nonnull final AnnotationMirror annotation, 1481 @Nonnull final ExecutableElement method, 1482 @Nonnull final ExecutableType methodType ) 1483 throws ProcessorException 1484 { 1485 MemberChecks.mustBeWrappable( component.getElement(), 1486 Constants.COMPONENT_CLASSNAME, 1487 Constants.ACTION_CLASSNAME, 1488 method ); 1489 1490 final String name = 1491 extractName( method, m -> m.getSimpleName().toString(), Constants.ACTION_CLASSNAME ); 1492 checkNameUnique( component, name, method, Constants.ACTION_CLASSNAME ); 1493 final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" ); 1494 final boolean requireNewTransaction = 1495 AnnotationsUtil.getAnnotationValueValue( annotation, "requireNewTransaction" ); 1496 final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" ); 1497 final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" ); 1498 final boolean verifyRequired = AnnotationsUtil.getAnnotationValueValue( annotation, "verifyRequired" ); 1499 final boolean skipIfDisposed = isSkipIfDisposed( component, annotation ); 1500 if ( !reportParameters && method.getParameters().isEmpty() ) 1501 { 1502 throw new ProcessorException( "@Action target must not specify reportParameters parameter " + 1503 "when no parameters are present", method ); 1504 } 1505 if ( skipIfDisposed && TypeKind.VOID != methodType.getReturnType().getKind() ) 1506 { 1507 throw new ProcessorException( "@Action target must not return a value when skipIfDisposed resolves to ENABLE", 1508 method ); 1509 } 1510 final ActionDescriptor action = 1511 new ActionDescriptor( component, 1512 name, 1513 requireNewTransaction, 1514 mutation, 1515 verifyRequired, 1516 reportParameters, 1517 reportResult, 1518 skipIfDisposed, 1519 method, 1520 methodType ); 1521 component.getActions().put( action.getName(), action ); 1522 } 1523 1524 private void addRequiresTransaction( @Nonnull final ComponentDescriptor component, 1525 @Nonnull final AnnotationMirror annotation, 1526 @Nonnull final ExecutableElement method, 1527 @Nonnull final ExecutableType methodType ) 1528 throws ProcessorException 1529 { 1530 MemberChecks.mustBeWrappable( component.getElement(), 1531 Constants.COMPONENT_CLASSNAME, 1532 Constants.REQUIRES_TRANSACTION_CLASSNAME, 1533 method ); 1534 1535 final VariableElement mode = AnnotationsUtil.getAnnotationValueValue( annotation, "mode" ); 1536 final VariableElement tracking = AnnotationsUtil.getAnnotationValueValue( annotation, "tracking" ); 1537 component.getRequiresTransactions().add( new RequiresTransactionDescriptor( component, 1538 mode.getSimpleName().toString(), 1539 tracking.getSimpleName().toString(), 1540 method, 1541 methodType ) ); 1542 } 1543 1544 private void addObserve( @Nonnull final ComponentDescriptor component, 1545 @Nonnull final AnnotationMirror annotation, 1546 @Nonnull final ExecutableElement method, 1547 @Nonnull final ExecutableType methodType ) 1548 throws ProcessorException 1549 { 1550 final String name = deriveObserveName( method, annotation ); 1551 checkNameUnique( component, name, method, Constants.OBSERVE_CLASSNAME ); 1552 final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" ); 1553 final boolean observeLowerPriorityDependencies = 1554 AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" ); 1555 final boolean nestedActionsAllowed = AnnotationsUtil.getAnnotationValueValue( annotation, "nestedActionsAllowed" ); 1556 final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" ); 1557 final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" ); 1558 final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" ); 1559 final VariableElement executor = AnnotationsUtil.getAnnotationValueValue( annotation, "executor" ); 1560 final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" ); 1561 1562 component 1563 .findOrCreateObserve( name ) 1564 .setObserveMethod( mutation, 1565 toPriority( component.getDefaultPriority(), priority ), 1566 executor.getSimpleName().toString().equals( "INTERNAL" ), 1567 reportParameters, 1568 reportResult, 1569 depType.getSimpleName().toString(), 1570 observeLowerPriorityDependencies, 1571 nestedActionsAllowed, 1572 method, 1573 methodType ); 1574 } 1575 1576 @Nonnull 1577 private String deriveObserveName( @Nonnull final ExecutableElement method, 1578 @Nonnull final AnnotationMirror annotation ) 1579 throws ProcessorException 1580 { 1581 final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 1582 if ( Constants.SENTINEL.equals( name ) ) 1583 { 1584 return method.getSimpleName().toString(); 1585 } 1586 else 1587 { 1588 if ( !SourceVersion.isIdentifier( name ) ) 1589 { 1590 throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " + 1591 "name must be a valid java identifier.", method ); 1592 } 1593 else if ( SourceVersion.isKeyword( name ) ) 1594 { 1595 throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " + 1596 "name must not be a java keyword.", method ); 1597 } 1598 return name; 1599 } 1600 } 1601 1602 private void addOnDepsChange( @Nonnull final ComponentDescriptor component, 1603 @Nonnull final AnnotationMirror annotation, 1604 @Nonnull final ExecutableElement method ) 1605 throws ProcessorException 1606 { 1607 final String name = 1608 deriveHookName( component, method, 1609 ON_DEPS_CHANGE_PATTERN, 1610 "DepsChange", 1611 AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) ); 1612 setOnDepsChange( component, component.findOrCreateObserve( name ), method ); 1613 } 1614 1615 private void addObserverRef( @Nonnull final ComponentDescriptor component, 1616 @Nonnull final AnnotationMirror annotation, 1617 @Nonnull final ExecutableElement method, 1618 @Nonnull final ExecutableType methodType ) 1619 throws ProcessorException 1620 { 1621 mustBeStandardRefMethod( processingEnv, 1622 component, 1623 method, 1624 Constants.OBSERVER_REF_CLASSNAME ); 1625 MemberChecks.mustReturnAnInstanceOf( processingEnv, 1626 method, 1627 Constants.OBSERVER_REF_CLASSNAME, 1628 Constants.OBSERVER_CLASSNAME ); 1629 1630 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 1631 final String name; 1632 if ( Constants.SENTINEL.equals( declaredName ) ) 1633 { 1634 name = deriveName( method, OBSERVER_REF_PATTERN, declaredName ); 1635 if ( null == name ) 1636 { 1637 throw new ProcessorException( "Method annotated with @ObserverRef should specify name or be " + 1638 "named according to the convention get[Name]Observer", method ); 1639 } 1640 } 1641 else 1642 { 1643 name = declaredName; 1644 if ( !SourceVersion.isIdentifier( name ) ) 1645 { 1646 throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " + 1647 "name must be a valid java identifier.", method ); 1648 } 1649 else if ( SourceVersion.isKeyword( name ) ) 1650 { 1651 throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " + 1652 "name must not be a java keyword.", method ); 1653 } 1654 } 1655 component.getObserverRefs().computeIfAbsent( name, s -> new ArrayList<>() ) 1656 .add( new CandidateMethod( method, methodType ) ); 1657 } 1658 1659 private void addMemoizeContextParameter( @Nonnull final ComponentDescriptor component, 1660 @Nonnull final AnnotationMirror annotation, 1661 @Nonnull final ExecutableElement method, 1662 @Nonnull final ExecutableType methodType ) 1663 throws ProcessorException 1664 { 1665 final String methodName = method.getSimpleName().toString(); 1666 final MemoizeContextParameterMethodType mcpMethodType = 1667 PUSH_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Push : 1668 POP_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Pop : 1669 MemoizeContextParameterMethodType.Capture; 1670 final String name = deriveMemoizeContextParameterName( method, annotation, mcpMethodType ); 1671 1672 checkNameUnique( component, name, method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME ); 1673 final boolean allowEmpty = AnnotationsUtil.getAnnotationValueValue( annotation, "allowEmpty" ); 1674 final String pattern = AnnotationsUtil.getAnnotationValueValue( annotation, "pattern" ); 1675 final MemoizeContextParameterDescriptor descriptor = component.findOrCreateMemoizeContextParameter( name ); 1676 1677 final Pattern compiledPattern; 1678 try 1679 { 1680 compiledPattern = Pattern.compile( pattern ); 1681 } 1682 catch ( final PatternSyntaxException e ) 1683 { 1684 throw new ProcessorException( "@MemoizeContextParameter target specified a pattern parameter " + 1685 "that is not a valid regular expression.", method ); 1686 } 1687 1688 if ( MemoizeContextParameterMethodType.Capture == mcpMethodType ) 1689 { 1690 descriptor.setCapture( method, methodType, allowEmpty, pattern, compiledPattern ); 1691 } 1692 else if ( MemoizeContextParameterMethodType.Push == mcpMethodType ) 1693 { 1694 descriptor.setPush( method, methodType, allowEmpty, pattern, compiledPattern ); 1695 } 1696 else // MemoizeContextParameterMethodType.Pop == mcpMethodType 1697 { 1698 descriptor.setPop( method, methodType, allowEmpty, pattern, compiledPattern ); 1699 } 1700 } 1701 1702 @Nonnull 1703 private String deriveMemoizeContextParameterName( @Nonnull final ExecutableElement method, 1704 @Nonnull final AnnotationMirror annotation, 1705 @Nonnull final MemoizeContextParameterMethodType mcpMethodType ) 1706 throws ProcessorException 1707 { 1708 final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 1709 if ( Constants.SENTINEL.equals( name ) ) 1710 { 1711 final Pattern pattern = 1712 MemoizeContextParameterMethodType.Push == mcpMethodType ? PUSH_PATTERN : 1713 MemoizeContextParameterMethodType.Pop == mcpMethodType ? POP_PATTERN : 1714 CAPTURE_PATTERN; 1715 final String methodName = method.getSimpleName().toString(); 1716 final Matcher matcher = pattern.matcher( methodName ); 1717 if ( matcher.find() ) 1718 { 1719 return NamesUtil.firstCharacterToLowerCase( matcher.group( 1 ) ); 1720 } 1721 else 1722 { 1723 // we get here for a capture method that does not start with capture 1724 return methodName; 1725 } 1726 } 1727 else 1728 { 1729 if ( !SourceVersion.isIdentifier( name ) ) 1730 { 1731 throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name + 1732 "'. The name must be a valid java identifier.", method ); 1733 } 1734 else if ( SourceVersion.isKeyword( name ) ) 1735 { 1736 throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name + 1737 "'. The name must not be a java keyword.", method ); 1738 } 1739 return name; 1740 } 1741 } 1742 1743 private void addMemoize( @Nonnull final ComponentDescriptor component, 1744 @Nonnull final AnnotationMirror annotation, 1745 @Nonnull final ExecutableElement method, 1746 @Nonnull final ExecutableType methodType ) 1747 throws ProcessorException 1748 { 1749 final String name = deriveMemoizeName( method, annotation ); 1750 checkNameUnique( component, name, method, Constants.MEMOIZE_CLASSNAME ); 1751 final boolean keepAlive = AnnotationsUtil.getAnnotationValueValue( annotation, "keepAlive" ); 1752 final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" ); 1753 final boolean observeLowerPriorityDependencies = 1754 AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" ); 1755 final VariableElement readOutsideTransaction = 1756 AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" ); 1757 final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" ); 1758 final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" ); 1759 final TypeMirror equalityComparator = 1760 AnnotationsUtil.getAnnotationValueValue( annotation, "equalityComparator" ); 1761 1762 final String depTypeAsString = depType.getSimpleName().toString(); 1763 component.findOrCreateMemoize( name ).setMemoize( method, 1764 methodType, 1765 keepAlive, 1766 toPriority( component.getDefaultPriority(), 1767 priority ), 1768 reportResult, 1769 observeLowerPriorityDependencies, 1770 readOutsideTransaction.getSimpleName().toString(), 1771 depTypeAsString, 1772 resolveEffectiveEqualityComparator( component.getElement(), 1773 Constants.MEMOIZE_CLASSNAME, 1774 method, 1775 methodType.getReturnType(), 1776 equalityComparator.toString() ) ); 1777 } 1778 1779 @Nonnull 1780 private Priority toPriority( @Nullable final Priority defaultPriority, 1781 @Nonnull final VariableElement priorityElement ) 1782 { 1783 final String priorityName = priorityElement.getSimpleName().toString(); 1784 return "DEFAULT".equals( priorityName ) ? 1785 null != defaultPriority ? defaultPriority : Priority.NORMAL : 1786 Priority.valueOf( priorityName ); 1787 } 1788 1789 private void autodetectObservableInitializers( @Nonnull final ComponentDescriptor component ) 1790 { 1791 for ( final ObservableDescriptor observable : component.getObservables().values() ) 1792 { 1793 if ( null == observable.getInitializer() && observable.hasGetter() ) 1794 { 1795 if ( observable.hasSetter() ) 1796 { 1797 final boolean initializer = 1798 autodetectInitializer( observable.getGetter() ) && autodetectInitializer( observable.getSetter() ); 1799 observable.setInitializer( initializer ); 1800 } 1801 else 1802 { 1803 observable.setInitializer( autodetectInitializer( observable.getGetter() ) ); 1804 } 1805 } 1806 } 1807 } 1808 1809 private boolean hasDependencyAnnotation( @Nonnull final ExecutableElement method ) 1810 { 1811 return AnnotationsUtil.hasAnnotationOfType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME ); 1812 } 1813 1814 private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component, 1815 @Nonnull final InverseDescriptor descriptor, 1816 @Nonnull final TypeMirror target ) 1817 { 1818 if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) ) 1819 { 1820 throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " + 1821 "a target type equal to " + component.getElement().asType() + " but the actual " + 1822 "target type is " + target, descriptor.getObservable().getGetter() ); 1823 } 1824 } 1825 1826 @Nullable 1827 private TypeElement getInverseManyTypeTarget( @Nonnull final ExecutableElement method ) 1828 { 1829 final TypeName typeName = TypeName.get( method.getReturnType() ); 1830 if ( typeName instanceof final ParameterizedTypeName type ) 1831 { 1832 if ( isSupportedInverseCollectionType( type.rawType().toString() ) && !type.typeArguments().isEmpty() ) 1833 { 1834 final TypeElement typeElement = getTypeElement( type.typeArguments().get( 0 ).toString() ); 1835 if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME ) ) 1836 { 1837 return typeElement; 1838 } 1839 else 1840 { 1841 throw new ProcessorException( "@Inverse target expected to return a type annotated with " + 1842 Constants.COMPONENT_CLASSNAME, method ); 1843 } 1844 } 1845 } 1846 return null; 1847 } 1848 1849 private boolean isSupportedInverseCollectionType( @Nonnull final String typeClassname ) 1850 { 1851 return Collection.class.getName().equals( typeClassname ) || 1852 Set.class.getName().equals( typeClassname ) || 1853 List.class.getName().equals( typeClassname ); 1854 } 1855 1856 @Nonnull 1857 private String getInverseReferenceNameParameter( @Nonnull final ComponentDescriptor component, 1858 @Nonnull final ExecutableElement method ) 1859 { 1860 final String declaredName = 1861 (String) AnnotationsUtil.getAnnotationValue( method, 1862 Constants.INVERSE_CLASSNAME, 1863 "referenceName" ).getValue(); 1864 final String name; 1865 if ( Constants.SENTINEL.equals( declaredName ) ) 1866 { 1867 name = NamesUtil.firstCharacterToLowerCase( component.getElement().getSimpleName().toString() ); 1868 } 1869 else 1870 { 1871 name = declaredName; 1872 if ( !SourceVersion.isIdentifier( name ) ) 1873 { 1874 throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " + 1875 "name must be a valid java identifier.", method ); 1876 } 1877 else if ( SourceVersion.isKeyword( name ) ) 1878 { 1879 throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " + 1880 "name must not be a java keyword.", method ); 1881 } 1882 } 1883 return name; 1884 } 1885 1886 private void linkDependencies( @Nonnull final ComponentDescriptor component, 1887 @Nonnull final Collection<CandidateMethod> candidates ) 1888 { 1889 component.getObservables() 1890 .values() 1891 .stream() 1892 .filter( ObservableDescriptor::hasGetter ) 1893 .filter( o -> hasDependencyAnnotation( o.getGetter() ) ) 1894 .forEach( o -> addOrUpdateDependency( component, o.getGetter(), o ) ); 1895 1896 component.getMemoizes() 1897 .values() 1898 .stream() 1899 .filter( MemoizeDescriptor::hasMemoize ) 1900 .map( MemoizeDescriptor::getMethod ) 1901 .filter( this::hasDependencyAnnotation ) 1902 .forEach( method1 -> component.addDependency( createMethodDependencyDescriptor( component, method1 ) ) ); 1903 1904 candidates 1905 .stream() 1906 .map( CandidateMethod::getMethod ) 1907 .filter( this::hasDependencyAnnotation ) 1908 .forEach( method -> component.addDependency( createMethodDependencyDescriptor( component, method ) ) ); 1909 } 1910 1911 private void linkCascadeDisposeObservables( @Nonnull final ComponentDescriptor component ) 1912 { 1913 for ( final ObservableDescriptor observable : component.getObservables().values() ) 1914 { 1915 final CascadeDisposeDescriptor cascadeDisposeDescriptor = observable.getCascadeDisposeDescriptor(); 1916 if ( null == cascadeDisposeDescriptor ) 1917 { 1918 //@CascadeDisposable can only occur on getter so if we don't have it then we look in 1919 // cascadeDisposableDescriptor list to see if we can match getter 1920 final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( observable.getGetter() ); 1921 if ( null != descriptor ) 1922 { 1923 descriptor.setObservable( observable ); 1924 } 1925 } 1926 } 1927 } 1928 1929 private void linkCascadeDisposeReferences( @Nonnull final ComponentDescriptor component ) 1930 { 1931 for ( final ReferenceDescriptor reference : component.getReferences().values() ) 1932 { 1933 final CascadeDisposeDescriptor cascadeDisposeDescriptor = reference.getCascadeDisposeDescriptor(); 1934 if ( null == cascadeDisposeDescriptor && reference.hasMethod() ) 1935 { 1936 final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( reference.getMethod() ); 1937 if ( null != descriptor ) 1938 { 1939 descriptor.setReference( reference ); 1940 } 1941 } 1942 } 1943 } 1944 1945 private void linkAutoObserveObservables( @Nonnull final ComponentDescriptor component ) 1946 { 1947 for ( final ObservableDescriptor observable : component.getObservables().values() ) 1948 { 1949 final AutoObserveDescriptor autoObserveDescriptor = observable.getAutoObserveDescriptor(); 1950 if ( null == autoObserveDescriptor ) 1951 { 1952 final AutoObserveDescriptor descriptor = component.getAutoObserves().get( observable.getGetter() ); 1953 if ( null != descriptor ) 1954 { 1955 descriptor.setObservable( observable ); 1956 } 1957 } 1958 } 1959 } 1960 1961 private void linkAutoObserveReferences( @Nonnull final ComponentDescriptor component ) 1962 { 1963 for ( final ReferenceDescriptor reference : component.getReferences().values() ) 1964 { 1965 final AutoObserveDescriptor autoObserveDescriptor = reference.getAutoObserveDescriptor(); 1966 if ( null == autoObserveDescriptor && reference.hasMethod() ) 1967 { 1968 final AutoObserveDescriptor descriptor = component.getAutoObserves().get( reference.getMethod() ); 1969 if ( null != descriptor ) 1970 { 1971 descriptor.setReference( reference ); 1972 } 1973 } 1974 } 1975 } 1976 1977 private void linkObserverRefs( @Nonnull final ComponentDescriptor component ) 1978 { 1979 for ( final Map.Entry<String, List<CandidateMethod>> entry : component.getObserverRefs().entrySet() ) 1980 { 1981 final String key = entry.getKey(); 1982 final List<CandidateMethod> methods = entry.getValue(); 1983 final ObserveDescriptor observed = component.getObserves().get( key ); 1984 if ( null != observed ) 1985 { 1986 methods.stream().map( CandidateMethod::getMethod ).forEach( observed::addRefMethod ); 1987 } 1988 else 1989 { 1990 throw new ProcessorException( "@ObserverRef target defined observer named '" + key + "' but no " + 1991 "@Observe method with that name exists", methods.get( 0 ).getMethod() ); 1992 } 1993 } 1994 } 1995 1996 private void linkObservableInitials( @Nonnull final ComponentDescriptor component ) 1997 { 1998 for ( final ObservableInitialDescriptor observableInitial : component.getObservableInitials().values() ) 1999 { 2000 final String name = observableInitial.getName(); 2001 final ObservableDescriptor observable = component.getObservables().get( name ); 2002 if ( null == observable ) 2003 { 2004 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but no " + 2005 "@Observable method with that name exists", observableInitial.getElement() ); 2006 } 2007 if ( !observable.hasGetter() ) 2008 { 2009 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but the " + 2010 "observable does not define a getter", observableInitial.getElement() ); 2011 } 2012 if ( !observable.isAbstract() ) 2013 { 2014 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but the " + 2015 "observable is not abstract", observableInitial.getElement() ); 2016 } 2017 2018 final TypeMirror observableType = observable.getGetterType().getReturnType(); 2019 final TypeMirror initialType = observableInitial.getType(); 2020 if ( !processingEnv.getTypeUtils().isSameType( initialType, observableType ) && 2021 !initialType.toString().equals( observableType.toString() ) ) 2022 { 2023 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + 2024 "' with incompatible type. Observable type: " + observableType + 2025 " Initial type: " + initialType + ".", observableInitial.getElement() ); 2026 } 2027 if ( observable.isGetterNonnull() && !AnnotationsUtil.hasNonnullAnnotation( observableInitial.getElement() ) ) 2028 { 2029 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but " + 2030 "the initializer is not annotated with @" + AnnotationsUtil.NONNULL_CLASSNAME, 2031 observableInitial.getElement() ); 2032 } 2033 2034 final Boolean initializer = observable.getInitializer(); 2035 if ( Boolean.TRUE.equals( initializer ) ) 2036 { 2037 throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but " + 2038 "the observable defines initializer = Feature.ENABLE which is not " + 2039 "compatible with @ObservableInitial", observableInitial.getElement() ); 2040 } 2041 if ( null == initializer ) 2042 { 2043 observable.setInitializer( Boolean.FALSE ); 2044 } 2045 observable.setObservableInitial( observableInitial ); 2046 } 2047 } 2048 2049 @Nullable 2050 private Boolean isInitializerRequired( @Nonnull final ExecutableElement element ) 2051 { 2052 final AnnotationMirror annotation = 2053 AnnotationsUtil.findAnnotationByType( element, Constants.OBSERVABLE_CLASSNAME ); 2054 final AnnotationValue v = 2055 null == annotation ? null : AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "initializer" ); 2056 final String value = null == v ? "AUTODETECT" : ( (VariableElement) v.getValue() ).getSimpleName().toString(); 2057 return switch ( value ) 2058 { 2059 case "ENABLE" -> Boolean.TRUE; 2060 case "DISABLE" -> Boolean.FALSE; 2061 default -> null; 2062 }; 2063 } 2064 2065 private boolean autodetectInitializer( @Nonnull final ExecutableElement element ) 2066 { 2067 return element.getModifiers().contains( Modifier.ABSTRACT ) && 2068 ( 2069 ( 2070 // Getter 2071 element.getReturnType().getKind() != TypeKind.VOID && 2072 AnnotationsUtil.hasNonnullAnnotation( element ) && 2073 !AnnotationsUtil.hasAnnotationOfType( element, Constants.INVERSE_CLASSNAME ) 2074 ) || 2075 ( 2076 // Setter 2077 1 == element.getParameters().size() && 2078 AnnotationsUtil.hasNonnullAnnotation( element.getParameters().get( 0 ) ) 2079 ) 2080 ); 2081 } 2082 2083 private void checkNameUnique( @Nonnull final ComponentDescriptor component, @Nonnull final String name, 2084 @Nonnull final ExecutableElement sourceMethod, 2085 @Nonnull final String sourceAnnotationName ) 2086 throws ProcessorException 2087 { 2088 final ActionDescriptor action = component.getActions().get( name ); 2089 if ( null != action ) 2090 { 2091 throw toException( name, 2092 sourceAnnotationName, 2093 sourceMethod, 2094 Constants.ACTION_CLASSNAME, 2095 action.getAction() ); 2096 } 2097 final MemoizeDescriptor memoize = component.getMemoizes().get( name ); 2098 if ( null != memoize && memoize.hasMemoize() ) 2099 { 2100 throw toException( name, 2101 sourceAnnotationName, 2102 sourceMethod, 2103 Constants.MEMOIZE_CLASSNAME, 2104 memoize.getMethod() ); 2105 } 2106 // Observe have pairs so let the caller determine whether a duplicate occurs in that scenario 2107 if ( !sourceAnnotationName.equals( Constants.OBSERVE_CLASSNAME ) ) 2108 { 2109 final ObserveDescriptor observed = component.getObserves().get( name ); 2110 if ( null != observed ) 2111 { 2112 throw toException( name, 2113 sourceAnnotationName, 2114 sourceMethod, 2115 Constants.OBSERVE_CLASSNAME, 2116 observed.getMethod() ); 2117 } 2118 } 2119 // Observables have pairs so let the caller determine whether a duplicate occurs in that scenario 2120 if ( !sourceAnnotationName.equals( Constants.OBSERVABLE_CLASSNAME ) ) 2121 { 2122 final ObservableDescriptor observable = component.getObservables().get( name ); 2123 if ( null != observable ) 2124 { 2125 throw toException( name, 2126 sourceAnnotationName, 2127 sourceMethod, 2128 Constants.OBSERVABLE_CLASSNAME, 2129 observable.getDefiner() ); 2130 } 2131 } 2132 } 2133 2134 @Nonnull 2135 private ProcessorException toException( @Nonnull final String name, 2136 @Nonnull final String sourceAnnotationName, 2137 @Nonnull final ExecutableElement sourceMethod, 2138 @Nonnull final String targetAnnotationName, 2139 @Nonnull final ExecutableElement targetElement ) 2140 { 2141 return new ProcessorException( "Method annotated with " + MemberChecks.toSimpleName( sourceAnnotationName ) + 2142 " specified name " + name + " that duplicates " + 2143 MemberChecks.toSimpleName( targetAnnotationName ) + " defined by method " + 2144 targetElement.getSimpleName(), sourceMethod ); 2145 } 2146 2147 private void processComponentDependencyFields( @Nonnull final ComponentDescriptor component ) 2148 { 2149 ElementsUtil.getFields( component.getElement() ) 2150 .stream() 2151 .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.COMPONENT_DEPENDENCY_CLASSNAME ) ) 2152 .forEach( field -> processComponentDependencyField( component, field ) ); 2153 } 2154 2155 private void processObservableInitialFields( @Nonnull final ComponentDescriptor component, 2156 @Nonnull final List<VariableElement> fields ) 2157 { 2158 fields 2159 .stream() 2160 .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.OBSERVABLE_INITIAL_CLASSNAME ) ) 2161 .forEach( field -> processObservableInitialField( component, field ) ); 2162 } 2163 2164 private void processComponentDependencyField( @Nonnull final ComponentDescriptor component, 2165 @Nonnull final VariableElement field ) 2166 { 2167 verifyNoDuplicateAnnotations( field ); 2168 MemberChecks.mustBeSubclassCallable( component.getElement(), 2169 Constants.COMPONENT_CLASSNAME, 2170 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 2171 field ); 2172 component.addDependency( createFieldDependencyDescriptor( component, field ) ); 2173 } 2174 2175 private void processObservableInitialField( @Nonnull final ComponentDescriptor component, 2176 @Nonnull final VariableElement field ) 2177 { 2178 verifyNoDuplicateAnnotations( field ); 2179 if ( !field.getModifiers().contains( Modifier.STATIC ) ) 2180 { 2181 throw new ProcessorException( "@ObservableInitial target must be static", field ); 2182 } 2183 if ( field.getModifiers().contains( Modifier.PRIVATE ) ) 2184 { 2185 throw new ProcessorException( "@ObservableInitial target must not be private", field ); 2186 } 2187 MemberChecks.mustBeFinal( Constants.OBSERVABLE_INITIAL_CLASSNAME, field ); 2188 2189 final AnnotationMirror annotation = 2190 AnnotationsUtil.getAnnotationByType( field, Constants.OBSERVABLE_INITIAL_CLASSNAME ); 2191 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 2192 final String name = deriveObservableInitialName( field, declaredName ); 2193 if ( null == name ) 2194 { 2195 throw new ProcessorException( "Field annotated with @ObservableInitial should specify name or be " + 2196 "named according to the convention INITIAL_[Name]", field ); 2197 } 2198 2199 addObservableInitial( component, new ObservableInitialDescriptor( name, field ) ); 2200 } 2201 2202 private void addObservableInitialMethod( @Nonnull final ComponentDescriptor component, 2203 @Nonnull final AnnotationMirror annotation, 2204 @Nonnull final ExecutableElement method, 2205 @Nonnull final ExecutableType methodType ) 2206 { 2207 if ( !method.getModifiers().contains( Modifier.STATIC ) ) 2208 { 2209 throw new ProcessorException( "@ObservableInitial target must be static", method ); 2210 } 2211 if ( method.getModifiers().contains( Modifier.PRIVATE ) ) 2212 { 2213 throw new ProcessorException( "@ObservableInitial target must not be private", method ); 2214 } 2215 MemberChecks.mustNotBeAbstract( Constants.OBSERVABLE_INITIAL_CLASSNAME, method ); 2216 MemberChecks.mustNotHaveAnyParameters( Constants.OBSERVABLE_INITIAL_CLASSNAME, method ); 2217 MemberChecks.mustReturnAValue( Constants.OBSERVABLE_INITIAL_CLASSNAME, method ); 2218 MemberChecks.mustNotThrowAnyExceptions( Constants.OBSERVABLE_INITIAL_CLASSNAME, method ); 2219 2220 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 2221 final String name = deriveObservableInitialName( method, declaredName ); 2222 if ( null == name ) 2223 { 2224 throw new ProcessorException( "Method annotated with @ObservableInitial should specify name or be " + 2225 "named according to the convention getInitial[Name]", method ); 2226 } 2227 2228 addObservableInitial( component, new ObservableInitialDescriptor( name, method, methodType ) ); 2229 } 2230 2231 private void addObservableInitial( @Nonnull final ComponentDescriptor component, 2232 @Nonnull final ObservableInitialDescriptor descriptor ) 2233 { 2234 final String name = descriptor.getName(); 2235 if ( component.getObservableInitials().containsKey( name ) ) 2236 { 2237 throw new ProcessorException( "@ObservableInitial target duplicates existing initializer for observable " + 2238 "named " + name, descriptor.getElement() ); 2239 } 2240 component.getObservableInitials().put( name, descriptor ); 2241 } 2242 2243 @Nullable 2244 private String deriveObservableInitialName( @Nonnull final ExecutableElement method, 2245 @Nonnull final String declaredName ) 2246 { 2247 if ( Constants.SENTINEL.equals( declaredName ) ) 2248 { 2249 return deriveName( method, OBSERVABLE_INITIAL_METHOD_PATTERN, declaredName ); 2250 } 2251 else 2252 { 2253 if ( !SourceVersion.isIdentifier( declaredName ) ) 2254 { 2255 throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName + 2256 "'. The name must be a valid java identifier.", method ); 2257 } 2258 else if ( SourceVersion.isKeyword( declaredName ) ) 2259 { 2260 throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName + 2261 "'. The name must not be a java keyword.", method ); 2262 } 2263 return declaredName; 2264 } 2265 } 2266 2267 @Nullable 2268 private String deriveObservableInitialName( @Nonnull final VariableElement field, 2269 @Nonnull final String declaredName ) 2270 { 2271 if ( Constants.SENTINEL.equals( declaredName ) ) 2272 { 2273 final String fieldName = field.getSimpleName().toString(); 2274 final Matcher matcher = OBSERVABLE_INITIAL_FIELD_PATTERN.matcher( fieldName ); 2275 if ( matcher.find() ) 2276 { 2277 return constantCaseToLowerCamel( matcher.group( 1 ) ); 2278 } 2279 else 2280 { 2281 return null; 2282 } 2283 } 2284 else 2285 { 2286 if ( !SourceVersion.isIdentifier( declaredName ) ) 2287 { 2288 throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName + 2289 "'. The name must be a valid java identifier.", field ); 2290 } 2291 else if ( SourceVersion.isKeyword( declaredName ) ) 2292 { 2293 throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName + 2294 "'. The name must not be a java keyword.", field ); 2295 } 2296 return declaredName; 2297 } 2298 } 2299 2300 @Nonnull 2301 private String constantCaseToLowerCamel( @Nonnull final String name ) 2302 { 2303 final String[] parts = name.split( "_" ); 2304 final StringBuilder sb = new StringBuilder(); 2305 for ( final String part : parts ) 2306 { 2307 if ( part.isEmpty() ) 2308 { 2309 continue; 2310 } 2311 final String lower = part.toLowerCase( Locale.ENGLISH ); 2312 if ( sb.isEmpty() ) 2313 { 2314 sb.append( lower ); 2315 } 2316 else 2317 { 2318 sb.append( Character.toUpperCase( lower.charAt( 0 ) ) ); 2319 if ( lower.length() > 1 ) 2320 { 2321 sb.append( lower.substring( 1 ) ); 2322 } 2323 } 2324 } 2325 return sb.toString(); 2326 } 2327 2328 private void addReference( @Nonnull final ComponentDescriptor component, 2329 @Nonnull final AnnotationMirror annotation, 2330 @Nonnull final ExecutableElement method, 2331 @Nonnull final ExecutableType methodType ) 2332 { 2333 MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_CLASSNAME, method ); 2334 MemberChecks.mustBeSubclassCallable( component.getElement(), 2335 Constants.COMPONENT_CLASSNAME, 2336 Constants.REFERENCE_CLASSNAME, 2337 method ); 2338 MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_CLASSNAME, method ); 2339 MemberChecks.mustReturnAValue( Constants.REFERENCE_CLASSNAME, method ); 2340 MemberChecks.mustBeAbstract( Constants.REFERENCE_CLASSNAME, method ); 2341 2342 final String name = getReferenceName( annotation, method ); 2343 final String linkType = getLinkType( method ); 2344 final String inverseName; 2345 final Multiplicity inverseMultiplicity; 2346 if ( hasInverse( annotation ) ) 2347 { 2348 inverseMultiplicity = getReferenceInverseMultiplicity( annotation ); 2349 inverseName = getReferenceInverseName( component, annotation, method, inverseMultiplicity ); 2350 final TypeMirror returnType = method.getReturnType(); 2351 if ( !( returnType instanceof DeclaredType ) || 2352 !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) returnType ).asElement(), 2353 Constants.COMPONENT_CLASSNAME ) ) 2354 { 2355 throw new ProcessorException( "@Reference target expected to return a type annotated with " + 2356 MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) + 2357 " if there is an inverse reference", method ); 2358 } 2359 } 2360 else 2361 { 2362 inverseName = null; 2363 inverseMultiplicity = null; 2364 } 2365 final ReferenceDescriptor descriptor = component.findOrCreateReference( name ); 2366 descriptor.setMethod( method, methodType, linkType, inverseName, inverseMultiplicity ); 2367 verifyMultiplicityOfAssociatedInverseMethod( component, descriptor ); 2368 } 2369 2370 private boolean hasInverse( @Nonnull final AnnotationMirror annotation ) 2371 { 2372 final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "inverse" ); 2373 return switch ( variableElement.getSimpleName().toString() ) 2374 { 2375 case "ENABLE" -> true; 2376 case "DISABLE" -> false; 2377 default -> null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseName" ) || 2378 null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseMultiplicity" ); 2379 }; 2380 } 2381 2382 private void verifyMultiplicityOfAssociatedReferenceMethod( @Nonnull final ComponentDescriptor component, 2383 @Nonnull final InverseDescriptor descriptor ) 2384 { 2385 final Multiplicity multiplicity = 2386 ElementsUtil 2387 .getMethods( descriptor.getTargetType(), 2388 processingEnv.getElementUtils(), 2389 processingEnv.getTypeUtils() ) 2390 .stream() 2391 .map( m -> { 2392 final AnnotationMirror a = 2393 AnnotationsUtil.findAnnotationByType( m, Constants.REFERENCE_CLASSNAME ); 2394 if ( null != a && getReferenceName( a, m ).equals( descriptor.getReferenceName() ) ) 2395 { 2396 if ( null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverse" ) && 2397 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseName" ) && 2398 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseMultiplicity" ) ) 2399 { 2400 throw new ProcessorException( "@Inverse target found an associated @Reference on the method '" + 2401 m.getSimpleName() + "' on type '" + 2402 descriptor.getTargetType().getQualifiedName() + "' but the " + 2403 "annotation has not configured an inverse.", 2404 descriptor.getObservable().getGetter() ); 2405 } 2406 ensureTargetTypeAligns( component, descriptor, m.getReturnType() ); 2407 return getReferenceInverseMultiplicity( a ); 2408 } 2409 else 2410 { 2411 return null; 2412 } 2413 } ) 2414 .filter( Objects::nonNull ) 2415 .findAny() 2416 .orElse( null ); 2417 if ( null == multiplicity ) 2418 { 2419 throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " + 2420 "a name parameter equal to '" + descriptor.getReferenceName() + "' on class " + 2421 descriptor.getTargetType().getQualifiedName() + " but is unable to " + 2422 "locate a matching method.", descriptor.getObservable().getGetter() ); 2423 } 2424 2425 if ( descriptor.getMultiplicity() != multiplicity ) 2426 { 2427 throw new ProcessorException( "@Inverse target has a multiplicity of " + descriptor.getMultiplicity() + 2428 " but that associated @Reference has a multiplicity of " + multiplicity + 2429 ". The multiplicity must align.", descriptor.getObservable().getGetter() ); 2430 } 2431 } 2432 2433 @Nonnull 2434 private String getLinkType( @Nonnull final ExecutableElement method ) 2435 { 2436 return AnnotationsUtil.getEnumAnnotationParameter( method, Constants.REFERENCE_CLASSNAME, "load" ); 2437 } 2438 2439 @Nonnull 2440 private String getReferenceName( @Nonnull final AnnotationMirror annotation, 2441 @Nonnull final ExecutableElement method ) 2442 { 2443 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 2444 final String name; 2445 if ( Constants.SENTINEL.equals( declaredName ) ) 2446 { 2447 final String candidate = deriveName( method, GETTER_PATTERN, declaredName ); 2448 if ( null == candidate ) 2449 { 2450 name = method.getSimpleName().toString(); 2451 } 2452 else 2453 { 2454 name = candidate; 2455 } 2456 } 2457 else 2458 { 2459 name = declaredName; 2460 if ( !SourceVersion.isIdentifier( name ) ) 2461 { 2462 throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " + 2463 "name must be a valid java identifier.", method ); 2464 } 2465 else if ( SourceVersion.isKeyword( name ) ) 2466 { 2467 throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " + 2468 "name must not be a java keyword.", method ); 2469 } 2470 } 2471 return name; 2472 } 2473 2474 @Nonnull 2475 private Multiplicity getReferenceInverseMultiplicity( @Nonnull final AnnotationMirror annotation ) 2476 { 2477 final VariableElement variableElement = 2478 AnnotationsUtil.getAnnotationValueValue( annotation, "inverseMultiplicity" ); 2479 return switch ( variableElement.getSimpleName().toString() ) 2480 { 2481 case "MANY" -> Multiplicity.MANY; 2482 case "ONE" -> Multiplicity.ONE; 2483 default -> Multiplicity.ZERO_OR_ONE; 2484 }; 2485 } 2486 2487 @Nonnull 2488 private String getReferenceInverseName( @Nonnull final ComponentDescriptor component, 2489 @Nonnull final AnnotationMirror annotation, 2490 @Nonnull final ExecutableElement method, 2491 @Nonnull final Multiplicity multiplicity ) 2492 { 2493 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "inverseName" ); 2494 final String name; 2495 if ( Constants.SENTINEL.equals( declaredName ) ) 2496 { 2497 final String baseName = component.getElement().getSimpleName().toString(); 2498 return NamesUtil.firstCharacterToLowerCase( baseName ) + ( Multiplicity.MANY == multiplicity ? "s" : "" ); 2499 } 2500 else 2501 { 2502 name = declaredName; 2503 if ( !SourceVersion.isIdentifier( name ) ) 2504 { 2505 throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " + 2506 "inverseName must be a valid java identifier.", method ); 2507 } 2508 else if ( SourceVersion.isKeyword( name ) ) 2509 { 2510 throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " + 2511 "inverseName must not be a java keyword.", method ); 2512 } 2513 } 2514 return name; 2515 } 2516 2517 private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component, 2518 @Nonnull final ReferenceDescriptor descriptor, 2519 @Nonnull final TypeMirror target ) 2520 { 2521 if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) ) 2522 { 2523 throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation with " + 2524 "a target type equal to " + component.getElement().getQualifiedName() + " but " + 2525 "the actual target type is " + target, descriptor.getMethod() ); 2526 } 2527 } 2528 2529 private void verifyMultiplicityOfAssociatedInverseMethod( @Nonnull final ComponentDescriptor component, 2530 @Nonnull final ReferenceDescriptor descriptor ) 2531 { 2532 final TypeElement element = 2533 (TypeElement) processingEnv.getTypeUtils().asElement( descriptor.getMethod().getReturnType() ); 2534 final String defaultInverseName = descriptor.hasInverse() ? 2535 null : 2536 NamesUtil.firstCharacterToLowerCase( component.getElement() 2537 .getSimpleName() 2538 .toString() ) + "s"; 2539 final Multiplicity multiplicity = 2540 ElementsUtil 2541 .getMethods( element, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ) 2542 .stream() 2543 .map( m -> { 2544 final AnnotationMirror a = AnnotationsUtil.findAnnotationByType( m, Constants.INVERSE_CLASSNAME ); 2545 if ( null == a ) 2546 { 2547 return null; 2548 } 2549 final String inverseName = getInverseName( a, m ); 2550 if ( !descriptor.hasInverse() && inverseName.equals( defaultInverseName ) ) 2551 { 2552 throw new ProcessorException( "@Reference target has not configured an inverse but there is an " + 2553 "associated @Inverse annotated method named '" + m.getSimpleName() + 2554 "' on type '" + element.getQualifiedName() + "'.", 2555 descriptor.getMethod() ); 2556 } 2557 if ( descriptor.hasInverse() && inverseName.equals( descriptor.getInverseName() ) ) 2558 { 2559 final TypeElement target = getInverseManyTypeTarget( m ); 2560 if ( null != target ) 2561 { 2562 ensureTargetTypeAligns( component, descriptor, target.asType() ); 2563 return Multiplicity.MANY; 2564 } 2565 else 2566 { 2567 ensureTargetTypeAligns( component, descriptor, m.getReturnType() ); 2568 return AnnotationsUtil.hasNonnullAnnotation( m ) ? Multiplicity.ONE : Multiplicity.ZERO_OR_ONE; 2569 } 2570 } 2571 else 2572 { 2573 return null; 2574 } 2575 } ) 2576 .filter( Objects::nonNull ) 2577 .findAny() 2578 .orElse( null ); 2579 2580 if ( descriptor.hasInverse() ) 2581 { 2582 if ( null == multiplicity ) 2583 { 2584 throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation " + 2585 "with a name parameter equal to '" + descriptor.getInverseName() + "' on " + 2586 "class " + descriptor.getMethod().getReturnType() + " but is unable to " + 2587 "locate a matching method.", descriptor.getMethod() ); 2588 } 2589 2590 final Multiplicity inverseMultiplicity = descriptor.getInverseMultiplicity(); 2591 if ( inverseMultiplicity != multiplicity ) 2592 { 2593 throw new ProcessorException( "@Reference target has an inverseMultiplicity of " + inverseMultiplicity + 2594 " but that associated @Inverse has a multiplicity of " + multiplicity + 2595 ". The multiplicity must align.", descriptor.getMethod() ); 2596 } 2597 } 2598 } 2599 2600 @Nonnull 2601 private DependencyDescriptor createMethodDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor, 2602 @Nonnull final ExecutableElement method ) 2603 { 2604 MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method ); 2605 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 2606 Constants.COMPONENT_CLASSNAME, 2607 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 2608 method ); 2609 MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method ); 2610 MemberChecks.mustReturnAValue( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method ); 2611 2612 final boolean validateTypeAtRuntime = isComponentDependencyValidateTypeAtRuntime( method ); 2613 final TypeMirror type = method.getReturnType(); 2614 if ( TypeKind.DECLARED != type.getKind() ) 2615 { 2616 throw new ProcessorException( "@ComponentDependency target must return a non-primitive value", method ); 2617 } 2618 if ( !validateTypeAtRuntime ) 2619 { 2620 final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME ); 2621 if ( !ElementsUtil.isAssignableTo( processingEnv, type, disposeNotifier ) ) 2622 { 2623 final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( type ); 2624 if ( !isArezComponentLikeAnnotated( typeElement ) && !isDisposeTrackableComponent( typeElement ) ) 2625 { 2626 throw new ProcessorException( "@ComponentDependency target must return an instance compatible with " + 2627 Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " + 2628 "with @ArezComponent(disposeNotifier=ENABLE) or " + 2629 AREZ_COMPONENT_LIKE_DESCRIPTION, method ); 2630 } 2631 } 2632 } 2633 2634 final boolean cascade = isActionCascade( method ); 2635 return new DependencyDescriptor( descriptor, method, cascade ); 2636 } 2637 2638 private static boolean isComponentDependencyValidateTypeAtRuntime( @Nonnull final AnnotatedConstruct annotatedConstruct ) 2639 { 2640 return Boolean.TRUE.equals( AnnotationsUtil 2641 .getAnnotationValue( annotatedConstruct, 2642 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 2643 "validateTypeAtRuntime" ) 2644 .getValue() ); 2645 } 2646 2647 @Nonnull 2648 private DependencyDescriptor createFieldDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor, 2649 @Nonnull final VariableElement field ) 2650 { 2651 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 2652 Constants.COMPONENT_CLASSNAME, 2653 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 2654 field ); 2655 emitWarningForManagedFieldAccess( descriptor, field, Constants.COMPONENT_DEPENDENCY_CLASSNAME ); 2656 MemberChecks.mustBeFinal( Constants.COMPONENT_DEPENDENCY_CLASSNAME, field ); 2657 2658 final boolean validateTypeAtRuntime = isComponentDependencyValidateTypeAtRuntime( field ); 2659 final TypeMirror type = processingEnv.getTypeUtils().asMemberOf( descriptor.asDeclaredType(), field ); 2660 if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() ) 2661 { 2662 throw new ProcessorException( "@ComponentDependency target must be a non-primitive value", field ); 2663 } 2664 if ( !validateTypeAtRuntime ) 2665 { 2666 final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME ); 2667 if ( !ElementsUtil.isAssignableTo( processingEnv, type, disposeNotifier ) ) 2668 { 2669 final Element element = processingEnv.getTypeUtils().asElement( type ); 2670 if ( !( element instanceof TypeElement ) || 2671 !isArezComponentLikeAnnotated( (TypeElement) element ) && 2672 !isDisposeTrackableComponent( (TypeElement) element ) ) 2673 { 2674 throw new ProcessorException( "@ComponentDependency target must be an instance compatible with " + 2675 Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " + 2676 "with @ArezComponent(disposeNotifier=ENABLE) or " + 2677 AREZ_COMPONENT_LIKE_DESCRIPTION, field ); 2678 } 2679 } 2680 } 2681 2682 if ( !isActionCascade( field ) ) 2683 { 2684 throw new ProcessorException( "@ComponentDependency target defined an action of 'SET_NULL' but the " + 2685 "dependency is on a final field and can not be set to null.", field ); 2686 2687 } 2688 2689 return new DependencyDescriptor( descriptor, field ); 2690 } 2691 2692 private boolean isActionCascade( @Nonnull final Element method ) 2693 { 2694 final String value = 2695 AnnotationsUtil.getEnumAnnotationParameter( method, 2696 Constants.COMPONENT_DEPENDENCY_CLASSNAME, 2697 "action" ); 2698 return "CASCADE".equals( value ); 2699 } 2700 2701 @SuppressWarnings( "BooleanMethodIsAlwaysInverted" ) 2702 private boolean isArezComponentAnnotated( @Nonnull final TypeElement typeElement ) 2703 { 2704 return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME ); 2705 } 2706 2707 @SuppressWarnings( "BooleanMethodIsAlwaysInverted" ) 2708 private boolean isArezComponentLikeAnnotated( @Nonnull final TypeElement typeElement ) 2709 { 2710 return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) || 2711 isAnnotatedByActAsArezComponent( typeElement ); 2712 } 2713 2714 private boolean isAnnotatedByActAsArezComponent( @Nonnull final TypeElement typeElement ) 2715 { 2716 for ( final AnnotationMirror annotation : typeElement.getAnnotationMirrors() ) 2717 { 2718 final Element annotationType = annotation.getAnnotationType().asElement(); 2719 if ( annotationType instanceof TypeElement && isActAsArezComponentAnnotated( (TypeElement) annotationType ) ) 2720 { 2721 return true; 2722 } 2723 } 2724 return false; 2725 } 2726 2727 private boolean isActAsArezComponentAnnotated( @Nonnull final TypeElement annotationType ) 2728 { 2729 for ( final AnnotationMirror annotation : annotationType.getAnnotationMirrors() ) 2730 { 2731 final Element metaAnnotationType = annotation.getAnnotationType().asElement(); 2732 if ( metaAnnotationType instanceof TypeElement && 2733 isActAsArezComponentAnnotationType( (TypeElement) metaAnnotationType ) ) 2734 { 2735 return true; 2736 } 2737 } 2738 return false; 2739 } 2740 2741 private boolean isActAsArezComponentAnnotationType( @Nonnull final TypeElement annotationType ) 2742 { 2743 return Constants.ACT_AS_AREZ_COMPONENT_CLASSNAME.equals( annotationType.getQualifiedName().toString() ) || 2744 annotationType.getSimpleName().contentEquals( Constants.ACT_AS_AREZ_COMPONENT_SIMPLE_NAME ); 2745 } 2746 2747 private boolean isArezComponentLikeType( @Nonnull final TypeMirror typeMirror ) 2748 { 2749 final Element element = processingEnv.getTypeUtils().asElement( typeMirror ); 2750 return element instanceof TypeElement && isArezComponentLikeAnnotated( (TypeElement) element ); 2751 } 2752 2753 @SuppressWarnings( "BooleanMethodIsAlwaysInverted" ) 2754 private boolean isDisposeTrackableComponent( @Nonnull final TypeElement typeElement ) 2755 { 2756 return isArezComponentAnnotated( typeElement ) && 2757 isDisposableTrackableRequired( typeElement ); 2758 } 2759 2760 private boolean isLivenessDisposedArezComponent( @Nonnull final TypeMirror typeMirror ) 2761 { 2762 final Element element = processingEnv.getTypeUtils().asElement( typeMirror ); 2763 final AnnotationMirror arezComponent = element instanceof TypeElement ? 2764 AnnotationsUtil.findAnnotationByType( element, 2765 Constants.COMPONENT_CLASSNAME ) : 2766 null; 2767 return null != arezComponent && this.<Boolean>getAnnotationParameter( arezComponent, "disposeOnDeactivate" ); 2768 } 2769 2770 @Nonnull 2771 private ComponentDescriptor parse( @Nonnull final TypeElement typeElement ) 2772 throws ProcessorException 2773 { 2774 MemberChecks.mustBeClassOrInterface( Constants.COMPONENT_CLASSNAME, typeElement ); 2775 if ( typeElement.getModifiers().contains( Modifier.FINAL ) ) 2776 { 2777 throw new ProcessorException( "@ArezComponent target must not be final", typeElement ); 2778 } 2779 MemberChecks.mustNotBeNonStaticNestedType( Constants.COMPONENT_CLASSNAME, typeElement ); 2780 final AnnotationMirror arezComponent = 2781 AnnotationsUtil.getAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ); 2782 final String declaredName = getAnnotationParameter( arezComponent, "name" ); 2783 final boolean disposeOnDeactivate = getAnnotationParameter( arezComponent, "disposeOnDeactivate" ); 2784 final boolean observableFlag = isComponentObservableRequired( arezComponent, disposeOnDeactivate ); 2785 final boolean service = isService( typeElement ); 2786 final boolean disposeNotifierFlag = isDisposableTrackableRequired( typeElement ); 2787 final boolean allowEmpty = getAnnotationParameter( arezComponent, "allowEmpty" ); 2788 final List<VariableElement> fields = ElementsUtil.getFields( typeElement ); 2789 ensureNoFieldInjections( fields ); 2790 ensureNoMethodInjections( typeElement ); 2791 final boolean sting = isStingIntegrationEnabled( arezComponent, service ); 2792 2793 final var defaultReadOutsideTransaction = 2794 AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultReadOutsideTransaction" ); 2795 final var defaultWriteOutsideTransaction = 2796 AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultWriteOutsideTransaction" ); 2797 final var defaultSkipIfDisposed = 2798 AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultSkipIfDisposed" ); 2799 2800 final var requireEquals = isEqualsRequired( arezComponent ); 2801 final var requireVerify = isVerifyRequired( arezComponent, typeElement ); 2802 2803 if ( !typeElement.getModifiers().contains( Modifier.ABSTRACT ) ) 2804 { 2805 throw new ProcessorException( "@ArezComponent target must be abstract", typeElement ); 2806 } 2807 2808 final var name = 2809 Constants.SENTINEL.equals( declaredName ) ? 2810 typeElement.getQualifiedName().toString().replace( ".", "_" ) : 2811 declaredName; 2812 2813 if ( !SourceVersion.isIdentifier( name ) ) 2814 { 2815 throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " + 2816 "name must be a valid java identifier.", typeElement ); 2817 } 2818 else if ( SourceVersion.isKeyword( name ) ) 2819 { 2820 throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " + 2821 "name must not be a java keyword.", typeElement ); 2822 } 2823 2824 verifyConstructors( typeElement, sting ); 2825 2826 if ( sting && !( (DeclaredType) typeElement.asType() ).getTypeArguments().isEmpty() ) 2827 { 2828 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 2829 "enable sting integration and be a parameterized type" ), 2830 typeElement ); 2831 } 2832 else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER ) ) 2833 { 2834 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 2835 "disable sting integration and be annotated with " + 2836 Constants.STING_EAGER ), 2837 typeElement ); 2838 } 2839 else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) ) 2840 { 2841 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 2842 "disable sting integration and be annotated with " + 2843 Constants.STING_TYPED ), 2844 typeElement ); 2845 } 2846 else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) ) 2847 { 2848 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 2849 "disable sting integration and be annotated with " + 2850 Constants.STING_NAMED ), 2851 typeElement ); 2852 } 2853 else if ( !observableFlag && disposeOnDeactivate ) 2854 { 2855 throw new ProcessorException( "@ArezComponent target has specified observable = DISABLE and " + 2856 "disposeOnDeactivate = true which is not a valid combination", typeElement ); 2857 } 2858 2859 if ( isWarningNotSuppressed( typeElement, Constants.WARNING_EXTENDS_COMPONENT ) ) 2860 { 2861 var parent = typeElement.getSuperclass(); 2862 while ( null != parent ) 2863 { 2864 final var parentElement = processingEnv.getTypeUtils().asElement( parent ); 2865 final var parentTypeElement = 2866 null != parentElement && ElementKind.CLASS == parentElement.getKind() ? (TypeElement) parentElement : null; 2867 2868 if ( null != parentTypeElement && 2869 AnnotationsUtil.hasAnnotationOfType( parentTypeElement, Constants.COMPONENT_CLASSNAME ) ) 2870 { 2871 final var message = 2872 MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME, 2873 "extend a class annotated with the " + Constants.COMPONENT_CLASSNAME + 2874 " annotation. " + suppressedBy( Constants.WARNING_EXTENDS_COMPONENT ) ); 2875 warning( message, typeElement ); 2876 } 2877 parent = null != parentTypeElement ? parentTypeElement.getSuperclass() : null; 2878 } 2879 } 2880 2881 final var methods = 2882 ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils(), true ); 2883 final var generateToString = methods.stream(). 2884 noneMatch( m -> m.getSimpleName().toString().equals( "toString" ) && 2885 m.getParameters().isEmpty() && 2886 !( m.getEnclosingElement().getSimpleName().toString().equals( "Object" ) && 2887 "java.lang".equals( processingEnv 2888 .getElementUtils() 2889 .getPackageOf( m.getEnclosingElement() ) 2890 .getQualifiedName() 2891 .toString() ) ) ); 2892 2893 final var priority = getDefaultPriority( arezComponent ); 2894 final var defaultPriority = 2895 null == priority ? null : "DEFAULT".equals( priority ) ? Priority.NORMAL : Priority.valueOf( priority ); 2896 2897 final var defaultReadOutsideTransactionValue = 2898 null == defaultReadOutsideTransaction ? 2899 null : 2900 ( (VariableElement) defaultReadOutsideTransaction.getValue() ).getSimpleName().toString(); 2901 final var defaultWriteOutsideTransactionValue = 2902 null == defaultWriteOutsideTransaction ? 2903 null : 2904 ( (VariableElement) defaultWriteOutsideTransaction.getValue() ).getSimpleName().toString(); 2905 final var defaultSkipIfDisposedValue = 2906 null == defaultSkipIfDisposed ? 2907 null : 2908 ( (VariableElement) defaultSkipIfDisposed.getValue() ).getSimpleName().toString(); 2909 2910 final var descriptor = 2911 new ComponentDescriptor( name, 2912 defaultPriority, 2913 observableFlag, 2914 disposeNotifierFlag, 2915 disposeOnDeactivate, 2916 sting, 2917 requireEquals, 2918 requireVerify, 2919 generateToString, 2920 typeElement, 2921 defaultReadOutsideTransactionValue, 2922 defaultWriteOutsideTransactionValue, 2923 defaultSkipIfDisposedValue ); 2924 2925 processObservableInitialFields( descriptor, fields ); 2926 analyzeCandidateMethods( descriptor, methods, processingEnv.getTypeUtils() ); 2927 validate( allowEmpty, descriptor ); 2928 2929 for ( final ObservableDescriptor observable : descriptor.getObservables().values() ) 2930 { 2931 final var returnType = observable.getGetterType().getReturnType(); 2932 if ( observable.expectSetter() ) 2933 { 2934 final var parameterType = observable.getSetterType().getParameterTypes().get( 0 ); 2935 if ( !processingEnv.getTypeUtils().isSameType( parameterType, returnType ) && 2936 !parameterType.toString().equals( returnType.toString() ) ) 2937 { 2938 throw new ProcessorException( "@Observable property defines a setter and getter with different types." + 2939 " Getter type: " + returnType + " Setter type: " + parameterType + ".", 2940 observable.getGetter() ); 2941 } 2942 } 2943 final var getterDeclaredComparator = observable.getGetterDeclaredEqualityComparator(); 2944 final var setterDeclaredComparator = observable.getSetterDeclaredEqualityComparator(); 2945 final var getterExplicit = !Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( getterDeclaredComparator ); 2946 final var setterExplicit = !Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( setterDeclaredComparator ); 2947 if ( getterExplicit && setterExplicit && !getterDeclaredComparator.equals( setterDeclaredComparator ) ) 2948 { 2949 throw new ProcessorException( "@Observable target specified equalityComparator of type '" + 2950 setterDeclaredComparator + "' but the paired accessor has already specified " + 2951 "equalityComparator of type '" + getterDeclaredComparator + "'.", 2952 observable.getSetter() ); 2953 } 2954 final var comparatorElement = 2955 getterExplicit ? observable.getGetter() : setterExplicit ? observable.getSetter() : observable.getDefiner(); 2956 final var comparatorClassName = 2957 getterExplicit ? getterDeclaredComparator : 2958 setterExplicit ? setterDeclaredComparator : 2959 Constants.EQUALITY_COMPARATOR_CLASSNAME; 2960 observable.setEqualityComparator( resolveEffectiveEqualityComparator( descriptor.getElement(), 2961 Constants.OBSERVABLE_CLASSNAME, 2962 comparatorElement, 2963 returnType, 2964 comparatorClassName ) ); 2965 } 2966 2967 final var idRequired = isIdRequired( arezComponent ); 2968 descriptor.setIdRequired( idRequired ); 2969 if ( !idRequired ) 2970 { 2971 if ( descriptor.hasComponentIdMethod() ) 2972 { 2973 throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " + 2974 "annotation parameter but also has annotated a method with @ComponentId " + 2975 "that requires idRequired = ENABLE.", typeElement ); 2976 } 2977 if ( !descriptor.getComponentIdRefs().isEmpty() ) 2978 { 2979 throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " + 2980 "annotation parameter but also has annotated a method with @ComponentIdRef " + 2981 "that requires idRequired = ENABLE.", typeElement ); 2982 } 2983 if ( !descriptor.getInverses().isEmpty() ) 2984 { 2985 throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " + 2986 "annotation parameter but also has annotated a method with @Inverse " + 2987 "that requires idRequired = ENABLE.", typeElement ); 2988 } 2989 } 2990 2991 warnOnUnmanagedComponentReferences( descriptor, fields ); 2992 2993 return descriptor; 2994 } 2995 2996 private boolean isStingIntegrationEnabled( @Nonnull final AnnotationMirror arezComponent, final boolean service ) 2997 { 2998 final VariableElement parameter = getAnnotationParameter( arezComponent, "sting" ); 2999 final var value = parameter.getSimpleName().toString(); 3000 return "ENABLE".equals( value ) || 3001 ( "AUTODETECT".equals( value ) && 3002 service && 3003 null != findTypeElement( Constants.STING_INJECTOR ) ); 3004 } 3005 3006 private void verifyConstructors( @Nonnull final TypeElement typeElement, final boolean sting ) 3007 { 3008 final var constructors = ElementsUtil.getConstructors( typeElement ); 3009 if ( constructors.size() > 1 && sting ) 3010 { 3011 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3012 "enable sting integration and have multiple constructors" ), 3013 typeElement ); 3014 } 3015 3016 for ( final var constructor : constructors ) 3017 { 3018 if ( constructor.getModifiers().contains( Modifier.PROTECTED ) && 3019 isWarningNotSuppressed( constructor, Constants.WARNING_PROTECTED_CONSTRUCTOR ) ) 3020 { 3021 final var message = 3022 MemberChecks.should( Constants.COMPONENT_CLASSNAME, 3023 "have a package access constructor. " + 3024 suppressedBy( Constants.WARNING_PROTECTED_CONSTRUCTOR ) ); 3025 warning( message, constructor ); 3026 } 3027 verifyConstructorParameters( constructor, sting ); 3028 } 3029 } 3030 3031 private void verifyConstructorParameters( @Nonnull final ExecutableElement constructor, final boolean sting ) 3032 { 3033 for ( final var parameter : constructor.getParameters() ) 3034 { 3035 final var type = parameter.asType(); 3036 if ( sting && TypesUtil.containsArrayType( type ) ) 3037 { 3038 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3039 "enable sting integration and contain a constructor with a parameter that contains an array type" ), 3040 parameter ); 3041 } 3042 else if ( sting && TypesUtil.containsWildcard( type ) ) 3043 { 3044 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3045 "enable sting integration and contain a constructor with a parameter that contains a wildcard type parameter" ), 3046 parameter ); 3047 } 3048 else if ( sting && TypesUtil.containsRawType( type ) ) 3049 { 3050 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3051 "enable sting integration and contain a constructor with a parameter that contains a raw type" ), 3052 parameter ); 3053 } 3054 else if ( !sting && AnnotationsUtil.hasAnnotationOfType( parameter, Constants.STING_NAMED ) ) 3055 { 3056 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3057 "disable sting integration and contain a constructor with a parameter that is annotated with the " + 3058 Constants.STING_NAMED + " annotation" ), 3059 parameter ); 3060 } 3061 else if ( sting && TypeKind.DECLARED == type.getKind() && !( (DeclaredType) type ).getTypeArguments().isEmpty() ) 3062 { 3063 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3064 "enable sting integration and contain a constructor with a parameter that contains a parameterized type" ), 3065 parameter ); 3066 } 3067 } 3068 } 3069 3070 private void ensureNoFieldInjections( @Nonnull final List<VariableElement> fields ) 3071 { 3072 for ( final var field : fields ) 3073 { 3074 if ( hasInjectAnnotation( field ) ) 3075 { 3076 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3077 "contain fields annotated by the " + 3078 Constants.INJECT_CLASSNAME + 3079 " annotation. Use constructor injection instead" ), 3080 field ); 3081 } 3082 } 3083 } 3084 3085 private void ensureNoMethodInjections( @Nonnull final TypeElement typeElement ) 3086 { 3087 final var methods = 3088 ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ); 3089 for ( final var method : methods ) 3090 { 3091 if ( hasInjectAnnotation( method ) ) 3092 { 3093 throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME, 3094 "contain methods annotated by the " + 3095 Constants.INJECT_CLASSNAME + 3096 " annotation. Use constructor injection instead" ), 3097 method ); 3098 } 3099 } 3100 } 3101 3102 private void analyzeCandidateMethods( @Nonnull final ComponentDescriptor componentDescriptor, 3103 @Nonnull final List<ExecutableElement> methods, 3104 @Nonnull final Types typeUtils ) 3105 throws ProcessorException 3106 { 3107 for ( final var method : methods ) 3108 { 3109 final var methodName = method.getSimpleName().toString(); 3110 if ( AREZ_SPECIAL_METHODS.contains( methodName ) && method.getParameters().isEmpty() ) 3111 { 3112 throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " + 3113 "reserved by Arez", method ); 3114 } 3115 else if ( methodName.startsWith( ComponentGenerator.FIELD_PREFIX ) || 3116 methodName.startsWith( ComponentGenerator.OBSERVABLE_DATA_FIELD_PREFIX ) || 3117 methodName.startsWith( ComponentGenerator.REFERENCE_FIELD_PREFIX ) || 3118 methodName.startsWith( ComponentGenerator.FRAMEWORK_PREFIX ) ) 3119 { 3120 throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " + 3121 "with a prefix reserved by Arez", method ); 3122 } 3123 } 3124 final var getters = new HashMap<String, CandidateMethod>(); 3125 final var captures = new HashMap<String, CandidateMethod>(); 3126 final var pushes = new HashMap<String, CandidateMethod>(); 3127 final var pops = new HashMap<String, CandidateMethod>(); 3128 final var setters = new HashMap<String, CandidateMethod>(); 3129 final var observes = new HashMap<String, CandidateMethod>(); 3130 final var onDepsChanges = new LinkedHashMap<String, List<CandidateMethod>>(); 3131 for ( final ExecutableElement method : methods ) 3132 { 3133 final var methodType = 3134 (ExecutableType) typeUtils.asMemberOf( (DeclaredType) componentDescriptor.getElement().asType(), method ); 3135 if ( !analyzeMethod( componentDescriptor, method, methodType ) ) 3136 { 3137 /* 3138 * If we get here the method was not annotated so we can try to detect if it is a 3139 * candidate arez method in case some arez annotations are implied via naming conventions. 3140 */ 3141 if ( method.getModifiers().contains( Modifier.STATIC ) ) 3142 { 3143 continue; 3144 } 3145 3146 final var candidateMethod = new CandidateMethod( method, methodType ); 3147 final var voidReturn = method.getReturnType().getKind() == TypeKind.VOID; 3148 final var parameterCount = method.getParameters().size(); 3149 String name; 3150 3151 name = deriveName( method, PUSH_PATTERN, Constants.SENTINEL ); 3152 if ( voidReturn && 1 == parameterCount && null != name ) 3153 { 3154 pushes.put( name, candidateMethod ); 3155 continue; 3156 } 3157 name = deriveName( method, POP_PATTERN, Constants.SENTINEL ); 3158 if ( voidReturn && 1 == parameterCount && null != name ) 3159 { 3160 pops.put( name, candidateMethod ); 3161 continue; 3162 } 3163 name = deriveName( method, CAPTURE_PATTERN, Constants.SENTINEL ); 3164 if ( !voidReturn && 0 == parameterCount && null != name ) 3165 { 3166 captures.put( name, candidateMethod ); 3167 continue; 3168 } 3169 3170 if ( !method.getModifiers().contains( Modifier.FINAL ) ) 3171 { 3172 name = deriveName( method, SETTER_PATTERN, Constants.SENTINEL ); 3173 if ( voidReturn && 1 == parameterCount && null != name ) 3174 { 3175 setters.put( name, candidateMethod ); 3176 continue; 3177 } 3178 name = deriveName( method, ISSER_PATTERN, Constants.SENTINEL ); 3179 if ( !voidReturn && 0 == parameterCount && null != name ) 3180 { 3181 getters.put( name, candidateMethod ); 3182 continue; 3183 } 3184 name = deriveName( method, GETTER_PATTERN, Constants.SENTINEL ); 3185 if ( !voidReturn && 0 == parameterCount && null != name ) 3186 { 3187 getters.put( name, candidateMethod ); 3188 continue; 3189 } 3190 } 3191 name = deriveName( method, ON_DEPS_CHANGE_PATTERN, Constants.SENTINEL ); 3192 if ( voidReturn && null != name ) 3193 { 3194 if ( 0 == parameterCount || 3195 ( 3196 1 == parameterCount && 3197 Constants.OBSERVER_CLASSNAME.equals( method.getParameters().get( 0 ).asType().toString() ) 3198 ) 3199 ) 3200 { 3201 onDepsChanges.computeIfAbsent( name, key -> new ArrayList<>() ).add( candidateMethod ); 3202 continue; 3203 } 3204 } 3205 3206 final var methodName = method.getSimpleName().toString(); 3207 if ( !OBJECT_METHODS.contains( methodName ) ) 3208 { 3209 observes.put( methodName, candidateMethod ); 3210 } 3211 } 3212 } 3213 3214 linkUnAnnotatedObservables( componentDescriptor, getters, setters ); 3215 linkUnAnnotatedObserves( componentDescriptor, observes, onDepsChanges ); 3216 linkUnMemoizeContextParameters( componentDescriptor, captures, pushes, pops ); 3217 linkObserverRefs( componentDescriptor ); 3218 linkCascadeDisposeObservables( componentDescriptor ); 3219 linkCascadeDisposeReferences( componentDescriptor ); 3220 linkAutoObserveObservables( componentDescriptor ); 3221 linkAutoObserveReferences( componentDescriptor ); 3222 linkObservableInitials( componentDescriptor ); 3223 3224 // CascadeDispose returned false but it was actually processed so lets remove them from getters set 3225 3226 componentDescriptor.getCascadeDisposes().keySet().forEach( method -> { 3227 for ( final var entry : new HashMap<>( getters ).entrySet() ) 3228 { 3229 if ( method.equals( entry.getValue().getMethod() ) ) 3230 { 3231 getters.remove( entry.getKey() ); 3232 } 3233 } 3234 } ); 3235 3236 linkMemoizeContextParametersToMemoizes( componentDescriptor ); 3237 3238 linkDependencies( componentDescriptor, getters.values() ); 3239 3240 autodetectObservableInitializers( componentDescriptor ); 3241 3242 /* 3243 * All of the maps will have called remove() for all matching candidates. 3244 * Thus any left are the non-arez methods. 3245 */ 3246 3247 ensureNoAbstractMethods( componentDescriptor, getters.values() ); 3248 ensureNoAbstractMethods( componentDescriptor, setters.values() ); 3249 ensureNoAbstractMethods( componentDescriptor, observes.values() ); 3250 ensureNoAbstractMethods( componentDescriptor, 3251 onDepsChanges.values().stream().flatMap( List::stream ).toList() ); 3252 3253 processCascadeDisposeFields( componentDescriptor ); 3254 processAutoObserveFields( componentDescriptor ); 3255 processComponentDependencyFields( componentDescriptor ); 3256 } 3257 3258 private static void linkMemoizeContextParametersToMemoizes( final @Nonnull ComponentDescriptor componentDescriptor ) 3259 { 3260 // Link MemoizeContextParameters to associated Memoize descriptors 3261 componentDescriptor 3262 .getMemoizes() 3263 .values() 3264 .forEach( m -> 3265 componentDescriptor 3266 .getMemoizeContextParameters() 3267 .values() 3268 .forEach( p -> p.tryMatchMemoizeDescriptor( m ) ) ); 3269 } 3270 3271 private void linkUnMemoizeContextParameters( @Nonnull final ComponentDescriptor componentDescriptor, 3272 @Nonnull final Map<String, CandidateMethod> captures, 3273 @Nonnull final Map<String, CandidateMethod> pushes, 3274 @Nonnull final Map<String, CandidateMethod> pops ) 3275 { 3276 final var parameters = componentDescriptor.getMemoizeContextParameters().values(); 3277 for ( final var parameter : parameters ) 3278 { 3279 if ( !parameter.hasCapture() ) 3280 { 3281 final var capture = captures.remove( parameter.getName() ); 3282 if ( null != capture ) 3283 { 3284 parameter.linkUnAnnotatedCapture( capture.getMethod(), capture.getMethodType() ); 3285 } 3286 } 3287 if ( !parameter.hasPop() ) 3288 { 3289 final var pop = pops.remove( parameter.getName() ); 3290 if ( null != pop ) 3291 { 3292 parameter.linkUnAnnotatedPop( pop.getMethod(), pop.getMethodType() ); 3293 } 3294 } 3295 if ( !parameter.hasPush() ) 3296 { 3297 final var push = pushes.remove( parameter.getName() ); 3298 if ( null != push ) 3299 { 3300 parameter.linkUnAnnotatedPush( push.getMethod(), push.getMethodType() ); 3301 } 3302 } 3303 } 3304 } 3305 3306 private void ensureNoAbstractMethods( @Nonnull final ComponentDescriptor componentDescriptor, 3307 @Nonnull final Collection<CandidateMethod> candidateMethods ) 3308 { 3309 candidateMethods 3310 .stream() 3311 .map( CandidateMethod::getMethod ) 3312 .filter( m -> m.getModifiers().contains( Modifier.ABSTRACT ) ) 3313 .forEach( m -> { 3314 throw new ProcessorException( "@ArezComponent target has an abstract method not implemented by " + 3315 "framework. The method is named " + m.getSimpleName(), 3316 componentDescriptor.getElement() ); 3317 } ); 3318 } 3319 3320 private boolean analyzeMethod( @Nonnull final ComponentDescriptor descriptor, 3321 @Nonnull final ExecutableElement method, 3322 @Nonnull final ExecutableType methodType ) 3323 throws ProcessorException 3324 { 3325 emitWarningForUnnecessaryProtectedMethod( descriptor, method ); 3326 emitWarningForUnnecessaryFinalMethod( descriptor, method ); 3327 verifyNoDuplicateAnnotations( method ); 3328 3329 final var action = AnnotationsUtil.findAnnotationByType( method, Constants.ACTION_CLASSNAME ); 3330 final var requiresTransaction = 3331 AnnotationsUtil.findAnnotationByType( method, Constants.REQUIRES_TRANSACTION_CLASSNAME ); 3332 final var jaxWsAction = AnnotationsUtil.findAnnotationByType( method, Constants.JAX_WS_ACTION_CLASSNAME ); 3333 final var observed = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVE_CLASSNAME ); 3334 final var observable = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_CLASSNAME ); 3335 final var observableInitial = 3336 AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_INITIAL_CLASSNAME ); 3337 final var observableValueRef = 3338 AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_VALUE_REF_CLASSNAME ); 3339 final var memoize = AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CLASSNAME ); 3340 final var memoizeContextParameter = 3341 AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME ); 3342 final var computableValueRef = 3343 AnnotationsUtil.findAnnotationByType( method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME ); 3344 final var contextRef = AnnotationsUtil.findAnnotationByType( method, Constants.CONTEXT_REF_CLASSNAME ); 3345 final var stateRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_STATE_REF_CLASSNAME ); 3346 final var componentRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_REF_CLASSNAME ); 3347 final var componentId = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_CLASSNAME ); 3348 final var componentIdRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_REF_CLASSNAME ); 3349 final var componentTypeName = 3350 AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME ); 3351 final var componentNameRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_NAME_REF_CLASSNAME ); 3352 final var postConstruct = AnnotationsUtil.findAnnotationByType( method, Constants.POST_CONSTRUCT_CLASSNAME ); 3353 final var ejbPostConstruct = AnnotationsUtil.findAnnotationByType( method, Constants.EJB_POST_CONSTRUCT_CLASSNAME ); 3354 final var preDispose = AnnotationsUtil.findAnnotationByType( method, Constants.PRE_DISPOSE_CLASSNAME ); 3355 final var postDispose = AnnotationsUtil.findAnnotationByType( method, Constants.POST_DISPOSE_CLASSNAME ); 3356 final var onActivate = AnnotationsUtil.findAnnotationByType( method, Constants.ON_ACTIVATE_CLASSNAME ); 3357 final var onDeactivate = AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEACTIVATE_CLASSNAME ); 3358 final var onDepsChange = AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEPS_CHANGE_CLASSNAME ); 3359 final var observerRef = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVER_REF_CLASSNAME ); 3360 final var dependency = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME ); 3361 final var autoObserve = AnnotationsUtil.findAnnotationByType( method, Constants.AUTO_OBSERVE_CLASSNAME ); 3362 final var reference = AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_CLASSNAME ); 3363 final var referenceId = AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_ID_CLASSNAME ); 3364 final var inverse = AnnotationsUtil.findAnnotationByType( method, Constants.INVERSE_CLASSNAME ); 3365 final var preInverseRemove = AnnotationsUtil.findAnnotationByType( method, Constants.PRE_INVERSE_REMOVE_CLASSNAME ); 3366 final var postInverseAdd = AnnotationsUtil.findAnnotationByType( method, Constants.POST_INVERSE_ADD_CLASSNAME ); 3367 final var cascadeDispose = AnnotationsUtil.findAnnotationByType( method, Constants.CASCADE_DISPOSE_CLASSNAME ); 3368 3369 if ( null != observable ) 3370 { 3371 final ObservableDescriptor observableDescriptor = addObservable( descriptor, 3372 observable, method, methodType ); 3373 if ( null != referenceId ) 3374 { 3375 addReferenceId( descriptor, referenceId, observableDescriptor, method ); 3376 } 3377 if ( null != inverse ) 3378 { 3379 addInverse( descriptor, inverse, observableDescriptor, method ); 3380 } 3381 if ( null != cascadeDispose ) 3382 { 3383 addCascadeDisposeMethod( descriptor, method, observableDescriptor ); 3384 } 3385 if ( null != autoObserve ) 3386 { 3387 addAutoObserveMethod( descriptor, method, observableDescriptor ); 3388 } 3389 return true; 3390 } 3391 else if ( null != observableInitial ) 3392 { 3393 addObservableInitialMethod( descriptor, observableInitial, method, methodType ); 3394 return true; 3395 } 3396 else if ( null != observableValueRef ) 3397 { 3398 addObservableValueRef( descriptor, observableValueRef, method, methodType ); 3399 return true; 3400 } 3401 else if ( null != action ) 3402 { 3403 if ( null != postConstruct ) 3404 { 3405 addPostConstruct( descriptor, method ); 3406 } 3407 addAction( descriptor, action, method, methodType ); 3408 return true; 3409 } 3410 else if ( null != requiresTransaction ) 3411 { 3412 addRequiresTransaction( descriptor, requiresTransaction, method, methodType ); 3413 return true; 3414 } 3415 else if ( null != observed ) 3416 { 3417 addObserve( descriptor, observed, method, methodType ); 3418 return true; 3419 } 3420 else if ( null != onDepsChange ) 3421 { 3422 addOnDepsChange( descriptor, onDepsChange, method ); 3423 return true; 3424 } 3425 else if ( null != observerRef ) 3426 { 3427 addObserverRef( descriptor, observerRef, method, methodType ); 3428 return true; 3429 } 3430 else if ( null != contextRef ) 3431 { 3432 addContextRef( descriptor, method ); 3433 return true; 3434 } 3435 else if ( null != stateRef ) 3436 { 3437 addComponentStateRef( descriptor, stateRef, method ); 3438 return true; 3439 } 3440 else if ( null != memoizeContextParameter ) 3441 { 3442 addMemoizeContextParameter( descriptor, memoizeContextParameter, method, methodType ); 3443 return true; 3444 } 3445 else if ( null != memoize ) 3446 { 3447 addMemoize( descriptor, memoize, method, methodType ); 3448 return true; 3449 } 3450 else if ( null != computableValueRef ) 3451 { 3452 addComputableValueRef( descriptor, computableValueRef, method, methodType ); 3453 return true; 3454 } 3455 else if ( null != reference ) 3456 { 3457 if ( null != cascadeDispose ) 3458 { 3459 addCascadeDisposeMethod( descriptor, method, null ); 3460 } 3461 addReference( descriptor, reference, method, methodType ); 3462 if ( null != autoObserve ) 3463 { 3464 addAutoObserveMethod( descriptor, method, null ); 3465 } 3466 return true; 3467 } 3468 else if ( null != autoObserve ) 3469 { 3470 addAutoObserveMethod( descriptor, method, null ); 3471 return true; 3472 } 3473 else if ( null != cascadeDispose ) 3474 { 3475 addCascadeDisposeMethod( descriptor, method, null ); 3476 // Return false so that it can be picked as the getter of an @Observable or linked to a @Reference 3477 return false; 3478 } 3479 else if ( null != componentIdRef ) 3480 { 3481 addComponentIdRef( descriptor, method ); 3482 return true; 3483 } 3484 else if ( null != componentRef ) 3485 { 3486 addComponentRef( descriptor, method ); 3487 return true; 3488 } 3489 else if ( null != componentId ) 3490 { 3491 setComponentId( descriptor, method, methodType ); 3492 return true; 3493 } 3494 else if ( null != componentNameRef ) 3495 { 3496 addComponentNameRef( descriptor, method ); 3497 return true; 3498 } 3499 else if ( null != componentTypeName ) 3500 { 3501 setComponentTypeNameRef( descriptor, method ); 3502 return true; 3503 } 3504 else if ( null != jaxWsAction ) 3505 { 3506 throw new ProcessorException( "@" + Constants.JAX_WS_ACTION_CLASSNAME + " annotation " + 3507 "not supported in components annotated with @ArezComponent, use the @" + 3508 Constants.ACTION_CLASSNAME + " annotation instead.", 3509 method ); 3510 } 3511 else if ( null != ejbPostConstruct ) 3512 { 3513 throw new ProcessorException( "@" + Constants.EJB_POST_CONSTRUCT_CLASSNAME + " annotation " + 3514 "not supported in components annotated with @ArezComponent, use the @" + 3515 Constants.POST_CONSTRUCT_CLASSNAME + " annotation instead.", 3516 method ); 3517 } 3518 else if ( null != postConstruct ) 3519 { 3520 addPostConstruct( descriptor, method ); 3521 return true; 3522 } 3523 else if ( null != preDispose ) 3524 { 3525 addPreDispose( descriptor, method ); 3526 return true; 3527 } 3528 else if ( null != postDispose ) 3529 { 3530 addPostDispose( descriptor, method ); 3531 return true; 3532 } 3533 else if ( null != onActivate ) 3534 { 3535 addOnActivate( descriptor, onActivate, method ); 3536 return true; 3537 } 3538 else if ( null != onDeactivate ) 3539 { 3540 addOnDeactivate( descriptor, onDeactivate, method ); 3541 return true; 3542 } 3543 else if ( null != dependency ) 3544 { 3545 descriptor.addDependency( createMethodDependencyDescriptor( descriptor, method ) ); 3546 return false; 3547 } 3548 else if ( null != referenceId ) 3549 { 3550 addReferenceId( descriptor, referenceId, method ); 3551 return true; 3552 } 3553 else if ( null != inverse ) 3554 { 3555 addInverse( descriptor, inverse, method, methodType ); 3556 return true; 3557 } 3558 else if ( null != preInverseRemove ) 3559 { 3560 addPreInverseRemove( descriptor, preInverseRemove, method ); 3561 return true; 3562 } 3563 else if ( null != postInverseAdd ) 3564 { 3565 addPostInverseAdd( descriptor, postInverseAdd, method ); 3566 return true; 3567 } 3568 else 3569 { 3570 return false; 3571 } 3572 } 3573 3574 private void emitWarningForUnnecessaryProtectedMethod( @Nonnull final ComponentDescriptor descriptor, 3575 @Nonnull final ExecutableElement method ) 3576 { 3577 if ( method.getModifiers().contains( Modifier.PROTECTED ) && 3578 Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) && 3579 ElementsUtil.isWarningNotSuppressed( method, 3580 Constants.WARNING_PROTECTED_METHOD, 3581 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) && 3582 !isMethodAProtectedOverride( descriptor.getElement(), method ) ) 3583 { 3584 final var message = 3585 MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME, 3586 "declare a protected method. " + 3587 MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD, 3588 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ); 3589 warning( message, method ); 3590 } 3591 } 3592 3593 private void emitWarningForManagedFieldAccess( @Nonnull final ComponentDescriptor descriptor, 3594 @Nonnull final VariableElement field, 3595 @Nonnull final String annotationClassname ) 3596 { 3597 emitWarningForPublicManagedField( field, annotationClassname ); 3598 emitWarningForUnnecessaryProtectedManagedField( descriptor, field ); 3599 } 3600 3601 private void emitWarningForPublicManagedField( @Nonnull final VariableElement field, 3602 @Nonnull final String annotationClassname ) 3603 { 3604 if ( field.getModifiers().contains( Modifier.PUBLIC ) && 3605 ElementsUtil.isWarningNotSuppressed( field, 3606 Constants.WARNING_PUBLIC_FIELD, 3607 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ) 3608 { 3609 final var message = 3610 MemberChecks.shouldNot( annotationClassname, 3611 "be public. " + 3612 MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_FIELD, 3613 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ); 3614 warning( message, field ); 3615 } 3616 } 3617 3618 private void emitWarningForUnnecessaryProtectedManagedField( @Nonnull final ComponentDescriptor descriptor, 3619 @Nonnull final VariableElement field ) 3620 { 3621 if ( field.getModifiers().contains( Modifier.PROTECTED ) && 3622 ElementsUtil.isWarningNotSuppressed( field, 3623 Constants.WARNING_PROTECTED_FIELD, 3624 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) && 3625 !isProtectedFieldOnInheritedTypeInDifferentPackage( descriptor.getElement(), field ) ) 3626 { 3627 final var message = 3628 MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME, 3629 "declare a protected field. " + 3630 MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_FIELD, 3631 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ); 3632 warning( message, field ); 3633 } 3634 } 3635 3636 private void emitWarningForUnnecessaryFinalMethod( @Nonnull final ComponentDescriptor descriptor, 3637 @Nonnull final ExecutableElement method ) 3638 { 3639 if ( method.getModifiers().contains( Modifier.FINAL ) && 3640 Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) && 3641 ElementsUtil.isWarningNotSuppressed( method, 3642 Constants.WARNING_FINAL_METHOD, 3643 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ) 3644 { 3645 final var message = 3646 MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME, 3647 "declare a final method. " + 3648 MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD, 3649 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ); 3650 warning( message, method ); 3651 } 3652 } 3653 3654 private void addReferenceId( @Nonnull final ComponentDescriptor descriptor, 3655 @Nonnull final AnnotationMirror annotation, 3656 @Nonnull final ObservableDescriptor observable, 3657 @Nonnull final ExecutableElement method ) 3658 { 3659 MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method ); 3660 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 3661 Constants.COMPONENT_CLASSNAME, 3662 Constants.REFERENCE_ID_CLASSNAME, 3663 method ); 3664 MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method ); 3665 MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method ); 3666 3667 final var name = getReferenceIdName( annotation, method ); 3668 descriptor.findOrCreateReference( name ).setObservable( observable ); 3669 } 3670 3671 private void addReferenceId( @Nonnull final ComponentDescriptor descriptor, 3672 @Nonnull final AnnotationMirror annotation, 3673 @Nonnull final ExecutableElement method ) 3674 { 3675 MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method ); 3676 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 3677 Constants.COMPONENT_CLASSNAME, 3678 Constants.REFERENCE_ID_CLASSNAME, 3679 method ); 3680 MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method ); 3681 MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method ); 3682 3683 final var name = getReferenceIdName( annotation, method ); 3684 descriptor.findOrCreateReference( name ).setIdMethod( method ); 3685 } 3686 3687 @Nonnull 3688 private String getReferenceIdName( @Nonnull final AnnotationMirror annotation, 3689 @Nonnull final ExecutableElement method ) 3690 { 3691 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 3692 final String name; 3693 if ( Constants.SENTINEL.equals( declaredName ) ) 3694 { 3695 final var candidate = deriveName( method, ID_GETTER_PATTERN, declaredName ); 3696 if ( null == candidate ) 3697 { 3698 final String candidate2 = deriveName( method, RAW_ID_GETTER_PATTERN, declaredName ); 3699 if ( null == candidate2 ) 3700 { 3701 throw new ProcessorException( "@ReferenceId target has not specified a name and does not follow " + 3702 "the convention \"get[Name]Id\" or \"[name]Id\"", method ); 3703 } 3704 else 3705 { 3706 name = candidate2; 3707 } 3708 } 3709 else 3710 { 3711 name = candidate; 3712 } 3713 } 3714 else 3715 { 3716 name = declaredName; 3717 if ( !SourceVersion.isIdentifier( name ) ) 3718 { 3719 throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " + 3720 "name must be a valid java identifier.", method ); 3721 } 3722 else if ( SourceVersion.isKeyword( name ) ) 3723 { 3724 throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " + 3725 "name must not be a java keyword.", method ); 3726 } 3727 } 3728 return name; 3729 } 3730 3731 private void addPreInverseRemove( @Nonnull final ComponentDescriptor component, 3732 @Nonnull final AnnotationMirror annotation, 3733 @Nonnull final ExecutableElement method ) 3734 throws ProcessorException 3735 { 3736 mustBeHookHook( component.getElement(), 3737 Constants.PRE_INVERSE_REMOVE_CLASSNAME, 3738 method ); 3739 shouldBeInternalHookMethod( processingEnv, 3740 component, 3741 method, 3742 Constants.PRE_INVERSE_REMOVE_CLASSNAME ); 3743 if ( 1 != method.getParameters().size() ) 3744 { 3745 throw new ProcessorException( MemberChecks.must( Constants.PRE_INVERSE_REMOVE_CLASSNAME, 3746 "have exactly 1 parameter" ), method ); 3747 } 3748 else 3749 { 3750 final var name = getPreInverseRemoveName( annotation, method ); 3751 findOrCreateInverseDescriptor( component, name ).addPreInverseRemoveHook( method ); 3752 } 3753 } 3754 3755 @Nonnull 3756 private String getPreInverseRemoveName( @Nonnull final AnnotationMirror annotation, 3757 @Nonnull final ExecutableElement method ) 3758 { 3759 final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 3760 if ( Constants.SENTINEL.equals( name ) ) 3761 { 3762 final var candidate = deriveName( method, PRE_INVERSE_REMOVE_PATTERN, name ); 3763 if ( null == candidate ) 3764 { 3765 throw new ProcessorException( "@PreInverseRemove target has not specified a name and does not follow " + 3766 "the convention \"pre[Name]Remove\"", method ); 3767 } 3768 else 3769 { 3770 return candidate; 3771 } 3772 } 3773 else 3774 { 3775 if ( !SourceVersion.isIdentifier( name ) ) 3776 { 3777 throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " + 3778 "name must be a valid java identifier", method ); 3779 } 3780 else if ( SourceVersion.isKeyword( name ) ) 3781 { 3782 throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " + 3783 "name must not be a java keyword", method ); 3784 } 3785 return name; 3786 } 3787 } 3788 3789 private void addPostInverseAdd( @Nonnull final ComponentDescriptor component, 3790 @Nonnull final AnnotationMirror annotation, 3791 @Nonnull final ExecutableElement method ) 3792 throws ProcessorException 3793 { 3794 mustBeHookHook( component.getElement(), 3795 Constants.POST_INVERSE_ADD_CLASSNAME, 3796 method ); 3797 shouldBeInternalHookMethod( processingEnv, 3798 component, 3799 method, 3800 Constants.POST_INVERSE_ADD_CLASSNAME ); 3801 if ( 1 != method.getParameters().size() ) 3802 { 3803 throw new ProcessorException( MemberChecks.must( Constants.POST_INVERSE_ADD_CLASSNAME, 3804 "have exactly 1 parameter" ), method ); 3805 } 3806 else 3807 { 3808 final var name = getPostInverseAddName( annotation, method ); 3809 findOrCreateInverseDescriptor( component, name ).addPostInverseAddHook( method ); 3810 } 3811 } 3812 3813 @Nonnull 3814 private String getPostInverseAddName( @Nonnull final AnnotationMirror annotation, 3815 @Nonnull final ExecutableElement method ) 3816 { 3817 final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 3818 if ( Constants.SENTINEL.equals( name ) ) 3819 { 3820 final var candidate = deriveName( method, POST_INVERSE_ADD_PATTERN, name ); 3821 if ( null == candidate ) 3822 { 3823 throw new ProcessorException( "@PostInverseAdd target has not specified a name and does not follow " + 3824 "the convention \"post[Name]Add\"", method ); 3825 } 3826 else 3827 { 3828 return candidate; 3829 } 3830 } 3831 else 3832 { 3833 if ( !SourceVersion.isIdentifier( name ) ) 3834 { 3835 throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " + 3836 "name must be a valid java identifier", method ); 3837 } 3838 else if ( SourceVersion.isKeyword( name ) ) 3839 { 3840 throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " + 3841 "name must not be a java keyword", method ); 3842 } 3843 return name; 3844 } 3845 } 3846 3847 private void addInverse( @Nonnull final ComponentDescriptor descriptor, 3848 @Nonnull final AnnotationMirror annotation, 3849 @Nonnull final ExecutableElement method, 3850 @Nonnull final ExecutableType methodType ) 3851 { 3852 MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method ); 3853 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 3854 Constants.COMPONENT_CLASSNAME, 3855 Constants.INVERSE_CLASSNAME, 3856 method ); 3857 MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method ); 3858 MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method ); 3859 MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method ); 3860 3861 final var name = getInverseName( annotation, method ); 3862 final var observable = descriptor.findOrCreateObservable( name ); 3863 observable.setGetter( method, methodType ); 3864 3865 addInverse( descriptor, annotation, observable, method ); 3866 } 3867 3868 private void addInverse( @Nonnull final ComponentDescriptor descriptor, 3869 @Nonnull final AnnotationMirror annotation, 3870 @Nonnull final ObservableDescriptor observable, 3871 @Nonnull final ExecutableElement method ) 3872 { 3873 MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method ); 3874 MemberChecks.mustBeSubclassCallable( descriptor.getElement(), 3875 Constants.COMPONENT_CLASSNAME, 3876 Constants.INVERSE_CLASSNAME, 3877 method ); 3878 MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method ); 3879 MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method ); 3880 MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method ); 3881 3882 final var name = getInverseName( annotation, method ); 3883 final var existing = descriptor.getInverses().get( name ); 3884 if ( null != existing && existing.hasObservable() ) 3885 { 3886 throw new ProcessorException( "@Inverse target defines duplicate inverse for name '" + name + 3887 "'. The other inverse is " + existing.getObservable().getGetter(), 3888 method ); 3889 } 3890 else 3891 { 3892 final var type = method.getReturnType(); 3893 3894 final Multiplicity multiplicity; 3895 var targetType = getInverseManyTypeTarget( method ); 3896 if ( null != targetType ) 3897 { 3898 multiplicity = Multiplicity.MANY; 3899 } 3900 else 3901 { 3902 if ( !( type instanceof DeclaredType ) || 3903 !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) type ).asElement(), 3904 Constants.COMPONENT_CLASSNAME ) ) 3905 { 3906 throw new ProcessorException( "@Inverse target expected to return a type annotated with " + 3907 Constants.COMPONENT_CLASSNAME, method ); 3908 } 3909 targetType = (TypeElement) ( (DeclaredType) type ).asElement(); 3910 if ( AnnotationsUtil.hasNonnullAnnotation( method ) ) 3911 { 3912 multiplicity = Multiplicity.ONE; 3913 } 3914 else if ( AnnotationsUtil.hasNullableAnnotation( method ) ) 3915 { 3916 multiplicity = Multiplicity.ZERO_OR_ONE; 3917 } 3918 else 3919 { 3920 throw new ProcessorException( "@Inverse target expected to be annotated with either " + 3921 AnnotationsUtil.NULLABLE_CLASSNAME + " or " + 3922 AnnotationsUtil.NONNULL_CLASSNAME, method ); 3923 } 3924 } 3925 final var referenceName = getInverseReferenceNameParameter( descriptor, method ); 3926 final var inverse = findOrCreateInverseDescriptor( descriptor, name ); 3927 final var otherName = NamesUtil.firstCharacterToLowerCase( targetType.getSimpleName().toString() ); 3928 inverse.setInverse( observable, referenceName, multiplicity, targetType, otherName ); 3929 verifyMultiplicityOfAssociatedReferenceMethod( descriptor, inverse ); 3930 } 3931 } 3932 3933 @Nonnull 3934 private InverseDescriptor findOrCreateInverseDescriptor( @Nonnull final ComponentDescriptor descriptor, 3935 @Nonnull final String name ) 3936 { 3937 return descriptor.getInverses().computeIfAbsent( name, n -> new InverseDescriptor( descriptor, name ) ); 3938 } 3939 3940 @Nonnull 3941 private String getInverseName( @Nonnull final AnnotationMirror annotation, 3942 @Nonnull final ExecutableElement method ) 3943 { 3944 final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 3945 final String name; 3946 if ( Constants.SENTINEL.equals( declaredName ) ) 3947 { 3948 final var candidate = deriveName( method, GETTER_PATTERN, declaredName ); 3949 name = null == candidate ? method.getSimpleName().toString() : candidate; 3950 } 3951 else 3952 { 3953 name = declaredName; 3954 if ( !SourceVersion.isIdentifier( name ) ) 3955 { 3956 throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " + 3957 "name must be a valid java identifier.", method ); 3958 } 3959 else if ( SourceVersion.isKeyword( name ) ) 3960 { 3961 throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " + 3962 "name must not be a java keyword.", method ); 3963 } 3964 } 3965 return name; 3966 } 3967 3968 private void warnOnUnmanagedComponentReferences( @Nonnull final ComponentDescriptor descriptor, 3969 @Nonnull final List<VariableElement> fields ) 3970 { 3971 final var disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME ); 3972 3973 for ( final var field : fields ) 3974 { 3975 if ( !field.getModifiers().contains( Modifier.STATIC ) && 3976 SuperficialValidation.validateElement( processingEnv, field ) ) 3977 { 3978 final var fieldType = getEffectiveFieldType( descriptor, field ); 3979 final var fieldTypeElement = ElementsUtil.asTypeElement( processingEnv, fieldType ); 3980 final var isDisposeNotifier = ElementsUtil.isAssignableTo( processingEnv, fieldType, disposeNotifier ); 3981 final var isTypeAnnotatedByComponentAnnotation = 3982 !isDisposeNotifier && null != fieldTypeElement && isArezComponentAnnotated( fieldTypeElement ); 3983 final var isTypeAnnotatedArezComponentLike = 3984 !isDisposeNotifier && 3985 !isTypeAnnotatedByComponentAnnotation && 3986 isArezComponentLikeType( fieldType ); 3987 if ( isTypeAnnotatedByComponentAnnotation ) 3988 { 3989 emitWarningForNonPrivateServiceField( field, fieldTypeElement ); 3990 } 3991 if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedArezComponentLike ) 3992 { 3993 if ( !descriptor.isDependencyDefined( field ) && 3994 !descriptor.isCascadeDisposeDefined( field ) && 3995 !descriptor.isAutoObserveDefined( field ) && 3996 ( isDisposeNotifier || 3997 isTypeAnnotatedArezComponentLike || 3998 verifyReferencesToComponent( fieldTypeElement ) ) && 3999 isUnmanagedComponentReferenceNotSuppressed( field ) ) 4000 { 4001 final var label = 4002 isDisposeNotifier ? "an implementation of DisposeNotifier" : 4003 isTypeAnnotatedByComponentAnnotation ? "an Arez component" : 4004 AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION; 4005 final var message = 4006 "Field named '" + field.getSimpleName() + "' has a type that is " + label + 4007 " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " + 4008 "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + " or @" + Constants.AUTO_OBSERVE_CLASSNAME + 4009 ". This scenario can cause errors if the value is disposed. Please " + 4010 "annotate the field as appropriate or suppress the warning by annotating the field with " + 4011 "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " + 4012 "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )"; 4013 warning( message, field ); 4014 } 4015 } 4016 } 4017 } 4018 4019 for ( final var observable : descriptor.getObservables().values() ) 4020 { 4021 if ( observable.isAbstract() ) 4022 { 4023 final var getter = observable.getGetter(); 4024 if ( SuperficialValidation.validateElement( processingEnv, getter ) ) 4025 { 4026 final var returnType = getter.getReturnType(); 4027 final var returnElement = 4028 TypeKind.DECLARED == returnType.getKind() ? 4029 ElementsUtil.asTypeElement( processingEnv, returnType ) : 4030 null; 4031 final var isDisposeNotifier = ElementsUtil.isAssignableTo( processingEnv, returnType, disposeNotifier ); 4032 final var isTypeAnnotatedByComponentAnnotation = 4033 !isDisposeNotifier && null != returnElement && isArezComponentAnnotated( returnElement ); 4034 final var isTypeAnnotatedArezComponentLike = 4035 !isDisposeNotifier && 4036 !isTypeAnnotatedByComponentAnnotation && 4037 null != returnElement && 4038 isArezComponentLikeAnnotated( returnElement ); 4039 if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedArezComponentLike ) 4040 { 4041 if ( !descriptor.isDependencyDefined( getter ) && 4042 !descriptor.isCascadeDisposeDefined( getter ) && 4043 !descriptor.isAutoObserveDefined( getter ) && 4044 ( isDisposeNotifier || 4045 isTypeAnnotatedArezComponentLike || 4046 verifyReferencesToComponent( returnElement ) ) && 4047 isUnmanagedComponentReferenceNotSuppressed( getter ) && 4048 ( observable.hasSetter() && isUnmanagedComponentReferenceNotSuppressed( observable.getSetter() ) ) ) 4049 { 4050 final var label = 4051 isDisposeNotifier ? "an implementation of DisposeNotifier" : 4052 isTypeAnnotatedByComponentAnnotation ? "an Arez component" : 4053 AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION; 4054 final var message = 4055 "Method named '" + getter.getSimpleName() + "' has a return type that is " + label + 4056 " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " + 4057 "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + " or @" + Constants.AUTO_OBSERVE_CLASSNAME + 4058 ". This scenario can cause errors. " + 4059 "Please annotate the method as appropriate or suppress the warning by annotating the method with " + 4060 "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " + 4061 "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )"; 4062 warning( message, getter ); 4063 } 4064 } 4065 } 4066 } 4067 } 4068 } 4069 4070 @Nonnull 4071 private TypeMirror getEffectiveFieldType( @Nonnull final ComponentDescriptor descriptor, 4072 @Nonnull final VariableElement field ) 4073 { 4074 return processingEnv.getTypeUtils().asMemberOf( descriptor.asDeclaredType(), field ); 4075 } 4076 4077 private boolean verifyReferencesToComponent( @Nonnull final TypeElement element ) 4078 { 4079 assert SuperficialValidation.validateElement( processingEnv, element ); 4080 4081 final var verifyReferencesToComponent = 4082 AnnotationsUtil.getEnumAnnotationParameter( element, 4083 Constants.COMPONENT_CLASSNAME, 4084 "verifyReferencesToComponent" ); 4085 return switch ( verifyReferencesToComponent ) 4086 { 4087 case "ENABLE" -> true; 4088 case "DISABLE" -> false; 4089 default -> isDisposableTrackableRequired( element ); 4090 }; 4091 } 4092 4093 private boolean isUnmanagedComponentReferenceNotSuppressed( @Nonnull final Element element ) 4094 { 4095 return !ElementsUtil.isWarningSuppressed( element, 4096 Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE, 4097 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 4098 } 4099 4100 private void emitWarningForNonPrivateServiceField( @Nonnull final VariableElement field, 4101 @Nonnull final TypeElement typeElement ) 4102 { 4103 if ( !field.getModifiers().contains( Modifier.PRIVATE ) && 4104 isService( typeElement ) && 4105 ElementsUtil.isWarningNotSuppressed( field, 4106 Constants.WARNING_NON_PRIVATE_SERVICE_FIELD, 4107 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) ) 4108 { 4109 final var message = 4110 "Field named '" + field.getSimpleName() + "' has a type annotated with @" + 4111 Constants.COMPONENT_CLASSNAME + "(service = ENABLE) and should be private. " + 4112 MemberChecks.suppressedBy( Constants.WARNING_NON_PRIVATE_SERVICE_FIELD, 4113 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 4114 warning( message, field ); 4115 } 4116 } 4117 4118 @SuppressWarnings( "SameParameterValue" ) 4119 private boolean isElementAnnotatedBy( @Nullable final Element element, @Nonnull final String annotation ) 4120 { 4121 return null != element && 4122 SuperficialValidation.validateElement( processingEnv, element ) && 4123 AnnotationsUtil.hasAnnotationOfType( element, annotation ); 4124 } 4125 4126 private boolean isService( @Nonnull final TypeElement typeElement ) 4127 { 4128 final var service = 4129 AnnotationsUtil.getEnumAnnotationParameter( typeElement, Constants.COMPONENT_CLASSNAME, "service" ); 4130 return switch ( service ) 4131 { 4132 case "ENABLE" -> true; 4133 case "DISABLE" -> false; 4134 default -> AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) || 4135 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) || 4136 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER ); 4137 }; 4138 } 4139 4140 private boolean isComponentObservableRequired( @Nonnull final AnnotationMirror arezComponent, 4141 final boolean disposeOnDeactivate ) 4142 { 4143 final VariableElement variableElement = getAnnotationParameter( arezComponent, "observable" ); 4144 return switch ( variableElement.getSimpleName().toString() ) 4145 { 4146 case "ENABLE" -> true; 4147 case "DISABLE" -> false; 4148 default -> disposeOnDeactivate; 4149 }; 4150 } 4151 4152 private boolean isVerifyRequired( @Nonnull final AnnotationMirror arezComponent, 4153 @Nonnull final TypeElement typeElement ) 4154 { 4155 final VariableElement parameter = getAnnotationParameter( arezComponent, "verify" ); 4156 return switch ( parameter.getSimpleName().toString() ) 4157 { 4158 case "ENABLE" -> true; 4159 case "DISABLE" -> false; 4160 default -> ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ). 4161 stream().anyMatch( this::hasReferenceAnnotations ); 4162 }; 4163 } 4164 4165 private boolean isSkipIfDisposed( @Nonnull final ComponentDescriptor component, 4166 @Nonnull final AnnotationMirror action ) 4167 { 4168 final VariableElement parameter = getAnnotationParameter( action, "skipIfDisposed" ); 4169 return switch ( parameter.getSimpleName().toString() ) 4170 { 4171 case "ENABLE" -> true; 4172 case "DISABLE" -> false; 4173 default -> component.defaultSkipIfDisposed(); 4174 }; 4175 } 4176 4177 private boolean hasReferenceAnnotations( @Nonnull final Element method ) 4178 { 4179 return AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_CLASSNAME ) || 4180 AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_ID_CLASSNAME ) || 4181 AnnotationsUtil.hasAnnotationOfType( method, Constants.INVERSE_CLASSNAME ); 4182 } 4183 4184 private boolean isEqualsRequired( @Nonnull final AnnotationMirror arezComponent ) 4185 { 4186 final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireEquals" ); 4187 return "ENABLE".equals( injectParameter.getSimpleName().toString() ); 4188 } 4189 4190 @Nullable 4191 private String getDefaultPriority( @Nonnull final AnnotationMirror arezComponent ) 4192 { 4193 final AnnotationValue value = 4194 AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultPriority" ); 4195 return null == value ? null : ( (VariableElement) value.getValue() ).getSimpleName().toString(); 4196 } 4197 4198 private boolean isIdRequired( @Nonnull final AnnotationMirror arezComponent ) 4199 { 4200 final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireId" ); 4201 return !"DISABLE".equals( injectParameter.getSimpleName().toString() ); 4202 } 4203 4204 private boolean hasInjectAnnotation( @Nonnull final Element method ) 4205 { 4206 return AnnotationsUtil.hasAnnotationOfType( method, Constants.INJECT_CLASSNAME ); 4207 } 4208 4209 @Nonnull 4210 private <T> T getAnnotationParameter( @Nonnull final AnnotationMirror annotation, 4211 @Nonnull final String parameterName ) 4212 { 4213 return AnnotationsUtil.getAnnotationValueValue( annotation, parameterName ); 4214 } 4215 4216 @Nonnull 4217 private TypeElement getDisposableTypeElement() 4218 { 4219 return getTypeElement( Constants.DISPOSABLE_CLASSNAME ); 4220 } 4221 4222 private boolean isDisposableTrackableRequired( @Nonnull final TypeElement element ) 4223 { 4224 final var disposeNotifier = 4225 AnnotationsUtil.getEnumAnnotationParameter( element, Constants.COMPONENT_CLASSNAME, "disposeNotifier" ); 4226 return switch ( disposeNotifier ) 4227 { 4228 case "ENABLE" -> true; 4229 case "DISABLE" -> false; 4230 default -> null == AnnotationsUtil.findAnnotationByType( element, Constants.COMPONENT_CLASSNAME ) || 4231 !isService( element ); 4232 }; 4233 } 4234 4235 @Nonnull 4236 private TypeElement getTypeElement( @Nonnull final String classname ) 4237 { 4238 final var typeElement = findTypeElement( classname ); 4239 assert null != typeElement; 4240 return typeElement; 4241 } 4242 4243 @Nullable 4244 private TypeElement findTypeElement( @Nonnull final String classname ) 4245 { 4246 return processingEnv.getElementUtils().getTypeElement( classname ); 4247 } 4248 4249 @Nonnull 4250 private String resolveEffectiveEqualityComparator( @Nonnull final TypeElement componentType, 4251 @Nonnull final String annotationName, 4252 @Nonnull final Element element, 4253 @Nonnull final TypeMirror valueType, 4254 @Nonnull final String comparatorClassName ) 4255 { 4256 final var effectiveComparator = 4257 Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( comparatorClassName ) ? 4258 deriveDefaultEqualityComparator( valueType ) : 4259 comparatorClassName; 4260 verifyValidEqualityComparator( componentType, annotationName, effectiveComparator, element ); 4261 return effectiveComparator; 4262 } 4263 4264 @Nonnull 4265 private String deriveDefaultEqualityComparator( @Nonnull final TypeMirror valueType ) 4266 { 4267 if ( TypeKind.DECLARED == valueType.getKind() ) 4268 { 4269 final var typeElement = ( (DeclaredType) valueType ).asElement(); 4270 final var annotation = 4271 AnnotationsUtil.findAnnotationByType( typeElement, Constants.DEFAULT_EQUALITY_COMPARATOR_CLASSNAME ); 4272 if ( null != annotation ) 4273 { 4274 final TypeMirror comparatorType = AnnotationsUtil.getAnnotationValueValue( annotation, "value" ); 4275 return comparatorType.toString(); 4276 } 4277 } 4278 return Constants.OBJECTS_EQUALS_COMPARATOR_CLASSNAME; 4279 } 4280 4281 private void verifyValidEqualityComparator( @Nonnull final TypeElement componentType, 4282 @Nonnull final String annotationName, 4283 @Nonnull final String comparatorClassName, 4284 @Nonnull final Element element ) 4285 { 4286 final var comparatorType = getTypeElement( comparatorClassName ); 4287 if ( ElementKind.CLASS != comparatorType.getKind() ) 4288 { 4289 throw new ProcessorException( annotationName + " resolved equalityComparator of type '" + 4290 comparatorClassName + "' but the comparator must be a class.", 4291 element ); 4292 } 4293 else if ( comparatorType.getModifiers().contains( Modifier.ABSTRACT ) ) 4294 { 4295 throw new ProcessorException( annotationName + " resolved equalityComparator of type '" + 4296 comparatorClassName + "' but the comparator must not be abstract.", 4297 element ); 4298 } 4299 else if ( ElementsUtil.isNonStaticNestedType( comparatorType ) ) 4300 { 4301 throw new ProcessorException( annotationName + " resolved equalityComparator of type '" + 4302 comparatorClassName + "' but the comparator must be static if nested.", 4303 element ); 4304 } 4305 else if ( !ElementsUtil.isTypeAccessibleFrom( componentType, comparatorType ) ) 4306 { 4307 throw new ProcessorException( annotationName + " resolved equalityComparator of type '" + 4308 comparatorClassName + "' but the comparator is not accessible from the " + 4309 "generated component.", 4310 element ); 4311 } 4312 else if ( !ElementsUtil.hasAccessibleNoArgConstructor( componentType, comparatorType ) ) 4313 { 4314 throw new ProcessorException( annotationName + " resolved equalityComparator of type '" + 4315 comparatorClassName + "' but the comparator must define an accessible " + 4316 "no-arg constructor.", 4317 element ); 4318 } 4319 } 4320 4321 private boolean isProtectedFieldOnInheritedTypeInDifferentPackage( @Nonnull final TypeElement componentType, 4322 @Nonnull final VariableElement field ) 4323 { 4324 final var declaringType = ElementsUtil.getOwningType( field ); 4325 return !Objects.equals( declaringType, componentType ) && 4326 ElementsUtil.areTypesInDifferentPackage( declaringType, componentType ); 4327 } 4328 4329 private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement, 4330 @Nonnull final ExecutableElement method ) 4331 { 4332 final var overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method ); 4333 return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED ); 4334 } 4335 4336 private void mustBeStandardRefMethod( @Nonnull final ProcessingEnvironment processingEnv, 4337 @Nonnull final ComponentDescriptor descriptor, 4338 @Nonnull final ExecutableElement method, 4339 @Nonnull final String annotationClassname ) 4340 { 4341 mustBeRefMethod( descriptor, method, annotationClassname ); 4342 MemberChecks.mustNotHaveAnyParameters( annotationClassname, method ); 4343 shouldBeInternalRefMethod( processingEnv, descriptor, method, annotationClassname ); 4344 } 4345 4346 private void mustBeRefMethod( @Nonnull final ComponentDescriptor descriptor, 4347 @Nonnull final ExecutableElement method, 4348 @Nonnull final String annotationClassname ) 4349 { 4350 MemberChecks.mustBeAbstract( annotationClassname, method ); 4351 final var typeElement = descriptor.getElement(); 4352 MemberChecks.mustNotBePackageAccessInDifferentPackage( typeElement, 4353 Constants.COMPONENT_CLASSNAME, 4354 annotationClassname, 4355 method ); 4356 MemberChecks.mustReturnAValue( annotationClassname, method ); 4357 MemberChecks.mustNotThrowAnyExceptions( annotationClassname, method ); 4358 } 4359 4360 private void mustBeHookHook( @Nonnull final TypeElement targetType, 4361 @Nonnull final String annotationName, 4362 @Nonnull final ExecutableElement method ) 4363 throws ProcessorException 4364 { 4365 MemberChecks.mustNotBeAbstract( annotationName, method ); 4366 MemberChecks.mustBeSubclassCallable( targetType, Constants.COMPONENT_CLASSNAME, annotationName, method ); 4367 MemberChecks.mustNotReturnAnyValue( annotationName, method ); 4368 MemberChecks.mustNotThrowAnyExceptions( annotationName, method ); 4369 } 4370 4371 private void shouldBeInternalRefMethod( @Nonnull final ProcessingEnvironment processingEnv, 4372 @Nonnull final ComponentDescriptor descriptor, 4373 @Nonnull final ExecutableElement method, 4374 @Nonnull final String annotationClassname ) 4375 { 4376 if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) ) 4377 { 4378 MemberChecks.shouldNotBePublic( processingEnv, 4379 method, 4380 annotationClassname, 4381 warningKind(), 4382 Constants.WARNING_PUBLIC_REF_METHOD, 4383 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 4384 } 4385 } 4386 4387 private void shouldBeInternalLifecycleMethod( @Nonnull final ProcessingEnvironment processingEnv, 4388 @Nonnull final ComponentDescriptor descriptor, 4389 @Nonnull final ExecutableElement method, 4390 @Nonnull final String annotationClassname ) 4391 { 4392 if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) ) 4393 { 4394 MemberChecks.shouldNotBePublic( processingEnv, 4395 method, 4396 annotationClassname, 4397 warningKind(), 4398 Constants.WARNING_PUBLIC_LIFECYCLE_METHOD, 4399 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 4400 } 4401 } 4402 4403 private void shouldBeInternalHookMethod( @Nonnull final ProcessingEnvironment processingEnv, 4404 @Nonnull final ComponentDescriptor descriptor, 4405 @Nonnull final ExecutableElement method, 4406 @Nonnull final String annotationClassname ) 4407 { 4408 if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) ) 4409 { 4410 MemberChecks.shouldNotBePublic( processingEnv, 4411 method, 4412 annotationClassname, 4413 warningKind(), 4414 Constants.WARNING_PUBLIC_HOOK_METHOD, 4415 Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ); 4416 } 4417 } 4418 4419 @Nullable 4420 private String deriveName( @Nonnull final ExecutableElement method, 4421 @Nonnull final Pattern pattern, 4422 @Nonnull final String name ) 4423 throws ProcessorException 4424 { 4425 return NamesUtil.deriveName( method, pattern, name, Constants.SENTINEL ); 4426 } 4427}