001package arez.persist.processor; 002 003import java.io.IOException; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012import javax.annotation.Nonnull; 013import javax.annotation.Nullable; 014import javax.annotation.processing.RoundEnvironment; 015import javax.annotation.processing.SupportedAnnotationTypes; 016import javax.annotation.processing.SupportedOptions; 017import javax.annotation.processing.SupportedSourceVersion; 018import javax.lang.model.SourceVersion; 019import javax.lang.model.element.AnnotationMirror; 020import javax.lang.model.element.Element; 021import javax.lang.model.element.ElementKind; 022import javax.lang.model.element.ExecutableElement; 023import javax.lang.model.element.TypeElement; 024import javax.lang.model.type.TypeKind; 025import javax.lang.model.type.TypeMirror; 026import org.realityforge.proton.AbstractStandardProcessor; 027import org.realityforge.proton.AnnotationsUtil; 028import org.realityforge.proton.DeferredElementSet; 029import org.realityforge.proton.ElementsUtil; 030import org.realityforge.proton.GeneratorUtil; 031import org.realityforge.proton.MemberChecks; 032import org.realityforge.proton.ProcessorException; 033import org.realityforge.proton.StopWatch; 034 035/** 036 * Annotation processor that analyzes Arez annotated source and generates models from the annotations. 037 */ 038@SupportedAnnotationTypes( { Constants.PERSIST_TYPE_CLASSNAME, 039 Constants.PERSIST_ID_CLASSNAME, 040 Constants.PERSIST_CLASSNAME } ) 041@SupportedSourceVersion( SourceVersion.RELEASE_17 ) 042@SupportedOptions( { "arez.persist.defer.unresolved", 043 "arez.persist.defer.errors", 044 "arez.persist.debug", 045 "arez.persist.format_generated_source", 046 "arez.persist.profile", 047 "arez.persist.verbose_out_of_round", 048 "arez.persist.warnings_as_errors" } ) 049public final class ArezPersistProcessor 050 extends AbstractStandardProcessor 051{ 052 @Nonnull 053 private static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" ); 054 @Nonnull 055 private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" ); 056 /** 057 * Sentinel value indicating the default value. 058 */ 059 @Nonnull 060 private static final String DEFAULT_SENTINEL = "<default>"; 061 @Nonnull 062 private final DeferredElementSet _deferredTypes = new DeferredElementSet(); 063 @Nonnull 064 private final StopWatch _analyzePersistTypeStopWatch = new StopWatch( "Analyze PersistType" ); 065 @Nonnull 066 private final StopWatch _analyzePersistIdStopWatch = new StopWatch( "Analyze PersistId" ); 067 068 @Override 069 @Nonnull 070 protected String getIssueTrackerURL() 071 { 072 return "https://github.com/arez/arez/issues"; 073 } 074 075 @Nonnull 076 @Override 077 protected String getOptionPrefix() 078 { 079 return "arez.persist"; 080 } 081 082 @Override 083 protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches ) 084 { 085 stopWatches.add( _analyzePersistTypeStopWatch ); 086 stopWatches.add( _analyzePersistIdStopWatch ); 087 } 088 089 @Override 090 public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env ) 091 { 092 debugAnnotationProcessingRootElements( env ); 093 collectRootTypeNames( env ); 094 // Validate persist first so we don't have to perform validation inside type processing 095 // The framework assumes that Arez is also running the annotation processor and validating 096 // the shape of the same methods as they are marked as @Observable so this annotation processor 097 // performs minimal validation. This can result in this annotation processor producing bad code 098 // and the arez annotation processor failing later in the round. The expectation is that we will 099 // need to get the arez annotation processor passing and once it does then the code generated by 100 // this annotation processor will start to work. 101 annotations 102 .stream() 103 .filter( a -> a.getQualifiedName().toString().equals( Constants.PERSIST_CLASSNAME ) ) 104 .findAny() 105 .ifPresent( a -> verifyPersistElements( env, env.getElementsAnnotatedWith( a ) ) ); 106 107 annotations 108 .stream() 109 .filter( a -> a.getQualifiedName().toString().equals( Constants.PERSIST_ID_CLASSNAME ) ) 110 .findAny() 111 .ifPresent( a -> verifyPersistIdElements( env, env.getElementsAnnotatedWith( a ) ) ); 112 113 processTypeElements( annotations, 114 env, 115 Constants.PERSIST_TYPE_CLASSNAME, 116 _deferredTypes, 117 _analyzePersistTypeStopWatch.getName(), 118 this::process, 119 _analyzePersistTypeStopWatch ); 120 121 errorIfProcessingOverAndInvalidTypesDetected( env ); 122 clearRootTypeNamesIfProcessingOver( env ); 123 return true; 124 } 125 126 private void process( @Nonnull final TypeElement element ) 127 throws Exception 128 { 129 if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) ) 130 { 131 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_TYPE_CLASSNAME, 132 "be present on a type annotated with the " + 133 MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) + 134 " annotation" ), 135 element ); 136 } 137 138 final AnnotationMirror annotation = 139 AnnotationsUtil.getAnnotationByType( element, Constants.PERSIST_TYPE_CLASSNAME ); 140 final String name = extractPersistTypeName( element, annotation ); 141 final boolean persistOnDispose = AnnotationsUtil.getAnnotationValueValue( annotation, "persistOnDispose" ); 142 final String defaultStore = extractDefaultStore( element, annotation ); 143 final List<ExecutableElement> methods = 144 ElementsUtil.getMethods( element, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ); 145 ExecutableElement idMethod = null; 146 final Map<String, PropertyDescriptor> properties = new HashMap<>(); 147 for ( final ExecutableElement method : methods ) 148 { 149 final AnnotationMirror persistAnnotation = 150 AnnotationsUtil.findAnnotationByType( method, Constants.PERSIST_CLASSNAME ); 151 final AnnotationMirror persistIdAnnotation = 152 AnnotationsUtil.findAnnotationByType( method, Constants.PERSIST_ID_CLASSNAME ); 153 if ( null != persistAnnotation && null != persistIdAnnotation ) 154 { 155 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_ID_CLASSNAME, 156 "also be annotated with the " + 157 MemberChecks.toSimpleName( Constants.PERSIST_CLASSNAME ) + 158 " annotation" ), 159 element ); 160 } 161 else if ( null != persistAnnotation ) 162 { 163 processPersistAnnotation( element, method, persistAnnotation, methods, defaultStore, properties ); 164 } 165 else if ( null != persistIdAnnotation ) 166 { 167 // These validations have already been performed by an earlier step in this annotation processor 168 // if and only if the type declaring the @PersistId is also compiled in this compilation round. 169 // If the type that encloses @PersistId annotated method is part of a library or compiled earlier 170 // we re-run the checks just in case to avoid scenarios where weird behaviour slips through in code 171 // generation step. 172 validatePersisIdMethod( method ); 173 MemberChecks.mustNotBePackageAccessInDifferentPackage( element, 174 Constants.PERSIST_CLASSNAME, 175 Constants.PERSIST_ID_CLASSNAME, 176 method ); 177 if ( null != idMethod ) 178 { 179 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_ID_CLASSNAME, 180 "be present multiple times within a " + 181 MemberChecks.toSimpleName( Constants.PERSIST_TYPE_CLASSNAME ) + 182 " annotated type but another method annotated with " + 183 MemberChecks.toSimpleName( Constants.PERSIST_ID_CLASSNAME ) + 184 " exists and is named " + 185 idMethod.getSimpleName() ), 186 element ); 187 } 188 idMethod = method; 189 } 190 } 191 192 if ( properties.isEmpty() ) 193 { 194 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_TYPE_CLASSNAME, 195 "contain one or more " + 196 MemberChecks.toSimpleName( Constants.OBSERVABLE_CLASSNAME ) + 197 " properties annotated with " + 198 MemberChecks.toSimpleName( Constants.PERSIST_CLASSNAME ) ), 199 element ); 200 } 201 202 emitSidecar( new TypeDescriptor( name, 203 persistOnDispose, 204 element, 205 idMethod, 206 new ArrayList<>( properties.values() ) ) ); 207 } 208 209 private void processPersistAnnotation( @Nonnull final TypeElement element, 210 @Nonnull final ExecutableElement method, 211 @Nonnull final AnnotationMirror persistAnnotation, 212 @Nonnull final List<ExecutableElement> methods, 213 @Nonnull final String defaultStore, 214 @Nonnull final Map<String, PropertyDescriptor> properties ) 215 { 216 final TypeMirror returnType = method.getReturnType(); 217 if ( TypeKind.VOID == returnType.getKind() ) 218 { 219 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 220 "be present on the accessor method of the " + 221 MemberChecks.toSimpleName( Constants.OBSERVABLE_CLASSNAME ) + 222 " property" ), 223 method ); 224 } 225 226 final String persistName = extractPersistName( method, persistAnnotation ); 227 final String persistStore = extractStore( element, method, persistAnnotation, defaultStore ); 228 final String setterName = extractSetterName( persistAnnotation, persistName ); 229 230 final ExecutableElement setter = methods 231 .stream() 232 .filter( m -> TypeKind.VOID == m.getReturnType().getKind() && 233 setterName.equals( m.getSimpleName().toString() ) && 234 1 == m.getParameters().size() && 235 processingEnv.getTypeUtils().isSameType( m.getParameters().get( 0 ).asType(), returnType ) ) 236 .findAny() 237 .orElse( null ); 238 if ( null == setter ) 239 { 240 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 241 "be paired with a setter named " + setterName ), 242 method ); 243 } 244 245 final PropertyDescriptor existing = properties.get( persistName ); 246 if ( null != existing ) 247 { 248 throw new ProcessorException( MemberChecks.must( Constants.PERSIST_CLASSNAME, 249 "has the same name '" + persistName + 250 "' as another persistent property declared by the name. " + 251 "The other property is accessed by the method named " + 252 existing.getGetter().getSimpleName() ), 253 method ); 254 } 255 else 256 { 257 properties.put( persistName, new PropertyDescriptor( persistName, persistStore, method, setter ) ); 258 } 259 } 260 261 @Nonnull 262 private String extractSetterName( @Nonnull final AnnotationMirror annotation, @Nonnull final String persistName ) 263 { 264 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "setterName" ); 265 return "<default>".equals( declaredValue ) ? "set" + firstCharacterToUpperCase( persistName ) : declaredValue; 266 } 267 268 @Nonnull 269 private String extractPersistTypeName( @Nonnull final TypeElement element, 270 @Nonnull final AnnotationMirror annotation ) 271 { 272 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 273 if ( DEFAULT_SENTINEL.equals( declaredValue ) ) 274 { 275 return element.getSimpleName().toString(); 276 } 277 else if ( SourceVersion.isIdentifier( declaredValue ) ) 278 { 279 return declaredValue; 280 } 281 else 282 { 283 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_TYPE_CLASSNAME, 284 "specify a name parameter that is not a valid java identifier" ), 285 element, 286 annotation ); 287 } 288 } 289 290 @Nonnull 291 private String extractDefaultStore( @Nonnull final TypeElement element, @Nonnull final AnnotationMirror annotation ) 292 { 293 final String defaultStore = AnnotationsUtil.getAnnotationValueValue( annotation, "defaultStore" ); 294 if ( !SourceVersion.isIdentifier( defaultStore ) ) 295 { 296 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_TYPE_CLASSNAME, 297 "specify a defaultStore parameter that is not a valid java identifier" ), 298 element, 299 annotation ); 300 } 301 return defaultStore; 302 } 303 304 @Nonnull 305 private String extractPersistName( @Nonnull final ExecutableElement method, 306 @Nonnull final AnnotationMirror annotation ) 307 { 308 final String declaredValue = AnnotationsUtil.getAnnotationValueValue( annotation, "name" ); 309 final String value = getPropertyAccessorName( method, declaredValue ); 310 if ( SourceVersion.isIdentifier( value ) ) 311 { 312 return value; 313 } 314 else 315 { 316 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_CLASSNAME, 317 "specify a name parameter that is not a valid java identifier" ), 318 method, 319 annotation ); 320 } 321 } 322 323 @Nonnull 324 private String extractStore( @Nonnull final TypeElement typeElement, 325 @Nonnull final ExecutableElement element, 326 @Nonnull final AnnotationMirror annotation, 327 @Nonnull final String defaultStore ) 328 { 329 final String store = AnnotationsUtil.getAnnotationValueValue( annotation, "store" ); 330 if ( DEFAULT_SENTINEL.equals( store ) ) 331 { 332 return defaultStore; 333 } 334 else if ( SourceVersion.isIdentifier( store ) ) 335 { 336 if ( defaultStore.equals( store ) && 337 typeElement == element.getEnclosingElement() && 338 ElementsUtil.isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_STORE ) ) 339 { 340 final String message = 341 MemberChecks.shouldNot( Constants.PERSIST_CLASSNAME, 342 "specify the store parameter when it is the same as the defaultStore " + 343 "parameter in the specified by the " + 344 MemberChecks.toSimpleName( Constants.PERSIST_TYPE_CLASSNAME ) + 345 " annotation on the enclosing type. " + 346 MemberChecks.suppressedBy( Constants.WARNING_UNNECESSARY_STORE ) ); 347 warning( message, element ); 348 } 349 return store; 350 } 351 else 352 353 { 354 throw new ProcessorException( MemberChecks.mustNot( Constants.PERSIST_CLASSNAME, 355 "specify a store parameter that is not a valid java identifier" ), 356 element, 357 annotation ); 358 } 359 } 360 361 private void verifyPersistElements( @Nonnull final RoundEnvironment env, 362 @Nonnull final Set<? extends Element> elements ) 363 { 364 for ( final Element element : elements ) 365 { 366 assert ElementKind.METHOD == element.getKind(); 367 if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.OBSERVABLE_CLASSNAME ) ) 368 { 369 reportError( env, 370 MemberChecks.must( Constants.PERSIST_CLASSNAME, 371 "be also be annotated with the " + Constants.OBSERVABLE_CLASSNAME + 372 " annotation" ), 373 element ); 374 } 375 } 376 } 377 378 private void verifyPersistIdElements( @Nonnull final RoundEnvironment env, 379 @Nonnull final Set<? extends Element> elements ) 380 { 381 for ( final Element element : elements ) 382 { 383 performAction( env, 384 _analyzePersistIdStopWatch.getName(), e -> verifyPersistIdElement( env, e ), element, 385 _analyzePersistIdStopWatch ); 386 } 387 } 388 389 private void verifyPersistIdElement( @Nonnull final RoundEnvironment env, @Nonnull final Element element ) 390 { 391 assert ElementKind.METHOD == element.getKind(); 392 final Element type = element.getEnclosingElement(); 393 if ( !AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_CLASSNAME ) && 394 !AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) ) 395 { 396 reportError( env, 397 MemberChecks.must( Constants.PERSIST_ID_CLASSNAME, 398 "be enclosed within a type annotated by either the " + 399 Constants.AREZ_COMPONENT_CLASSNAME + " annotation or the " + 400 Constants.AREZ_COMPONENT_LIKE_CLASSNAME + " annotation" ), 401 element ); 402 } 403 404 validatePersisIdMethod( (ExecutableElement) element ); 405 } 406 407 private void validatePersisIdMethod( @Nonnull final ExecutableElement method ) 408 { 409 MemberChecks.mustNotBeAbstract( Constants.PERSIST_ID_CLASSNAME, method ); 410 MemberChecks.mustNotThrowAnyExceptions( Constants.PERSIST_ID_CLASSNAME, method ); 411 MemberChecks.mustNotHaveAnyTypeParameters( Constants.PERSIST_ID_CLASSNAME, method ); 412 MemberChecks.mustNotHaveAnyParameters( Constants.PERSIST_ID_CLASSNAME, method ); 413 MemberChecks.mustReturnAValue( Constants.PERSIST_ID_CLASSNAME, method ); 414 MemberChecks.mustNotBeStatic( Constants.PERSIST_ID_CLASSNAME, method ); 415 MemberChecks.mustNotBePrivate( Constants.PERSIST_ID_CLASSNAME, method ); 416 MemberChecks.mustNotBeProtected( Constants.PERSIST_ID_CLASSNAME, method ); 417 } 418 419 private void emitSidecar( @Nonnull final TypeDescriptor type ) 420 throws IOException 421 { 422 final String packageName = GeneratorUtil.getQualifiedPackageName( type.getElement() ); 423 emitTypeSpec( packageName, SidecarGenerator.buildType( processingEnv, type ) ); 424 } 425 426 @SuppressWarnings( "SameParameterValue" ) 427 @Nonnull 428 private String getPropertyAccessorName( @Nonnull final ExecutableElement method, @Nonnull final String specifiedName ) 429 throws ProcessorException 430 { 431 String name = deriveName( method, GETTER_PATTERN, specifiedName ); 432 if ( null != name ) 433 { 434 return name; 435 } 436 if ( method.getReturnType().getKind() == TypeKind.BOOLEAN ) 437 { 438 name = deriveName( method, ISSER_PATTERN, specifiedName ); 439 if ( null != name ) 440 { 441 return name; 442 } 443 } 444 return method.getSimpleName().toString(); 445 } 446 447 @Nullable 448 private String deriveName( @Nonnull final ExecutableElement method, 449 @Nonnull final Pattern pattern, 450 @Nonnull final String name ) 451 throws ProcessorException 452 { 453 if ( DEFAULT_SENTINEL.equals( name ) ) 454 { 455 final String methodName = method.getSimpleName().toString(); 456 final Matcher matcher = pattern.matcher( methodName ); 457 if ( matcher.find() ) 458 { 459 final String candidate = matcher.group( 1 ); 460 return firstCharacterToLowerCase( candidate ); 461 } 462 else 463 { 464 return null; 465 } 466 } 467 else 468 { 469 return name; 470 } 471 } 472 473 @Nonnull 474 private String firstCharacterToLowerCase( @Nonnull final String name ) 475 { 476 return Character.toLowerCase( name.charAt( 0 ) ) + name.substring( 1 ); 477 } 478 479 @Nonnull 480 private String firstCharacterToUpperCase( @Nonnull final String name ) 481 { 482 return Character.toUpperCase( name.charAt( 0 ) ) + name.substring( 1 ); 483 } 484}