org.jsweet.transpiler.JSweetTranspiler.java Source code

Java tutorial

Introduction

Here is the source code for org.jsweet.transpiler.JSweetTranspiler.java

Source

/* 
 * JSweet transpiler - http://www.jsweet.org
 * Copyright (C) 2015 CINCHEO SAS <renaud.pawlak@cincheo.fr>
 * 
 * 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 java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.jsweet.transpiler.util.Util.toJavaFileObjects;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;

import org.apache.commons.io.FileUtils;
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.candies.CandiesProcessor;
import org.jsweet.transpiler.typescript.Java2TypeScriptTranslator;
import org.jsweet.transpiler.util.AbstractTreePrinter;
import org.jsweet.transpiler.util.DirectedGraph;
import org.jsweet.transpiler.util.DirectedGraph.Node;
import org.jsweet.transpiler.util.ErrorCountTranspilationHandler;
import org.jsweet.transpiler.util.EvaluationResult;
import org.jsweet.transpiler.util.Position;
import org.jsweet.transpiler.util.ProcessUtil;
import org.jsweet.transpiler.util.SourceMap;
import org.jsweet.transpiler.util.SourceMap.Entry;
import org.jsweet.transpiler.util.Util;

import com.google.debugging.sourcemap.FilePosition;
import com.google.debugging.sourcemap.SourceMapConsumerFactory;
import com.google.debugging.sourcemap.SourceMapFormat;
import com.google.debugging.sourcemap.SourceMapGenerator;
import com.google.debugging.sourcemap.SourceMapGeneratorFactory;
import com.google.debugging.sourcemap.SourceMapGeneratorV3;
import com.google.debugging.sourcemap.SourceMapping;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.gson.Gson;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.BasicDiagnosticFormatter;
import com.sun.tools.javac.util.JCDiagnostic;
import com.sun.tools.javac.util.JavacMessages;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;

/**
 * The actual JSweet transpiler.
 * 
 * <p>
 * Instantiate this class to transpile Java to TypeScript/JavaScript.
 * 
 * @author Renaud Pawlak
 */
public class JSweetTranspiler<C extends JSweetContext> implements JSweetOptions {

    /**
     * The TypeScript version to be installed/used with this version of JSweet
     * (WARNING: so far, having multiple JSweet versions for the same user
     * account may lead to performance issues - could be fixed if necessary).
     */
    public static final String TSC_VERSION = "2.1";

    static {
        JSweetConfig.initClassPath(null);
    }

    /**
     * The constant for the name of the directory that stores temporary files.
     */
    public static final String TMP_WORKING_DIR_NAME = ".jsweet";

    /**
     * A constant that is used for exporting variables.
     * 
     * @see TraceBasedEvaluationResult
     * @see #eval(TranspilationHandler, SourceFile...)
     */
    public static final String EXPORTED_VAR_BEGIN = "EXPORT ";
    /**
     * A constant that is used for exporting variables.
     * 
     * @see TraceBasedEvaluationResult
     * @see #eval(TranspilationHandler, SourceFile...)
     */
    public static final String EXPORTED_VAR_END = ";";
    private static Pattern exportedVarRE = Pattern.compile(EXPORTED_VAR_BEGIN + "(\\w*)=(.*)" + EXPORTED_VAR_END);

    private final static Logger logger = Logger.getLogger(JSweetTranspiler.class);

    /**
     * The name of the file generated in the root package to avoid the
     * TypeScript compiler to skip empty directories.
     */
    public final static String TSCROOTFILE = ".tsc-rootfile.ts";

    private JSweetFactory<C> factory;
    private long transpilationStartTimestamp;
    private ArrayList<File> auxiliaryTsModuleFiles = new ArrayList<>();
    private C context;
    private Options options;
    private JavaFileManager fileManager;
    private JavaCompiler compiler;
    private Log log;
    private CandiesProcessor candiesProcessor;
    private boolean generateSourceMap = false;
    private File workingDir;
    private File tsOutputDir;
    private File jsOutputDir;
    private String classPath;
    private boolean generateJsFiles = true;
    private boolean tscWatchMode = false;
    private File[] tsDefDirs = {};
    private ModuleKind moduleKind = ModuleKind.none;
    private EcmaScriptComplianceLevel ecmaTargetVersion = EcmaScriptComplianceLevel.ES3;
    private boolean bundle = false;
    private String encoding = null;
    private boolean noRootDirectories = false;
    private boolean ignoreAssertions = false;
    private boolean ignoreJavaFileNameError = false;
    private boolean generateDeclarations = false;
    private File declarationsOutputDir;
    private boolean interfaceTracking = true;
    private boolean supportGetClass = true;
    private boolean supportSaticLazyInitialization = true;
    private boolean generateDefinitions = false;
    private ArrayList<File> jsLibFiles = new ArrayList<>();
    private File sourceRoot = null;

    @Override
    public String toString() {
        return "workingDir=" + workingDir + "\ntsOutputDir=" + tsOutputDir + "\njsOutputDir=" + jsOutputDir
                + "\nclassPath=" + classPath + "\ngenerateJsFiles=" + generateJsFiles + "\ntscWatchMode="
                + tscWatchMode + "\ntsDefDirs=" + (tsDefDirs == null ? null : Arrays.asList(tsDefDirs))
                + "\nmoduleKind=" + moduleKind + "\necmaTargertVersion=" + ecmaTargetVersion + "\nbundle=" + bundle
                + "\nencoding=" + encoding + "\nnoRootDirectories=" + noRootDirectories + "\nignoreAssertions="
                + ignoreAssertions + "\nignoreJavaFileNameError=" + ignoreJavaFileNameError
                + "\ngenerateDeclarations=" + generateDeclarations + "\ndeclarationsOutputDir="
                + declarationsOutputDir + "\ninterfaceTracking=" + interfaceTracking + "\nsupportGetClass="
                + supportGetClass + "\nsupportSaticLazyInitialization=" + supportSaticLazyInitialization
                + "\ngenerateDefinitions=" + generateDefinitions + "\njsLibFiles=" + jsLibFiles;
    }

    /**
     * Creates a JSweet transpiler, with the default values.
     * 
     * <p>
     * TypeScript and JavaScript output directories are set to
     * <code>System.getProperty("java.io.tmpdir")</code>. The classpath is set
     * to <code>System.getProperty("java.class.path")</code>.
     * 
     * @param factory
     *            the factory used to create the transpiler objects
     */
    public JSweetTranspiler(JSweetFactory<C> factory) {
        this(factory, new File(System.getProperty("java.io.tmpdir")), null, null,
                System.getProperty("java.class.path"));
    }

    /**
     * Creates a JSweet transpiler.
     * 
     * @param factory
     *            the factory used to create the transpiler objects
     * @param tsOutputDir
     *            the directory where TypeScript files are written
     * @param jsOutputDir
     *            the directory where JavaScript files are written
     * @param extractedCandiesJavascriptDir
     *            see {@link #getExtractedCandyJavascriptDir()}
     * @param classPath
     *            the classpath as a string (check out system-specific
     *            requirements for Java classpathes)
     */
    public JSweetTranspiler(JSweetFactory<C> factory, File tsOutputDir, File jsOutputDir,
            File extractedCandiesJavascriptDir, String classPath) {
        this(factory, new File(TMP_WORKING_DIR_NAME), tsOutputDir, jsOutputDir, extractedCandiesJavascriptDir,
                classPath);
    }

    private Map<String, Map<String, Object>> configuration;

    private void readConfiguration() {
        File confFile = new File(JSweetConfig.CONFIGURATION_FILE_NAME);
        if (confFile.exists()) {
            try {
                logger.info("configuration file found");
                @SuppressWarnings("unchecked")
                Map<String, Map<String, Object>> fromJson = new Gson()
                        .fromJson(FileUtils.readFileToString(confFile), Map.class);
                configuration = fromJson;
                System.out.println(configuration);
            } catch (Exception e) {
                logger.warn("error reading configuration file", e);
            }
        } else {
            logger.info("no configuration file found");
        }
    }

    /**
     * Creates a JSweet transpiler.
     * 
     * @param factory
     *            the factory used to create the transpiler objects
     * @param workingDir
     *            the working directory
     * @param tsOutputDir
     *            the directory where TypeScript files are written
     * @param jsOutputDir
     *            the directory where JavaScript files are written
     * @param extractedCandiesJavascriptDir
     *            see {@link #getExtractedCandyJavascriptDir()}
     * @param classPath
     *            the classpath as a string (check out system-specific
     *            requirements for Java classpaths)
     */
    public JSweetTranspiler(JSweetFactory<C> factory, File workingDir, File tsOutputDir, File jsOutputDir,
            File extractedCandiesJavascriptDir, String classPath) {
        this.factory = factory;
        readConfiguration();
        this.workingDir = workingDir.getAbsoluteFile();
        this.extractedCandyJavascriptDir = extractedCandiesJavascriptDir;
        try {
            tsOutputDir.mkdirs();
            this.tsOutputDir = tsOutputDir.getCanonicalFile();
            if (jsOutputDir != null && generateJsFiles) {
                jsOutputDir.mkdirs();
                this.jsOutputDir = jsOutputDir.getCanonicalFile();
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("cannot locate output dirs", e);
        }
        this.classPath = classPath == null ? System.getProperty("java.class.path") : classPath;
        logger.info("creating transpiler version " + JSweetConfig.getVersionNumber() + " (build date: "
                + JSweetConfig.getBuildDate() + ")");
        logger.info("curent dir: " + new File(".").getAbsolutePath());
        logger.info("tsOut: " + tsOutputDir + (tsOutputDir == null ? "" : " - " + tsOutputDir.getAbsolutePath()));
        logger.info("jsOut: " + jsOutputDir + (jsOutputDir == null ? "" : " - " + jsOutputDir.getAbsolutePath()));
        logger.info("candyJsOut: " + extractedCandiesJavascriptDir);
        logger.debug("compile classpath: " + classPath);
        logger.debug("runtime classpath: " + System.getProperty("java.class.path"));
        this.candiesProcessor = new CandiesProcessor(workingDir, classPath, extractedCandyJavascriptDir);
    }

    /**
     * Gets this transpiler working directory (where the temporary files are
     * stored).
     */
    public File getWorkingDirectory() {
        return this.workingDir;
    }

    public void initNode(TranspilationHandler transpilationHandler) throws Exception {
        ProcessUtil.initNode();
        logger.debug("extra path: " + ProcessUtil.EXTRA_PATH);
        File initFile = new File(workingDir, ".node-init");
        boolean initialized = initFile.exists();
        if (!initialized) {
            ProcessUtil.runCommand(ProcessUtil.NODE_COMMAND, null, () -> {
                transpilationHandler.report(JSweetProblem.NODE_CANNOT_START, null,
                        JSweetProblem.NODE_CANNOT_START.getMessage());
                throw new RuntimeException("cannot find node.js");
            }, "--version");
            initFile.mkdirs();
            initFile.createNewFile();
        }

        String v = "";
        File tscVersionFile = new File(ProcessUtil.NPM_DIR, "tsc-version");
        if (tscVersionFile.exists()) {
            v = FileUtils.readFileToString(tscVersionFile);
        }
        if (!ProcessUtil.isInstalledWithNpm("tsc") || !TSC_VERSION.equals(v.trim())) {
            // this will lead to performances issues if having multiple versions
            // of JSweet installed
            if (ProcessUtil.isInstalledWithNpm("tsc")) {
                ProcessUtil.uninstallNodePackage("typescript", true);
            }
            ProcessUtil.installNodePackage("typescript", TSC_VERSION, true);
            FileUtils.writeStringToFile(tscVersionFile, TSC_VERSION);
        }
    }

    /**
     * Sets one or more directories that contain TypeScript definition files
     * (sub-directories are scanned recursively to find all .d.ts files).
     * 
     * @param tsDefDirs
     *            a list of directories to scan for .d.ts files
     */
    public void setTsDefDirs(File... tsDefDirs) {
        this.tsDefDirs = tsDefDirs;
    }

    /**
     * Adds a directory that contains TypeScript definition files
     * (sub-directories are scanned recursively to find all .d.ts files).
     * 
     * @param tsDefDir
     *            a directory to scan for .d.ts files
     */
    public void addTsDefDir(File tsDefDir) {
        if (!ArrayUtils.contains(tsDefDirs, tsDefDir)) {
            tsDefDirs = ArrayUtils.add(tsDefDirs, tsDefDir);
        }
    }

    /**
     * Undo previous calls to {@link #setTsDefDirs(File...)} and
     * {@link #addTsDefDir(File)}.
     */
    public void clearTsDefDirs() {
        tsDefDirs = new File[0];
    }

    private void initJavac(final TranspilationHandler transpilationHandler) {
        context = factory.createContext(this);
        options = Options.instance(context);
        if (classPath != null) {
            options.put(Option.CLASSPATH, classPath);
            for (String s : classPath.split(File.pathSeparator)) {
                if (s.contains(JSweetConfig.MAVEN_JAVA_OVERRIDE_ARTIFACT)) {
                    context.strictMode = true;
                    options.put(Option.BOOTCLASSPATH, s);
                }
            }
        }
        if (encoding != null) {
            options.put(Option.ENCODING, encoding);
        }
        logger.debug("classpath: " + options.get(Option.CLASSPATH));
        logger.debug("bootclasspath: " + options.get(Option.BOOTCLASSPATH));
        logger.debug("strict mode: " + context.strictMode);
        options.put(Option.XLINT, "path");
        JavacFileManager.preRegister(context);
        fileManager = context.get(JavaFileManager.class);
        compiler = JavaCompiler.instance(context);
        compiler.attrParseOnly = true;
        compiler.verbose = false;
        compiler.genEndPos = true;
        compiler.keepComments = true;
        log = Log.instance(context);
        log.dumpOnError = false;
        log.emitWarnings = false;
        log.setWriters(new PrintWriter(new StringWriter() {
            @Override
            public void write(String str) {
            }
        }));
        log.setDiagnosticFormatter(new BasicDiagnosticFormatter(JavacMessages.instance(context)) {
            @Override
            public String format(JCDiagnostic diagnostic, Locale locale) {
                if (diagnostic.getKind() == Kind.ERROR) {
                    if (!(ignoreJavaFileNameError
                            && "compiler.err.class.public.should.be.in.file".equals(diagnostic.getCode()))) {
                        transpilationHandler.report(JSweetProblem.INTERNAL_JAVA_ERROR,
                                new SourcePosition(new File(diagnostic.getSource().getName()), null,
                                        (int) diagnostic.getLineNumber(), (int) diagnostic.getColumnNumber()),
                                diagnostic.getMessage(locale));
                    }
                }
                switch (diagnostic.getKind()) {
                case ERROR:
                    logger.error(diagnostic);
                    break;
                case WARNING:
                case MANDATORY_WARNING:
                    logger.debug(diagnostic);
                    break;
                case NOTE:
                case OTHER:
                default:
                    logger.trace(diagnostic);
                    break;
                }
                if (diagnostic.getSource() != null) {
                    return diagnostic.getMessage(locale) + " at " + diagnostic.getSource().getName() + "("
                            + diagnostic.getLineNumber() + ")";
                } else {
                    return diagnostic.getMessage(locale);
                }
            }
        });
    }

    private boolean areAllTranspiled(SourceFile... sourceFiles) {
        for (SourceFile file : sourceFiles) {
            if (file.getJsFile() == null) {
                return false;
            }
        }
        return true;
    }

    /**
     * Evaluates the given Java source files with the default JavaScript engine
     * (Nashorn).
     * <p>
     * This function automatically transpile the source files if needed.
     * 
     * @param transpilationHandler
     *            the transpilation handler
     * @param sourceFiles
     *            the source files to be evaluated
     * @return an object that holds the evaluation result
     * @throws Exception
     *             when an internal error occurs
     */
    public EvaluationResult eval(TranspilationHandler transpilationHandler, SourceFile... sourceFiles)
            throws Exception {
        return eval("JavaScript", transpilationHandler, sourceFiles);
    }

    private static class MainMethodFinder extends TreeScanner {
        public MethodSymbol mainMethod;

        public void visitMethodDef(JCMethodDecl methodDecl) {
            MethodSymbol method = methodDecl.sym;
            if ("main(java.lang.String[])".equals(method.toString())) {
                if (method.isStatic()) {
                    mainMethod = method;
                    throw new RuntimeException();
                }
            }
        }
    };

    private void initExportedVarMap() throws Exception {
        Field f = Thread.currentThread().getContextClassLoader().loadClass("jsweet.util.Globals")
                .getDeclaredField("EXPORTED_VARS");
        f.setAccessible(true);
        @SuppressWarnings("unchecked")
        ThreadLocal<Map<String, Object>> exportedVars = (ThreadLocal<Map<String, Object>>) f.get(null);
        exportedVars.set(new HashMap<>());
    }

    private Map<String, Object> getExportedVarMap() throws Exception {
        Field f = Thread.currentThread().getContextClassLoader().loadClass("jsweet.util.Globals")
                .getDeclaredField("EXPORTED_VARS");
        f.setAccessible(true);
        @SuppressWarnings("unchecked")
        ThreadLocal<Map<String, Object>> exportedVars = (ThreadLocal<Map<String, Object>>) f.get(null);
        return new HashMap<>(exportedVars.get());
    }

    /**
     * Evaluates the given source files with the given evaluation engine.
     * <p>
     * If given engine name is "Java", this function looks up for the classes in
     * the classpath and run the main methods when found.
     * 
     * @param engineName
     *            the engine name: either "Java" or any valid and installed
     *            JavaScript engine.
     * @param transpilationHandler
     *            the log handler
     * @param sourceFiles
     *            the source files to be evaluated (transpiled first if needed)
     * @return the evaluation result
     * @throws Exception
     *             when an internal error occurs
     */
    public EvaluationResult eval(String engineName, TranspilationHandler transpilationHandler,
            SourceFile... sourceFiles) throws Exception {
        logger.info("[" + engineName + " engine] eval files: " + Arrays.asList(sourceFiles));
        if ("Java".equals(engineName)) {
            // search for main functions
            JSweetContext context = new JSweetContext(this);
            Options options = Options.instance(context);
            if (classPath != null) {
                options.put(Option.CLASSPATH, classPath);
            }
            options.put(Option.XLINT, "path");
            JavacFileManager.preRegister(context);
            JavaFileManager fileManager = context.get(JavaFileManager.class);

            List<JavaFileObject> fileObjects = toJavaFileObjects(fileManager,
                    Arrays.asList(SourceFile.toFiles(sourceFiles)));

            JavaCompiler compiler = JavaCompiler.instance(context);
            compiler.attrParseOnly = true;
            compiler.verbose = true;
            compiler.genEndPos = false;

            logger.info("parsing: " + fileObjects);
            List<JCCompilationUnit> compilationUnits = compiler.enterTrees(compiler.parseFiles(fileObjects));
            MainMethodFinder mainMethodFinder = new MainMethodFinder();
            try {
                for (JCCompilationUnit cu : compilationUnits) {
                    cu.accept(mainMethodFinder);
                }
            } catch (Exception e) {
                // swallow on purpose
            }
            if (mainMethodFinder.mainMethod != null) {
                try {
                    initExportedVarMap();
                    Class<?> c = Class.forName(
                            mainMethodFinder.mainMethod.getEnclosingElement().getQualifiedName().toString());
                    c.getMethod("main", String[].class).invoke(null, (Object) null);
                } catch (Exception e) {
                    throw new Exception("evalution error", e);
                }
            }

            final Map<String, Object> map = getExportedVarMap();
            return new EvaluationResult() {

                @SuppressWarnings("unchecked")
                @Override
                public <T> T get(String variableName) {
                    return (T) map.get("_exportedVar_" + variableName);
                }

                @Override
                public String toString() {
                    return map.toString();
                }

                @Override
                public String getExecutionTrace() {
                    return "<not available>";
                }
            };
        } else {
            if (!areAllTranspiled(sourceFiles)) {
                ErrorCountTranspilationHandler errorHandler = new ErrorCountTranspilationHandler(
                        transpilationHandler);
                transpile(errorHandler, sourceFiles);
                if (errorHandler.getErrorCount() > 0) {
                    throw new Exception("unable to evaluate: transpilation errors remain");
                }
            }

            StringWriter trace = new StringWriter();

            Process runProcess;
            if (context.useModules) {
                File f = null;
                if (!context.entryFiles.isEmpty()) {
                    f = context.entryFiles.get(0);
                    for (SourceFile sf : sourceFiles) {
                        if (sf.getJavaFile().equals(f)) {
                            f = sf.getJsFile();
                        }
                    }
                }
                if (f == null) {
                    f = sourceFiles[sourceFiles.length - 1].getJsFile();
                }
                logger.info("[modules] eval file: " + f);
                runProcess = ProcessUtil.runCommand(ProcessUtil.NODE_COMMAND, line -> trace.append(line + "\n"),
                        null, f.getPath());
            } else {
                File tmpFile = new File(new File(TMP_WORKING_DIR_NAME), "eval.tmp.js");
                FileUtils.deleteQuietly(tmpFile);
                if (jsLibFiles != null) {
                    for (File jsLibFile : jsLibFiles) {
                        String script = FileUtils.readFileToString(jsLibFile);
                        FileUtils.write(tmpFile, script + "\n", true);
                    }
                }
                for (SourceFile sourceFile : sourceFiles) {
                    String script = FileUtils.readFileToString(sourceFile.getJsFile());
                    FileUtils.write(tmpFile, script + "\n", true);
                }
                logger.info("[no modules] eval file: " + tmpFile);
                runProcess = ProcessUtil.runCommand(ProcessUtil.NODE_COMMAND, line -> trace.append(line + "\n"),
                        null, tmpFile.getPath());
            }

            int returnCode = runProcess.exitValue();
            logger.info("return code=" + returnCode);
            if (returnCode != 0) {
                throw new Exception("evaluation error (code=" + returnCode + ") - trace=" + trace);
            }
            return new TraceBasedEvaluationResult(trace.getBuffer().toString());
        }
    }

    static private class TraceBasedEvaluationResult implements EvaluationResult {
        private String trace;

        public TraceBasedEvaluationResult(String trace) {
            super();
            this.trace = trace;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <T> T get(String variableName) {
            String[] var = null;
            Matcher matcher = exportedVarRE.matcher(trace);
            int index = 0;
            boolean match = true;
            while (match) {
                match = matcher.find(index);
                if (match) {
                    if (variableName.equals(matcher.group(1))) {
                        var = new String[] { matcher.group(1), matcher.group(2) };
                        match = false;

                    }
                    index = matcher.end() - 1;
                }
            }
            if (var == null) {
                return null;
            } else {
                String stringValue = var[1];
                try {
                    return (T) (Integer) Integer.parseInt(stringValue);
                } catch (Exception e1) {
                    try {
                        return (T) (Double) Double.parseDouble(stringValue);
                    } catch (Exception e2) {
                        if ("true".equals(stringValue)) {
                            return (T) Boolean.TRUE;
                        }
                        if ("false".equals(stringValue)) {
                            return (T) Boolean.FALSE;
                        }
                        if ("undefined".equals(stringValue)) {
                            return null;
                        }
                    }
                }
                return (T) stringValue;
            }
        }

        @Override
        public String getExecutionTrace() {
            return trace;
        }
    }

    public List<JCCompilationUnit> setupCompiler(java.util.List<File> files,
            ErrorCountTranspilationHandler transpilationHandler) throws IOException {
        initJavac(transpilationHandler);
        List<JavaFileObject> fileObjects = toJavaFileObjects(fileManager, files);

        logger.info("parsing: " + fileObjects);
        List<JCCompilationUnit> compilationUnits = compiler.enterTrees(compiler.parseFiles(fileObjects));
        if (transpilationHandler.getErrorCount() > 0) {
            return null;
        }
        logger.info("attribution phase");
        compiler.attribute(compiler.todo);

        if (transpilationHandler.getErrorCount() > 0) {
            return null;
        }
        context.useModules = isUsingModules();

        if (context.useModules && bundle) {
            transpilationHandler.report(JSweetProblem.BUNDLE_WITH_MODULE, null,
                    JSweetProblem.BUNDLE_WITH_MODULE.getMessage());
            return null;
        }
        return compilationUnits;
    }

    private String ts2js(ErrorCountTranspilationHandler handler, String tsCode, String targetFileName)
            throws IOException {
        SourceFile sf = new SourceFile(null);
        sf.setTsFile(File.createTempFile(targetFileName, ".ts", tsOutputDir));
        sf.setJsFile(File.createTempFile(targetFileName, ".js", jsOutputDir));
        try {
            sf.tsFile.getParentFile().mkdirs();
            sf.tsFile.createNewFile();
            Files.write(sf.tsFile.toPath(), Arrays.asList(tsCode));
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
        runTSC(handler, new SourceFile[] { sf }, "--target", ecmaTargetVersion.name(), "--outFile",
                sf.getJsFile().toString(), sf.getTsFile().toString());
        try {
            return new String(Files.readAllBytes(sf.jsFile.toPath()));
        } catch (IOException ex) {
            return null;
        }
    }

    /**
     * Transpiles the given Java source files. When the transpiler is in watch
     * mode ({@link #setTscWatchMode(boolean)}), the first invocation to this
     * method determines the files to be watched by the Tsc process.
     * 
     * @param transpilationHandler
     *            the log handler
     * @param files
     *            the files to be transpiled
     * @throws IOException
     */
    synchronized public void transpile(TranspilationHandler transpilationHandler, SourceFile... files)
            throws IOException {
        transpilationStartTimestamp = System.currentTimeMillis();
        try {
            initNode(transpilationHandler);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return;
        }
        candiesProcessor.processCandies(transpilationHandler);
        addTsDefDir(candiesProcessor.getCandiesTsdefsDir());

        ErrorCountTranspilationHandler errorHandler = new ErrorCountTranspilationHandler(transpilationHandler);
        Collection<SourceFile> jsweetSources = asList(files).stream() //
                .filter(source -> source.getJavaFile() != null).collect(toList());
        java2ts(errorHandler, jsweetSources.toArray(new SourceFile[0]));

        if (errorHandler.getErrorCount() == 0 && generateJsFiles) {
            Collection<SourceFile> tsSources = asList(files).stream() //
                    .filter(source -> source.getTsFile() != null).collect(toList());
            ts2js(errorHandler, tsSources.toArray(new SourceFile[0]));
        }

        if (!generateJsFiles) {
            transpilationHandler.onCompleted(this, !isTscWatchMode(), files);
        }

        logger.info("transpilation process finished in "
                + (System.currentTimeMillis() - transpilationStartTimestamp) + " ms");
    }

    private void java2ts(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files)
            throws IOException {
        List<JCCompilationUnit> compilationUnits = setupCompiler(Arrays.asList(SourceFile.toFiles(files)),
                transpilationHandler);
        if (compilationUnits == null) {
            return;
        }
        context.sourceFiles = files;
        factory.createBeforeTranslationScanner(transpilationHandler, context).process(compilationUnits);

        if (context.useModules) {
            generateTsFiles(transpilationHandler, files, compilationUnits);
        } else {
            if (bundle) {
                generateTsBundle(transpilationHandler, files, compilationUnits);
            } else {
                generateTsFiles(transpilationHandler, files, compilationUnits);
            }
        }
        log.flush();
        getOrCreateTscRootFile();
    }

    private void generateModuleDefs(JCCompilationUnit moduleDefs) throws IOException {
        StringBuilder out = new StringBuilder();
        for (String line : FileUtils.readLines(new File(moduleDefs.getSourceFile().getName()))) {
            if (line.startsWith("///")) {
                out.append(line.substring(3));
            }
        }
        FileUtils.write(new File(tsOutputDir, "module_defs.d.ts"), out, false);
    }

    private void generateTsFiles(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files,
            List<JCCompilationUnit> compilationUnits) throws IOException {
        // regular file-to-file generation
        new OverloadScanner<C>(transpilationHandler, context).process(compilationUnits);
        for (int i = 0; i < compilationUnits.length(); i++) {
            JCCompilationUnit cu = compilationUnits.get(i);
            if (isModuleDefsFile(cu)) {
                if (context.useModules) {
                    generateModuleDefs(cu);
                }
                continue;
            }
            logger.info("scanning " + cu.sourcefile.getName() + "...");
            AbstractTreePrinter<C> printer = factory.createTranslator(factory.createAdapter(context),
                    transpilationHandler, context, cu, generateSourceMap);
            printer.print(cu);
            if (StringUtils.isWhitespace(printer.getResult())) {
                continue;
            }
            String[] s = cu.getSourceFile().getName().split(File.separator.equals("\\") ? "\\\\" : File.separator);
            String cuName = s[s.length - 1];
            s = cuName.split("\\.");
            cuName = s[0];
            String javaSourceFileRelativeFullName = (cu.packge.getQualifiedName().toString().replace(".",
                    File.separator) + File.separator + cuName + ".java");
            files[i].javaSourceDirRelativeFile = new File(javaSourceFileRelativeFullName);
            files[i].javaSourceDir = new File(cu.getSourceFile().getName().substring(0,
                    cu.getSourceFile().getName().length() - javaSourceFileRelativeFullName.length()));
            String packageName = isNoRootDirectories() ? context.getRootRelativeJavaName(cu.packge)
                    : cu.packge.getQualifiedName().toString();
            String outputFileRelativePathNoExt = packageName.replace(".", File.separator) + File.separator + cuName;
            String outputFileRelativePath = outputFileRelativePathNoExt
                    + (cu.packge.fullname.toString().startsWith("def.") ? ".d.ts" : ".ts");
            logger.info("output file: " + outputFileRelativePath);
            File outputFile = new File(tsOutputDir, outputFileRelativePath);
            outputFile.getParentFile().mkdirs();
            String outputFilePath = outputFile.getPath();
            PrintWriter out = new PrintWriter(outputFilePath);
            try {
                out.println(printer.getResult());
                out.print(context.getGlobalsMappingString());
                out.print(context.poolFooterStatements());
            } finally {
                out.close();
            }
            files[i].tsFile = outputFile;
            files[i].javaFileLastTranspiled = files[i].getJavaFile().lastModified();
            files[i].setSourceMap(printer.sourceMap);
            if (generateSourceMap && !generateJsFiles) {
                generateTypeScriptSourceMapFile(files[i]);
            }
            logger.info("created " + outputFilePath);
        }
    }

    private void generateTypeScriptSourceMapFile(SourceFile sourceFile) throws IOException {
        if (sourceFile.getSourceMap() == null) {
            return;
        }
        SourceMapGenerator generator = SourceMapGeneratorFactory.getInstance(SourceMapFormat.V3);
        String javaSourceFilePath = sourceFile.getTsFile().getAbsoluteFile().getCanonicalFile().getParentFile()
                .toPath().relativize(sourceFile.getJavaFile().getAbsoluteFile().getCanonicalFile().toPath())
                .toString();
        for (Entry entry : sourceFile.getSourceMap().getSortedEntries(new Comparator<SourceMap.Entry>() {
            @Override
            public int compare(Entry e1, Entry e2) {
                return e1.getOutputPosition().compareTo(e2.getOutputPosition());
            }
        })) {
            generator.addMapping(javaSourceFilePath, null,
                    new FilePosition(entry.getInputPosition().getLine(), entry.getInputPosition().getColumn()),
                    new FilePosition(entry.getOutputPosition().getLine(), entry.getOutputPosition().getColumn()),
                    new FilePosition(entry.getOutputPosition().getLine(),
                            entry.getOutputPosition().getColumn() + 1));
        }
        File outputFile = new File(sourceFile.getTsFile().getPath() + ".map");
        try (FileWriter writer = new FileWriter(outputFile, false)) {
            generator.appendTo(writer, sourceFile.getTsFile().getName());
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private boolean isModuleDefsFile(JCCompilationUnit cu) {
        return cu.getSourceFile().getName().equals("module_defs.java")
                || cu.getSourceFile().getName().endsWith("/module_defs.java");
    }

    private void generateTsBundle(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files,
            List<JCCompilationUnit> compilationUnits) throws IOException {
        if (context.useModules) {
            return;
        }
        StaticInitilializerAnalyzer analizer = new StaticInitilializerAnalyzer(context);
        analizer.process(compilationUnits);
        ArrayList<Node<JCCompilationUnit>> sourcesInCycle = new ArrayList<>();
        java.util.List<JCCompilationUnit> orderedCompilationUnits = analizer.globalStaticInitializersDependencies
                .topologicalSort(n -> {
                    sourcesInCycle.add(n);
                });
        if (!sourcesInCycle.isEmpty()) {
            transpilationHandler.report(JSweetProblem.CYCLE_IN_STATIC_INITIALIZER_DEPENDENCIES, null,
                    JSweetProblem.CYCLE_IN_STATIC_INITIALIZER_DEPENDENCIES.getMessage(sourcesInCycle.stream()
                            .map(n -> n.element.sourcefile.getName()).collect(Collectors.toList())));

            DirectedGraph.dumpCycles(sourcesInCycle, u -> u.sourcefile.getName());

            return;
        }

        new OverloadScanner<C>(transpilationHandler, context).process(orderedCompilationUnits);

        logger.debug("ordered compilation units: " + orderedCompilationUnits.stream().map(cu -> {
            return cu.sourcefile.getName();
        }).collect(Collectors.toList()));
        logger.debug("count: " + compilationUnits.size() + " (initial), " + orderedCompilationUnits.size()
                + " (ordered)");
        int[] permutation = new int[orderedCompilationUnits.size()];
        StringBuilder permutationString = new StringBuilder();
        for (int i = 0; i < orderedCompilationUnits.size(); i++) {
            permutation[i] = compilationUnits.indexOf(orderedCompilationUnits.get(i));
            permutationString.append("" + i + "=" + permutation[i] + ";");
        }
        logger.debug("permutation: " + permutationString.toString());
        createBundle(transpilationHandler, files, permutation, orderedCompilationUnits, false);
        if (isGenerateDefinitions()) {
            createBundle(transpilationHandler, files, permutation, orderedCompilationUnits, true);
        }
    }

    private void initSourceFileJavaPaths(SourceFile file, JCCompilationUnit cu) {
        String[] s = cu.getSourceFile().getName().split(File.separator.equals("\\") ? "\\\\" : File.separator);
        String cuName = s[s.length - 1];
        s = cuName.split("\\.");
        cuName = s[0];

        String javaSourceFileRelativeFullName = (cu.packge.getQualifiedName().toString().replace(".",
                File.separator) + File.separator + cuName + ".java");
        file.javaSourceDirRelativeFile = new File(javaSourceFileRelativeFullName);
        file.javaSourceDir = new File(cu.getSourceFile().getName().substring(0,
                cu.getSourceFile().getName().length() - javaSourceFileRelativeFullName.length()));
    }

    private void createBundle(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files,
            int[] permutation, java.util.List<JCCompilationUnit> orderedCompilationUnits, boolean definitionBundle)
            throws FileNotFoundException {
        context.bundleMode = true;
        StringBuilder sb = new StringBuilder();
        int lineCount = 0;
        for (int i = 0; i < orderedCompilationUnits.size(); i++) {
            JCCompilationUnit cu = orderedCompilationUnits.get(i);
            if (isModuleDefsFile(cu)) {
                continue;
            }
            if (cu.packge.fullname.toString().startsWith("def.")) {
                if (!definitionBundle) {
                    continue;
                }
            } else {
                if (definitionBundle) {
                    continue;
                }
            }
            logger.info("scanning " + cu.sourcefile.getName() + "...");
            AbstractTreePrinter<C> printer = factory.createTranslator(factory.createAdapter(context),
                    transpilationHandler, context, cu, generateSourceMap);
            printer.print(cu);
            printer.sourceMap.shiftOutputPositions(lineCount);
            files[permutation[i]].setSourceMap(printer.sourceMap);

            sb.append(printer.getOutput());
            lineCount += (printer.getCurrentLine() - 1);

            initSourceFileJavaPaths(files[permutation[i]], cu);
        }

        context.bundleMode = false;

        File bundleDirectory = tsOutputDir;
        if (!bundleDirectory.exists()) {
            bundleDirectory.mkdirs();
        }
        String bundleName = "bundle" + (definitionBundle ? ".d.ts" : ".ts");

        File outputFile = new File(bundleDirectory, bundleName);

        logger.info("creating bundle file: " + outputFile);
        outputFile.getParentFile().mkdirs();
        String outputFilePath = outputFile.getPath();
        PrintWriter out = new PrintWriter(outputFilePath);
        try {
            out.println(sb.toString());
            out.print(context.getGlobalsMappingString());
            out.print(context.poolFooterStatements());
            if (definitionBundle && context.getExportedElements() != null) {
                for (java.util.Map.Entry<String, java.util.List<Symbol>> exportedElements : context
                        .getExportedElements().entrySet()) {
                    out.println();
                    out.print("declare module \"" + exportedElements.getKey() + "\"");
                    boolean exported = false;
                    for (Symbol element : exportedElements.getValue()) {
                        if (element instanceof PackageSymbol && !context.isRootPackage(element)) {
                            out.print(" {");
                            out.println();
                            out.print("    export = " + context.getActualName(element) + ";");
                            out.println();
                            out.print("}");
                            exported = true;
                            break;
                        }
                    }
                    if (!exported) {
                        out.print(";");
                    }
                    out.println();
                }
            }
        } finally {
            out.close();
        }
        for (int i = 0; i < orderedCompilationUnits.size(); i++) {
            JCCompilationUnit cu = orderedCompilationUnits.get(i);
            if (cu.packge.fullname.toString().startsWith("def.")) {
                if (!definitionBundle) {
                    continue;
                }
            } else {
                if (definitionBundle) {
                    continue;
                }
            }
            files[permutation[i]].tsFile = outputFile;
            files[permutation[i]].javaFileLastTranspiled = files[permutation[i]].getJavaFile().lastModified();
        }
        logger.info("created " + outputFilePath);

    }

    private File getOrCreateTscRootFile() throws IOException {
        File tscRootFile = new File(tsOutputDir, TSCROOTFILE);

        if (!tscRootFile.exists()) {
            FileUtils.write(tscRootFile, "// Root empty file generated by JSweet to avoid tsc behavior, which\n"
                    + "// does not preserve the entire file hierarchy for empty directories.", false);
        }
        return tscRootFile;
    }

    private static class TscOutput {
        public SourcePosition position;
        public String message;

        @Override
        public String toString() {
            return message + " - " + position;
        }
    }

    private static Pattern errorRE = Pattern.compile("(.*)\\((.*)\\): error TS[0-9]+: (.*)");

    private static TscOutput parseTscOutput(String outputString) {
        Matcher m = errorRE.matcher(outputString);
        TscOutput error = new TscOutput();
        if (m.matches()) {
            String[] pos = m.group(2).split(",");
            error.position = new SourcePosition(new File(m.group(1)), null, Integer.parseInt(pos[0]),
                    Integer.parseInt(pos[1]));
            StringBuilder sb = new StringBuilder(m.group(3));
            sb.setCharAt(0, Character.toLowerCase(sb.charAt(0)));
            if (sb.charAt(sb.length() - 1) == '.') {
                sb.deleteCharAt(sb.length() - 1);
            }
            error.message = sb.toString();
        } else {
            error.message = outputString;
        }
        return error;
    }

    private Process tsCompilationProcess;
    private SourceFile[] watchedFiles;

    private File extractedCandyJavascriptDir;

    private Path relativizeTsFile(File file) {
        try {
            return getTsOutputDir().getAbsoluteFile().getCanonicalFile().toPath()
                    .relativize(file.getAbsoluteFile().getCanonicalFile().toPath());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Returns the watched files when the transpiler is in watch mode. See
     * {@link #setTscWatchMode(boolean)}. The watched file list corresponds to
     * the one given at the first invocation of
     * {@link #transpile(TranspilationHandler, SourceFile...)} after the
     * transpiler was set to watch mode. All subsequent invocations of
     * {@link #transpile(TranspilationHandler, SourceFile...)} will not change
     * the initial watched files. In order to change the watch files, invoke
     * {@link #resetTscWatchMode()} and call
     * {@link #transpile(TranspilationHandler, SourceFile...)} with a new file
     * list.
     */
    synchronized public SourceFile[] getWatchedFiles() {
        return watchedFiles;
    }

    /**
     * Gets the watched files that corresponds to the given Java file. See
     * {@link #setTscWatchMode(boolean)}.
     */
    synchronized public SourceFile getWatchedFile(File javaFile) {
        if (watchedFiles != null) {
            for (SourceFile f : watchedFiles) {
                if (f.getJavaFile().getAbsoluteFile().equals(javaFile.getAbsoluteFile())) {
                    return f;
                }
            }
        }
        return null;
    }

    private void ts2js(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files) throws IOException {
        if (tsCompilationProcess != null && isTscWatchMode()) {
            return;
        }
        if (isTscWatchMode()) {
            watchedFiles = files;
        }

        logger.debug("ts2js: " + Arrays.asList(files));
        LinkedList<String> args = new LinkedList<>();
        if (System.getProperty("os.name").startsWith("Windows")) {
            args.addAll(asList("--target", ecmaTargetVersion.name()));
        } else {
            args.addAll(asList("--target", ecmaTargetVersion.name()));
        }

        if (isUsingModules()) {
            if (ecmaTargetVersion.higherThan(EcmaScriptComplianceLevel.ES5)) {
                logger.warn("cannot use old fashionned modules with ES>5 target");
            } else {
                args.add("--module");
                args.add(moduleKind.toString());
            }
        }

        if (ecmaTargetVersion.ordinal() >= EcmaScriptComplianceLevel.ES5.ordinal()) {
            args.add("--experimentalDecorators");
        }

        if (isTscWatchMode()) {
            args.add("--watch");
        }
        if (isPreserveSourceLineNumbers()) {
            args.add("--sourceMap");
        }
        if (isGenerateDeclarations()) {
            args.add("--declaration");
        }
        args.addAll(asList("--rootDir", tsOutputDir.getAbsolutePath()));
        // args.addAll(asList("--sourceRoot", tsOutputDir.toString()));

        if (jsOutputDir != null) {
            args.addAll(asList("--outDir", jsOutputDir.getAbsolutePath()));
        }
        File tscRootFile = getOrCreateTscRootFile();
        if (tscRootFile.exists()) {
            args.add(relativizeTsFile(tscRootFile).toString());
        }
        for (SourceFile sourceFile : files) {
            String filePath = relativizeTsFile(sourceFile.getTsFile()).toString();
            if (!args.contains(filePath)) {
                args.add(filePath);
            }
        }
        // this may not be necessary because tsc seems to add required modules
        // automatically
        for (File f : auxiliaryTsModuleFiles) {
            String filePath = relativizeTsFile(f).toString();
            if (!args.contains(filePath)) {
                args.add(filePath);
            }
        }

        for (File dir : tsDefDirs) {
            LinkedList<File> tsDefFiles = new LinkedList<>();
            Util.addFiles(".d.ts", dir, tsDefFiles);
            for (File f : tsDefFiles) {
                args.add(relativizeTsFile(f).toString());
            }
        }

        LinkedList<File> tsDefFiles = new LinkedList<>();
        Util.addFiles(".d.ts", tsOutputDir, tsDefFiles);
        for (File f : tsDefFiles) {
            args.add(relativizeTsFile(f).toString());
        }

        try {
            logger.info("launching tsc...");
            runTSC(transpilationHandler, files, args.toArray(new String[0]));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void runTSC(ErrorCountTranspilationHandler transpilationHandler, SourceFile[] files, String... args) {
        boolean[] fullPass = { true };

        tsCompilationProcess = ProcessUtil.runCommand("tsc", getTsOutputDir(), isTscWatchMode(), line -> {
            logger.info(line);
            TscOutput output = parseTscOutput(line);
            if (output.position != null) {
                SourcePosition position = SourceFile.findOriginPosition(output.position, Arrays.asList(files));
                if (position == null) {
                    transpilationHandler.report(JSweetProblem.INTERNAL_TSC_ERROR, output.position, output.message);
                } else {
                    transpilationHandler.report(JSweetProblem.MAPPED_TSC_ERROR, position, output.message);
                }
            } else {
                if (output.message.startsWith("message TS6042:")) {
                    onTsTranspilationCompleted(fullPass[0], transpilationHandler, files);
                    fullPass[0] = false;
                } else {
                    // TODO enhance tsc feedbacks support: some
                    // messages are swallowed here: for instance
                    // error TS1204: Cannot compile modules into
                    // 'commonjs', 'amd', 'system' or 'umd' when
                    // targeting 'ES6' or higher.
                }
            }
        }, process -> {
            tsCompilationProcess = null;
            onTsTranspilationCompleted(fullPass[0], transpilationHandler, files);
            fullPass[0] = false;
        }, () -> {
            if (transpilationHandler.getProblemCount() == 0) {
                transpilationHandler.report(JSweetProblem.INTERNAL_TSC_ERROR, null, "Unknown tsc error");
            }
        }, args);

        // tsCompilationProcess.waitFor();
        // if (tsCompilationProcess != null &&
        // tsCompilationProcess.exitValue() == 1) {
        // transpilationHandler.report(JSweetProblem.TSC_CANNOT_START, null,
        // JSweetProblem.TSC_CANNOT_START.getMessage());
        // }
    }

    private void onTsTranspilationCompleted(boolean fullPass, ErrorCountTranspilationHandler handler,
            SourceFile[] files) {
        try {
            if (isGenerateDeclarations()) {
                if (getDeclarationsOutputDir() != null) {
                    logger.info("moving d.ts files to " + getDeclarationsOutputDir());
                    LinkedList<File> dtsFiles = new LinkedList<File>();
                    File rootDir = jsOutputDir == null ? tsOutputDir : jsOutputDir;
                    Util.addFiles(".d.ts", rootDir, dtsFiles);
                    for (File dtsFile : dtsFiles) {
                        String relativePath = Util.getRelativePath(rootDir.getAbsolutePath(),
                                dtsFile.getAbsolutePath());
                        File targetFile = new File(getDeclarationsOutputDir(), relativePath);
                        logger.info("moving " + dtsFile + " to " + targetFile);
                        if (targetFile.exists()) {
                            FileUtils.deleteQuietly(targetFile);
                        }
                        try {
                            FileUtils.moveFile(dtsFile, targetFile);
                        } catch (Exception e) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
            }
            if (handler.getErrorCount() == 0) {
                Set<File> handledFiles = new HashSet<>();
                for (SourceFile sourceFile : files) {
                    if (!sourceFile.getTsFile().getAbsolutePath().startsWith(tsOutputDir.getAbsolutePath())) {
                        throw new RuntimeException("ts directory isn't configured properly, please use setTsDir: "
                                + sourceFile.getTsFile().getAbsolutePath() + " != "
                                + tsOutputDir.getAbsolutePath());
                    }
                    String outputFileRelativePath = sourceFile.getTsFile().getAbsolutePath()
                            .substring(tsOutputDir.getAbsolutePath().length());
                    File outputFile = new File(jsOutputDir == null ? tsOutputDir : jsOutputDir,
                            Util.removeExtension(outputFileRelativePath) + ".js");
                    sourceFile.jsFile = outputFile;
                    if (outputFile.lastModified() > sourceFile.jsFileLastTranspiled) {
                        if (handledFiles.contains(outputFile)) {
                            continue;
                        }
                        handledFiles.add(outputFile);
                        logger.info("js output file: " + outputFile);
                        File mapFile = new File(outputFile.getAbsolutePath() + ".map");

                        if (mapFile.exists() && generateSourceMap) {

                            SourceMapGeneratorV3 generator = (SourceMapGeneratorV3) SourceMapGeneratorFactory
                                    .getInstance(SourceMapFormat.V3);
                            Path javaSourcePath = sourceFile.javaSourceDir.getCanonicalFile().toPath();
                            String sourceRoot = getSourceRoot() != null ? getSourceRoot().toString()
                                    : sourceFile.getJsFile().getParentFile().getCanonicalFile().toPath()
                                            .relativize(javaSourcePath) + "/";
                            generator.setSourceRoot(sourceRoot);

                            sourceFile.jsMapFile = mapFile;
                            logger.info("redirecting map file: " + mapFile);
                            String contents = FileUtils.readFileToString(mapFile);
                            SourceMapping mapping = SourceMapConsumerFactory.parse(contents);

                            int line = 1;
                            int columnIndex = 0;
                            for (String lineContent : FileUtils.readLines(outputFile, (Charset) null)) {
                                columnIndex = 0;
                                while (columnIndex < lineContent.length() && (lineContent.charAt(columnIndex) == ' '
                                        || lineContent.charAt(columnIndex) == '\t')) {
                                    columnIndex++;
                                }

                                OriginalMapping originalMapping = mapping.getMappingForLine(line, columnIndex + 1);
                                if (originalMapping != null) {
                                    // TODO: this is quite slow and should be
                                    // optimized
                                    SourcePosition originPosition = SourceFile
                                            .findOriginPosition(new SourcePosition(sourceFile.tsFile, null,
                                                    new Position(originalMapping.getLineNumber(),
                                                            originalMapping.getColumnPosition())),
                                                    files);
                                    if (originPosition != null) {
                                        // as a first approximation, we only map
                                        // line numbers (ignore columns)
                                        generator.addMapping(
                                                javaSourcePath.relativize(
                                                        originPosition.getFile().getCanonicalFile().toPath())
                                                        .toString(),
                                                null, new FilePosition(originPosition.getStartLine() - 1, 0),
                                                new FilePosition(line - 1, 0),
                                                new FilePosition(line - 1, lineContent.length() - 1));
                                    }
                                }

                                line++;
                            }

                            try (FileWriter writer = new FileWriter(mapFile, false)) {
                                generator.appendTo(writer, outputFile.getName());
                            } catch (Exception ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            handler.onCompleted(this, fullPass, files);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#isPreserveSourceLineNumbers()
     */
    @Override
    public boolean isPreserveSourceLineNumbers() {
        return generateSourceMap;
    }

    /**
     * Sets the flag that tells if the transpiler preserves the generated
     * TypeScript source line numbers wrt the Java original source file (allows
     * for Java debugging through js.map files).
     */
    public void setPreserveSourceLineNumbers(boolean preserveSourceLineNumbers) {
        this.generateSourceMap = preserveSourceLineNumbers;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#getTsOutputDir()
     */
    @Override
    public File getTsOutputDir() {
        return tsOutputDir;
    }

    /**
     * Sets the current TypeScript output directory.
     */
    public void setTsOutputDir(File tsOutputDir) {
        this.tsOutputDir = tsOutputDir;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#getJsOutputDir()
     */
    @Override
    public File getJsOutputDir() {
        return jsOutputDir;
    }

    /**
     * Sets the current JavaScript output directory.
     */
    public void setJsOutputDir(File jsOutputDir) {
        this.jsOutputDir = jsOutputDir;
    }

    /**
     * Tells if the JavaScript generation is enabled/disabled.
     */
    @Override
    public boolean isGenerateJsFiles() {
        return generateJsFiles;
    }

    /**
     * Sets the flag to enable/disable JavaScript generation.
     */
    public void setGenerateJsFiles(boolean generateJsFiles) {
        this.generateJsFiles = generateJsFiles;
    }

    /**
     * Tells if this transpiler is using a Tsc watch process to automatically
     * regenerate the javascript when one of the source file changes.
     */
    synchronized public boolean isTscWatchMode() {
        return tscWatchMode;
    }

    /**
     * Enables or disable this transpiler watch mode. When watch mode is
     * enabled, the first invocation to
     * {@link #transpile(TranspilationHandler, SourceFile...)} will start the
     * Tsc watch process, which regenerates the JavaScript files when one of the
     * input file changes.
     * 
     * @param tscWatchMode
     *            true: enables the watch mode (do nothing is already enabled),
     *            false: disables the watch mode and stops the current Tsc
     *            watching process
     * @see #getWatchedFile(File)
     */
    synchronized public void setTscWatchMode(boolean tscWatchMode) {
        this.tscWatchMode = tscWatchMode;
        if (!tscWatchMode) {
            if (tsCompilationProcess != null) {
                tsCompilationProcess.destroyForcibly();
                while (tsCompilationProcess != null && tsCompilationProcess.isAlive()) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        logger.error(e.getMessage(), e);
                    }
                    logger.error("tsc did not terminate");
                }
                try {
                    if (tsCompilationProcess != null) {
                        tsCompilationProcess.waitFor();
                    }
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                tsCompilationProcess = null;
                watchedFiles = null;
            }
        }
    }

    /**
     * Resets the watch mode (clears the watched files and restarts the Tsc
     * process on the next invocation of
     * {@link #transpile(TranspilationHandler, SourceFile...)}).
     */
    synchronized public void resetTscWatchMode() {
        setTscWatchMode(false);
        setTscWatchMode(true);
    }

    /**
     * Gets the candies processor.
     */
    public CandiesProcessor getCandiesProcessor() {
        return candiesProcessor;
    }

    /**
     * Sets target ECMA script version for generated JavaScript
     * 
     * @param ecmaTargetVersion
     *            The target version
     */
    public void setEcmaTargetVersion(EcmaScriptComplianceLevel ecmaTargetVersion) {
        this.ecmaTargetVersion = ecmaTargetVersion;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#getModuleKind()
     */
    @Override
    public ModuleKind getModuleKind() {
        return moduleKind;
    }

    /**
     * Sets the module kind when transpiling to code using JavaScript modules.
     */
    public void setModuleKind(ModuleKind moduleKind) {
        this.moduleKind = moduleKind;
    }

    /**
     * Tells if this transpiler transpiles to code using JavaScript modules.
     */
    public boolean isUsingModules() {
        return moduleKind != null && moduleKind != ModuleKind.none;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#isBundle()
     */
    @Override
    public boolean isBundle() {
        return bundle;
    }

    /**
     * Sets this transpiler to generate JavaScript bundles for running in a Web
     * browser.
     */
    public void setBundle(boolean bundle) {
        this.bundle = bundle;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#getEncoding()
     */
    @Override
    public String getEncoding() {
        return encoding;
    }

    /**
     * Sets the expected Java source code encoding.
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#isNoRootDirectories()
     */
    @Override
    public boolean isNoRootDirectories() {
        return noRootDirectories;
    }

    /**
     * Sets this transpiler to skip the root directories (packages annotated
     * with @jsweet.lang.Root) so that the generated file hierarchy starts at
     * the root directories rather than including the entire directory
     * structure.
     */
    public void setNoRootDirectories(boolean noRootDirectories) {
        this.noRootDirectories = noRootDirectories;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#isIgnoreAssertions()
     */
    @Override
    public boolean isIgnoreAssertions() {
        return ignoreAssertions;
    }

    /**
     * Sets the transpiler to ignore the 'assert' statements or generate
     * appropriate code.
     */
    public void setIgnoreAssertions(boolean ignoreAssertions) {
        this.ignoreAssertions = ignoreAssertions;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jsweet.transpiler.JSweetOptions#isIgnoreJavaFileNameError()
     */
    @Override
    public boolean isIgnoreJavaFileNameError() {
        return ignoreJavaFileNameError;
    }

    public void setIgnoreJavaFileNameError(boolean ignoreJavaFileNameError) {
        this.ignoreJavaFileNameError = ignoreJavaFileNameError;
    }

    @Override
    public boolean isGenerateDeclarations() {
        return generateDeclarations;
    }

    public void setGenerateDeclarations(boolean generateDeclarations) {
        this.generateDeclarations = generateDeclarations;
    }

    @Override
    public File getDeclarationsOutputDir() {
        return declarationsOutputDir;
    }

    public void setDeclarationsOutputDir(File declarationsOutputDir) {
        this.declarationsOutputDir = declarationsOutputDir;
    }

    @Override
    public File getExtractedCandyJavascriptDir() {
        return extractedCandyJavascriptDir;
    }

    /**
     * Add JavaScript libraries that are used for the JavaScript evaluation.
     * 
     * @see #eval(TranspilationHandler, SourceFile...)
     */
    public void addJsLibFiles(File... files) {
        jsLibFiles.addAll(Arrays.asList(files));
    }

    /**
     * Clears JavaScript libraries that are used for the JavaScript evaluation.
     * 
     * @see #eval(TranspilationHandler, SourceFile...)
     */
    public void clearJsLibFiles() {
        jsLibFiles.clear();
    }

    /**
     * Transpiles the given Java AST.
     * 
     * @param transpilationHandler
     *            the log handler
     * @param tree
     *            the AST to be transpiled
     * @param targetFileName
     *            the name of the file (without any extension) where to put the
     *            transpilation output
     * @throws IOException
     */
    public String transpile(ErrorCountTranspilationHandler handler, JCTree tree, String targetFileName)
            throws IOException {
        Java2TypeScriptTranslator<C> translator = factory.createTranslator(factory.createAdapter(context), handler,
                context, null, false);
        translator.enterScope();
        translator.scan(tree);
        translator.exitScope();
        String tsCode = translator.getResult();
        return ts2js(handler, tsCode, targetFileName);
    }

    @Override
    public boolean isInterfaceTracking() {
        return interfaceTracking;
    }

    public void setInterfaceTracking(boolean interfaceTracking) {
        this.interfaceTracking = interfaceTracking;
    }

    @Override
    public boolean isSupportGetClass() {
        return supportGetClass;
    }

    public void setSupportGetClass(boolean supportGetClass) {
        this.supportGetClass = supportGetClass;
    }

    @Override
    public boolean isSupportSaticLazyInitialization() {
        return supportSaticLazyInitialization;
    }

    public void setSupportSaticLazyInitialization(boolean supportSaticLazyInitialization) {
        this.supportSaticLazyInitialization = supportSaticLazyInitialization;
    }

    @Override
    public boolean isGenerateDefinitions() {
        return generateDefinitions;
    }

    public void setGenerateDefinitions(boolean generateDefinitions) {
        this.generateDefinitions = generateDefinitions;
    }

    @Override
    public File getSourceRoot() {
        return sourceRoot;
    }

    public void setSourceRoot(File sourceRoot) {
        this.sourceRoot = sourceRoot;
    }

    @Override
    public Map<String, Map<String, Object>> getConfiguration() {
        return configuration;
    }

}