Skip to content

Commit

Permalink
Desugar producer attribute if the requesting attribute is desugared (#…
Browse files Browse the repository at this point in the history
…11372)

This can be the case if an attribute on a dependency is published
and the resolved target of the dependency is a local project.
For example, a published platform dependency to a local java-platform
project.

We support 'Named' and 'Enum' for desugaring as that are the only
non-primitve types we currently allow to be published in Gradle
Module Metadata.
  • Loading branch information
jjohannes committed Nov 15, 2019
1 parent 295f13b commit 3700778
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import org.gradle.api.Named;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeContainer;
import org.gradle.internal.Cast;
Expand Down Expand Up @@ -162,6 +163,19 @@ public Object get() {
return value.isolate();
}

private String desugar() {
// We support desugaring for all non-primitive types supported in GradleModuleMetadataWriter.writeAttributes(), which are:
// - Named
// - Enum
if (Named.class.isAssignableFrom(attribute.getType())) {
return ((Named) get()).getName();
}
if (Enum.class.isAssignableFrom(attribute.getType())) {
return ((Enum<?>) get()).name();
}
return null;
}

@Nullable
private <S> S coerce(Class<S> type) {
if (value != null) {
Expand All @@ -181,20 +195,32 @@ public <S> S coerce(Attribute<S> otherAttribute) {
}

private <S> S uncachedCoerce(Attribute<S> otherAttribute) {
Class<S> attributeType = otherAttribute.getType();
if (attributeType.isAssignableFrom(attribute.getType())) {
Class<S> otherAttributeType = otherAttribute.getType();
// If attribute types are already compatible, go with it. There are two cases covered here:
// 1) Both attributes are strongly typed and match, usually the case if both are sourced from the local build
// 2) Both attributes are desugared, usually the case if both are sourced from published metadata
if (otherAttributeType.isAssignableFrom(attribute.getType())) {
return (S) get();
}

S converted = coerce(attributeType);
// Attempt to coerce myself into the other attribute's type
// - I am desugared and the other attribute is strongly typed, usually the case if I am sourced from published metadata and the other from the local build
S converted = coerce(otherAttributeType);
if (converted != null) {
return converted;
} else if (otherAttributeType.isAssignableFrom(String.class)) {
// Attempt to desugar myself
// - I am strongly typed and the other is desugared, usually the case if I am sourced from the local build and the other is sourced from published metadata
converted = (S) desugar();
if (converted != null) {
return converted;
}
}
String foundType = get().getClass().getName();
if (foundType.equals(attributeType.getName())) {
if (foundType.equals(otherAttributeType.getName())) {
foundType += " with a different ClassLoader";
}
throw new IllegalArgumentException(String.format("Unexpected type for attribute '%s' provided. Expected a value of type %s but found a value of type %s.", attribute.getName(), attributeType.getName(), foundType));
throw new IllegalArgumentException(String.format("Unexpected type for attribute '%s' provided. Expected a value of type %s but found a value of type %s.", attribute.getName(), otherAttributeType.getName(), foundType));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.gradle.internal.hash.ClassLoaderHierarchyHasher
import org.gradle.internal.hash.HashCode
import org.gradle.internal.snapshot.ValueSnapshotter
import org.gradle.internal.snapshot.impl.DefaultValueSnapshotter
import org.gradle.internal.state.DefaultManagedFactoryRegistry

class SnapshotTestUtil {
static ValueSnapshotter valueSnapshotter() {
Expand All @@ -29,6 +30,6 @@ class SnapshotTestUtil {
HashCode getClassLoaderHash(ClassLoader classLoader) {
return HashCode.fromInt(classLoader.hashCode())
}
}, null)
}, new DefaultManagedFactoryRegistry(null))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class JavaPlatformResolveIntegrationTest extends AbstractHttpDependencyResolutio
settingsFile << "rootProject.name = 'test'"
buildFile << """
apply plugin: 'java-library'
allprojects {
repositories {
maven { url "${mavenHttpRepo.uri}" }
Expand Down Expand Up @@ -506,6 +506,44 @@ class JavaPlatformResolveIntegrationTest extends AbstractHttpDependencyResolutio
'runtime' | 'runtime'
}

def "Can handle a published platform dependency that is resolved to a local platform project"() {
given:
file("src/main/java/SomeClass.java") << "public class SomeClass {}"
platformModule('')
mavenHttpRepo.module("org.test", "platform", "1.9").withModuleMetadata().withoutDefaultVariants()
.withVariant('apiElements') {
useDefaultArtifacts = false
attribute(Usage.USAGE_ATTRIBUTE.name, Usage.JAVA_API)
attribute(Category.CATEGORY_ATTRIBUTE.name, Category.REGULAR_PLATFORM)
}
.withVariant('runtimeElements') {
useDefaultArtifacts = false
attribute(Usage.USAGE_ATTRIBUTE.name, Usage.JAVA_RUNTIME)
attribute(Category.CATEGORY_ATTRIBUTE.name, Category.REGULAR_PLATFORM)
}.publish()
def moduleA = mavenHttpRepo.module("org.test", "b", "1.9").withModuleMetadata().withVariant("runtime") {
dependsOn("org.test", "platform", "1.9", null, [(Category.CATEGORY_ATTRIBUTE.name): Category.REGULAR_PLATFORM])
}.withVariant("api") {
dependsOn("org.test", "platform", "1.9", null, [(Category.CATEGORY_ATTRIBUTE.name): Category.REGULAR_PLATFORM])
}.publish()

when:
buildFile << """
dependencies {
implementation platform(project(":platform"))
implementation "org.test:b:1.9"
}
"""


moduleA.pom.expectGet()
moduleA.moduleMetadata.expectGet()
moduleA.artifact.expectGet()

then:
succeeds ":compileJava"
}

private void checkConfiguration(String configuration) {
resolve = new ResolveTestFixture(buildFile, configuration)
resolve.expectDefaultConfiguration("compile")
Expand All @@ -520,7 +558,7 @@ class JavaPlatformResolveIntegrationTest extends AbstractHttpDependencyResolutio
plugins {
id 'java-platform'
}
dependencies {
$dependencies
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.gradle.internal.component.model

import com.google.common.collect.LinkedListMultimap
import com.google.common.collect.Multimap
import org.gradle.api.Named
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeDisambiguationRule
import org.gradle.api.attributes.MultipleCandidatesDetails
Expand All @@ -26,6 +27,7 @@ import org.gradle.api.internal.attributes.ImmutableAttributes
import org.gradle.util.AttributeTestUtil
import spock.lang.Specification
import static org.gradle.util.AttributeTestUtil.attributes
import static org.gradle.util.TestUtil.objectFactory

class ComponentAttributeMatcherTest extends Specification {

Expand Down Expand Up @@ -414,6 +416,98 @@ class ComponentAttributeMatcherTest extends Specification {
matcher.match(schema, [c1, c2], requested, null) == [c1]
}

def "can match when producer uses desugared attribute of type Named"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", NamedTestAttribute)
def key2 = Attribute.of("a", String)
schema.attribute(key1)

def requested = attrs().attribute(key1, objectFactory().named(NamedTestAttribute, "name1"))
def c1 = attrs().attribute(key2, "name1")
def c2 = attrs().attribute(key2, "name2")

expect:
matcher.match(schema, [c1, c2], requested, null) == [c1]
}

def "can match when consumer uses desugared attribute of type Named"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", String)
def key2 = Attribute.of("a", NamedTestAttribute)
schema.attribute(key1)

def requested = attrs().attribute(key1, "name1")
def c1 = attrs().attribute(key2, objectFactory().named(NamedTestAttribute, "name1"))
def c2 = attrs().attribute(key2, objectFactory().named(NamedTestAttribute, "name2"))

expect:
matcher.match(schema, [c1, c2], requested, null) == [c1]
}

def "can match when producer uses desugared attribute of type Enum"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", EnumTestAttribute)
def key2 = Attribute.of("a", String)
schema.attribute(key1)

def requested = attrs().attribute(key1, EnumTestAttribute.NAME1)
def c1 = attrs().attribute(key2, "NAME1")
def c2 = attrs().attribute(key2, "NAME2")

expect:
matcher.match(schema, [c1, c2], requested, null) == [c1]
}

def "can match when consumer uses desugared attribute of type Enum"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", String)
def key2 = Attribute.of("a", EnumTestAttribute)
schema.attribute(key1)

def requested = attrs().attribute(key1, "NAME1")
def c1 = attrs().attribute(key2, EnumTestAttribute.NAME1)
def c2 = attrs().attribute(key2, EnumTestAttribute.NAME2)

expect:
matcher.match(schema, [c1, c2], requested, null) == [c1]
}

def "cannot match when producer uses desugared attribute of unsupported type"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", NotSerializableInGradleMetadataAttribute)
def key2 = Attribute.of("a", String)
schema.attribute(key1)

def requested = attrs().attribute(key1, new NotSerializableInGradleMetadataAttribute("name1"))
def c1 = attrs().attribute(key2, "name1")
def c2 = attrs().attribute(key2, "name2")

when:
matcher.match(schema, [c1, c2], requested, null)

then:
IllegalArgumentException e = thrown()
e.message == "Unexpected type for attribute 'a' provided. Expected a value of type org.gradle.internal.component.model.ComponentAttributeMatcherTest${'$'}NotSerializableInGradleMetadataAttribute but found a value of type java.lang.String."
}

def "cannot match when consumer uses desugared attribute of unsupported type"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", String)
def key2 = Attribute.of("a", NotSerializableInGradleMetadataAttribute)
schema.attribute(key1)

def requested = attrs().attribute(key1, "name1")
def c1 = attrs().attribute(key2, new NotSerializableInGradleMetadataAttribute("name1"))
def c2 = attrs().attribute(key2, new NotSerializableInGradleMetadataAttribute("name2"))

when:
matcher.match(schema, [c1, c2], requested, null)

then:
IllegalArgumentException e = thrown()
e.message == "Unexpected type for attribute 'a' provided. Expected a value of type java.lang.String but found a value of type org.gradle.internal.component.model.ComponentAttributeMatcherTest${'$'}NotSerializableInGradleMetadataAttribute."
}

def "matching fails when attribute has incompatible types in consumer and producer"() {
def matcher = new ComponentAttributeMatcher()
def key1 = Attribute.of("a", String)
Expand Down Expand Up @@ -504,6 +598,16 @@ class ComponentAttributeMatcherTest extends Specification {
factory.mutable()
}

interface NamedTestAttribute extends Named { }
enum EnumTestAttribute { NAME1, NAME2 }
static class NotSerializableInGradleMetadataAttribute implements Serializable {
String name

NotSerializableInGradleMetadataAttribute(String name) {
this.name = name
}
}

private static class TestSchema implements AttributeSelectionSchema {
Set<Attribute<?>> attributes = []
Map<String, Attribute<?>> attributesByName = [:]
Expand Down

0 comments on commit 3700778

Please sign in to comment.