Skip to content

Commit

Permalink
fix(module): Spoon is able to build Java 9 code with modules in full …
Browse files Browse the repository at this point in the history
…classpath (#2070)
  • Loading branch information
surli authored and monperrus committed Jun 18, 2018
1 parent a8ae146 commit e0f2b79
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 32 deletions.
13 changes: 0 additions & 13 deletions src/main/java/spoon/reflect/factory/ModuleFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,6 @@ public String toString() {
@Override
public void accept(CtVisitor visitor) {
visitor.visitCtModule(this);

// allModules can be null during the deserialization
// then java will use hashCode and go through this method
// which may cause a NPE (to see it: comment this condition
// and run SerializableTest.
if (allModules != null) {
allModules.forEach(module -> {
if (!module.getSimpleName().equals(CtModule.TOP_LEVEL_MODULE_NAME)) {
module.accept(visitor);
}
});
}

}

@Override
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/spoon/reflect/factory/PackageFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package spoon.reflect.factory;

import spoon.reflect.declaration.CtModule;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtType;
import spoon.reflect.reference.CtPackageReference;
Expand Down Expand Up @@ -98,17 +99,31 @@ public CtPackage create(CtPackage parent, String simpleName) {
}

/**
* Gets or creates a package.
* Gets or creates a package for the unnamed module
*
* @param qualifiedName
* the full name of the package
*
*/
public CtPackage getOrCreate(String qualifiedName) {
return this.getOrCreate(qualifiedName, factory.getModel().getUnnamedModule());
}

/**
* Gets or creates a package and make its parent the given module
*
* @param qualifiedName
* the full name of the package
*
* @param rootModule
* The parent module of the package
*/
public CtPackage getOrCreate(String qualifiedName, CtModule rootModule) {
if (qualifiedName.isEmpty()) {
return factory.getModel().getRootPackage();
return rootModule.getRootPackage();
}
StringTokenizer token = new StringTokenizer(qualifiedName, CtPackage.PACKAGE_SEPARATOR);
CtPackage last = factory.getModel().getRootPackage();
CtPackage last = rootModule.getRootPackage();

while (token.hasMoreElements()) {
String name = token.nextToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void initializeCompiler(JDTBatchCompiler compiler) {
String fName = f.isActualFile() ? f.getPath() : f.getName();
inputStream = f.getContent();
char[] content = IOUtils.toCharArray(inputStream, jdtCompiler.getEnvironment().getEncoding());
cuList.add(new CompilationUnit(content, fName, null));
cuList.add(new CompilationUnit(content, fName, jdtCompiler.getEnvironment().getEncoding().displayName()));
IOUtils.closeQuietly(inputStream);
}
} catch (Exception e) {
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/spoon/support/compiler/jdt/JDTBatchCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.apache.commons.io.output.NullOutputStream;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.compiler.CompilationProgress;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
Expand All @@ -29,16 +30,20 @@
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.core.util.CommentRecorderParser;
import spoon.SpoonException;
import spoon.support.compiler.SpoonProgress;

import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/*
Expand Down Expand Up @@ -66,8 +71,53 @@ public JDTBatchCompiler(JDTBasedSpoonCompiler jdtCompiler) {
}
}

/**
* This method returns the compilation units that will be processed and/or compiled by JDT.
* Note that this method also process the CUs to associate the right module information.
* Warning: this method cannot be replaced by a call to its supermethod as we manage the CUs differently
* in Spoon. We might indeed have CUs coming from virtual files or ignored CU due to the configuration.
* The the CUs are created from the {@link FileCompilerConfig}.
*/
@Override
public CompilationUnit[] getCompilationUnits() {

Map<String, CompilationUnit> pathToModCU = new HashMap<>();

for (int round = 0; round < 2; round++) {
for (CompilationUnit compilationUnit : this.compilationUnits) {
char[] charName = compilationUnit.getFileName();
boolean isModuleInfo = CharOperation.endsWith(charName, TypeConstants.MODULE_INFO_FILE_NAME);
if (isModuleInfo == (round == 0)) { // 1st round: modules, 2nd round others (to ensure populating pathToModCU well in time)

String fileName = new String(charName);
if (isModuleInfo) {
int lastSlash = CharOperation.lastIndexOf(File.separatorChar, charName);
if (lastSlash != -1) {
char[] modulePath = CharOperation.subarray(charName, 0, lastSlash);
pathToModCU.put(String.valueOf(modulePath), compilationUnit);

lastSlash = CharOperation.lastIndexOf(File.separatorChar, modulePath);
if (lastSlash == -1) {
lastSlash = 0;
} else {
lastSlash += 1;
}

char[] moduleName = CharOperation.subarray(modulePath, lastSlash, modulePath.length);
compilationUnit.module = moduleName;
}
} else {
for (Map.Entry<String, CompilationUnit> entry : pathToModCU.entrySet()) {
if (fileName.startsWith(entry.getKey())) { // associate CUs to module by common prefix
compilationUnit.setModule(entry.getValue());
break;
}
}
}
}
}
}

return compilationUnits;
}

Expand Down
23 changes: 20 additions & 3 deletions src/main/java/spoon/support/compiler/jdt/JDTTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.ModuleBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemMethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
Expand Down Expand Up @@ -733,7 +734,16 @@ public boolean visit(Javadoc javadoc, ClassScope scope) {
public boolean visit(CompilationUnitDeclaration compilationUnitDeclaration, CompilationUnitScope scope) {
context.compilationunitdeclaration = scope.referenceContext;
context.compilationUnitSpoon = getFactory().CompilationUnit().getOrCreate(new String(context.compilationunitdeclaration.getFileName()));
context.compilationUnitSpoon.setDeclaredPackage(getFactory().Package().getOrCreate(CharOperation.toString(scope.currentPackageName)));
ModuleBinding enclosingModule = scope.fPackage.enclosingModule;

CtModule module;
if (!enclosingModule.isUnnamed() && enclosingModule.shortReadableName() != null && enclosingModule.shortReadableName().length > 0) {
module = getFactory().Module().getOrCreate(String.valueOf(enclosingModule.shortReadableName()));
} else {
module = getFactory().Module().getUnnamedModule();
}

context.compilationUnitSpoon.setDeclaredPackage(getFactory().Package().getOrCreate(CharOperation.toString(scope.currentPackageName), module));
return true;
}

Expand Down Expand Up @@ -1562,11 +1572,18 @@ public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope
context.enter(factory.Package().getOrCreate(new String(typeDeclaration.binding.fPackage.readableName())), typeDeclaration);
return true;
} else {
CtModule module;
if (typeDeclaration.binding.module != null && !typeDeclaration.binding.module.isUnnamed() && typeDeclaration.binding.module.shortReadableName() != null && typeDeclaration.binding.module.shortReadableName().length > 0) {
module = factory.Module().getOrCreate(String.valueOf(typeDeclaration.binding.module.shortReadableName()));
} else {
module = factory.Module().getUnnamedModule();
}

CtPackage pack;
if (typeDeclaration.binding.fPackage.shortReadableName() != null && typeDeclaration.binding.fPackage.shortReadableName().length > 0) {
pack = factory.Package().getOrCreate(new String(typeDeclaration.binding.fPackage.shortReadableName()));
pack = factory.Package().getOrCreate(new String(typeDeclaration.binding.fPackage.shortReadableName()), module);
} else {
pack = factory.Package().getRootPackage();
pack = module.getRootPackage();
}
context.enter(pack, typeDeclaration);
pack.addType(helper.createType(typeDeclaration));
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/spoon/support/compiler/jdt/TreeBuilderCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@
*/
package spoon.support.compiler.jdt;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.compiler.CompilationProgress;
import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
import org.eclipse.jdt.internal.compiler.IProblemFactory;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.util.Messages;

import java.io.PrintWriter;
import java.util.ArrayList;

class TreeBuilderCompiler extends org.eclipse.jdt.internal.compiler.Compiler {

Expand All @@ -37,6 +42,20 @@ class TreeBuilderCompiler extends org.eclipse.jdt.internal.compiler.Compiler {
super(environment, policy, options, requestor, problemFactory, out, progress);
}

// This code is directly inspired from Compiler class.
private void sortModuleDeclarationsFirst(ICompilationUnit[] sourceUnits) {
Arrays.sort(sourceUnits, (u1, u2) -> {
char[] fn1 = u1.getFileName();
char[] fn2 = u2.getFileName();
boolean isMod1 = CharOperation.endsWith(fn1, TypeConstants.MODULE_INFO_FILE_NAME) || CharOperation.endsWith(fn1, TypeConstants.MODULE_INFO_CLASS_NAME);
boolean isMod2 = CharOperation.endsWith(fn2, TypeConstants.MODULE_INFO_FILE_NAME) || CharOperation.endsWith(fn2, TypeConstants.MODULE_INFO_CLASS_NAME);
if (isMod1 == isMod2) {
return 0;
}
return isMod1 ? -1 : 1;
});
}

// this method is not meant to be in the public API
protected CompilationUnitDeclaration[] buildUnits(CompilationUnit[] sourceUnits) {

Expand All @@ -48,6 +67,8 @@ protected CompilationUnitDeclaration[] buildUnits(CompilationUnit[] sourceUnits)

CompilationUnitDeclaration unit = null;
int i = 0;

this.sortModuleDeclarationsFirst(sourceUnits);
// build and record parsed units
beginToCompile(sourceUnits);

Expand Down
45 changes: 35 additions & 10 deletions src/test/java/spoon/test/module/TestModule.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package spoon.test.module;

import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import spoon.Launcher;
import spoon.reflect.CtModel;
import spoon.reflect.code.CtComment;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtModule;
import spoon.reflect.declaration.CtModuleDirective;
import spoon.reflect.declaration.CtPackage;
Expand All @@ -15,6 +19,8 @@
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtUsedService;
import spoon.reflect.reference.CtModuleReference;
import spoon.reflect.visitor.filter.NamedElementFilter;
import spoon.reflect.visitor.filter.TypeFilter;

import java.io.File;
import java.io.IOException;
Expand All @@ -32,6 +38,15 @@
public class TestModule {
private static final String MODULE_RESOURCES_PATH = "./src/test/resources/spoon/test/module";

private void checkJavaVersion() {
String property = System.getProperty("java.version");
if (property != null && !property.isEmpty()) {

// java 8 and less are versionning "1.X" where 9 and more are directly versioned "X"
Assume.assumeFalse(property.startsWith("1."));
}
}

@BeforeClass
public static void setUp() throws IOException {
File directory = new File(MODULE_RESOURCES_PATH);
Expand Down Expand Up @@ -261,20 +276,32 @@ public void testGetModuleAfterChangingItsName() {
assertSame(module, moduleNewName);
}

@Ignore
@Test
public void testSimpleModuleCanBeBuiltAndCompiled() {
// contract: Spoon is able to build and compile a model with a module

public void testSimpleModuleCanBeBuilt() {
checkJavaVersion();
// contract: Spoon is able to build a simple model with a module in full classpath
final Launcher launcher = new Launcher();
launcher.getEnvironment().setShouldCompile(true);
launcher.getEnvironment().setComplianceLevel(9);
//launcher.addModulePath("./src/test/resources/spoon/test/module/simple_module_with_code");
launcher.getEnvironment().setNoClasspath(false);
launcher.addInputResource("./src/test/resources/spoon/test/module/simple_module_with_code");
launcher.run();

assertEquals(2, launcher.getModel().getAllModules().size());
assertEquals(1, launcher.getModel().getAllTypes().size());
CtModel model = launcher.getModel();

// unnamed module and module 'simple_module_with_code'
assertEquals(2, model.getAllModules().size());
assertEquals(1, model.getAllTypes().size());

CtClass simpleClass = model.getElements(new TypeFilter<>(CtClass.class)).get(0);
assertEquals("SimpleClass", simpleClass.getSimpleName());

CtModule simpleModule = model.getElements(new NamedElementFilter<>(CtModule.class, "simple_module_with_code")).get(0);
assertNotNull(simpleModule);
assertEquals("simple_module_with_code", simpleModule.getSimpleName());

CtModule module = simpleClass.getParent(CtModule.class);
assertNotNull(module);
assertSame(simpleModule, module);
}

@Ignore
Expand All @@ -284,8 +311,6 @@ public void testMultipleModulesAndParents() {

final Launcher launcher = new Launcher();
launcher.getEnvironment().setComplianceLevel(9);
//launcher.addModulePath("./src/test/resources/spoon/test/module/code-multiple-modules/foo");
//launcher.addModulePath("./src/test/resources/spoon/test/module/code-multiple-modules/bar");
launcher.addInputResource(MODULE_RESOURCES_PATH+"/code-multiple-modules");
launcher.run();

Expand Down

0 comments on commit e0f2b79

Please sign in to comment.