Skip to content
Open
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
24 changes: 24 additions & 0 deletions src/main/java/com/solubris/enforcer/Artifact.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,28 @@ public boolean hasExplicitVersion() {
public boolean hasImplicitVersion() {
return !Objects.equals(getVersion(), getEffectiveVersion());
}

/**
* Uniqueness is used to avoid violations on coincidental artifact versions.
*
* <p>What about groupId's like this
* - org.junit.jupiter
* - org.junit.platform
* Could remove one level of the package.
*
* <p>The coincidence problem puts the whole rule into jeopardy.
* Without a good solution, can only report warnings.
*
* <p>How is each violation affected:
* - redundantPropertyViolation
* grouping by groupId ruins this - property could be used for multiple artifacts with the different groupIds.
* - unusedPropertyViolation
* grouping by groupId is ok - some locations should not use the property
* - missingPropertyViolation
* grouping by groupId is ok - some locations should not use the property
*
*/
public String uniqueness() {
return String.join(":", getGroupId(), getEffectiveVersion());
}
}
13 changes: 7 additions & 6 deletions src/main/java/com/solubris/enforcer/VersionPropertyRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ protected Stream<String> scan() {
.map(a -> a.withEffectiveVersion(fetchVersion(a, byKey)))
.filter(a -> a.getEffectiveVersion() != null)
.filter(artifact -> !isExcluded(artifact.getVersion()))
.collect(groupingBy(Artifact::getEffectiveVersion, toList()));
.collect(groupingBy(Artifact::uniqueness, toList()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Grouping by uniqueness() causes false-positive redundant-property violations when a shared property is used across different groupIds

The change from groupingBy(Artifact::getEffectiveVersion) to groupingBy(Artifact::uniqueness) (where uniqueness() returns groupId:effectiveVersion) breaks the detection of shared properties used across different groupIds.

Consider two dependencies from different groupIds that both reference the same property: withDependency("com.a", "artifact-a", "common.version", "1.0.0") and withDependency("com.b", "artifact-b", "common.version", "1.0.0"). Previously these were grouped together under effective version 1.0.0 (2 artifacts, both using the property → no violation). Now they form two separate groups (com.a:1.0.0 and com.b:1.0.0), each with 1 artifact that has hasImplicitVersion() == true, which triggers redundantPropertyViolation for each — a false positive, since the property IS legitimately shared.

Concrete example of the false positive path through scan()

For each single-artifact group where propertyCount == 1 (line 89), redundantPropertyViolation is called. The property is actually used in 2 places, but the grouping hides this. The fix should consider a uniqueness key that avoids coincidental version matches (the original problem) without breaking shared property detection across groupIds.

Prompt for agents
In src/main/java/com/solubris/enforcer/VersionPropertyRule.java at line 73, the grouping by Artifact::uniqueness (which is groupId:effectiveVersion) incorrectly splits artifacts that share a property across different groupIds, causing false-positive redundant-property violations.

The fix needs to handle two competing concerns:
1. Coincidental versions (same version string, different groupIds, no shared property) should NOT be flagged as needing a shared property.
2. Shared properties across different groupIds SHOULD be recognized as legitimate multi-use properties.

One approach: keep grouping by effectiveVersion, but in the missingPropertyViolation check (the propertyCount == 0 case on line 86), add an additional filter to only flag artifacts that share the same groupId. Alternatively, do a two-pass analysis: first detect shared properties, then check for coincidental explicit versions only among same-groupId artifacts.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// byVersion maps from property to list of artifacts
// now can go through property keys and check usage

return usages.entrySet().stream()
.map(e -> {
String effectiveVersion = e.getKey();
String effectiveVersion = e.getKey().split(":")[1];
List<Artifact> artifacts = e.getValue();
long propertyCount = artifacts.stream()
.filter(Artifact::hasImplicitVersion)
Expand Down Expand Up @@ -108,7 +108,8 @@ private String redundantPropertyViolation(Artifact artifact) {
* The artifacts could either have the explicit version or a reference to the property.
* Where the property represents the same version.
* If the property uses a different version than the explicit version,
* then that would be detected at the moment - could be a different violation (property value doesn't match usage).
* then that would not be detected at the moment
* - could be a different violation (property value doesn't match usage).
*
* <p>Could the artifacts refer to different properties that have the same value?
* That's possible due to coincidental properties - another edge case to consider.
Expand All @@ -128,17 +129,17 @@ private static String unusedPropertyViolation(String effectiveVersion, List<Arti
propertyName, effectiveVersion, unused);
}

private String missingPropertyViolation(String version, List<Artifact> artifacts) {
private String missingPropertyViolation(String effectiveVersion, List<Artifact> artifacts) {
if (!requirePropertiesForDuplicates) return null;

String unused = artifacts.stream()
.filter(a -> Objects.equals(a.getVersion(), a.getEffectiveVersion()))
.filter(Artifact::hasExplicitVersion) // TODO is this required since all artifacts at this point should be explicitly versioned
.map(Artifact::key)
.collect(joining(", "));
return String.format(
"Version '%s' exists in multiple locations, please extract a version property. " +
"Unused locations: %s",
version, unused);
effectiveVersion, unused);
}

/**
Expand Down
20 changes: 17 additions & 3 deletions src/test/java/com/solubris/enforcer/VersionPropertyRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ void detectsSingleUseOfProperty(boolean allowed) {

@Test
void multipleUseOfPropertyAllowed() {
modelStubber.withDependency("junit", "junit", "junit.version", "4.13.2");
modelStubber.withDependency("org.junit.jupiter", "junit-engine", "junit.version", "4.13.2");
modelStubber.withDependency("org.junit.jupiter", "junit-jupiter-api", "junit.version", "4.13.2");

Stream<String> violations = rule.scan();
Expand All @@ -63,7 +63,7 @@ void multipleUseOfPropertyAllowed() {
class RequirePropertiesForDuplicates {
@Test
void multipleExplicitVersionsNotAllowed() {
modelStubber.withDependency("junit", "junit", "4.13.2");
modelStubber.withDependency("org.junit.jupiter", "junit-engine", "4.13.2");
modelStubber.withDependency("org.junit.jupiter", "junit-jupiter-api", "4.13.2");

// DependencyManagement depMgmt = new DependencyManagement();
Expand All @@ -75,14 +75,28 @@ void multipleExplicitVersionsNotAllowed() {
assertThat(violations).hasSize(1);
}

/**
* Entirely legitimate to use the same version for disparate dependencies.
* Assume they must have a different groupId.
*/
@Test
void coincidentalVersionsAllowed() {
modelStubber.withDependency("com.pmd", "pmd.core", "4.13.2");
modelStubber.withDependency("org.junit.jupiter", "junit-jupiter-api", "4.13.2");

Stream<String> violations = rule.scan();

assertThat(violations).isEmpty();
}

@Test
void multipleExplicitVersionsNotAllowedCoveringAllTypes() {
modelStubber.withAllTypes(() -> "4.13.2");

Stream<String> violations = rule.scan();

// TODO how to assert the multiple locations for this violation?
assertThat(violations).hasSize(1);
assertThat(violations).isNotEmpty();
}

@Test
Expand Down
Loading