org.jsweet.transpiler.JSweetContext Maven / Gradle / Ivy
/*
* JSweet transpiler - http://www.jsweet.org
* Copyright (C) 2015 CINCHEO SAS
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jsweet.transpiler;
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.File;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.jsweet.JSweetConfig;
import org.jsweet.transpiler.OverloadScanner.Overload;
import org.jsweet.transpiler.extension.AnnotationManager;
import org.jsweet.transpiler.extension.AnnotationManager.Action;
import org.jsweet.transpiler.model.ExtendedElement;
import org.jsweet.transpiler.util.DirectedGraph;
import org.jsweet.transpiler.util.Util;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Attribute.Compound;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCExpressionStatement;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCNewClass;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.JCTree.JCWildcard;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Names;
/**
* The transpiler context, which is an extension of the Java compiler context.
*
* @author Renaud Pawlak
*/
public class JSweetContext extends Context {
protected static Logger logger = Logger.getLogger(Java2TypeScriptTranslator.class);
private static class AnnotationFilterDescriptor {
public final Collection inclusionPatterns;
public final Collection exclusionPatterns;
public final String parameter;
public AnnotationFilterDescriptor(Collection inclusionPatterns, Collection exclusionPatterns,
String parameter) {
super();
this.inclusionPatterns = inclusionPatterns;
this.exclusionPatterns = exclusionPatterns;
this.parameter = parameter;
}
@Override
public String toString() {
return "FILTER" + (parameter == null ? "" : "('" + parameter + "')") + ": INCLUDES=" + inclusionPatterns
+ ", EXCLUDES=" + exclusionPatterns;
}
}
private Map jdkSubclasses = new HashMap<>();
public StaticInitilializerAnalyzer referenceAnalyzer;
/**
* Maps the name of a class to the JDK type it extends.
*/
public void addJdkSubclass(String subclassName, TypeMirror extendedJdkClass) {
jdkSubclasses.put(subclassName, extendedJdkClass);
}
/**
* Maps the name of a class to the JDK type it extends.
*/
public Map getJdkSubclasses() {
return jdkSubclasses;
}
/**
* Gets the JDK type extended by the given subclass, if any.
*/
public TypeMirror getJdkSuperclass(String subclassName, Set excludedJdkTypes) {
TypeMirror jdkType = jdkSubclasses.get(subclassName);
if (jdkType == null) {
return null;
} else {
if (!excludedJdkTypes.contains(jdkType.toString())) {
return jdkType;
} else {
return null;
}
}
}
private List annotationManagers = new ArrayList<>();
private Map typesMapping = new HashMap();
private List> functionalTypesMapping = new ArrayList<>();
protected Map langTypesMapping = new HashMap();
protected Set langTypesSimpleNames = new HashSet();
protected Set baseThrowables = new HashSet();
private Map globalMethods = new HashMap<>();
private Map decoratorAnnotations = new HashMap<>();
/**
* Registers a global method in the context.
*
* @see #lookupGlobalMethod(String)
*/
public void registerGlobalMethod(JCClassDecl owner, JCMethodDecl method) {
String name = method.sym.getEnclosingElement().getQualifiedName().toString();
name += "." + method.name;
name = name.replace(JSweetConfig.GLOBALS_CLASS_NAME + ".", "");
globalMethods.put(name, new JCTree[] { owner, method });
}
/**
* Looks up a registered method from its fully qualified name.
*
* @param fullyQualifiedName
* fully qualified owning type name + "." + method name
* @return an array containing the class and method AST objects of any
* matching method (in theory several methods could match because of
* overloading but we ignore it here)
* @see #registerGlobalMethod(JCMethodDecl)
*/
public JCTree[] lookupGlobalMethod(String fullyQualifiedName) {
return globalMethods.get(fullyQualifiedName);
}
/**
* Registers a decorator annotation in the context.
*/
public void registerDecoratorAnnotation(JCClassDecl annotationDeclaration) {
decoratorAnnotations.put(annotationDeclaration.sym.getQualifiedName().toString(), annotationDeclaration);
}
/**
* Registers a decorator annotation in the context.
*/
public JCClassDecl lookupDecoratorAnnotation(String fullyQualifiedName) {
return decoratorAnnotations.get(fullyQualifiedName);
}
/**
* Adds a type mapping in the context.
*
* @param sourceTypeName
* the fully qualified name of the source type
* @param targetTypeName
* the fully Qualified name of the type the source type is mapped
* to
*/
public final void addTypeMapping(String sourceTypeName, String targetTypeName) {
typesMapping.put(sourceTypeName, targetTypeName);
}
/**
* Adds a set of name-based type mappings. This method is equivalent to
* calling {@link #addTypeMapping(String, String)} for each entry of the
* given map.
*/
public final void addTypeMappings(Map nameMappings) {
typesMapping.putAll(nameMappings);
}
/**
* Returns true if the given type name is mapped through the
* {@link #addTypeMapping(String, String)} or
* {@link #addTypeMapping(String, String)} function.
*/
public final boolean isMappedType(String sourceTypeName) {
return typesMapping.containsKey(sourceTypeName);
}
/**
* Returns the type the given type name is mapped through the
* {@link #addTypeMapping(String, String)} or
* {@link #addTypeMapping(String, String)} function.
*/
public final String getTypeMappingTarget(String sourceTypeName) {
return typesMapping.get(sourceTypeName);
}
/**
* Adds a functional type mapping.
*
* @param mappingFunction
* a function that takes the type tree, the type name, and
* returns a mapped type (either under the form of a string, or
* of a string, or of another type tree).
*/
public final void addTypeMapping(BiFunction mappingFunction) {
functionalTypesMapping.add(mappingFunction);
}
/**
* Returns the functional type mappings.
*/
public final List> getFunctionalTypeMappings() {
return functionalTypesMapping;
}
/**
* Adds an annotation manager that will tune (add or remove) annotations on
* the AST. Lastly added managers have precedence over firstly added ones.
*/
public final void addAnnotationManager(AnnotationManager annotationManager) {
annotationManagers.add(annotationManager);
}
/**
* Removes the given annotation manager from the existing annotation manager
* chain.
*
* @see #addAnnotationManager(AnnotationManager)
*/
public final void removeAnnotationManager(AnnotationManager annotationManager) {
annotationManagers.remove(annotationManager);
}
private static boolean testStringAt(StringBuilder sb, int i, String string) {
if (i < 0) {
return false;
} else if (i + string.length() > sb.length()) {
return false;
} else {
return sb.subSequence(i, i + string.length()).equals(string);
}
}
private static String toRegexp(String pattern) {
boolean argsEnv = false;
StringBuilder sb = new StringBuilder(pattern);
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
switch (c) {
case '(':
argsEnv = true;
sb.insert(i++, '\\');
break;
case ')':
argsEnv = false;
sb.insert(i++, '\\');
break;
case '.':
if (testStringAt(sb, i + 1, ".")) {
sb.deleteCharAt(i);
sb.deleteCharAt(i);
sb.insert(i++, ".*");
} else {
sb.insert(i++, '\\');
}
break;
case '*':
if (testStringAt(sb, i + 1, "*")) {
sb.deleteCharAt(i);
sb.deleteCharAt(i);
sb.insert(i++, ".*");
} else {
sb.deleteCharAt(i);
if (argsEnv) {
sb.insert(i, "[^,]*");
i += 4;
} else {
sb.insert(i, "[^.]*");
i += 4;
}
}
break;
}
}
return sb.toString();
}
private Pattern annotationWithParameterPattern = Pattern.compile("@([^(]*)\\((.*)\\)");
private Map> annotationFilters = new HashMap<>();
private Collection getAnnotationFilterDescriptors(String annotationType) {
Collection descrs = annotationFilters.get(annotationType);
if (descrs == null) {
descrs = new ArrayList<>();
annotationFilters.put(annotationType, descrs);
}
return descrs;
}
private boolean hasAnnotationFilters() {
return !annotationFilters.isEmpty();
}
/**
* Creates a new JSweet transpilation context.
*
* @param options
* the JSweet transpilation options
*/
public JSweetContext(JSweetOptions options) {
this.options = options;
if (options.getConfiguration() != null) {
for (Entry entry : options.getConfiguration().entrySet()) {
addConfigurationEntry(entry);
}
}
for (Entry> e : annotationFilters.entrySet()) {
logger.info("annotation filter descriptor: " + e);
}
}
/**
* Adds an annotation on the AST through global filters.
*
*
* Example:
*
*
* context.addAnnotation(FunctionalInterface.class, "*.MyInterface");
*
*
*
* Filters are simplified regular expressions matching on the Java AST.
* Special characters are the following:
*
*
* - *: matches any token/identifier in the signature of the AST element
*
* - **: matches any list of tokens in signature of the AST element (same
* as ..)
* - ..: matches any list of tokens in signature of the AST element (same
* as **)
* - !: negates the filter (first character only)
*
*
*
* For example, to match:
*
*
* - all the elements in the x.y.z package: x.y.z.*
* - all the elements and subelements (fields, methods, ...) in the x.y.z
* package: x.y.z.**
* - all the methods in the x.y.z.A class: x.y.z.A.*(..)
* - all the methods taking 2 arguments in the x.y.z.A class:
* x.y.z.A.*(*,*)
* - all fields called aField in all the classes: **.aField
*
*
* @param annotationType
* the annotation type
* @param filters
* the annotation is activated if one of the filters match and no
* negative filter matches
*/
public final void addAnnotation(Class annotationType, String... filters) {
addAnnotation(annotationType.getName(), filters);
}
/**
* Adds an annotation on the AST through global filters.
*
* The annotation to be added is described by its type and by a value, which
* is passed as is to the annotation's value. If the annotation type does
* not accept a value parameter, no annotations will be added.
*
* @see #addAnnotation(String, String...)
*/
public final void addAnnotationWithValue(Class annotationType, Object value,
String... filters) {
addAnnotation(annotationType.getName() + "('" + value.toString() + "')", filters);
}
/**
* Adds an annotation on the AST through global filters.
*
*
* Example:
*
*
* context.addAnnotation("@Erased", "*.writeObject(*)");
* context.addAnnotation("@Name('newName')", "*.MyDeclarationToBeRenamed");
*
*
*
* Filters are simplified regular expressions matching on the Java AST.
* Special characters are the following:
*
*
* - *: matches any character in the signature of the AST element
* - !: negates the filter (first character only)
*
*
* @param annotationDescriptor
* the annotation type name, optionally preceded with a @, and
* optionally defining a value (fully qualified name is not
* necessary for JSweet annotations)
* @param filters
* the annotation is activated if one of the filters match and no
* negative filter matches
*/
public final void addAnnotation(String annotationDescriptor, String... filters) {
if (!annotationDescriptor.startsWith("@")) {
annotationDescriptor = "@" + annotationDescriptor;
}
Map map = new HashMap<>();
Entry entry = new SimpleEntry<>(annotationDescriptor, map);
for (String filter : filters) {
String listKind = filter.startsWith("!") ? "exclude" : "include";
@SuppressWarnings("unchecked")
List list = (List) map.get(listKind);
if (list == null) {
list = new ArrayList<>();
map.put(listKind, list);
}
list.add(listKind.equals("exclude") ? filter.substring(1).trim() : filter.trim());
}
addConfigurationEntry(entry);
}
private void addConfigurationEntry(Entry untypedEntry) {
if (untypedEntry.getKey().startsWith("@")) {
@SuppressWarnings("unchecked")
Entry> entry = (Entry>) (Object) untypedEntry;
String annotationType = null;
Matcher m = annotationWithParameterPattern.matcher(entry.getKey());
String parameter = null;
if (m.matches()) {
if (m.group(1).contains(".")) {
annotationType = m.group(1);
} else {
annotationType = JSweetConfig.LANG_PACKAGE + "." + m.group(1);
}
parameter = m.group(2);
} else {
if (entry.getKey().contains(".")) {
annotationType = entry.getKey().substring(1);
} else {
annotationType = JSweetConfig.LANG_PACKAGE + "." + entry.getKey().substring(1);
}
}
Object include = entry.getValue().get("include");
Collection filterDescriptors = getAnnotationFilterDescriptors(annotationType);
Collection inclusionPatterns = null;
Collection exclusionPatterns = null;
if (include != null) {
inclusionPatterns = new ArrayList<>();
if (include instanceof Collection) {
for (Object o : (Collection) include) {
try {
inclusionPatterns.add(Pattern.compile(toRegexp(o.toString())));
} catch (Exception e) {
logger.warn("invalid pattern '" + o + "' for " + entry.getKey() + ".include");
}
}
} else {
try {
inclusionPatterns.add(Pattern.compile(toRegexp(include.toString())));
} catch (Exception e) {
logger.warn("invalid pattern '" + include + "' for " + entry.getKey() + ".include");
}
}
} else {
logger.warn("annotation entry " + entry.getKey() + " does not have a mandatory 'include' entry");
}
Object exclude = entry.getValue().get("exclude");
if (exclude != null) {
exclusionPatterns = new ArrayList<>();
if (exclude instanceof Collection) {
for (Object o : (Collection) exclude) {
try {
exclusionPatterns.add(Pattern.compile(toRegexp(o.toString())));
} catch (Exception e) {
logger.warn("invalid pattern '" + o + "' for " + entry.getKey() + ".exclude");
}
}
} else {
try {
exclusionPatterns.add(Pattern.compile(toRegexp(exclude.toString())));
} catch (Exception e) {
logger.warn("invalid pattern '" + exclude + "' for " + entry.getKey() + ".exclude");
}
}
}
filterDescriptors.add(new AnnotationFilterDescriptor(inclusionPatterns, exclusionPatterns, parameter));
} else {
switch (untypedEntry.getKey()) {
case "typeMapping":
@SuppressWarnings("unchecked")
Entry> entry = (Entry>) (Object) untypedEntry;
for (Entry e : entry.getValue().entrySet()) {
addTypeMapping(e.getKey(), (String) e.getValue());
}
break;
}
}
}
private boolean usingJavaRuntime = false;
/**
* JSweet transpilation options.
*/
public final JSweetOptions options;
/**
* A cache of method overloads.
*
* @see OverloadScanner
* @see OverloadScanner.Overload
*/
private Map> overloads = new HashMap<>();
/**
* A cache of static method overloads.
*
* @see OverloadScanner
* @see OverloadScanner.Overload
*/
private Map> staticOverloads = new HashMap<>();
/**
* Dump all the overloads gathered by {@link OverloadScanner}.
*/
public void dumpOverloads(PrintStream out) {
for (Entry> e1 : overloads.entrySet()) {
out.println("*** " + e1.getKey());
for (Entry e2 : e1.getValue().entrySet()) {
out.println(" - " + e2.getValue());
}
}
for (Entry> e1 : staticOverloads.entrySet()) {
out.println("*** " + e1.getKey() + " [STATIC]");
for (Entry e2 : e1.getValue().entrySet()) {
out.println(" - " + e2.getValue());
}
}
}
/**
* Returns all the overloads in this context.
*/
public Set getAllOverloads() {
Set result = new HashSet<>();
overloads.values().forEach(m -> result.addAll(m.values()));
staticOverloads.values().forEach(m -> result.addAll(m.values()));
return result;
}
/**
* Gets or create an overload instance for the given class and method.
*/
public Overload getOrCreateOverload(TypeElement type, ExecutableElement executable) {
ClassSymbol clazz = (ClassSymbol) type;
MethodSymbol method = (MethodSymbol) executable;
Map> actualOverloads = method.isStatic() ? staticOverloads : overloads;
Map m = actualOverloads.get(clazz);
if (m == null) {
m = new HashMap<>();
actualOverloads.put(clazz, m);
}
String name = method.name.toString();
Overload overload = m.get(name);
if (overload == null) {
overload = new Overload();
overload.methodName = name;
m.put(name, overload);
}
return overload;
}
/**
* Gets an overload instance for the given class and method.
*/
public Overload getOverload(TypeElement type, ExecutableElement executable) {
ClassSymbol clazz = (ClassSymbol) type;
MethodSymbol method = (MethodSymbol) executable;
Map> actualOverloads = method.isStatic() ? staticOverloads : overloads;
Map m = actualOverloads.get(clazz);
if (m == null) {
return null;
}
Overload overload = m.get(method.name.toString());
if (overload == null) {
return null;
}
return overload;
}
/**
* Tells if that method is part of an invalid overload in its declaring
* class.
*/
public boolean isInvalidOverload(ExecutableElement method) {
Overload overload = getOverload((ClassSymbol) method.getEnclosingElement(), method);
return overload != null && overload.methods.size() > 1 && !overload.isValid;
}
/**
* Contains the classes that have a wrong constructor overload.
*/
public Set classesWithWrongConstructorOverload = new HashSet<>();
/**
* The Java compiler symbol table for fast access.
*/
public Symtab symtab;
/**
* The Java compiler names for fast access.
*/
public Names names;
/**
* The Java compiler types for fast access.
*/
public Types types;
public javax.lang.model.util.Types modelTypes;
/**
* A flag to tell if the transpiler is in module mode or not.
*/
public boolean useModules = false;
/**
* A flag to tell if the transpiler should transpiler modules to old
* fashioned require instructions rather than the ES2015 flavored syntax.
* import foo = require("foo"); instead of import * as foo from 'foo';
*/
public boolean useRequireForModules = true;
/**
* The source files that are being transpiled.
*/
public SourceFile[] sourceFiles;
/**
* The source file paths that are being used for transpilation but not
* generated.
*/
public Set excludedSourcePaths;
/**
* Returns true if the source path corresponds to a source file that is part
* of the extra input (not generated).
*/
public boolean isExcludedSourcePath(String sourcePath) {
return excludedSourcePaths != null && excludedSourcePaths.contains(sourcePath);
}
/**
* The compilation units that correspond to the source files.
*/
public JCCompilationUnit[] compilationUnits;
private List usedModules = new ArrayList<>();
/**
* A flag that indicates if we are building a bundle. In bundle mode, static
* fields will be initialized at the end of the bundle.
*/
public boolean bundleMode = false;
public boolean moduleBundleMode = false;
/**
* Holds all the static fields that are lazy intitialized.
*/
public Set lazyInitializedStatics = new HashSet<>();
private Map staticInitializerCounts = new HashMap<>();
/**
* Increments the count of static initialization blocks for the given class.
*/
public void countStaticInitializer(ClassSymbol clazz) {
staticInitializerCounts.put(clazz,
(staticInitializerCounts.containsKey(clazz) ? staticInitializerCounts.get(clazz) : 0) + 1);
}
/**
* Gets the static initializer count for the given class.
*/
public int getStaticInitializerCount(ClassSymbol clazz) {
Integer count = null;
return (count = staticInitializerCounts.get(clazz)) == null ? 0 : count;
}
/**
* Register a module that is used by the transpiled program.
*
* @param moduleName
* the module being used
*/
public void registerUsedModule(String moduleName) {
if (!usedModules.contains(moduleName)) {
usedModules.add(moduleName);
}
}
/**
* The list of modules used by the transpiled program.
*/
public List getUsedModules() {
return usedModules;
}
private Map> importedNamesInModules = new HashMap<>();
private Map> importedElementsInModules = new HashMap<>();
/**
* Register a name that is imported by the given package of the transpiled
* program.
*
*
* import targetName = require("sourceName");
*
*
* @param moduleName
* the module that is importing the name
* @param sourceElement
* the source element if any (null if not applicable)
* @param targetName
* the target name being imported
*/
public void registerImportedName(String moduleName, Symbol sourceElement, String targetName) {
Set importedNames = importedNamesInModules.get(moduleName);
if (importedNames == null) {
importedNames = new HashSet<>();
importedNamesInModules.put(moduleName, importedNames);
}
if (!importedNames.contains(targetName)) {
importedNames.add(targetName);
}
if (sourceElement != null) {
Map importedElements = importedElementsInModules.get(moduleName);
if (importedElements == null) {
importedElements = new HashMap<>();
importedElementsInModules.put(moduleName, importedElements);
}
if (!importedElements.containsKey(sourceElement)) {
importedElements.put(sourceElement, targetName);
}
}
}
/**
* The list of names imported by the given module of the transpiled program.
*/
public Set getImportedNames(String moduleName) {
Set importedNames = importedNamesInModules.get(moduleName);
if (importedNames == null) {
importedNames = new HashSet<>();
importedNamesInModules.put(moduleName, importedNames);
}
return importedNames;
}
/**
* The list of package names imported by the given m of the transpiled
* program.
*/
public Map getImportedElements(String moduleName) {
Map importedElements = importedElementsInModules.get(moduleName);
if (importedElements == null) {
importedElements = new HashMap<>();
importedElementsInModules.put(moduleName, importedElements);
}
return importedElements;
}
/**
* Clears the names imported by the given module.
*/
public void clearImportedNames(String moduleName) {
Set importedNames = new HashSet<>();
importedNamesInModules.put(moduleName, importedNames);
Map importedModulesForNames = new HashMap<>();
importedElementsInModules.put(moduleName, importedModulesForNames);
}
private Map> exportedElements = new HashMap<>();
private Map exportedNames = new HashMap<>();
/**
* Gets the exported elements for all the modules defined in the program.
*/
public Map> getExportedElements() {
return exportedElements;
}
/**
* Returns the idenfier of the given exported symbol, including Module
* annotation's name if specified
*/
public String getExportedElementName(Symbol exportedElement) {
String name = exportedNames.get(exportedElement);
String forcedName = getAnnotationValue(exportedElement, JSweetConfig.ANNOTATION_MODULE, "exportedElement",
String.class, null);
if (StringUtils.isNotBlank(forcedName)) {
name = forcedName;
}
return name;
}
/**
* Adds an exported element for a module.
*/
public void addExportedElement(String moduleName, Symbol exportedElement, JCCompilationUnit compilationUnit) {
List exportedNamesForModule = exportedElements.get(moduleName);
if (exportedNamesForModule == null) {
exportedNamesForModule = new ArrayList();
exportedElements.put(moduleName, exportedNamesForModule);
}
exportedNames.put(exportedElement, getRootRelativeName(
useModules ? getImportedElements(compilationUnit.getSourceFile().getName()) : null, exportedElement));
exportedNamesForModule.add(exportedElement);
}
/**
* Source files containing a main method.
*/
public List entryFiles = new ArrayList<>();
/**
* A graph containing the module dependencies when using modules (empty
* otherwise).
*/
public DirectedGraph packageDependencies = new DirectedGraph<>();
/**
* Stores the root package names (i.e. packages contained in the default
* package or in a package annotated with the {@link jsweet.lang.Root}
* annotation).
*/
public Set topLevelPackageNames = new HashSet<>();
/**
* Store root packages (i.e. packages contained in the default package or in
* a package annotated with the {@link jsweet.lang.Root} annotation,
* including null, i.e. default package).
*/
public HashSet rootPackages = new HashSet<>();
/**
* A flag to keep track of wether a multiple root packages problem was
* already reported (shall report only once).
*/
public boolean reportedMultipleRootPackages = false;
/**
* Globally imported name (in the global namespace).
*/
public Set globalImports = new HashSet<>();
/**
* Imported top packages (used to avoid clashes with local variables when
* bundle is on).
*/
public Set importedTopPackages = new HashSet<>();
/**
* A flag that indicates if the transpilation is in "strict" mode, which
* means that the jsweet-core-strict
jar is in the classpath.
*/
public boolean strictMode = false;
/**
* A flag that indicates if the transpiler should transform old-fashionned
* apply method to lambda or use the new convention ($apply)
*/
public boolean deprecatedApply = false;
private List> footerStatements = new LinkedList>();
/**
* Clears the footer statements.
*/
public void clearFooterStatements() {
footerStatements.clear();
}
/**
* Gets the footer statements.
*/
public String getFooterStatements() {
StringBuilder sb = new StringBuilder();
for (Entry footerStatement : footerStatements) {
sb.append("\n");
sb.append(footerStatement.getValue());
sb.append("\n");
}
return sb.toString();
}
/**
* Adds a footer statement.
*/
public void addFooterStatement(String key, String footerStatement) {
footerStatements.add(new AbstractMap.SimpleEntry(key, footerStatement));
}
/**
* Adds a footer statement.
*/
public void addFooterStatement(String footerStatement) {
footerStatements.add(new AbstractMap.SimpleEntry("", footerStatement));
}
/**
* Adds a footer statement at the first position.
*/
public void addTopFooterStatement(String key, String footerStatement) {
footerStatements.add(0, new AbstractMap.SimpleEntry(key, footerStatement));
}
/**
* Adds a footer statement at the first position.
*/
public void addTopFooterStatement(String footerStatement) {
footerStatements.add(0, new AbstractMap.SimpleEntry("", footerStatement));
}
/**
* A flag to force import generation at the top of the file.
*/
public boolean forceTopImports() {
forceTopImports = true;
return forceTopImports;
}
private boolean forceTopImports = false;
private Map headers = new LinkedHashMap();
/**
* Clear the headers.
*/
public void clearHeaders() {
headers.clear();
forceTopImports = false;
}
/**
* Gets the headers.
*/
public String getHeaders() {
if (forceTopImports) {
for (Entry statement : new ArrayList<>(footerStatements)) {
if (statement.getKey().startsWith("import.")) {
footerStatements.remove(statement);
headers.put(statement.getKey(), statement.getValue());
}
}
}
StringBuilder sb = new StringBuilder();
if (!headers.isEmpty()) {
for (String header : headers.values()) {
sb.append(header);
}
sb.append("\n");
}
return sb.toString();
}
/**
* Sets a header (associated to a key).
*
* @see #getHeader(String)
*/
public void addHeader(String key, String header) {
headers.put(key, header);
}
/**
* Returns a header for the given key (null if never set).
*
* @see #addHeader(String, String)
*/
public String getHeader(String key) {
return headers.get(key);
}
private Map globalsMapping = new HashMap<>();
/**
* Adds a globals mapping.
*/
public void addGlobalsMapping(String from, String to) {
globalsMapping.put(from, to);
}
/**
* Returns the TypeScript string for globals mapping.
*/
public String getGlobalsMappingString() {
StringBuilder b = new StringBuilder();
for (Map.Entry e : globalsMapping.entrySet()) {
b.append("var " + e.getValue() + " = " + e.getKey() + ";\n");
}
return b.toString();
}
private Map>> defaultMethods = new HashMap<>();
private Map defaultMethodsCompilationUnits = new HashMap<>();
/**
* Gets the default methods declared in the given type.
*/
public Set> getDefaultMethods(TypeSymbol type) {
return defaultMethods.get(type);
}
/**
* Stores a default method AST for the given type.
*/
public void addDefaultMethod(JCCompilationUnit compilationUnit, JCClassDecl type, JCMethodDecl defaultMethod) {
Set> methods = defaultMethods.get(type.sym);
if (methods == null) {
methods = new HashSet<>();
defaultMethods.put(type.sym, methods);
}
methods.add(new AbstractMap.SimpleEntry<>(type, defaultMethod));
defaultMethodsCompilationUnits.put(defaultMethod, compilationUnit);
}
/**
* Gets the compilation unit the given default method belongs to.
*/
public JCCompilationUnit getDefaultMethodCompilationUnit(JCMethodDecl defaultMethod) {
return defaultMethodsCompilationUnits.get(defaultMethod);
}
private Map fieldNameMapping = new HashMap<>();
/**
* Adds a name mapping to a field (rename it to avoid name clashes).
*/
public void addFieldNameMapping(VarSymbol field, String name) {
fieldNameMapping.put(field, name);
}
/**
* Gets a field name mapping if any (null otherwise).
*/
public String getFieldNameMapping(Symbol field) {
return fieldNameMapping.get(field);
}
/**
* Tells if the given field has a field name mapping.
*
* @see #addFieldNameMapping(VarSymbol, String)
* @see #getFieldNameMapping(Symbol)
*/
public boolean hasFieldNameMapping(Symbol field) {
return fieldNameMapping.containsKey(field);
}
private Map classNameMapping = new HashMap<>();
/**
* Adds a name mapping to a class (rename it to avoid name clashes).
*/
public void addClassNameMapping(ClassSymbol clazz, String name) {
classNameMapping.put(clazz, name);
}
/**
* Gets a class name mapping if any (null otherwise).
*/
public String getClassNameMapping(Symbol clazz) {
return classNameMapping.get(clazz);
}
/**
* Tells if the given class has a class name mapping.
*
* @see #addClassNameMapping(ClassSymbol, String)
* @see #getClassNameMapping(Symbol)
*/
public boolean hasClassNameMapping(Symbol clazz) {
return classNameMapping.containsKey(clazz);
}
/**
* Tells JSweet to ignore wildcard bounds. For instance if ignored:
*
*
* void f(C c)
*
*
* will transpile to:
*
*
* f(c: C)
*
*
* otherwise:
*
*
* void f(C c)
*
*
* will transpile to:
*
*
* f<__T1 extends string>(c: C<__T1>)
*
*
*/
public boolean ignoreWildcardBounds = true;
private Map wildcardNames = new HashMap<>();
private Map> wildcards = new HashMap<>();
/**
* Registers a wilcard for a given container (type parameterized element).
*/
public void registerWildcard(Symbol holder, JCWildcard wildcard) {
if (wildcard.getBound() == null) {
return;
}
List l = wildcards.get(holder);
if (l == null) {
l = new ArrayList<>();
wildcards.put(holder, l);
}
l.add(wildcard);
wildcardNames.put(wildcard, "__T" + l.size());
}
/**
* Gets the wildcard name if any.
*/
public String getWildcardName(JCWildcard wildcard) {
return wildcardNames.get(wildcard);
}
/**
* Gets the registered wildcards for the given type parameter holder.
*/
public List getWildcards(Symbol holder) {
return wildcards.get(holder);
}
private static Pattern libPackagePattern = Pattern.compile(JSweetConfig.LIBS_PACKAGE + "\\.[^.]*");
/**
* Returns true if the given symbol is a root package (annotated with @Root
* or a definition package).
*/
public boolean isRootPackage(Element element) {
Symbol symbol = (Symbol) element;
return hasAnnotationType(symbol, JSweetConfig.ANNOTATION_ROOT) || (symbol instanceof PackageSymbol
&& libPackagePattern.matcher(symbol.getQualifiedName().toString()).matches());
}
/**
* Tells if the given type is a Java interface.
*/
public boolean isInterface(TypeSymbol typeSymbol) {
return (typeSymbol.type.isInterface() || hasAnnotationType(typeSymbol, JSweetConfig.ANNOTATION_INTERFACE));
}
/**
* Tells if the given symbol is annotated with one of the given annotation
* types.
*/
public boolean hasAnnotationType(Symbol symbol, String... annotationTypes) {
String[] types = annotationTypes;
for (AnnotationManager annotationIntrospector : annotationManagers) {
for (String annotationType : types) {
Action state = annotationIntrospector.manageAnnotation(symbol, annotationType);
if (state == Action.ADD) {
return true;
} else if (state == Action.REMOVE) {
types = ArrayUtils.removeElement(annotationTypes, annotationType);
}
}
}
if (hasAnnotationFilters()) {
String signature = symbol.toString();
if (!(symbol instanceof TypeSymbol) && symbol.getEnclosingElement() != null) {
signature = symbol.getEnclosingElement().getQualifiedName().toString() + "." + signature;
}
for (String annotationType : annotationTypes) {
Collection filterDescriptors = annotationFilters.get(annotationType);
if (filterDescriptors != null) {
for (AnnotationFilterDescriptor filterDescriptor : filterDescriptors) {
if (filterDescriptor.inclusionPatterns == null) {
logger.error("no inclusion patterns found for annotation filter: " + annotationType);
}
for (Pattern include : filterDescriptor.inclusionPatterns) {
if (include.matcher(signature).matches()) {
boolean excluded = false;
Collection excludePatterns = filterDescriptor.exclusionPatterns;
if (excludePatterns != null) {
for (Pattern exclude : excludePatterns) {
if (exclude.matcher(signature).matches()) {
excluded = true;
break;
}
}
}
if (!excluded) {
return true;
}
}
}
}
}
}
}
return hasActualAnnotationType(symbol, annotationTypes);
}
/**
* Gets the actual name of a symbol from a JSweet convention, so including
* potential jsweet.lang.Name
annotation.
*/
public String getActualName(Symbol symbol) {
String name = symbol.getSimpleName().toString();
if (hasAnnotationType(symbol, JSweetConfig.ANNOTATION_NAME)) {
String originalName = getAnnotationValue(symbol, JSweetConfig.ANNOTATION_NAME, String.class, null);
if (!isBlank(originalName)) {
name = originalName;
}
}
return name;
}
private void getRootRelativeName(Map nameMapping, StringBuilder sb, Symbol symbol) {
if (useModules && symbol instanceof PackageSymbol
&& !symbol.toString().startsWith(JSweetConfig.LIBS_PACKAGE + ".")) {
return;
}
if (!isRootPackage(symbol)) {
if (sb.length() > 0 && !"".equals(symbol.toString())) {
sb.insert(0, ".");
}
String name = symbol.getSimpleName().toString();
if (nameMapping != null && nameMapping.containsKey(symbol)) {
name = nameMapping.get(symbol);
} else {
if (hasAnnotationType(symbol, JSweetConfig.ANNOTATION_NAME)) {
String originalName = getAnnotationValue(symbol, JSweetConfig.ANNOTATION_NAME, String.class, null);
if (!isBlank(originalName)) {
name = originalName;
}
}
}
sb.insert(0, name);
symbol = (symbol instanceof PackageSymbol) ? ((PackageSymbol) symbol).owner : symbol.getEnclosingElement();
if (symbol != null) {
getRootRelativeName(nameMapping, sb, symbol);
}
}
}
/**
* Gets the top-level package enclosing the given symbol. The top-level
* package is the one that is enclosed within a root package (see
* jsweet.lang.Root
) or the one in the default (unnamed)
* package.
*/
public PackageSymbol getTopLevelPackage(Symbol symbol) {
if ((symbol instanceof PackageSymbol) && isRootPackage(symbol)) {
return null;
}
Symbol parent = (symbol instanceof PackageSymbol) ? ((PackageSymbol) symbol).owner
: symbol.getEnclosingElement();
if (parent != null && isRootPackage(parent)) {
if (symbol instanceof PackageSymbol) {
return (PackageSymbol) symbol;
} else {
return null;
}
} else {
if (parent == null || (parent instanceof PackageSymbol && StringUtils.isBlank(parent.getSimpleName()))) {
if (symbol instanceof PackageSymbol) {
return (PackageSymbol) symbol;
} else {
return null;
}
} else {
return getTopLevelPackage(parent);
}
}
}
/**
* Finds the first (including itself) enclosing package annotated
* with @Root.
*/
public PackageSymbol getFirstEnclosingRootPackage(PackageSymbol packageSymbol) {
if (packageSymbol == null) {
return null;
}
if (isRootPackage(packageSymbol)) {
return packageSymbol;
}
return getFirstEnclosingRootPackage((PackageSymbol) packageSymbol.owner);
}
private void getRootRelativeJavaName(StringBuilder sb, Symbol symbol) {
if (!isRootPackage(symbol)) {
if (sb.length() > 0 && !"".equals(symbol.toString())) {
sb.insert(0, ".");
}
String name = symbol.getSimpleName().toString();
sb.insert(0, name);
symbol = (symbol instanceof PackageSymbol) ? ((PackageSymbol) symbol).owner : symbol.getEnclosingElement();
if (symbol != null) {
getRootRelativeJavaName(sb, symbol);
}
}
}
/**
* Gets the qualified name of a symbol relatively to the root package
* (potentially annotated with jsweet.lang.Root
).
*
* @param nameMapping
* a map to redirect names
* @param symbol
* the symbol to get the name of
* @param useJavaNames
* if true uses plain Java names, if false uses
* jsweet.lang.Name
annotations
* @return
*/
public String getRootRelativeName(Map nameMapping, Symbol symbol, boolean useJavaNames) {
if (useJavaNames) {
return getRootRelativeJavaName(symbol);
} else {
return getRootRelativeName(nameMapping, symbol);
}
}
/**
* Gets the qualified name of a symbol relatively to the root package
* (potentially annotated with jsweet.lang.Root
). This function
* takes into account potential jsweet.lang.Name
annotations).
*/
public String getRootRelativeName(Map nameMapping, Symbol symbol) {
StringBuilder sb = new StringBuilder();
getRootRelativeName(nameMapping, sb, symbol);
if (sb.length() > 0 && sb.charAt(0) == '.') {
sb.deleteCharAt(0);
}
return sb.toString();
}
/**
* Gets the qualified name of a symbol relatively to the root package
* (potentially annotated with jsweet.lang.Root
). This function
* ignores jsweet.lang.Name
annotations).
*/
public String getRootRelativeJavaName(Symbol symbol) {
StringBuilder sb = new StringBuilder();
getRootRelativeJavaName(sb, symbol);
return sb.toString();
}
/**
* Gets the first value of the 'value' property for the given annotation
* type if found on the given symbol.
*/
public final T getAnnotationValue(Symbol symbol, String annotationType, Class propertyClass,
T defaultValue) {
return getAnnotationValue(symbol, annotationType, null, propertyClass, defaultValue);
}
/**
* Gets the first value of the given property for the given annotation type
* if found on the given symbol.
*/
@SuppressWarnings("unchecked")
public T getAnnotationValue(Symbol symbol, String annotationType, String propertyName, Class propertyClass,
T defaultValue) {
for (AnnotationManager annotationIntrospector : annotationManagers) {
T value = annotationIntrospector.getAnnotationValue(symbol, annotationType, propertyName, propertyClass,
defaultValue);
if (value != null) {
return value;
}
}
if (hasAnnotationFilters()) {
String signature = symbol.toString();
if (symbol.getEnclosingElement() != null) {
signature = symbol.getEnclosingElement().getQualifiedName().toString() + "." + signature;
}
Collection filterDescriptors = annotationFilters.get(annotationType);
if (filterDescriptors != null) {
for (AnnotationFilterDescriptor filterDescriptor : filterDescriptors) {
for (Pattern include : filterDescriptor.inclusionPatterns) {
if (include.matcher(signature).matches()) {
boolean excluded = false;
Collection excludePatterns = filterDescriptor.exclusionPatterns;
if (excludePatterns != null) {
for (Pattern exclude : excludePatterns) {
if (exclude.matcher(signature).matches()) {
excluded = true;
break;
}
}
}
if (!excluded) {
if (filterDescriptor.parameter == null) {
return defaultValue;
} else if (filterDescriptor.parameter.startsWith("'")) {
return (T) filterDescriptor.parameter.substring(1,
filterDescriptor.parameter.length() - 1);
} else if (filterDescriptor.parameter.endsWith(".class")) {
return (T) filterDescriptor.parameter.substring(0,
filterDescriptor.parameter.length() - 6);
} else {
return (T) filterDescriptor.parameter;
}
}
}
}
}
}
}
AnnotationMirror anno = getAnnotation(symbol, annotationType);
T val = defaultValue;
if (anno != null) {
T firstVal = getFirstAnnotationValue(anno, propertyName, propertyClass, null);
if (firstVal != null) {
val = firstVal;
}
}
return val;
}
/**
* Gets the first value of the 'value' property.
*/
@SuppressWarnings("unchecked")
private static T getFirstAnnotationValue(AnnotationMirror annotation, String propertyName,
Class propertyClass, T defaultValue) {
for (Map.Entry valueEntry : annotation
.getElementValues().entrySet()) {
if (propertyName == null || propertyName.equals(valueEntry.getKey().getSimpleName().toString())) {
if (propertyClass.isArray()) {
List