Skip to content

Commit

Permalink
Codegen: JavaGenerator [7] - produce complete NativeModule spec .java…
Browse files Browse the repository at this point in the history
… files

Summary:
This implements the full NativeModuleResolvedType for each NativeModule spec, producing .java spec files in the output directory. The output files are now compiled during build time for :ReactAndroid and :packages:rn-tester:android:app.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D23312858

fbshipit-source-id: c521b9d6602677fd275891cf44329740c6bd7387
  • Loading branch information
fkgozali authored and facebook-github-bot committed Aug 25, 2020
1 parent 92a3dff commit 8ce57ec
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

package com.facebook.react.codegen.generator;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.facebook.react.codegen.generator.model.CodegenException;
import com.facebook.react.codegen.generator.model.TypeData;
import com.facebook.react.codegen.generator.resolver.ResolvedType;
import com.facebook.react.codegen.generator.resolver.TypeResolver;
Expand All @@ -15,9 +18,32 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;

// TODO: Implement proper generator - this is a sample usage of JavaPoet
/**
* Given a react-native-codegen JSON schema, generate a set of .java files for React Native. The
* generator is isolated to a single schema, and a single Java package output.
*/
public final class JavaGenerator {
public static String LICENSE_HEADER =
"/*\n"
+ " * Copyright (c) Facebook, Inc. and its affiliates.\n"
+ " *\n"
+ " * This source code is licensed under the MIT license found in the\n"
+ " * LICENSE file in the root directory of this source tree.\n"
+ " *\n"
+ " * Generated by react-native-codegen JavaGenerator.\n"
+ " *\n"
+ " * @"
+ "generated\n"
+ " * @"
+ "nolint\n"
+ " */\n\n";
private final File mSchemaFile;
private final String mJavaPackageName;
private final File mOutputDir;
Expand All @@ -28,24 +54,53 @@ public JavaGenerator(final File schemaFile, final String javaPackageName, final
mOutputDir = outputDir;
}

public void build() throws FileNotFoundException, IOException {
TypeData typeData = SchemaJsonParser.parse(mSchemaFile);
typeData
.getAllTypes()
.forEach(
t -> {
ResolvedType resolvedType =
TypeResolver.resolveType(typeData.getType(t), typeData, false);
TypeSpec spec = resolvedType.getGeneratedCode(mJavaPackageName);
if (spec != null) {
final JavaFile javaFile = JavaFile.builder(mJavaPackageName, spec).build();
System.out.println(javaFile.toString());
try {
javaFile.writeTo(mOutputDir);
} catch (IOException ex) {
// TODO: Handle this in a different way.
}
}
});
public void build() throws CodegenException, FileNotFoundException, IOException {
// Step 1: Given a schema JSON, collect all types.
final TypeData typeData = SchemaJsonParser.parse(mSchemaFile);

// Step 2: Resolve each type, then collect those that produce a class or interface (TypeSpec).
final List<TypeSpec> typeSpecsToWrite =
typeData.getAllTypes().stream()
.map(
t -> {
final ResolvedType resolvedType =
TypeResolver.resolveType(typeData.getType(t), typeData, false);
final TypeSpec spec = resolvedType.getGeneratedCode(mJavaPackageName);
return spec;
})
.filter(f -> f != null)
.collect(Collectors.toList());

// Step 3: Write all of the TypeSpec's into the output directory.
for (final TypeSpec typeSpec : typeSpecsToWrite) {
writeTypeSpecToFile(typeSpec);
}
}

private final void writeTypeSpecToFile(final TypeSpec typeSpec)
throws CodegenException, IOException {
JavaFile file = JavaFile.builder(mJavaPackageName, typeSpec).skipJavaLangImports(true).build();

// Instead of using JavaFile.writeTo() API, manage the output files ourselves because
// JavaFile.addFileComment() does not support "block comment" style.
// See https://github.com/square/javapoet/issues/682#issuecomment-512238075.
Path outputDirPath = mOutputDir.toPath();

if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) {
throw new CodegenException(
"Output path " + outputDirPath + " exists but is not a directory.");
}

if (!mJavaPackageName.isEmpty()) {
for (String packageComponent : mJavaPackageName.split("\\.")) {
outputDirPath = outputDirPath.resolve(packageComponent);
}
Files.createDirectories(outputDirPath);
}

Path outputPath = outputDirPath.resolve(typeSpec.name + ".java");
try (Writer writer = new OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8)) {
writer.write(LICENSE_HEADER + file.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.codegen.generator.resolver;

import com.squareup.javapoet.AnnotationSpec;
import javax.annotation.Nullable;

public class Annotations {
public static final AnnotationSpec OVERRIDE = AnnotationSpec.builder(Override.class).build();
public static final AnnotationSpec NULLABLE = AnnotationSpec.builder(Nullable.class).build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.codegen.generator.resolver;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import java.util.Map;

/** Names of Java classes required by generated code. */
public class ClassNames {

// Java standard classes
public static final TypeName STRING = ClassName.get(String.class);

public static final ParameterizedTypeName CONSTANTS_MAP =
ParameterizedTypeName.get(ClassName.get(Map.class), ClassNames.STRING, ClassName.OBJECT);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@

import com.facebook.react.codegen.generator.model.FunctionType;
import com.facebook.react.codegen.generator.model.TypeData;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.Modifier;

public final class FunctionResolvedType extends ResolvedType<FunctionType> {
private final Map<String, ResolvedType> mResolvedArgTypes;
Expand Down Expand Up @@ -47,4 +53,70 @@ public Map<String, ResolvedType> getResolvedArgTypes() {
public TypeName getNativeType(final NativeTypeContext typeContext) {
return TypeUtils.makeNullable(ReactClassNames.REACT_CALLBACK, mNullable);
}

public MethodSpec getGeneratedMethodWithReactAnnotation(String methodName) {
TypeName resolvedReturnTypeName =
mResolvedReturnType.getNativeType(NativeTypeContext.FUNCTION_RETURN);

boolean isReturnTypePromise = resolvedReturnTypeName == ReactClassNames.REACT_PROMISE;
TypeName returnTypeName = isReturnTypePromise ? TypeName.VOID : resolvedReturnTypeName;

MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(methodName).addModifiers(Modifier.PUBLIC);
methodBuilder.returns(returnTypeName);

if (!mNullable) {
methodBuilder.addModifiers(Modifier.ABSTRACT);
} else {
String returnStatement = getFalsyReturnStatement(returnTypeName);
if (returnStatement != null) {
CodeBlock.Builder methodBody = CodeBlock.builder();
methodBody.addStatement(returnStatement);
methodBuilder.addCode(methodBody.build());
}
}

mResolvedArgTypes
.entrySet()
.forEach(
e -> {
String argName = e.getKey();
ResolvedType argResolvedType = e.getValue();
methodBuilder.addParameter(
ParameterSpec.builder(
argResolvedType.getNativeType(NativeTypeContext.FUNCTION_ARGUMENT),
argName)
.build());
});

AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(ReactClassNames.REACT_METHOD);

// Special case: Promise inserts additional method arg at the end.
if (isReturnTypePromise) {
methodBuilder.addParameter(
ParameterSpec.builder(ReactClassNames.REACT_PROMISE, "promise").build());
} else if (!TypeName.VOID.equals(returnTypeName)) {
// A non-promise non-void return type means the method is synchronous.
annotationBuilder.addMember("isBlockingSynchronousMethod", "$L", true);
}

// React methods need special `@ReactMethod` annotation for now.
methodBuilder.addAnnotation(annotationBuilder.build());

return methodBuilder.build();
}

private static @Nullable String getFalsyReturnStatement(TypeName returnType) {
if (returnType == TypeName.BOOLEAN) {
return "return false";
} else if (returnType == TypeName.DOUBLE) {
return "return 0.0";
} else if (returnType == ClassNames.STRING
|| returnType == ReactClassNames.REACT_WRITABLE_ARRAY
|| returnType == ReactClassNames.REACT_WRITABLE_MAP) {
return "return null";
}

return null;
}
}
Loading

0 comments on commit 8ce57ec

Please sign in to comment.