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