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