Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public class LibraryArchRules implements ArchRulesService {
}
```

You will also need to create a file at `src/archRules/resources/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService` and add the fully qualified name of your rules class to that file.

When authoring rules about the usage of your own library code, it is recommended to colocate your rules library in the
same project as the library code. The ArchRules plugin will publish the rules in a separate Jar, and the Runner plugin
will select that jar for running rules, but these rule classes will not end up in the runtime classpath.
Expand Down Expand Up @@ -106,6 +108,32 @@ public class LibraryArchRulesTest {
}
```

## Running Rules

In order to run rules in a project, add the runner plugin:
```kotlin
plugins {
id("com.netflix.nebula.archrules.runner") version ("latest.release")
}
```

This will create a task for running rules against each source set, eg. `checkArchRulesMain` for the Main source set.
These tasks will run as dependencies of the `check` task.

If you want to run rules on all source sets, add the rule library as a dependency to the `archRules` configuration:
```kotlin
dependencies {
archRules("your:rules:1.0.0")
}
```

Rules that exist in a library on each sourceSet's classpath will also be used:
```kotlin
dependencies {
implementation("some.library:which-also-has-rules:1.0.0")
}
```

## How it works

The Archrules Library plugin produces a separate Jar for the `archRules` sourceset, which is exposed as an alternate variant of the library.
Expand Down
2 changes: 1 addition & 1 deletion nebula-archrules-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ testing {
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(8)
}
}
dependencyLocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public ConditionEvent invert() {

@Override
public List<String> getDescriptionLines() {
return List.of(NO_MATCH_MESSAGE);
return Collections.singletonList(NO_MATCH_MESSAGE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static EvaluationResult check(ArchRule rule, JavaClasses classesToCheck)
return rule.evaluate(classesToCheck);
} catch (AssertionError e) {
// evaluate the rule again with more leniency so we can get the priority
final var result2 = rule.allowEmptyShould(true).evaluate(classesToCheck);
final EvaluationResult result2 = rule.allowEmptyShould(true).evaluate(classesToCheck);
if (result2.hasViolation()) {
return result2;
} else {
Expand All @@ -26,12 +26,13 @@ public static EvaluationResult check(ArchRule rule, JavaClasses classesToCheck)
/**
* Check a rule against some classes.
* This can be invoked from the real Gradle plugin or unit tests for rules to ensure the same logic is observed there.
* @param rule the rule to run
*
* @param rule the rule to run
* @param classesToCheck the classes to run the rule against
* @return the result, which contains information about failure
*/
public static EvaluationResult check(ArchRule rule, Class<?>... classesToCheck) {
final var classes = new ClassFileImporter()
final JavaClasses classes = new ClassFileImporter()
.importClasses(classesToCheck);
return check(rule, classes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.netflix.nebula.archrules.core;

import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.lang.Priority;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.junit.jupiter.api.Test;
Expand All @@ -11,19 +12,19 @@
public class RunnerTest {
@Test
public void test_pass() {
final var result = Runner.check(noDeprecatedRule, PassingClass.class);
final EvaluationResult result = Runner.check(noDeprecatedRule, PassingClass.class);
assertThat(result.hasViolation()).isFalse();
}

@Test
public void test_fail() {
final var result = Runner.check(noDeprecatedRule, FailingClass.class);
final EvaluationResult result = Runner.check(noDeprecatedRule, FailingClass.class);
assertThat(result.hasViolation()).isTrue();
}

@Test
public void test_fail_and_pass() {
final var result = Runner.check(noDeprecatedRule, PassingClass.class, FailingClass.class);
final EvaluationResult result = Runner.check(noDeprecatedRule, PassingClass.class, FailingClass.class);
assertThat(result.hasViolation()).isTrue();
assertThat(result.getFailureReport().getDetails()).hasSize(1);
}
Expand Down Expand Up @@ -51,13 +52,13 @@ static class SmokeTest {

@Test
public void test_smoke_pass() {
final var result = Runner.check(smokeTestRule, SmokeTest.class);
final EvaluationResult result = Runner.check(smokeTestRule, SmokeTest.class);
assertThat(result.hasViolation()).isFalse();
}

@Test
public void test_smoke_fail() {
final var result = Runner.check(smokeTestRule, SmokeTestFail.class);
final EvaluationResult result = Runner.check(smokeTestRule, SmokeTestFail.class);
assertThat(result.hasViolation()).isTrue();
assertThat(result.getFailureReport().getDetails())
.contains(NoClassesMatchedEvent.NO_MATCH_MESSAGE);
Expand Down
3 changes: 2 additions & 1 deletion nebula-archrules-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ repositories {
mavenCentral()
}
dependencies {
implementation(project(":nebula-archrules-core"))
testImplementation("net.javacrumbs.json-unit:json-unit-assertj:5.0.0")
testImplementation("org.json:json:20250517")
}
Expand All @@ -30,7 +31,7 @@ gradlePlugin {
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(17)
}
}
testing {
Expand Down
5 changes: 3 additions & 2 deletions nebula-archrules-gradle-plugin/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
cglib:cglib-nodep:3.2.2=integTestRuntimeClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.netflix.nebula:nebula-test:11.7.1=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.4.1=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.7=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.javacrumbs.json-unit:json-unit-assertj:5.0.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.javacrumbs.json-unit:json-unit-core:5.0.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
Expand Down Expand Up @@ -44,5 +45,5 @@ org.junit.platform:junit-platform-launcher:1.14.0=integTestCompileClasspath,inte
org.objenesis:objenesis:2.4=integTestRuntimeClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.3=integTestRuntimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.11=integTestRuntimeClasspath,testRuntimeClasspath
empty=annotationProcessor,integTestAnnotationProcessor,integTestKotlinScriptDefExtensions,kotlinScriptDefExtensions,runtimeClasspath,testAnnotationProcessor,testKotlinScriptDefExtensions
org.slf4j:slf4j-api:2.0.17=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=annotationProcessor,integTestAnnotationProcessor,integTestKotlinScriptDefExtensions,kotlinScriptDefExtensions,testAnnotationProcessor,testKotlinScriptDefExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.netflix.nebula.archrules.gradle;

import org.gradle.api.Named;
import org.gradle.api.attributes.Attribute;
import org.jspecify.annotations.NonNull;

public interface ArchRuleAttribute extends Named {
Attribute<@NonNull ArchRuleAttribute> ARCH_RULES_ATTRIBUTE =
Attribute.of("com.netflix.nebula.archrules", ArchRuleAttribute.class);
String ARCH_RULES = "arch-rules";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.netflix.nebula.archrules.gradle;

import com.tngtech.archunit.lang.Priority;

import java.io.Serializable;

public record Rule(String ruleClass, String ruleName, String description, Priority priority) implements Serializable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.netflix.nebula.archrules.gradle;

import java.io.Serializable;

public record RuleResult(
Rule rule,
String message,
RuleResultStatus status
) implements Serializable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.netflix.nebula.archrules.gradle;

public enum RuleResultStatus {
PASS, FAIL, NO_MATCH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.netflix.nebula.archrules.gradle;

import com.tngtech.archunit.lang.Priority;

import java.io.Serializable;

public record RuleSummary(
String ruleClass,
String ruleName,
Priority priority,
int failures
) implements Serializable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.netflix.nebula.archrules.gradle;

import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Property;
import org.gradle.workers.WorkParameters;
import org.jspecify.annotations.NonNull;

import java.io.File;

public interface RunRulesParams extends WorkParameters {
ConfigurableFileCollection getClassesToCheck();

Property<@NonNull File> getDataOutputFile();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.netflix.nebula.archrules.gradle;

import com.netflix.nebula.archrules.core.ArchRulesService;
import com.netflix.nebula.archrules.core.Runner;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.gradle.workers.WorkAction;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

import static com.netflix.nebula.archrules.core.NoClassesMatchedEvent.NO_MATCH_MESSAGE;

public abstract class RunRulesWorkAction implements WorkAction<RunRulesParams> {

@Override
public void execute() {
ServiceLoader<ArchRulesService> ruleClasses = ServiceLoader.load(ArchRulesService.class);
final var classesToCheck = new ClassFileImporter()
.importPaths(getParameters().getClassesToCheck().getFiles().stream().map(File::toPath).toList());
final List<RuleResult> violationList = new ArrayList<>();
ruleClasses.forEach(ruleClass -> ruleClass.getRules().forEach((id, archRule) -> {
final var result = Runner.check(archRule, classesToCheck);
//TODO: allow priority overrides by rule name
final var rule = new Rule(ruleClass.getClass().getCanonicalName(), id, archRule.getDescription(), result.getPriority());
if (result.hasViolation()) {
result.getFailureReport().getDetails().forEach(detail -> {
if (detail.equals(NO_MATCH_MESSAGE)) {
violationList.add(new RuleResult(rule, detail, RuleResultStatus.NO_MATCH));
} else {
violationList.add(new RuleResult(rule, detail, RuleResultStatus.FAIL));
}
});
} else {
violationList.add(new RuleResult(rule, "", RuleResultStatus.PASS));
}
}));

try (var out = new ObjectOutputStream(new FileOutputStream(getParameters().getDataOutputFile().get()))) {
out.writeInt(violationList.size());
violationList.forEach((v) -> {
try {
out.writeObject(v);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
package com.netflix.nebula.archrules.gradle

import com.netflix.nebula.archrules.gradle.ArchRuleAttribute.ARCH_RULES
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Usage
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.invoke
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register
import org.gradle.testing.base.TestingExtension

class ArchrulesLibraryPlugin : Plugin<Project> {
override fun apply(project: Project) {
val version = ArchrulesLibraryPlugin::class.java.`package`.implementationVersion ?: "latest.release"
project.plugins.withId("java") {
project.pluginManager.withPlugin("java") {
val ext = project.extensions.getByType<JavaPluginExtension>()
val archRulesSourceSet = ext.sourceSets.create("archRules")
project.dependencies.add(
archRulesSourceSet.implementationConfigurationName,
"com.netflix.nebula:nebula-archrules-core:$version"
)
val jarTask = project.tasks.register<Jar>("archRulesJar") {
archiveClassifier.set("archrules")
from(archRulesSourceSet.output)
ext.registerFeature("archRules") {
usingSourceSet(archRulesSourceSet)
capability(project.group.toString(), project.name, project.version.toString())
}
val runtimeElements = project.configurations.getByName("runtimeElements")
runtimeElements.outgoing.variants.create("archRulesElements") {
project.configurations.named("archRulesRuntimeElements") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, project.objects.named("arch-rules"))
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
}
artifact(jarTask)
}
project.plugins.withId("jvm-test-suite") {
project.configurations.named("archRulesApiElements") {
attributes {
attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES))
}
}

project.pluginManager.withPlugin("jvm-test-suite") {
val ext = project.extensions.getByType<TestingExtension>()
ext.suites {
register("archRulesTest", JvmTestSuite::class.java) {
Expand All @@ -51,4 +53,4 @@ class ArchrulesLibraryPlugin : Plugin<Project> {
}
}
}
}
}
Loading