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