com.google.dart.compiler.backend.js.ClosureJsBackend.java Source code

Java tutorial

Introduction

Here is the source code for com.google.dart.compiler.backend.js.ClosureJsBackend.java

Source

// Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

package com.google.dart.compiler.backend.js;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import com.google.common.io.Closeables;
import com.google.common.io.LimitInputStream;
import com.google.dart.compiler.DartCompilationError;
import com.google.dart.compiler.DartCompilerContext;
import com.google.dart.compiler.DartSource;
import com.google.dart.compiler.LibrarySource;
import com.google.dart.compiler.Source;
import com.google.dart.compiler.ast.DartUnit;
import com.google.dart.compiler.ast.LibraryNode;
import com.google.dart.compiler.ast.LibraryUnit;
import com.google.dart.compiler.backend.js.ast.JsProgram;
import com.google.dart.compiler.common.SourceInfo;
import com.google.dart.compiler.metrics.CompilerMetrics;
import com.google.dart.compiler.resolver.CoreTypeProvider;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerInput;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.DiagnosticGroups;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.JSModule;
import com.google.javascript.jscomp.JSSourceFile;
import com.google.javascript.jscomp.PropertyRenamingPolicy;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceAst;
import com.google.javascript.jscomp.SourceMap.DetailLevel;
import com.google.javascript.jscomp.SourceMap.Format;
import com.google.javascript.jscomp.VariableRenamingPolicy;
import com.google.javascript.jscomp.WarningLevel;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * A compiler backend that produces raw Javascript.
 * @author johnlenz@google.com (John Lenz)
 */
public class ClosureJsBackend extends AbstractJsBackend {
    private static final String EXTENSION_OPT_JS = "opt.js";
    private static final String EXTENSION_OPT_JS_SRC_MAP = "opt.js.map";

    // A map of possible input sources to use when building the optimized output.
    private Map<String, DartUnit> dartSrcToUnitMap = Maps.newHashMap();
    private long totalJsOutputCharCount;

    // Generate "readable" output for debugging
    private final boolean generateHumanReadableOutput;
    // Generate "good" instead of "best" output.
    private final boolean fastOutput;
    // TODO(johnlenz): Currently we can only support incremential builds
    // if we aren't building source maps.
    private final boolean incremental;

    // Validate the generated JavaScript
    private final boolean validate;

    // Whether the generated code is "checked".
    private final boolean checkedMode;

    public ClosureJsBackend() {
        this(false, false);
    }

    /**
     * @param generateHumanReadableOutput - generates human readable javascript output.
     */
    public ClosureJsBackend(boolean checkedMode, boolean generateHumanReadableOutput) {
        // Current default settings
        this(false, false, true, checkedMode, generateHumanReadableOutput);
    }

    public ClosureJsBackend(boolean fastOutput, boolean incremental, boolean validate, boolean checkedMode,
            boolean generateHumanReadableOutput) {
        this.fastOutput = fastOutput;
        // can't currently produce a valid source map incrementally
        this.incremental = incremental;
        this.validate = validate;
        this.checkedMode = checkedMode;
        this.generateHumanReadableOutput = generateHumanReadableOutput;
    }

    @Override
    public boolean isOutOfDate(DartSource src, DartCompilerContext context) {
        if (!incremental) {
            return true;
        } else {
            return super.isOutOfDate(src, context);
        }
    }

    @Override
    public void compileUnit(DartUnit unit, DartSource src, DartCompilerContext context,
            CoreTypeProvider typeProvider) throws IOException {
        if (!incremental) {
            dartSrcToUnitMap.put(src.getName(), unit);
        } else {
            super.compileUnit(unit, src, context, typeProvider);
        }
    }

    private Map<String, CompilerInput> createClosureJsAst(Map<String, JsProgram> parts, Source source) {
        String name = source.getName();
        Preconditions.checkState(name != null && !name.isEmpty(), "A source name is required");

        Map<String, CompilerInput> translatedParts = new HashMap<String, CompilerInput>();
        for (Map.Entry<String, JsProgram> part : parts.entrySet()) {
            String partName = part.getKey();
            String inputName = name + ':' + partName;
            SourceAst sourceAst = new ClosureJsAst(part.getValue(), inputName, source, validate);
            CompilerInput input = new CompilerInput(sourceAst, false);
            translatedParts.put(part.getKey(), input);
        }
        return translatedParts;
    }

    private class DepsWritingCallback implements DepsCallback {
        private final DartCompilerContext context;
        private final CoreTypeProvider typeProvider;
        private final List<CompilerInput> inputs;
        private final Map<String, Source> sourcesByName;
        private final Map<DartUnit, Map<String, CompilerInput>> translatedUnits = Maps.newHashMap();

        DepsWritingCallback(DartCompilerContext context, CoreTypeProvider typeProvider, List<CompilerInput> inputs,
                Map<String, Source> sourcesByName) {
            this.context = context;
            this.typeProvider = typeProvider;
            this.inputs = inputs;
            this.sourcesByName = sourcesByName;
        }

        @Override
        public void visitNative(LibraryUnit libUnit, LibraryNode node) throws IOException {
            String name = node.getText();
            DartSource nativeSrc = libUnit.getSource().getSourceFor(name);
            StringWriter w = new StringWriter();
            Reader r = nativeSrc.getSourceReader();
            CharStreams.copy(r, w);
            inputs.add(new CompilerInput(createSource(name, w), false));
        }

        @Override
        public void visitPart(Part part) throws IOException {
            DartSource src = part.unit.getSource();
            if (!incremental) {
                Map<String, CompilerInput> translatedParts = translatedUnits.get(part.unit);
                if (translatedParts == null) {
                    assert !sourcesByName.containsKey(src.getName());
                    sourcesByName.put(src.getName(), src);
                    Preconditions.checkNotNull(part.unit, "src: " + src.getName());
                    translatedParts = translateUnit(part.unit, src, context, typeProvider);
                    Preconditions.checkState(!translatedUnits.containsKey(part.unit));
                    translatedUnits.put(part.unit, translatedParts);
                }
                inputs.add(translatedParts.get(part.part));
                return;
            }

            Reader r = context.getArtifactReader(src, part.part, EXTENSION_JS);
            if (r == null) {
                return;
            }
            StringWriter w = new StringWriter();
            CharStreams.copy(r, w);
            String inputName = src.getName() + ':' + part.part;
            inputs.add(new CompilerInput(createSource(inputName, w), false));
        }

        private JSSourceFile createSource(String name, Writer w) {
            return JSSourceFile.fromCode(name, w.toString());
        }
    }

    private void packageAppOptimized(LibrarySource app, Collection<LibraryUnit> libraries,
            DartCompilerContext context, CoreTypeProvider typeProvider) throws IOException {

        List<CompilerInput> inputs = Lists.newLinkedList();
        Map<String, Source> sourcesByName = Maps.newHashMap();
        DepsWritingCallback callback = new DepsWritingCallback(context, typeProvider, inputs, sourcesByName);
        DependencyBuilder.build(context.getAppLibraryUnit(), callback);

        // Lastly, add the entry point.
        inputs.add(getCompilerInputForEntry(context));

        // Currently, there is only a single module, add all the sources to it.
        JSModule mainModule = new JSModule("main");
        for (CompilerInput input : inputs) {
            if (input != null) {
                mainModule.add(input);
            }
        }

        Writer out = context.getArtifactWriter(app, "", getAppExtension());
        boolean failed = true;
        try {
            compileModule(app, context, mainModule, sourcesByName, out);
            failed = false;
        } finally {
            Closeables.close(out, failed);
        }
    }

    private Map<String, CompilerInput> translateUnit(DartUnit unit, DartSource src, DartCompilerContext context,
            CoreTypeProvider typeProvider) {
        Map<String, JsProgram> parts = translateToJS(unit, context, typeProvider);

        // Translate the AST and cache it for later use.
        return createClosureJsAst(parts, src);
    }

    private CompilerInput getCompilerInputForEntry(DartCompilerContext context) throws IOException {
        StringWriter entry = new StringWriter();
        writeEntryPointCall(getMangledEntryPoint(context), entry);
        return new CompilerInput(JSSourceFile.fromCode("entry", entry.toString()), false);
    }

    class MockSource implements Source {
        private String sourceName;

        MockSource(String sourceName) {
            this.sourceName = sourceName;
        }

        @Override
        public boolean exists() {
            return true;
        }

        @Override
        public long getLastModified() {
            return 0;
        }

        @Override
        public String getName() {
            return sourceName;
        }

        @Override
        public Reader getSourceReader() {
            return new StringReader("");
        }

        @Override
        public URI getUri() {
            return null;
        }
    }

    // Stub source info object for reporting errors coming from the Closure Compiler
    static class JSErrorSourceInfo implements SourceInfo {
        final JSError error;
        final Source source;

        JSErrorSourceInfo(JSError error, Source source) {
            this.error = error;
            this.source = source;
        }

        @Override
        public Source getSource() {
            return source;
        }

        @Override
        public int getSourceColumn() {
            return error.getCharno();
        }

        @Override
        public int getSourceLength() {
            return -1;
        }

        @Override
        public int getSourceLine() {
            return error.lineNumber;
        }

        @Override
        public int getSourceStart() {
            return -1;
        }
    }

    private void compileModule(LibrarySource src, DartCompilerContext context, JSModule module,
            Map<String, Source> sourcesByName, Writer out) throws IOException {
        // Turn off Closure Compiler logging
        CompilerOptions options = getClosureCompilerOptions(context);
        Logger.getLogger("com.google.javascript.jscomp").setLevel(Level.OFF);

        Compiler compiler = new Compiler();

        List<JSSourceFile> externs = getDefaultExterns();
        List<JSModule> modules = Lists.newLinkedList();
        modules.add(module);
        Result result = compiler.compileModules(externs, modules, options);

        if (processResults(src, context, compiler, result, module, out) != 0) {
            for (JSError error : result.errors) {
                // Use the real dart source object when we can.
                Source source = sourcesByName.get(error.sourceName);
                if (source == null) {
                    // This might be a compiler generate source, whatever it is
                    // report it.
                    source = new MockSource(error.sourceName);
                }
                System.err.println("error optimizing:" + error.toString());
                DartCompilationError dartError = new DartCompilationError(new JSErrorSourceInfo(error, source),
                        ClosureJsErrorCode.INTERNAL_ERROR, error.description);
                context.onError(dartError);
            }
        }

        out.close();
    }

    /**
     * Processes the results of the compile job, and returns an error code.
     */
    private int processResults(LibrarySource src, DartCompilerContext context, Compiler compiler, Result result,
            JSModule module, Writer out) throws IOException {
        if (result.success) {
            // TODO(johnlenz): Append directly to the writer.
            String output = compiler.toSource(module);
            out.append(output);
            out.append('\n');

            if (generateSourceMap(context)) {
                Writer srcMapOut = context.getArtifactWriter(src, "", getSourceMapExtension());
                boolean failed = true;
                try {
                    compiler.getSourceMap().appendTo(srcMapOut, module.getName());
                    failed = false;
                } finally {
                    Closeables.close(srcMapOut, failed);
                }
            }
            totalJsOutputCharCount = output.length();
        }

        // return 0 if no errors, the error count otherwise
        return Math.min(result.errors.length, 0x7f);
    }

    private CompilerOptions getClosureCompilerOptions(DartCompilerContext context) {
        CompilerOptions options = new CompilerOptions();
        options.setCodingConvention(new ClosureJsCodingConvention());

        // Set the optimization passes that we want.
        if (fastOutput) {
            options.smartNameRemoval = true;
            options.collapseProperties = true;
            options.removeUnusedPrototypeProperties = true;
            options.removeUnusedVars = true;
            options.setRenamingPolicy(VariableRenamingPolicy.ALL, PropertyRenamingPolicy.ALL_UNQUOTED);
            // On by default
            options.setReplaceIdGenerators(false);
        } else {
            CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
            options.setAssumeStrictThis(true);
            // TODO(johnlenz): try out experimential inlining
            // options.setAssumeClosuresOnlyCaptureReferences(true);

            // TODO(johnlenz): rewriteFunctionExpressions kills the Richards benchmark,
            // it needs some better heuristics.
            options.rewriteFunctionExpressions = false;

            // AliasKeywords has a runtime performance hit, disable it.
            options.aliasKeywords = false;

            // slow for little value
            options.setPropertyAffinity(false);

            // TODO(johnlenz): These passes use SimpleDefinitionFinder or equivalent, and operate
            // based on property name, not object type. DisambiguateProperties helps but is not
            // a complete fix even with complete type information.
            // See http://code.google.com/p/closure-compiler/issues/detail?id=437.
            // We need to develop a plan for how to deal with them.

            options.computeFunctionSideEffects = false;
            options.devirtualizePrototypeMethods = true;
            options.inlineGetters = true;

            // TODO(johnlenz): Some DOM definitions look like unused prototype property
            // definitions because they are only referenced using dynamically generated
            // names.
            options.removeUnusedPrototypePropertiesInExterns = false;
        }

        if (generateSourceMap(context)) {
            options.sourceMapOutputPath = "placeholder"; // anything will do
            options.sourceMapDetailLevel = DetailLevel.SYMBOLS;
            options.sourceMapFormat = Format.V3;
        }

        // Turn off the default checks.

        // Dart doesn't currently need the Closure Library checks
        // or optimizations.
        options.closurePass = false;

        // Disable type warnings as we don't provide any type information.
        options.setInferTypes(false);
        options.checkTypes = false;
        options.setWarningLevel(DiagnosticGroups.CHECK_TYPES, CheckLevel.OFF);

        // Disable other checks, that don't make sense for generated code
        options.setWarningLevel(DiagnosticGroups.GLOBAL_THIS, CheckLevel.OFF);
        options.checkSuspiciousCode = false;
        options.checkGlobalThisLevel = CheckLevel.OFF;
        options.checkMissingReturn = CheckLevel.OFF;
        options.checkGlobalNamesLevel = CheckLevel.OFF;
        options.aggressiveVarCheck = CheckLevel.OFF;
        options.setWarningLevel(DiagnosticGroups.DEPRECATED, CheckLevel.OFF);

        // Optionally turn on the checks that are useful to Dart
        if (validate) {
            options.checkSymbols = true;
            options.setWarningLevel(DiagnosticGroups.ES5_STRICT, CheckLevel.ERROR);
            // options.setAggressiveVarCheck(CheckLevel.ERROR);
        } else {
            // A lot of warnings don't make sense for generated code, or require type
            // information. Turn them all off by default and make the ones we care
            // about errors.
            WarningLevel.QUIET.setOptionsForWarningLevel(options);

            options.checkSymbols = false;
            options.setWarningLevel(DiagnosticGroups.ES5_STRICT, CheckLevel.OFF);
        }

        // To ease debugging, try enabling these options:
        if (generateHumanReadableOutput) {
            options.prettyPrint = true;
            options.generatePseudoNames = true;
            options.printInputDelimiter = true;
            options.inputDelimiter = "// Input %name%";
        }
        // If those aren't enough, try disabling these:
        // options.setRenamingPolicy(VariableRenamingPolicy.OFF, PropertyRenamingPolicy.OFF);
        // options.coalesceVariableNames = false;
        // options.setShadowVariables(false);
        // options.inlineFunctions = false;

        /*
         * NOTE: We turn this off because TypeErrors or anything that relies on a type name will fail
         * due to the class renaming.
         */
        if (checkedMode) {
            options.setReplaceIdGenerators(false);
        }

        return options;
    }

    // The externs expected in externs.zip, in sorted order.
    private static final List<String> DEFAULT_EXTERNS_NAMES = ImmutableList.of(
            // JS externs
            "es3.js", "es5.js",
            // "json.js", // TODO(johnlenz): add this.

            // Event APIs
            "w3c_event.js", "w3c_event3.js", "gecko_event.js", "ie_event.js", "webkit_event.js",

            // DOM apis
            "w3c_dom1.js", "w3c_dom2.js", "w3c_dom3.js", "gecko_dom.js", "ie_dom.js", "webkit_dom.js",

            // CSS apis
            "w3c_css.js", "gecko_css.js", "ie_css.js", "webkit_css.js",

            // Top-level namespaces
            "google.js",

            "deprecated.js", "fileapi.js", "flash.js", "gears_symbols.js", "gears_types.js", "gecko_xml.js",
            "html5.js", "ie_vml.js", "iphone.js", "webstorage.js", "w3c_anim_timing.js", "w3c_css3d.js",
            "w3c_elementtraversal.js", "w3c_geolocation.js", "w3c_indexeddb.js", "w3c_navigation_timing.js",
            "w3c_range.js", "w3c_selectors.js", "w3c_xml.js", "window.js", "webkit_notifications.js", "webgl.js");

    // Add a declarations for the V8 logging function.
    private static final String UNIT_TEST_EXTERN_STUBS = "var write;";

    private static final String CLOSURE_PRIMITIVES = "function JSCompiler_renameProperty() {};";

    // TODO(johnlenz): include json.js in the default set of externs.
    private static final String MISSING_EXTERNS = "var JSON = {};\n" + "/**\n"
            + " * @param {string} jsonStr The string to parse.\n"
            + " * @param {(function(string, *) : *)=} opt_reviver\n" + " * @return {*} The JSON object.\n" + " */\n"
            + "JSON.parse = function(jsonStr, opt_reviver) {};\n" + "\n" + "/**\n"
            + " * @param {*} jsonObj Input object.\n"
            + " * @param {(Array.<string>|(function(string, *) : *)|null)=} opt_replacer\n"
            + " * @param {(number|string)=} opt_space\n"
            + " * @return {string} json string which represents jsonObj.\n" + " */\n"
            + "JSON.stringify = function(jsonObj, opt_replacer, opt_space) {};" + "\n";

    /**
     * @return a mutable list
     * @throws IOException
     */
    private static List<JSSourceFile> getDefaultExterns() throws IOException {
        Class<ClosureJsBackend> clazz = ClosureJsBackend.class;
        InputStream input = clazz.getResourceAsStream("/com/google/javascript/jscomp/externs.zip");
        if (input == null) {
            /*
             * HACK - the open source version of the closure compiler maps the
             * resource into a different location.
             */
            input = clazz.getResourceAsStream("/externs.zip");
        }
        ZipInputStream zip = new ZipInputStream(input);
        Map<String, JSSourceFile> externsMap = Maps.newHashMap();
        for (ZipEntry entry = null; (entry = zip.getNextEntry()) != null;) {
            InputStream entryStream = new BufferedInputStream(new LimitInputStream(zip, entry.getSize()));
            externsMap.put(entry.getName(), JSSourceFile.fromInputStream(
                    // Give the files an odd prefix, so that they do not conflict
                    // with the user's files.
                    "externs.zip//" + entry.getName(), entryStream));
        }

        Preconditions.checkState(externsMap.keySet().equals(Sets.newHashSet(DEFAULT_EXTERNS_NAMES)),
                "Externs zip must match our hard-coded list of externs.");

        // Order matters, so the resources must be added to the result list
        // in the expected order.
        List<JSSourceFile> externs = Lists.newArrayList();
        for (String key : DEFAULT_EXTERNS_NAMES) {
            externs.add(externsMap.get(key));
        }

        // Add methods used when running the unit tests.
        externs.add(JSSourceFile.fromCode("missingExterns", MISSING_EXTERNS));

        // Add methods used when running the unit tests.
        externs.add(JSSourceFile.fromCode("unitTestStubs", UNIT_TEST_EXTERN_STUBS));

        // Add methods used by Closure Compiler itself.
        externs.add(JSSourceFile.fromCode("closureCompilerPrimitives", CLOSURE_PRIMITIVES));

        return externs;
    }

    @Override
    public void packageApp(LibrarySource app, Collection<LibraryUnit> libraries, DartCompilerContext context,
            CoreTypeProvider typeProvider) throws IOException {
        totalJsOutputCharCount = 0;
        packageAppOptimized(app, libraries, context, typeProvider);
        CompilerMetrics compilerMetrics = context.getCompilerMetrics();
        if (compilerMetrics != null) {
            compilerMetrics.packagedJsApplication(totalJsOutputCharCount, -1);
        }
    }

    @Override
    public String getAppExtension() {
        return (incremental) ? EXTENSION_APP_JS : EXTENSION_OPT_JS;
    }

    @Override
    public String getSourceMapExtension() {
        return (incremental) ? EXTENSION_APP_JS_SRC_MAP : EXTENSION_OPT_JS_SRC_MAP;
    }

    @Override
    protected boolean shouldOptimize() {
        return (fastOutput) ? false : true;
    }

    @Override
    protected boolean generateClosureCompatibleCode() {
        return true;
    }
}