diff --git a/README.md b/README.md index b74b1f6..f005834 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Rules can be sourced from OSS libraries or private internal libraries. ### Runner Plugin [![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/com.netflix.nebula.archrules.runner?style=for-the-badge&color=01AF01)](https://plugins.gradle.org/plugin/com.netflix.nebula.archrules.runner) +### Aggregate Console Report Plugin +[![Gradle Plugin Portal](https://img.shields.io/gradle-plugin-portal/v/com.netflix.nebula.archrules.aggregate?style=for-the-badge&color=01AF01)](https://plugins.gradle.org/plugin/com.netflix.nebula.archrules.aggregate) + ## Authoring Rules diff --git a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java index b7d64c5..cbd49ef 100644 --- a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java +++ b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java @@ -1,16 +1,13 @@ package com.netflix.nebula.archrules.core; import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ClassFileImporterWithPackage; import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.core.importer.Locations; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.EvaluationResult; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; import java.util.*; import java.util.stream.Collectors; @@ -52,22 +49,7 @@ public static EvaluationResult check(ArchRule rule, Class... classesToCheck) .map(Locations::ofClass) .flatMap(Collection::stream) .collect(Collectors.toSet()); - List uris = Arrays.stream(classesToCheck) - .map(clazz -> clazz.getPackage().getName()) - .map(Locations::ofPackage) - .flatMap(it -> it.stream().map(Location::asURI)) - .map(u -> URI.create(u.toASCIIString() + "package-info.class")) - .map(uri -> { - try { - return uri.toURL(); - } catch (MalformedURLException e) { - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - locs.addAll(Locations.of(uris)); - final JavaClasses classes = new ClassFileImporter() + final JavaClasses classes = new ClassFileImporterWithPackage() .importLocations(locs); return check(rule, classes); } diff --git a/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContextWithPackage.java b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContextWithPackage.java new file mode 100644 index 0000000..1ff5208 --- /dev/null +++ b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContextWithPackage.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.core.domain; + +import java.util.Arrays; + +/** + * logic from package dependency scanning + */ +public class DomainObjectCreationContextWithPackage { + + public static void completePackage(JavaClass javaClass, ImportContext importContext) { + JavaPackage javaPackage = JavaPackage.from(Arrays.asList( + javaClass, + importContext.resolveClass(javaClass.getPackageName() + ".package-info"))); + javaClass.setPackage(javaPackage); + } +} diff --git a/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporterWithPackage.java b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporterWithPackage.java new file mode 100644 index 0000000..cb1d9fe --- /dev/null +++ b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporterWithPackage.java @@ -0,0 +1,65 @@ +package com.tngtech.archunit.core.importer; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.thirdparty.com.google.common.collect.Iterables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toSet; + +/** + * copy of ClassFileImporter + * changed to use custom elements for package info resolution + */ +public class ClassFileImporterWithPackage { + private static final Logger LOG = LoggerFactory.getLogger(ClassFileImporterWithPackage.class); + private final ImportOptions importOptions; + + public ClassFileImporterWithPackage() { + this(new ImportOptions()); + } + + public ClassFileImporterWithPackage(Collection importOptions) { + this(new ImportOptions().with(importOptions)); + } + + private ClassFileImporterWithPackage(ImportOptions importOptions) { + this.importOptions = importOptions; + } + + public JavaClasses importLocations(Collection locations) { + List sources = new ArrayList<>(); + for (Location location : locations) { + tryAdd(sources, location); + } + return new ClassFileProcessorWithPackage().process(unify(sources)); + } + + public JavaClasses importPaths(String... paths) { + return importPaths(stream(paths).map(Paths::get).collect(toSet())); + } + + public JavaClasses importPaths(Collection paths) { + return importLocations(paths.stream().map(Location::of).collect(toSet())); + } + + private void tryAdd(List sources, Location location) { + try { + sources.add(location.asClassFileSource(importOptions)); + } catch (Exception e) { + LOG.warn(String.format("Couldn't derive %s from %s", + ClassFileSource.class.getSimpleName(), location), e); + } + } + + private ClassFileSource unify(List sources) { + return Iterables.concat(sources)::iterator; + } +} diff --git a/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessorWithPackage.java b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessorWithPackage.java new file mode 100644 index 0000000..600c68c --- /dev/null +++ b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessorWithPackage.java @@ -0,0 +1,340 @@ +package com.tngtech.archunit.core.importer; + +import com.tngtech.archunit.ArchConfiguration; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClassDescriptor; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.importer.resolvers.ClassResolver; +import com.tngtech.archunit.thirdparty.org.objectweb.asm.ClassReader; +import com.tngtech.archunit.thirdparty.org.objectweb.asm.Label; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; +import static java.util.stream.Collectors.toSet; + +/** + * copy of ClassFileProcessor + * with changes from package dependency scanning applied + */ +public class ClassFileProcessorWithPackage extends ClassFileProcessor { + private static final Logger LOG = LoggerFactory.getLogger(ClassFileProcessor.class); + private final boolean md5InClassSourcesEnabled = ArchConfiguration.get().md5InClassSourcesEnabled(); + private final ClassResolver.Factory classResolverFactory = new ClassResolver.Factory(); + + @Override + JavaClasses process(ClassFileSource source) { + ClassFileImportRecord importRecord = new ClassFileImportRecord(); + DependencyResolutionProcessWithPackage dependencyResolutionProcess = new DependencyResolutionProcessWithPackage(); + RecordAccessHandler accessHandler = new RecordAccessHandler(importRecord, dependencyResolutionProcess); + ClassDetailsRecorder classDetailsRecorder = new ClassDetailsRecorder(importRecord, dependencyResolutionProcess); + for (ClassFileLocation location : source) { + try (InputStream s = location.openStream()) { + JavaClassProcessor javaClassProcessor = + new JavaClassProcessor(new SourceDescriptor(location.getUri(), md5InClassSourcesEnabled), classDetailsRecorder, accessHandler); + new ClassReader(s).accept(javaClassProcessor, 0); + javaClassProcessor.createJavaClass().ifPresent(importRecord::add); + } catch (Exception e) { + LOG.warn(String.format("Couldn't import class from %s", location.getUri()), e); + } + } + return new ClassGraphCreatorWithPackage(importRecord, dependencyResolutionProcess, getClassResolver(classDetailsRecorder)).complete(); + } + private ClassResolver getClassResolver(ClassDetailsRecorder classDetailsRecorder) { + ClassResolver classResolver = classResolverFactory.create(); + classResolver.setClassUriImporter(new UriImporterOfProcessor(classDetailsRecorder, md5InClassSourcesEnabled)); + return classResolver; + } + + private static class UriImporterOfProcessor implements ClassResolver.ClassUriImporter { + private final DeclarationHandler declarationHandler; + private final boolean md5InClassSourcesEnabled; + + UriImporterOfProcessor(DeclarationHandler declarationHandler, boolean md5InClassSourcesEnabled) { + this.declarationHandler = declarationHandler; + this.md5InClassSourcesEnabled = md5InClassSourcesEnabled; + } + + @Override + public Optional tryImport(URI uri) { + try (InputStream inputStream = uri.toURL().openStream()) { + JavaClassProcessor classProcessor = new JavaClassProcessor(new SourceDescriptor(uri, md5InClassSourcesEnabled), declarationHandler); + new ClassReader(inputStream).accept(classProcessor, 0); + return classProcessor.createJavaClass(); + } catch (Exception e) { + LOG.warn(String.format("Error during import from %s, falling back to simple import", uri), e); + return Optional.empty(); + } + } + } + private static class ClassDetailsRecorder implements DeclarationHandler { + private final ClassFileImportRecord importRecord; + private final DependencyResolutionProcessWithPackage dependencyResolutionProcess; + private String ownerName; + + private ClassDetailsRecorder(ClassFileImportRecord importRecord, DependencyResolutionProcessWithPackage dependencyResolutionProcess) { + this.importRecord = importRecord; + this.dependencyResolutionProcess = dependencyResolutionProcess; + } + + @Override + public boolean isNew(String className) { + return !importRecord.getClasses().containsKey(className); + } + + @Override + public void onNewClass(String className, Optional superclassName, List interfaceNames) { + ownerName = className; + if (superclassName.isPresent()) { + importRecord.setSuperclass(ownerName, superclassName.get()); + dependencyResolutionProcess.registerSupertype(superclassName.get()); + } + importRecord.addInterfaces(ownerName, interfaceNames); + dependencyResolutionProcess.registerSupertypes(interfaceNames); + if(!className.endsWith(".package-info")) { + if(className.contains(".")) { + String packageName = className.substring(0, className.lastIndexOf(".")); + dependencyResolutionProcess.registerPackageInfo(packageName + ".package-info"); + }else { + dependencyResolutionProcess.registerPackageInfo("package-info"); + } + } + } + + @Override + public void onDeclaredTypeParameters(DomainBuilders.JavaClassTypeParametersBuilder typeParametersBuilder) { + importRecord.addTypeParameters(ownerName, typeParametersBuilder); + } + + @Override + public void onGenericSuperclass(DomainBuilders.JavaParameterizedTypeBuilder genericSuperclassBuilder) { + importRecord.addGenericSuperclass(ownerName, genericSuperclassBuilder); + } + + @Override + public void onGenericInterfaces(List> genericInterfaceBuilders) { + importRecord.addGenericInterfaces(ownerName, genericInterfaceBuilders); + } + + @Override + public void onDeclaredField(DomainBuilders.JavaFieldBuilder fieldBuilder, String fieldTypeName) { + importRecord.addField(ownerName, fieldBuilder); + dependencyResolutionProcess.registerMemberType(fieldTypeName); + } + + @Override + public void onDeclaredConstructor(DomainBuilders.JavaConstructorBuilder constructorBuilder, Collection rawParameterTypeNames) { + importRecord.addConstructor(ownerName, constructorBuilder); + dependencyResolutionProcess.registerMemberTypes(rawParameterTypeNames); + } + + @Override + public void onDeclaredMethod(DomainBuilders.JavaMethodBuilder methodBuilder, Collection rawParameterTypeNames, String rawReturnTypeName) { + importRecord.addMethod(ownerName, methodBuilder); + dependencyResolutionProcess.registerMemberTypes(rawParameterTypeNames); + dependencyResolutionProcess.registerMemberType(rawReturnTypeName); + } + + @Override + public void onDeclaredStaticInitializer(DomainBuilders.JavaStaticInitializerBuilder staticInitializerBuilder) { + importRecord.setStaticInitializer(ownerName, staticInitializerBuilder); + } + + @Override + public void onDeclaredClassAnnotations(Set annotationBuilders) { + importRecord.addClassAnnotations(ownerName, annotationBuilders); + registerAnnotationTypesToResolve(annotationBuilders); + } + + @Override + public void onDeclaredMemberAnnotations(String memberName, String descriptor, Set annotationBuilders) { + importRecord.addMemberAnnotations(ownerName, memberName, descriptor, annotationBuilders); + registerAnnotationTypesToResolve(annotationBuilders); + } + + private void registerAnnotationTypesToResolve(Set annotationBuilders) { + for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : annotationBuilders) { + dependencyResolutionProcess.registerAnnotationType(annotationBuilder.getFullyQualifiedClassName()); + } + } + + @Override + public void onDeclaredAnnotationValueType(String valueTypeName) { + dependencyResolutionProcess.registerAnnotationType(valueTypeName); + } + + @Override + public void onDeclaredAnnotationDefaultValue(String methodName, String methodDescriptor, DomainBuilders.JavaAnnotationBuilder.ValueBuilder valueBuilder) { + importRecord.addAnnotationDefaultValue(ownerName, methodName, methodDescriptor, valueBuilder); + } + + @Override + public void registerEnclosingClass(String ownerName, String enclosingClassName) { + importRecord.setEnclosingClass(ownerName, enclosingClassName); + dependencyResolutionProcess.registerEnclosingType(enclosingClassName); + } + + @Override + public void registerEnclosingCodeUnit(String ownerName, RawAccessRecord.CodeUnit enclosingCodeUnit) { + importRecord.setEnclosingCodeUnit(ownerName, enclosingCodeUnit); + } + + @Override + public void onDeclaredClassObject(String typeName) { + dependencyResolutionProcess.registerAccessToType(typeName); + } + + @Override + public void onDeclaredInstanceofCheck(String typeName) { + dependencyResolutionProcess.registerAccessToType(typeName); + } + + @Override + public void onDeclaredThrowsClause(Collection exceptionTypeNames) { + dependencyResolutionProcess.registerMemberTypes(exceptionTypeNames); + } + + @Override + public void onDeclaredGenericSignatureType(String typeName) { + dependencyResolutionProcess.registerGenericSignatureType(typeName); + } + } + + + private static class RecordAccessHandler implements JavaClassProcessor.AccessHandler, TryCatchRecorder.TryCatchBlocksFinishedListener { + private static final Logger LOG = LoggerFactory.getLogger(RecordAccessHandler.class); + + private final ClassFileImportRecord importRecord; + private final DependencyResolutionProcessWithPackage dependencyResolutionProcess; + private RawAccessRecord.CodeUnit codeUnit; + private int lineNumber; + private final TryCatchRecorder tryCatchRecorder = new TryCatchRecorder(this); + + private RecordAccessHandler(ClassFileImportRecord importRecord, DependencyResolutionProcessWithPackage dependencyResolutionProcess) { + this.importRecord = importRecord; + this.dependencyResolutionProcess = dependencyResolutionProcess; + } + + @Override + public void setContext(RawAccessRecord.CodeUnit codeUnit) { + this.codeUnit = codeUnit; + } + + @Override + public void onLineNumber(int lineNumber, Label label) { + this.lineNumber = lineNumber; + tryCatchRecorder.onEncounteredLabel(label, lineNumber); + } + + @Override + public void onLabel(Label label) { + tryCatchRecorder.onEncounteredLabel(label); + } + + @Override + public void handleFieldInstruction(int opcode, String owner, String name, String desc) { + JavaFieldAccess.AccessType accessType = JavaFieldAccess.AccessType.forOpCode(opcode); + LOG.trace("Found {} access to field {}.{}:{} in line {}", accessType, owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + RawAccessRecord.ForField accessRecord = filled(new RawAccessRecord.ForField.Builder(), target) + .withAccessType(accessType) + .build(); + importRecord.registerFieldAccess(accessRecord); + tryCatchRecorder.registerAccess(accessRecord); + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleMethodInstruction(String owner, String name, String desc) { + LOG.trace("Found call of method {}.{}:{} in line {}", owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + RawAccessRecord accessRecord = filled(new RawAccessRecord.Builder(), target).build(); + if (CONSTRUCTOR_NAME.equals(name)) { + importRecord.registerConstructorCall(accessRecord); + } else { + importRecord.registerMethodCall(accessRecord); + } + tryCatchRecorder.registerAccess(accessRecord); + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleMethodReferenceInstruction(String owner, String name, String desc) { + LOG.trace("Found method reference {}.{}:{} in line {}", owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + RawAccessRecord accessRecord = filled(new RawAccessRecord.Builder(), target).build(); + if (CONSTRUCTOR_NAME.equals(name)) { + importRecord.registerConstructorReference(accessRecord); + } else { + importRecord.registerMethodReference(accessRecord); + } + tryCatchRecorder.registerAccess(accessRecord); + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleLambdaInstruction(String owner, String name, String desc) { + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + importRecord.registerLambdaInvocation(filled(new RawAccessRecord.Builder(), target).build()); + } + + @Override + public void handleReferencedClassObject(JavaClassDescriptor type, int lineNumber) { + importRecord.registerReferencedClassObject(new RawReferencedClassObject.Builder() + .withOrigin(codeUnit) + .withTarget(type) + .withLineNumber(lineNumber) + .withDeclaredInLambda(false) + .build()); + } + + @Override + public void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber) { + importRecord.registerInstanceofCheck(new RawInstanceofCheck.Builder() + .withOrigin(codeUnit) + .withTarget(instanceOfCheckType) + .withLineNumber(lineNumber) + .withDeclaredInLambda(false) + .build()); + } + + @Override + public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) { + LOG.trace("Found try/catch block between {} and {} for throwable {}", start, end, throwableType); + tryCatchRecorder.registerTryCatchBlock(start, end, handler, throwableType); + } + + @Override + public void handleTryFinallyBlock(Label start, Label end, Label handler) { + LOG.trace("Found try/finally block between {} and {}", start, end); + tryCatchRecorder.registerTryFinallyBlock(start, end, handler); + } + + @Override + public void onMethodEnd() { + tryCatchRecorder.onEncounteredMethodEnd(); + } + + @Override + public void onTryCatchBlocksFinished(Set tryCatchBlocks) { + tryCatchBlocks.forEach(it -> it.withDeclaringCodeUnit(codeUnit)); + importRecord.addTryCatchBlocks(tryCatchBlocks.stream().map(RawTryCatchBlock.Builder::build).collect(toSet())); + } + + private > BUILDER filled(BUILDER builder, + RawAccessRecord.TargetInfo target) { + return builder + .withOrigin(codeUnit) + .withTarget(target) + .withLineNumber(lineNumber); + } + } +} diff --git a/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreatorWithPackage.java b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreatorWithPackage.java new file mode 100644 index 0000000..c434d16 --- /dev/null +++ b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreatorWithPackage.java @@ -0,0 +1,380 @@ +package com.tngtech.archunit.core.importer; + +import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.*; +import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; +import com.tngtech.archunit.core.domain.AccessTarget.ConstructorReferenceTarget; +import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; +import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; +import com.tngtech.archunit.core.importer.AccessRecord.FieldAccessRecord; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorReferenceBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaFieldAccessBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodReferenceBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.TryCatchBlockBuilder; +import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; +import com.tngtech.archunit.core.importer.resolvers.ClassResolver; +import com.tngtech.archunit.thirdparty.com.google.common.collect.HashMultimap; +import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableList; +import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.thirdparty.com.google.common.collect.Multimap; +import com.tngtech.archunit.thirdparty.com.google.common.collect.SetMultimap; +import com.tngtech.archunit.thirdparty.com.google.common.collect.Sets; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeAnnotations; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeClassHierarchy; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeEnclosingDeclaration; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeGenericInterfaces; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeGenericSuperclass; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeMembers; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeParameters; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClasses; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContextWithPackage.completePackage; +import static com.tngtech.archunit.core.importer.DomainBuilders.BuilderWithBuildParameter.BuildFinisher.build; +import static com.tngtech.archunit.core.importer.DomainBuilders.buildAnnotations; +import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethodName; +import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isSyntheticAccessMethodName; +import static com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableSet.toImmutableSet; + +/** + * Copy of ClassGraphCreator + * with changes from package dependency scanning applied + */ +public class ClassGraphCreatorWithPackage implements ImportContext { + private final ImportedClasses classes; + + private final ClassFileImportRecord importRecord; + private final DependencyResolutionProcessWithPackage dependencyResolutionProcess; + + private final SetMultimap processedFieldAccessRecords = HashMultimap.create(); + private final SetMultimap> processedMethodCallRecords = HashMultimap.create(); + private final SetMultimap> processedConstructorCallRecords = HashMultimap.create(); + private final SetMultimap> processedMethodReferenceRecords = HashMultimap.create(); + private final SetMultimap> processedConstructorReferenceRecords = HashMultimap.create(); + private final SetMultimap processedReferencedClassObjects = HashMultimap.create(); + private final SetMultimap processedInstanceofChecks = HashMultimap.create(); + private final SetMultimap processedTryCatchBlocks = HashMultimap.create(); + + ClassGraphCreatorWithPackage(ClassFileImportRecord importRecord, + DependencyResolutionProcessWithPackage dependencyResolutionProcess, + ClassResolver classResolver) { + this.importRecord = importRecord; + this.dependencyResolutionProcess = dependencyResolutionProcess; + classes = new ImportedClasses(importRecord.getClasses(), classResolver, this::getMethodReturnType); + } + + JavaClasses complete() { + dependencyResolutionProcess.resolve(classes); + completeClasses(); + completeCodeUnitDependencies(); + return createJavaClasses(classes.getDirectlyImported(), classes.getAllWithOuterClassesSortedBeforeInnerClasses(), this); + } + + private void completeClasses() { + for (JavaClass javaClass : classes.getAllWithOuterClassesSortedBeforeInnerClasses()) { + completeClassHierarchy(javaClass, this); + completeEnclosingDeclaration(javaClass, this); + completeTypeParameters(javaClass, this); + completeGenericSuperclass(javaClass, this); + completeGenericInterfaces(javaClass, this); + completeMembers(javaClass, this); + completeAnnotations(javaClass, this); + completePackage(javaClass, this); + } + } + + private void completeCodeUnitDependencies() { + importRecord.forEachRawFieldAccessRecord(record -> + tryProcess(record, AccessRecord.Factory.forFieldAccessRecord(), processedFieldAccessRecords)); + importRecord.forEachRawMethodCallRecord(record -> + tryProcess(record, AccessRecord.Factory.forMethodCallRecord(), processedMethodCallRecords)); + importRecord.forEachRawConstructorCallRecord(record -> + tryProcess(record, AccessRecord.Factory.forConstructorCallRecord(), processedConstructorCallRecords)); + importRecord.forEachRawMethodReferenceRecord(record -> + tryProcess(record, AccessRecord.Factory.forMethodReferenceRecord(), processedMethodReferenceRecords)); + importRecord.forEachRawConstructorReferenceRecord(record -> + tryProcess(record, AccessRecord.Factory.forConstructorReferenceRecord(), processedConstructorReferenceRecords)); + importRecord.forEachRawReferencedClassObject(this::processReferencedClassObject); + importRecord.forEachRawInstanceofCheck(this::processInstanceofCheck); + importRecord.forEachRawTryCatchBlock(this::processTryCatchBlock); + } + + private , B extends RawAccessRecord> void tryProcess( + B rawRecord, + AccessRecord.Factory factory, + Multimap processedAccessRecords) { + + T processed = factory.create(rawRecord, classes); + processedAccessRecords.put(processed.getOrigin(), processed); + } + + private void processReferencedClassObject(RawReferencedClassObject rawReferencedClassObject) { + JavaCodeUnit origin = rawReferencedClassObject.getOrigin().resolveFrom(classes); + ReferencedClassObject referencedClassObject = createReferencedClassObject( + origin, + classes.getOrResolve(rawReferencedClassObject.getClassName()), + rawReferencedClassObject.getLineNumber(), + rawReferencedClassObject.isDeclaredInLambda() + ); + processedReferencedClassObjects.put(origin, referencedClassObject); + } + + private void processInstanceofCheck(RawInstanceofCheck rawInstanceofCheck) { + JavaCodeUnit origin = rawInstanceofCheck.getOrigin().resolveFrom(classes); + InstanceofCheck instanceofCheck = createInstanceofCheck( + origin, + classes.getOrResolve(rawInstanceofCheck.getTarget().getFullyQualifiedClassName()), + rawInstanceofCheck.getLineNumber(), + rawInstanceofCheck.isDeclaredInLambda() + ); + processedInstanceofChecks.put(origin, instanceofCheck); + } + + private void processTryCatchBlock(RawTryCatchBlock rawTryCatchBlock) { + JavaCodeUnit declaringCodeUnit = rawTryCatchBlock.getDeclaringCodeUnit().resolveFrom(classes); + TryCatchBlockBuilder tryCatchBlockBuilder = new TryCatchBlockBuilder() + .withCaughtThrowables( + rawTryCatchBlock.getCaughtThrowables().stream() + .map(it -> classes.getOrResolve(it.getFullyQualifiedClassName())) + .collect(toImmutableSet()) + ) + .withLineNumber(rawTryCatchBlock.getLineNumber()) + .withRawAccessesContainedInTryBlock(rawTryCatchBlock.getAccessesInTryBlock()) + .withDeclaredInLambda(rawTryCatchBlock.isDeclaredInLambda()); + processedTryCatchBlocks.put(declaringCodeUnit, tryCatchBlockBuilder); + } + + @Override + public Set createFieldAccessesFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (FieldAccessRecord record : processedFieldAccessRecords.get(codeUnit)) { + JavaFieldAccess access = accessBuilderFrom(new JavaFieldAccessBuilder(), record) + .withAccessType(record.getAccessType()) + .build(); + result.add(access); + handlePossibleTryBlockAccess(tryCatchBlockBuilders, record, access); + } + return result.build(); + } + + @Override + public Set createMethodCallsFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (AccessRecord record : processedMethodCallRecords.get(codeUnit)) { + JavaMethodCall call = accessBuilderFrom(new JavaMethodCallBuilder(), record).build(); + result.add(call); + handlePossibleTryBlockAccess(tryCatchBlockBuilders, record, call); + } + return result.build(); + } + + @Override + public Set createConstructorCallsFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (AccessRecord record : processedConstructorCallRecords.get(codeUnit)) { + JavaConstructorCall call = accessBuilderFrom(new JavaConstructorCallBuilder(), record).build(); + result.add(call); + handlePossibleTryBlockAccess(tryCatchBlockBuilders, record, call); + } + return result.build(); + } + + @Override + public Set createMethodReferencesFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (AccessRecord record : processedMethodReferenceRecords.get(codeUnit)) { + JavaMethodReference methodReference = accessBuilderFrom(new JavaMethodReferenceBuilder(), record).build(); + result.add(methodReference); + handlePossibleTryBlockAccess(tryCatchBlockBuilders, record, methodReference); + } + return result.build(); + } + + @Override + public Set createConstructorReferencesFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (AccessRecord record : processedConstructorReferenceRecords.get(codeUnit)) { + JavaConstructorReference constructorReference = accessBuilderFrom(new JavaConstructorReferenceBuilder(), record).build(); + result.add(constructorReference); + handlePossibleTryBlockAccess(tryCatchBlockBuilders, record, constructorReference); + } + return result.build(); + } + + private void handlePossibleTryBlockAccess(Set tryCatchBlockBuilders, AccessRecord record, JavaAccess access) { + tryCatchBlockBuilders.forEach(builder -> builder.addIfContainedInTryBlock(record.getRaw(), access)); + } + + private > + B accessBuilderFrom(B builder, AccessRecord record) { + return builder + .withOrigin(record.getOrigin()) + .withTarget(record.getTarget()) + .withLineNumber(record.getLineNumber()) + .withDeclaredInLambda(record.isDeclaredInLambda()); + } + + @Override + public Optional createSuperclass(JavaClass owner) { + Optional superclassName = importRecord.getSuperclassFor(owner.getName()); + return superclassName.map(classes::getOrResolve); + } + + @Override + public Optional createGenericSuperclass(JavaClass owner) { + Optional> genericSuperclassBuilder = importRecord.getGenericSuperclassFor(owner); + return genericSuperclassBuilder.map(javaClassJavaParameterizedTypeBuilder -> + javaClassJavaParameterizedTypeBuilder.build(owner, getTypeParametersInContextOf(owner), classes)); + } + + @Override + public Optional> createGenericInterfaces(JavaClass owner) { + Optional>> genericInterfaceBuilders = importRecord.getGenericInterfacesFor(owner); + if (!genericInterfaceBuilders.isPresent()) { + return Optional.empty(); + } + + ImmutableList.Builder result = ImmutableList.builder(); + for (JavaParameterizedTypeBuilder builder : genericInterfaceBuilders.get()) { + result.add(builder.build(owner, getTypeParametersInContextOf(owner), classes)); + } + return Optional.of(result.build()); + } + + private static Iterable> getTypeParametersInContextOf(JavaClass javaClass) { + Set> result = Sets.newHashSet(javaClass.getTypeParameters()); + while (javaClass.getEnclosingClass().isPresent()) { + javaClass = javaClass.getEnclosingClass().get(); + result.addAll(javaClass.getTypeParameters()); + } + return result; + } + + @Override + public List createInterfaces(JavaClass owner) { + ImmutableList.Builder result = ImmutableList.builder(); + for (String interfaceName : importRecord.getInterfaceNamesFor(owner.getName())) { + result.add(classes.getOrResolve(interfaceName)); + } + return result.build(); + } + + @Override + public List> createTypeParameters(JavaClass owner) { + JavaClassTypeParametersBuilder typeParametersBuilder = importRecord.getTypeParameterBuildersFor(owner.getName()); + return typeParametersBuilder.build(owner, classes); + } + + @Override + public Set createFields(JavaClass owner) { + return build(importRecord.getFieldBuildersFor(owner.getName()), owner, classes); + } + + @Override + public Set createMethods(JavaClass owner) { + Stream methodBuilders = getNonSyntheticMethodBuildersFor(owner); + if (owner.isAnnotation()) { + methodBuilders = methodBuilders.map(methodBuilder -> methodBuilder + .withAnnotationDefaultValue(method -> + importRecord.getAnnotationDefaultValueBuilderFor(method) + .flatMap(builder -> builder.build(method, classes)) + )); + } + return build(methodBuilders, owner, classes); + } + + private Stream getNonSyntheticMethodBuildersFor(JavaClass owner) { + return importRecord.getMethodBuildersFor(owner.getName()).stream() + .filter(methodBuilder -> + !isLambdaMethodName(methodBuilder.getName()) + && !isSyntheticAccessMethodName(methodBuilder.getName())); + } + + @Override + public Set createConstructors(JavaClass owner) { + return build(importRecord.getConstructorBuildersFor(owner.getName()), owner, classes); + } + + @Override + public Optional createStaticInitializer(JavaClass owner) { + Optional builder = importRecord.getStaticInitializerBuilderFor(owner.getName()); + if (!builder.isPresent()) { + return Optional.empty(); + } + JavaStaticInitializer staticInitializer = builder.get().build(owner, classes); + return Optional.of(staticInitializer); + } + + @Override + public Map> createAnnotations(JavaClass owner) { + return createAnnotations(owner, importRecord.getAnnotationsFor(owner)); + } + + @Override + public Map> createAnnotations(JavaMember owner) { + return createAnnotations(owner, importRecord.getAnnotationsFor(owner)); + } + + private Map> createAnnotations(OWNER owner, Set annotationBuilders) { + return buildAnnotations(owner, annotationBuilders, classes); + } + + @Override + public Optional createEnclosingClass(JavaClass owner) { + Optional enclosingClassName = importRecord.getEnclosingClassFor(owner.getName()); + return enclosingClassName.map(classes::getOrResolve); + } + + @Override + public Optional createEnclosingCodeUnit(JavaClass owner) { + Optional enclosingCodeUnit = importRecord.getEnclosingCodeUnitFor(owner.getName()); + if (!enclosingCodeUnit.isPresent()) { + return Optional.empty(); + } + + CodeUnit codeUnit = enclosingCodeUnit.get(); + JavaClass enclosingClass = classes.getOrResolve(codeUnit.getDeclaringClassName()); + return enclosingClass.tryGetCodeUnitWithParameterTypeNames(codeUnit.getName(), codeUnit.getRawParameterTypeNames()); + } + + @Override + public Set createTryCatchBlockBuilders(JavaCodeUnit codeUnit) { + return processedTryCatchBlocks.get(codeUnit); + } + + @Override + public Set createReferencedClassObjectsFor(JavaCodeUnit codeUnit) { + return ImmutableSet.copyOf(processedReferencedClassObjects.get(codeUnit)); + } + + @Override + public Set createInstanceofChecksFor(JavaCodeUnit codeUnit) { + return ImmutableSet.copyOf(processedInstanceofChecks.get(codeUnit)); + } + + @Override + public JavaClass resolveClass(String fullyQualifiedClassName) { + return classes.getOrResolve(fullyQualifiedClassName); + } + + private Optional getMethodReturnType(String declaringClassName, String methodName) { + for (JavaMethodBuilder methodBuilder : importRecord.getMethodBuildersFor(declaringClassName)) { + if (methodBuilder.getName().equals(methodName) && methodBuilder.hasNoParameters()) { + return Optional.of(classes.getOrResolve(methodBuilder.getReturnTypeName())); + } + } + return Optional.empty(); + } +} diff --git a/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcessWithPackage.java b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcessWithPackage.java new file mode 100644 index 0000000..4ec055d --- /dev/null +++ b/nebula-archrules-core/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcessWithPackage.java @@ -0,0 +1,157 @@ +package com.tngtech.archunit.core.importer; + +import com.tngtech.archunit.ArchConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import static com.tngtech.archunit.core.importer.ImportedClasses.ImportedClassState.HAD_TO_BE_IMPORTED; +import static java.lang.System.lineSeparator; + +/** + * Copy of DependencyResolutionProcess + * with changes from package dependency scanning applied + */ +public class DependencyResolutionProcessWithPackage { + private static final Logger log = LoggerFactory.getLogger(DependencyResolutionProcessWithPackage.class); + + static final String DEPENDENCY_RESOLUTION_PROCESS_PROPERTY_PREFIX = "import.dependencyResolutionProcess"; + private final Properties resolutionProcessProperties = ArchConfiguration.get().getSubProperties(DEPENDENCY_RESOLUTION_PROCESS_PROPERTY_PREFIX); + + static final String MAX_ITERATIONS_FOR_MEMBER_TYPES_PROPERTY_NAME = "maxIterationsForMemberTypes"; + static final int MAX_ITERATIONS_FOR_MEMBER_TYPES_DEFAULT_VALUE = 1; + private final int maxRunsForMemberTypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_MEMBER_TYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_MEMBER_TYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME = "maxIterationsForAccessesToTypes"; + static final int MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_DEFAULT_VALUE = 1; + private final int maxRunsForAccessesToTypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_SUPERTYPES_PROPERTY_NAME = "maxIterationsForSupertypes"; + static final int MAX_ITERATIONS_FOR_SUPERTYPES_DEFAULT_VALUE = -1; + private final int maxRunsForSupertypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_SUPERTYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_SUPERTYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_ENCLOSING_TYPES_PROPERTY_NAME = "maxIterationsForEnclosingTypes"; + static final int MAX_ITERATIONS_FOR_ENCLOSING_TYPES_DEFAULT_VALUE = -1; + private final int maxRunsForEnclosingTypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_ENCLOSING_TYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_ENCLOSING_TYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_ANNOTATION_TYPES_PROPERTY_NAME = "maxIterationsForAnnotationTypes"; + static final int MAX_ITERATIONS_FOR_ANNOTATION_TYPES_DEFAULT_VALUE = -1; + private final int maxRunsForAnnotationTypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_ANNOTATION_TYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_ANNOTATION_TYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_GENERIC_SIGNATURE_TYPES_PROPERTY_NAME = "maxIterationsForGenericSignatureTypes"; + static final int MAX_ITERATIONS_FOR_GENERIC_SIGNATURE_TYPES_DEFAULT_VALUE = -1; + private final int maxRunsForGenericSignatureTypes = getConfiguredIterations( + MAX_ITERATIONS_FOR_GENERIC_SIGNATURE_TYPES_PROPERTY_NAME, MAX_ITERATIONS_FOR_GENERIC_SIGNATURE_TYPES_DEFAULT_VALUE); + + static final String MAX_ITERATIONS_FOR_PACKAGE_INFO_PROPERTY_NAME = "maxIterationsForPackageInfo"; + static final int MAX_ITERATIONS_FOR_PACKAGE_INFO_DEFAULT_VALUE = -1; + private final int maxRunsForPackageInfo = getConfiguredIterations( + MAX_ITERATIONS_FOR_PACKAGE_INFO_PROPERTY_NAME, MAX_ITERATIONS_FOR_PACKAGE_INFO_DEFAULT_VALUE); + + private Set currentTypeNames = new HashSet<>(); + private int runNumber = 1; + private boolean shouldContinue; + + void registerMemberType(String typeName) { + if (runNumberHasNotExceeded(maxRunsForMemberTypes)) { + currentTypeNames.add(typeName); + } + } + + void registerMemberTypes(Collection typeNames) { + for (String typeName : typeNames) { + registerMemberType(typeName); + } + } + + void registerAccessToType(String typeName) { + if (runNumberHasNotExceeded(maxRunsForAccessesToTypes)) { + currentTypeNames.add(typeName); + } + } + + void registerSupertype(String typeName) { + if (runNumberHasNotExceeded(maxRunsForSupertypes)) { + currentTypeNames.add(typeName); + } + } + + void registerSupertypes(Collection typeNames) { + for (String typeName : typeNames) { + registerSupertype(typeName); + } + } + + void registerEnclosingType(String typeName) { + if (runNumberHasNotExceeded(maxRunsForEnclosingTypes)) { + currentTypeNames.add(typeName); + } + } + + void registerAnnotationType(String typeName) { + if (runNumberHasNotExceeded(maxRunsForAnnotationTypes)) { + currentTypeNames.add(typeName); + } + } + + void registerGenericSignatureType(String typeName) { + if (runNumberHasNotExceeded(maxRunsForGenericSignatureTypes)) { + currentTypeNames.add(typeName); + } + } + + void registerPackageInfo(String typeName) { + if (runNumberHasNotExceeded(maxRunsForPackageInfo)) { + currentTypeNames.add(typeName); + } + } + + void resolve(ImportedClasses classes) { + logConfiguration(); + do { + executeRun(classes); + } while (shouldContinue); + } + + private void logConfiguration() { + log.trace("Automatically resolving transitive class dependencies with the following configuration:{}{}{}{}{}{}", + formatConfigProperty(MAX_ITERATIONS_FOR_MEMBER_TYPES_PROPERTY_NAME, maxRunsForMemberTypes), + formatConfigProperty(MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME, maxRunsForAccessesToTypes), + formatConfigProperty(MAX_ITERATIONS_FOR_SUPERTYPES_PROPERTY_NAME, maxRunsForSupertypes), + formatConfigProperty(MAX_ITERATIONS_FOR_ENCLOSING_TYPES_PROPERTY_NAME, maxRunsForEnclosingTypes), + formatConfigProperty(MAX_ITERATIONS_FOR_ANNOTATION_TYPES_PROPERTY_NAME, maxRunsForAnnotationTypes), + formatConfigProperty(MAX_ITERATIONS_FOR_GENERIC_SIGNATURE_TYPES_PROPERTY_NAME, maxRunsForGenericSignatureTypes)); + } + + private String formatConfigProperty(String propertyName, int number) { + return lineSeparator() + DEPENDENCY_RESOLUTION_PROCESS_PROPERTY_PREFIX + "." + propertyName + " = " + number; + } + + private void executeRun(ImportedClasses classes) { + runNumber++; + Set typeNamesToResolve = this.currentTypeNames; + currentTypeNames = new HashSet<>(); + shouldContinue = false; + for (String typeName : typeNamesToResolve) { + ImportedClasses.ImportedClassState classState = classes.ensurePresent(typeName); + shouldContinue = shouldContinue || (classState == HAD_TO_BE_IMPORTED); + } + } + + private boolean runNumberHasNotExceeded(int maxRuns) { + return maxRuns < 0 || runNumber <= maxRuns; + } + + private int getConfiguredIterations(String propertyName, int defaultValue) { + return Integer.parseInt(resolutionProcessProperties.getProperty(propertyName, String.valueOf(defaultValue))); + } +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java index 8c760c1..a6b3a79 100644 --- a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java @@ -3,6 +3,7 @@ import com.netflix.nebula.archrules.core.ArchRulesService; import com.netflix.nebula.archrules.core.Runner; import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ClassFileImporterWithPackage; import com.tngtech.archunit.lang.Priority; import org.gradle.workers.WorkAction; import org.jspecify.annotations.NullMarked; @@ -69,7 +70,7 @@ public void execute() { .map(it -> it.type().getCanonicalName()) .collect(Collectors.joining(","))); } - final var classesToCheck = new ClassFileImporter() + final var classesToCheck = new ClassFileImporterWithPackage() .importPaths(getParameters().getClassesToCheck().getFiles().stream().map(File::toPath).toList()); final List violationList = new ArrayList<>(); diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt index 9408067..3d03cba 100644 --- a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt @@ -7,11 +7,10 @@ import org.gradle.api.Project import org.gradle.api.artifacts.type.ArtifactTypeDefinition import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category +import org.gradle.api.attributes.LibraryElements import org.gradle.api.attributes.Usage import org.gradle.api.attributes.VerificationType -import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.plugins.internal.JavaConfigurationVariantMapping import org.gradle.api.tasks.SourceSet import org.gradle.internal.extensions.stdlib.capitalized import org.gradle.kotlin.dsl.add @@ -108,6 +107,7 @@ class ArchrulesRunnerPlugin : Plugin { attributes { attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES)) attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(ARCH_RULES)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements.CLASSES)) } } diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt index 07bfcae..bf78bc8 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt @@ -1,8 +1,18 @@ package com.netflix.nebula.archrules.gradle import com.tngtech.archunit.lang.Priority -import nebula.test.dsl.* import nebula.test.dsl.TestKitAssertions.assertThat +import nebula.test.dsl.TestProjectBuilder +import nebula.test.dsl.main +import nebula.test.dsl.plugins +import nebula.test.dsl.properties +import nebula.test.dsl.repositories +import nebula.test.dsl.rootProject +import nebula.test.dsl.run +import nebula.test.dsl.settings +import nebula.test.dsl.src +import nebula.test.dsl.test +import nebula.test.dsl.testProject import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.named import org.gradle.testfixtures.ProjectBuilder @@ -177,12 +187,14 @@ class ArchrulesRunnerPluginTest { forwardOutput() } - containsInOrder(result.output, + containsInOrder( + result.output, "Variant archRulesReportElements", "- org.gradle.category = verification", "- org.gradle.verificationtype = arch-rules", "- build/reports/archrules/main.data (artifactType = binary)", - "- build/reports/archrules/test.data (artifactType = binary)") + "- build/reports/archrules/test.data (artifactType = binary)" + ) } @Test diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ExternalPackageInfoTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ExternalPackageInfoTest.kt new file mode 100644 index 0000000..381905a --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ExternalPackageInfoTest.kt @@ -0,0 +1,161 @@ +package com.netflix.nebula.archrules.gradle + +import nebula.test.dsl.TestKitAssertions.assertThat +import nebula.test.dsl.main +import nebula.test.dsl.plugins +import nebula.test.dsl.properties +import nebula.test.dsl.repositories +import nebula.test.dsl.sourceSet +import nebula.test.dsl.src +import nebula.test.dsl.subProject +import nebula.test.dsl.testProject +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +/** + * This test reproduces the issue fixed by https://github.com/TNG/ArchUnit/pull/1565/ + */ +class ExternalPackageInfoTest { + @TempDir + lateinit var projectDir: File + + @Test + fun `package rules work for external packages`() { + val runner = testProject(projectDir) { + properties { + buildCache(true) + property("org.gradle.configuration-cache", "true") + } + subProject("app") { + plugins { + id("java") + id("com.netflix.nebula.archrules.runner") + } + repositories { + mavenCentral() + } + dependencies( + """implementation(project(":lib"))""" + ) + javaToolchain(17) + src { + main { + java( + "Usage.java", + // language=java + """ +import test.library.LibraryClass; +class Usage { + LibraryClass libraryClass; +} + """ + ) + } + } + } + subProject("lib") { + plugins { + id("java-library") + } + repositories { + mavenCentral() + } + dependencies( + """implementation(project(":annotation"))""" + ) + javaToolchain(17) + src { + main { + java( + "test/library/LibraryClass.java", """ +package test.library; +public class LibraryClass { +} +""" + ) + java( + "test/library/package-info.java", """ +@PackageAnnotation +package test.library; +import ann.PackageAnnotation; +""" + ) + } + } + } + subProject("annotation") { + plugins { + id("java-library") + id("com.netflix.nebula.archrules.library") + } + repositories { + mavenCentral() + } + javaToolchain(17) + dependencies("""archRulesImplementation("com.netflix.nebula:archrules-common:0.+")""") + src { + main { + java( + "ann/PackageAnnotation.java", + // language=java + """ +package ann; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PACKAGE) +public @interface PackageAnnotation { +} +""" + ) + } + sourceSet("archRules") { + java( + "PackageAnnotationRule.java", + // language=java + """ +import java.util.Collections; +import java.util.Map; +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.Priority; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; +import static com.netflix.nebula.archrules.common.JavaClass.Predicates.resideInAPackageThat; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; + +public class PackageAnnotationRule implements ArchRulesService { + public static final ArchRule RULE = ArchRuleDefinition.priority(Priority.HIGH) + .noClasses() + .should() + .dependOnClassesThat(resideInAPackageThat(is(annotatedWith("ann.PackageAnnotation")))) + .allowEmptyShould(true); + @Override + public Map getRules() { + return Collections.singletonMap("rule", RULE); + } +} +""" + ) + } + } + } + } + + val result = runner.run("check") + + assertThat(result.task(":app:archRulesConsoleReport")) + .`as`("archRules console report runs") + .hasOutcome(TaskOutcome.SUCCESS) + assertThat(result.output) + .doesNotContain("(No failures)") + .contains("rule HIGH (1 failures)") + } +}