Skip to content

Commit

Permalink
Add Creatable api. Remove @Dependencies.
Browse files Browse the repository at this point in the history
  • Loading branch information
Leland Takamine committed Dec 2, 2019
1 parent 56b1729 commit dca40c2
Show file tree
Hide file tree
Showing 111 changed files with 552 additions and 252 deletions.
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,18 @@ ChildScope childScope = mainScope.child();

## Root Scopes

If Motif finds a nested interface annotated with `@Dependencies` on a Scope, it uses that interface to define exactly what this scope needs from its parent. This is required in order for Motif to tell you when you have unsatisfied dependencies. The recommended pattern is to always declare an empty `@Dependencies` interface on root Scopes:
By extending `Creatable<D>` you can specify exactly the dependencies you expect from the parent `Scope`. This allows
Motif to report missing dependencies at compile time.

```java
@motif.Scope
interface MainScope {
interface MainScope extends Creatable<MainDependencies> {

// ...

@motif.Dependencies
interface Dependencies {}
}
```

With the root `Dependencies` interface in place, Motif will report any missing dependencies at build time. Without it, missing dependencies will still cause the build to fail, but the error messages will be less intuitive.
interface MainDependencies {}
```

## Convenience APIs

Expand Down
2 changes: 1 addition & 1 deletion ast/src/main/kotlin/motif/ast/IrClass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlin.reflect.KClass
interface IrClass : IrAnnotated, IrHasModifiers {

val type: IrType
val superclass: IrType?
val supertypes: List<IrType>
val typeArguments: List<IrType>
val kind: Kind
val methods: List<IrMethod>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,8 @@ class CompilerClass(

override val type: IrType by lazy { CompilerType(env, declaredType) }

override val superclass: IrType? by lazy {
val superType = env.typeUtils.directSupertypes(declaredType).firstOrNull {
it.kind == TypeKind.DECLARED && (it as DeclaredType).asElement().kind == ElementKind.CLASS
} ?: return@lazy null
CompilerType(env, superType as DeclaredType)
override val supertypes: List<IrType> by lazy {
env.typeUtils.directSupertypes(declaredType).map { CompilerType(env, it) }
}

override val typeArguments: List<IrType> by lazy { declaredType.typeArguments.map { CompilerType(env, it) } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import org.junit.Test
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.element.TypeElement
import javax.lang.model.type.TypeKind

class CompilerClassTest {

Expand All @@ -38,7 +37,7 @@ class CompilerClassTest {
class Foo extends Bar<String> {}
""".trimIndent())

val superClass = fooClass.superclass!!.resolveClass() as CompilerClass
val superClass = fooClass.supertypes.single().resolveClass() as CompilerClass
assertThat(superClass.typeArguments.map { it.qualifiedName }).containsExactly("java.lang.String")
}

Expand All @@ -54,8 +53,8 @@ class CompilerClassTest {
class Foo extends Bar<String> {}
""".trimIndent())

val barClass = fooClass.superclass!!.resolveClass() as CompilerClass
val bazClass = barClass.superclass!!.resolveClass() as CompilerClass
val barClass = fooClass.supertypes.single().resolveClass() as CompilerClass
val bazClass = barClass.supertypes.single().resolveClass() as CompilerClass
assertThat(bazClass.typeArguments.map { it.qualifiedName }).containsExactly("java.lang.String")
}

Expand All @@ -69,25 +68,25 @@ class CompilerClassTest {
class Foo<T> extends Bar<T> {}
""".trimIndent())

val superClass = fooClass.superclass!!.resolveClass() as CompilerClass
val superClass = fooClass.supertypes.single().resolveClass() as CompilerClass
assertThat(superClass.typeArguments).isEmpty()
}

@Test
fun testObjectSuperclass() {
fun testObjectSupertype() {
val fooClass = createClass("test.Foo", """
package test;
class Foo {}
""".trimIndent())

val objectClass = fooClass.superclass!!.resolveClass()!!
val objectClass = fooClass.supertypes.single().resolveClass()!!
assertThat(objectClass.qualifiedName).isEqualTo("java.lang.Object")
assertThat(objectClass.superclass).isNull()
assertThat(objectClass.supertypes).isEmpty()
}

@Test
fun testSuperclassOnlyInterface() {
fun testSupertypeOnlyInterface() {
val fooClass = createClass("test.Foo", """
package test;
Expand All @@ -96,9 +95,24 @@ class CompilerClassTest {
class Foo implements Bar {}
""".trimIndent())

val objectClass = fooClass.superclass!!.resolveClass()!!
assertThat(objectClass.qualifiedName).isEqualTo("java.lang.Object")
assertThat(objectClass.superclass).isNull()
val superTypes = fooClass.supertypes.map { it.qualifiedName }
assertThat(superTypes).containsExactly("java.lang.Object", "test.Bar")
}

@Test
fun testSupertypeMultipleInterfaces() {
val fooClass = createClass("test.Foo", """
package test;
interface Bar {}
interface Baz {}
class Foo implements Bar, Baz {}
""".trimIndent())

val superTypes = fooClass.supertypes.map { it.qualifiedName }
assertThat(superTypes).containsExactly("java.lang.Object", "test.Bar", "test.Baz")
}

private fun createClass(qualifiedName: String, @Language("JAVA") text: String): CompilerClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object JavaCodeGenerator {
addMethod(scopeProviderMethod.spec())
factoryProviderMethods.forEach { addMethods(it.specs()) }
dependencyProviderMethods.forEach { addMethod(it.spec()) }
addType(dependencies.spec())
dependencies?.let { addType(it.spec()) }
objectsImpl?.let { addType(it.spec()) }
}.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object KotlinCodeGenerator {
addFunction(scopeProviderMethod.spec())
factoryProviderMethods.forEach { addFunctions(it.specs()) }
dependencyProviderMethods.forEach { addFunction(it.spec()) }
addType(dependencies.spec())
dependencies?.let { addType(it.spec()) }
objectsImpl?.let { addType(it.spec()) }
}.build()
}
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ScopeImpl(
val factoryProviderMethods: List<FactoryProviderMethod>,
val dependencyProviderMethods: List<DependencyProviderMethod>,
val objectsImpl: ObjectsImpl?,
val dependencies: Dependencies)
val dependencies: Dependencies?)

/**
* ```
Expand Down Expand Up @@ -122,7 +122,7 @@ class AccessMethodImpl(
* ```
* @Override
* public ChildScope childScope([ ChildMethpdImplParameters ]) {
* return [ ChildDependenciesImpl ];
* return new ChildScopeImpl([ ChildDependenciesImpl ]);
* }
* ```
*/
Expand Down
14 changes: 11 additions & 3 deletions compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ class ScopeImplFactory private constructor(
abstractMethods)
}

private fun dependencies(): Dependencies {
private fun dependencies(): Dependencies? {
if (scope.dependencies != null) {
return null
}
val methods = getDependencyMethodData(scope).map { methodData ->
val qualifier = methodData.returnType.qualifier?.let { annotation ->
Qualifier(annotation as CompilerAnnotation)
Expand Down Expand Up @@ -309,12 +312,17 @@ class ScopeImplFactory private constructor(

private fun createDependencyMethods(scope: Scope): List<DependencyMethodData> {
val nameScope = NameScope()
fun getName(type: Type): String {
val dependencies = scope.dependencies ?: return nameScope.name(type)
val method = dependencies.methodByType[type] ?: throw IllegalStateException("Could not find Dependencies method for type: $type")
return method.method.name
}
return graph.getUnsatisfied(scope)
.toSortedMap()
.entries
.map { (type, sinks) ->
DependencyMethodData(
nameScope.name(type),
getName(type),
type.type.typeName,
type,
sinks)
Expand All @@ -339,7 +347,7 @@ class ScopeImplFactory private constructor(

private val Scope.dependenciesClassName: ClassName
get() = dependenciesClassNames.computeIfAbsent(this) {
implClassName.nestedClass("Dependencies")
dependencies?.clazz?.typeName ?: implClassName.nestedClass("Dependencies")
}

private val Scope.objectsClassName: ClassName?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ class IntelliJClass(

override val type: IrType by lazy { IntelliJType(project, psiClassType) }

override val superclass: IrType? by lazy {
val superType = psiClassType.superTypes.firstOrNull() ?: return@lazy null
IntelliJType(project, superType)
override val supertypes: List<IrType> by lazy {
psiClassType.superTypes.map { IntelliJType(project, it) }
}

override val typeArguments: List<IrType> by lazy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package motif.ast.intellij

import com.google.common.truth.Truth
import com.intellij.pom.java.LanguageLevel
import com.intellij.psi.PsiElementFactory
import com.intellij.testFramework.LightProjectDescriptor
Expand Down Expand Up @@ -189,7 +190,7 @@ class IntelliJClassTest : LightCodeInsightFixtureTestCase() {
class Bar<T> {}
""".trimIndent())

val superClass = fooClass.superclass!!.resolveClass() as IntelliJClass
val superClass = fooClass.supertypes.single().resolveClass() as IntelliJClass
assertThat(superClass.typeArguments.map { it.qualifiedName }).containsExactly("java.lang.String")
}

Expand All @@ -205,8 +206,8 @@ class IntelliJClassTest : LightCodeInsightFixtureTestCase() {
class Baz<T> {}
""".trimIndent())

val barClass = fooClass.superclass!!.resolveClass() as IntelliJClass
val bazClass = barClass.superclass!!.resolveClass() as IntelliJClass
val barClass = fooClass.supertypes.single().resolveClass() as IntelliJClass
val bazClass = barClass.supertypes.single().resolveClass() as IntelliJClass
assertThat(bazClass.typeArguments.map { it.qualifiedName }).containsExactly("java.lang.String")
}

Expand All @@ -219,7 +220,7 @@ class IntelliJClassTest : LightCodeInsightFixtureTestCase() {
class Bar<T> {}
""".trimIndent())

val superClass = fooClass.superclass!!.resolveClass() as IntelliJClass
val superClass = fooClass.supertypes.single().resolveClass() as IntelliJClass
assertThat(superClass.typeArguments).isEmpty()
}

Expand All @@ -230,9 +231,9 @@ class IntelliJClassTest : LightCodeInsightFixtureTestCase() {
class Foo {}
""".trimIndent())

val objectClass = fooClass.superclass!!.resolveClass()!!
val objectClass = fooClass.supertypes.single().resolveClass()!!
assertThat(objectClass.qualifiedName).isEqualTo("java.lang.Object")
assertThat(objectClass.superclass).isNull()
assertThat(objectClass.supertypes).isEmpty()
}

fun testSuperclassOnlyInterface() {
Expand All @@ -244,9 +245,24 @@ class IntelliJClassTest : LightCodeInsightFixtureTestCase() {
interface Bar {}
""".trimIndent())

val objectClass = fooClass.superclass!!.resolveClass()!!
assertThat(objectClass.qualifiedName).isEqualTo("java.lang.Object")
assertThat(objectClass.superclass).isNull()
val superTypes = fooClass.supertypes.map { it.qualifiedName }
assertThat(superTypes).containsExactly("java.lang.Object", "test.Bar")
}

@Test
fun testSupertypeMultipleInterfaces() {
val fooClass = createIntelliJClass("""
package test;
class Foo implements Bar, Baz {}
interface Bar {}
interface Baz {}
""".trimIndent())

val superTypes = fooClass.supertypes.map { it.qualifiedName }
assertThat(superTypes).containsExactly("java.lang.Object", "test.Bar", "test.Baz")
}

private fun createIntelliJClass(@Language("JAVA") classText: String): IntelliJClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
*/
package motif;

public @interface Dependencies {}
public interface Creatable<D> {}
21 changes: 20 additions & 1 deletion models/src/main/kotlin/motif/models/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package motif.models

import motif.Creatable
import motif.ast.IrClass
import motif.ast.IrMethod

Expand All @@ -31,6 +32,8 @@ class Dependencies(val clazz: IrClass, val scope: Scope) {
Method(this, method, type)
}

val methodByType: Map<Type, Method> = methods.associateBy { it.returnType }.toSortedMap()

val types: List<Type> = methods.map { it.returnType }

class Method(val dependencies: Dependencies, val method: IrMethod, val returnType: Type) {
Expand All @@ -43,8 +46,24 @@ class Dependencies(val clazz: IrClass, val scope: Scope) {
companion object {

fun fromScope(scope: Scope): Dependencies? {
val dependenciesClass = scope.clazz.annotatedInnerClass(motif.Dependencies::class) ?: return null
val creatable = findCreatableSuperinterface(scope.clazz) ?: return null
val dependenciesType = creatable.typeArguments.singleOrNull() ?: return null
val dependenciesClass = dependenciesType.resolveClass() ?: return null
return Dependencies(dependenciesClass, scope)
}

private fun findCreatableSuperinterface(clazz: IrClass): IrClass? {
if (clazz.qualifiedName.takeWhile { it != '<' } == Creatable::class.java.name) {
return clazz
}

clazz.supertypes
.mapNotNull { it.resolveClass() }
.forEach { superinterface ->
findCreatableSuperinterface(superinterface)?.let { return it }
}

return null
}
}
}
3 changes: 1 addition & 2 deletions models/src/test/kotlin/motif/models/motif/ModelsSmokeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,8 @@ class ModelsSmokeTest : BaseTest() {
package test;
@motif.Scope
interface FooScope {
interface FooScope extends motif.Creatable<FooScope.Dependencies> {
@motif.Dependencies
interface Dependencies {
String string();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@
package motif.sample.app.root;

import android.content.Context;
import motif.Creatable;
import motif.Scope;

/**
* This is a convenience Scope defined simply to build the RootScope. This additional layer is useful since RootScope
* requires a Context that must be provided from outside of the graph. RootFactory can declare an empty @Dependencies
* interface and pass the ViewGroup in to the RootScope child method. In RootActivity, we now have a nice API to
* instantiate the RootScope.
*/
@motif.Scope
public interface RootFactory {
@Scope
public interface RootFactory extends Creatable<RootFactory.Dependencies> {

RootScope create(Context context);

@motif.Dependencies
interface Dependencies {}
}
Loading

0 comments on commit dca40c2

Please sign in to comment.