/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.jjs.impl; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.CompilerContext; import com.google.gwt.dev.PrecompileTaskOptionsImpl; import com.google.gwt.dev.cfg.MockModuleDef; import com.google.gwt.dev.javac.CheckerTestCase; import com.google.gwt.dev.javac.CompilationState; import com.google.gwt.dev.javac.CompilationStateBuilder; import com.google.gwt.dev.javac.JdtCompiler.AdditionalTypeProviderDelegate; import com.google.gwt.dev.javac.testing.impl.MockJavaResource; import com.google.gwt.dev.javac.testing.impl.MockResourceOracle; import com.google.gwt.dev.jjs.JavaAstConstructor; import com.google.gwt.dev.jjs.ast.Context; import com.google.gwt.dev.jjs.ast.JBlock; import com.google.gwt.dev.jjs.ast.JDeclaredType; import com.google.gwt.dev.jjs.ast.JExpression; import com.google.gwt.dev.jjs.ast.JField; import com.google.gwt.dev.jjs.ast.JLocal; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodBody; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JReferenceType; import com.google.gwt.dev.jjs.ast.JReturnStatement; import com.google.gwt.dev.jjs.ast.JType; import com.google.gwt.dev.jjs.ast.JVisitor; import com.google.gwt.dev.resource.Resource; import com.google.gwt.dev.util.arg.SourceLevel; import com.google.gwt.dev.util.log.AbstractTreeLogger; import com.google.gwt.dev.util.log.PrintWriterTreeLogger; import com.google.gwt.thirdparty.guava.common.base.Function; import com.google.gwt.thirdparty.guava.common.base.Joiner; import com.google.gwt.thirdparty.guava.common.base.Predicates; import com.google.gwt.thirdparty.guava.common.collect.FluentIterable; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.google.gwt.thirdparty.guava.common.collect.Sets; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A useful base class for tests that build JJS ASTs. */ public abstract class JJSTestBase extends CheckerTestCase { public static final String MAIN_METHOD_NAME = "onModuleLoad"; /** * Finds a field with a type. */ public static JField findField(JDeclaredType type, String fieldName) { for (JField field : type.getFields()) { if (field.getName().equals(fieldName)) { return field; } } return null; } /** * Finds a field by name, e.g. <code>Foo.field</code>. */ public static JField findField(JProgram program, String qualifiedFieldName) { int pos = qualifiedFieldName.lastIndexOf('.'); assertTrue(pos > 0); String typeName = qualifiedFieldName.substring(0, pos); String fieldName = qualifiedFieldName.substring(pos + 1); JDeclaredType type = findDeclaredType(program, typeName); JField field = findField(type, fieldName); return field; } /** * Find a local variable declared within a JMethod. */ public static JLocal findLocal(JMethod method, final String localName) { class LocalVisitor extends JVisitor { JLocal found; @Override public void endVisit(JLocal x, Context ctx) { if (x.getName().equals(localName)) { found = x; } } } LocalVisitor v = new LocalVisitor(); v.accept(method); return v.found; } public static JMethod findMainMethod(JProgram program) { return findMethod(program, MAIN_METHOD_NAME); } public static JMethod findMethod(JDeclaredType type, String methodNameOrSignature) { // Signatures and names never collide (names never have parens but signatures always do). for (JMethod method : type.getMethods()) { if (method.getSignature().equals(methodNameOrSignature) || method.getName().equals(methodNameOrSignature)) { return method; } } return null; } public static JMethod findMethod(JProgram program, String methodName) { int lastDot = methodName.lastIndexOf("."); if (lastDot != -1) { String className = methodName.substring(0, lastDot); JDeclaredType clazz = program.getFromTypeMap(className); assertNotNull("Did not find class " + className, clazz); return clazz.findMethod(methodName.substring(lastDot + 1), true); } return findMethod(program.getFromTypeMap("test.EntryPoint"), methodName); } public static JMethod findQualifiedMethod(JProgram program, String methodName) { int pos = methodName.lastIndexOf('.'); assertTrue(pos > 0); String typeName = methodName.substring(0, pos); String unqualMethodName = methodName.substring(pos + 1); JDeclaredType type = findDeclaredType(program, typeName); return findMethod(type, unqualMethodName); } private static Pattern typeNamePattern = Pattern.compile("([^\\[\\]]*)((?:\\[\\])*)"); /** * Finds a type by name including arrays and primitives. */ public static JType findType(JProgram program, String typeName) { Matcher matcher = typeNamePattern.matcher(typeName); if (!matcher.matches()) { return null; } String bareName = matcher.group(1); int dimensions = matcher.group(2).length() / 2; JType type = JPrimitiveType.getType(bareName); if (type == null) { type = findDeclaredType(program, bareName); } if (type != null && dimensions > 0) { type = program.getOrCreateArrayType(type, dimensions); } return type; } /** * Finds a declared type by name. The type name may be short, e.g. <code>"Foo"</code>, * or fully-qualified, e.g. <code>"com.google.example.Foo"</code>. If a short * name is used, it must be unambiguous. */ public static JDeclaredType findDeclaredType(JProgram program, String typeName) { JDeclaredType type = program.getFromTypeMap(typeName); if (type != null || typeName.indexOf('.') != -1) { return type; } // Do a slow lookup by short name. for (JDeclaredType checkType : program.getDeclaredTypes()) { if (!checkType.getShortName().equals(typeName)) { continue; } if (type != null) { fail("Ambiguous type reference '" + typeName + "' might be '" + type.getName() + "' or '" + checkType.getName() + "' (possibly more matches)"); } type = checkType; } return type; } public static String getMainMethodSource(JProgram program) { JMethod mainMethod = findMainMethod(program); return mainMethod.getBody().toSource(); } /** * Tweak this if you want to see the log output. */ private static TreeLogger createTreeLogger() { boolean reallyLog = true; if (reallyLog) { AbstractTreeLogger logger = new PrintWriterTreeLogger(); logger.setMaxDetail(TreeLogger.WARN); return logger; } return TreeLogger.NULL; } protected TreeLogger logger = createTreeLogger(); protected final MockResourceOracle sourceOracle = new MockResourceOracle(); private final List<String> snippetClassDecls = Lists.newArrayList(); private final Set<String> snippetImports = Sets.newTreeSet(); public JJSTestBase() { sourceOracle.add(JavaAstConstructor.getCompilerTypes()); } /** * Adds a snippet of code, for example a field declaration, to the class that * encloses the snippet subsequently passed to * {@link #compileSnippet(String, String, boolean)}. */ protected void addSnippetClassDecl(String...decl) { snippetClassDecls.add(Joiner.on("\n").join(decl)); } /** * Adds an import statement for any code subsequently passed to * {@link #compileSnippet(String, String, boolean)}. */ protected void addSnippetImport(String typeName) { snippetImports.add(typeName); } /** * Returns the program that results from compiling the specified code snippet * as the body of an entry point method. * @param logger a logger where to log, the default logger will be used if null. * @param returnType the return type of the method to compile; use "void" if * the code snippet has no return statement * @param codeSnippet the body of the entry method */ protected JProgram compileSnippet(TreeLogger logger, final String returnType, final String codeSnippet) throws UnableToCompleteException { return compileSnippet(logger, returnType, "", codeSnippet, false); } /** * Returns the program that results from compiling the specified code snippet * as the body of an entry point method. * @param logger a logger where to log, the default logger will be used if null. * @param returnType the return type of the method to compile; use "void" if * the code snippet has no return statement * @param codeSnippet the body of the entry method * @param staticMethod whether to make the method static */ protected JProgram compileSnippet(TreeLogger logger, final String returnType, final String codeSnippet, boolean staticMethod) throws UnableToCompleteException { return compileSnippet(logger, returnType, "", codeSnippet, staticMethod); } /** * Returns the program that results from compiling the specified code snippet * as the body of an entry point method. * @param returnType the return type of the method to compile; use "void" if * the code snippet has no return statement * @param codeSnippet the body of the entry method * @param staticMethod whether to make the method static */ protected JProgram compileSnippet(final String returnType, final String codeSnippet, boolean staticMethod) throws UnableToCompleteException { return compileSnippet(logger, returnType, "", codeSnippet, staticMethod); } /** * Returns the program that results from compiling the specified code snippet * as the body of an entry point method. * @param returnType the return type of the method to compile; use "void" if * the code snippet has no return statement * @param codeSnippet the body of the entry method */ protected JProgram compileSnippet(final String returnType, final String codeSnippet) throws UnableToCompleteException { return compileSnippet(logger, returnType, "", codeSnippet, false); } /** * Returns the program that results from compiling the specified code snippet * as the body of an entry point method. * * @param logger a logger where to log, the default logger will be used if null. * @param returnType the return type of the method to compile; use "void" if * the code snippet has no return statement * @param params the parameter list of the method to compile * @param codeSnippet the body of the entry method * @param staticMethod whether the entryPoint should be static */ protected JProgram compileSnippet(TreeLogger logger, final String returnType, final String params, final String codeSnippet, final boolean staticMethod) throws UnableToCompleteException { sourceOracle.addOrReplace(new MockJavaResource("test.EntryPoint") { @Override public CharSequence getContent() { StringBuilder code = new StringBuilder(); code.append("package test;\n"); for (String snippetImport : snippetImports) { code.append("import " + snippetImport + ";\n"); } code.append("public class EntryPoint {\n"); for (String snippetClassDecl : snippetClassDecls) { code.append(snippetClassDecl + ";\n"); } code.append(" public " + (staticMethod ? "static " : "") + returnType + " onModuleLoad(" + params + ") {\n"); code.append(codeSnippet); code.append(" }\n"); code.append("}\n"); return code; } }); CompilerContext compilerContext = provideCompilerContext(); if (logger == null) { logger = this.logger; } CompilationState state = CompilationStateBuilder.buildFrom(logger, compilerContext, sourceOracle.getResources(), getAdditionalTypeProviderDelegate()); JProgram program = JavaAstConstructor.construct(logger, state, compilerContext, null, "test.EntryPoint", "com.google.gwt.lang.Exceptions"); return program; } /** * Returns a compiler context to be used for compiling code within the test. */ protected CompilerContext provideCompilerContext() { CompilerContext compilerContext = new CompilerContext.Builder().module(new MockModuleDef()) .options(new PrecompileTaskOptionsImpl() { @Override public boolean shouldJDTInlineCompileTimeConstants() { return false; } } ).build(); compilerContext.getOptions().setSourceLevel(sourceLevel); compilerContext.getOptions().setStrict(true); compilerContext.getOptions().setGenerateJsInteropExports(true); return compilerContext; } /** * Return an AdditionalTypeProviderDelegate that will be able to provide * new sources for unknown classnames. */ protected AdditionalTypeProviderDelegate getAdditionalTypeProviderDelegate() { return null; } /** * Java source level compatibility option. */ protected SourceLevel sourceLevel = SourceLevel.DEFAULT_SOURCE_LEVEL; protected static <T> void assertContainsAll(Iterable<T> expectedMethodSnippets, Set<T> actualMethodSnippets) { List<T> missing = FluentIterable.from(expectedMethodSnippets) .filter(Predicates.not(Predicates.in(actualMethodSnippets))) .toList(); assertTrue(missing + " not contained in " + actualMethodSnippets, missing.size() == 0); } protected void assertEqualBlock(String expected, String input) throws UnableToCompleteException { JBlock testExpression = getStatement(input); assertEquals(formatSource("{ " + expected + "}"), formatSource(testExpression.toSource())); } protected static void assertParameterTypes(JMethod method, JType... parameterTypes) { for (int i = 0; i < parameterTypes.length; i++) { JType parameterType = parameterTypes[i]; assertEquals(parameterType, method.getParams().get(i).getType().getUnderlyingType()); } } protected static void assertParameterTypes( final JProgram program, String methodName, String... parameterTypeNames) { JMethod method = findMethod(program, methodName); assertNotNull("Did not find method " + methodName, method); assertEquals(parameterTypeNames.length, method.getParams().size()); JType[] parameterTypes = FluentIterable.from(Arrays.asList(parameterTypeNames)) .transform(new Function<String, JType>() { @Override public JType apply(String typeName) { return findType(program, typeName); } }) .toArray(JType.class); assertParameterTypes(method, parameterTypes); } protected static void assertReturnType( JProgram program, String methodName, String resultTypeName) { JMethod method = findMethod(program, methodName); assertNotNull("Did not find method " + methodName, method); JDeclaredType resultType = program.getFromTypeMap(resultTypeName); assertNotNull("Did not find class " + resultTypeName, resultType); assertEquals(resultType, method.getType().getUnderlyingType()); } public Result assertTransform(String codeSnippet, JVisitor visitor) throws UnableToCompleteException { JProgram program = compileSnippet("void", codeSnippet, true); JMethod mainMethod = findMainMethod(program); visitor.accept(mainMethod); return new Result("void", codeSnippet, mainMethod.getBody().toSource()); } protected JMethod getMethod(JProgram program, String name) { return findMethod(program, name); } protected JReferenceType getType(JProgram program, String name) { return program.getFromTypeMap(name); } protected JBlock getStatement(String statement) throws UnableToCompleteException { JProgram program = compileSnippet("void", statement, false); JMethod mainMethod = findMainMethod(program); JMethodBody body = (JMethodBody) mainMethod.getBody(); return body.getBlock(); } /** * Removes most whitespace while still leaving one space separating words. * * Used to make the assertEquals ignore whitespace (mostly) while still retaining meaningful * output when the test fails. */ protected String formatSource(String source) { return source.replaceAll("\\s+", " ") // substitutes multiple whitespaces into one. .replaceAll("\\s([\\p{Punct}&&[^$]])", "$1") // removes whitespace preceding symbols // (except $ which can be part of an identifier) .replaceAll("([\\p{Punct}&&[^$]])\\s", "$1"); // removes whitespace succeeding symbols. } protected void addAll(Resource... sourceFiles) { for (Resource sourceFile : sourceFiles) { if (sourceFile != null) { sourceOracle.addOrReplace(sourceFile); } } } protected JExpression getExpression(String type, String expression) throws UnableToCompleteException { JProgram program = compileSnippet(type, "return " + expression + ";", false); JMethod mainMethod = findMainMethod(program); JMethodBody body = (JMethodBody) mainMethod.getBody(); JReturnStatement returnStmt = (JReturnStatement) body.getStatements().get(0); return returnStmt.getExpr(); } /** * Holds the result of a optimizations to compare with expected results. */ protected final class Result { private final String optimized; private final String returnType; private final String userCode; public Result(String returnType, String userCode, String optimized) { this.returnType = returnType; this.userCode = userCode; this.optimized = optimized; } public void into(String expected) throws UnableToCompleteException { JProgram program = compileSnippet(returnType, expected, true); expected = getMainMethodSource(program); assertEquals(userCode, expected, optimized); } } }