diff --git a/litho-it/src/test/java/com/facebook/litho/specmodels/processor/PropNameInterStageStoreTest.java b/litho-it/src/test/java/com/facebook/litho/specmodels/processor/PropNameInterStageStoreTest.java new file mode 100644 index 00000000000..e9deb9120ad --- /dev/null +++ b/litho-it/src/test/java/com/facebook/litho/specmodels/processor/PropNameInterStageStoreTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.facebook.litho.specmodels.processor; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.facebook.litho.annotations.ResType; +import com.facebook.litho.specmodels.internal.ImmutableList; +import com.facebook.litho.specmodels.model.PropModel; +import com.facebook.litho.testing.assertj.LithoAssertions; +import com.facebook.litho.testing.specmodels.MockMethodParamModel; +import com.facebook.litho.testing.specmodels.MockSpecModel; +import com.squareup.javapoet.ClassName; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Optional; +import javax.annotation.processing.Filer; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.StandardLocation; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests {@link PropNameInterStageStore} */ +public class PropNameInterStageStoreTest { + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Filer mFiler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLoad() throws IOException { + final PropNameInterStageStore store = new PropNameInterStageStore(mFiler); + + final FileObject fileObject = makeFileObjectForString("arg0\narg1\n"); + when(mFiler.getResource(any(JavaFileManager.Location.class), anyString(), anyString())) + .thenReturn(fileObject); + + final Optional> strings = + store.loadNames(new MockName("com.example.MyComponentSpec")); + LithoAssertions.assertThat(strings.isPresent()).isTrue(); + LithoAssertions.assertThat(strings.get()).containsExactly("arg0", "arg1"); + + verify(mFiler) + .getResource( + StandardLocation.CLASS_PATH, "", "META-INF/litho/com.example.MyComponentSpec.props"); + } + + @Test + public void testSave() throws IOException { + final PropNameInterStageStore store = new PropNameInterStageStore(mFiler); + + final MockSpecModel specModel = + MockSpecModel.newBuilder() + .props(ImmutableList.of(makePropModel("param0"), makePropModel("param1"))) + .specTypeName(ClassName.get(MyTestSpec.class)) + .build(); + store.saveNames(specModel); + + verify(mFiler) + .createResource( + StandardLocation.CLASS_OUTPUT, + "", + "META-INF/litho/com.facebook.litho.specmodels.processor.PropNameInterStageStoreTest.MyTestSpec.props"); + + // Not checking the actually written values here because Java IO is a horrible mess. + } + + public static class MyTestSpec {} + + static FileObject makeFileObjectForString(String value) throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(value.getBytes()); + final FileObject file = mock(FileObject.class); + when(file.openInputStream()).thenReturn(inputStream); + return file; + } + + static PropModel makePropModel(String name) { + return new PropModel( + MockMethodParamModel.newBuilder().name(name).build(), false, ResType.BOOL, ""); + } +} diff --git a/litho-processor/src/main/java/com/facebook/litho/specmodels/processor/PropNameInterStageStore.java b/litho-processor/src/main/java/com/facebook/litho/specmodels/processor/PropNameInterStageStore.java index 1d1298beeaa..3395ce7bf55 100644 --- a/litho-processor/src/main/java/com/facebook/litho/specmodels/processor/PropNameInterStageStore.java +++ b/litho-processor/src/main/java/com/facebook/litho/specmodels/processor/PropNameInterStageStore.java @@ -10,24 +10,110 @@ package com.facebook.litho.specmodels.processor; import com.facebook.litho.specmodels.internal.ImmutableList; +import com.facebook.litho.specmodels.model.PropModel; import com.facebook.litho.specmodels.model.SpecModel; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import javax.annotation.processing.Filer; import javax.lang.model.element.Name; +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.StandardLocation; -/** This will serve as store for parameter names. TODO(T21953762) */ +/** + * This store retains prop names across multi-module annotation processor runs. This is needed as + * prop names are derived from method parameters which aren't persisted in the Java 7 bytecode and + * thus cannot be inferred if compilation occurs across modules. + * + *

The props names are serialized and stored as resources within the output JAR, where they can + * be read from again at a later point in time. + */ public class PropNameInterStageStore { private final Filer mFiler; + private static final String BASE_PATH = "META-INF/litho/"; + private static final String FILE_EXT = ".props"; + public PropNameInterStageStore(Filer filer) { this.mFiler = filer; } + /** + * @return List of names in order of definition. List may be empty if there are no custom props + * defined. Value may not be present if loading for the given spec model failed, i.e. we don't + * have inter-stage resources on the class path to facilitate the lookup. + */ public Optional> loadNames(Name qualifiedName) { - return Optional.empty(); + final Optional resource = + getResource(mFiler, StandardLocation.CLASS_PATH, "", BASE_PATH + qualifiedName + FILE_EXT); + + return resource.map( + r -> { + final List props = new ArrayList<>(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(r.openInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + props.add(line); + } + } catch (final IOException err) { + // This can only happen due to buggy build systems. + throw new RuntimeException(err); + } + + return ImmutableList.copyOf(props); + }); } /** Saves the prop names of the given spec model at a well-known path within the resources. */ - public void saveNames(SpecModel specModel) throws IOException {} + public void saveNames(SpecModel specModel) throws IOException { + // This is quite important, because we must not open resources without writing to them + // due to a bug in the Buck caching layer. + if (specModel.getProps().isEmpty()) { + return; + } + + final FileObject outputFile = + mFiler.createResource( + StandardLocation.CLASS_OUTPUT, "", BASE_PATH + specModel.getSpecTypeName() + FILE_EXT); + + try (Writer writer = + new BufferedWriter(new OutputStreamWriter(outputFile.openOutputStream()))) { + for (final PropModel propModel : specModel.getProps()) { + writer.write(propModel.getName() + "\n"); + } + } + } + + /** + * Helper method for obtaining resources from a {@link Filer}, taking care of some javac + * peculiarities. + */ + private static Optional getResource( + final Filer filer, + final JavaFileManager.Location location, + final String packageName, + final String filePath) { + try { + final FileObject resource = filer.getResource(location, packageName, filePath); + resource.openInputStream().close(); + return Optional.of(resource); + } catch (final Exception e) { + // ClientCodeException can be thrown by a bug in the javac ClientCodeWrapper + if (!(e instanceof FileNotFoundException + || e.getClass().getName().equals("com.sun.tools.javac.util.ClientCodeException"))) { + throw new RuntimeException( + String.format("Error opening resource %s/%s", packageName, filePath), e.getCause()); + } + return Optional.empty(); + } + } }