com.google.javascript.jscomp.NpmCommandLineRunner.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.NpmCommandLineRunner.java

Source

/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

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.ByteStreams;
import com.google.common.io.Files;
import com.google.javascript.rhino.InputId;
import com.google.javascript.rhino.Node;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.FieldSetter;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
import org.kohsuke.args4j.spi.StringOptionHandler;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.AnnotatedElement;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * A CommandLine interface that reads a package.json file and runs
 * the type-checker against the code.
 *
 * @author nicholas.j.santos@gmail.com (Nick Santos)
 */
public class NpmCommandLineRunner extends AbstractCommandLineRunner<Compiler, CompilerOptions> {

    final static String NODEJS_LIB_PREFIX = "nodejs.zip//";
    final static String NODEJS_LIB_GLOBALS = "nodejs.zip//globals.js";

    // I don't really care about unchecked warnings in this class.
    @SuppressWarnings("unchecked")
    private static class Flags extends CommandLineRunner.AbstractFlags {
        @Option(name = "--help", handler = BooleanOptionHandler.class, usage = "Displays this message")
        private boolean displayHelp = false;

        // Used to define the flag, values are stored by the handler.
        @SuppressWarnings("unused")
        @Option(name = "--jscomp_error", handler = WarningGuardErrorOptionHandler.class, usage = "Make the named class of warnings an error. Options:"
                + DiagnosticGroups.DIAGNOSTIC_GROUP_NAMES)
        private List<String> jscompError = Lists.newArrayList();

        // Used to define the flag, values are stored by the handler.
        @SuppressWarnings("unused")
        @Option(name = "--jscomp_warning", handler = WarningGuardWarningOptionHandler.class, usage = "Make the named class of warnings a normal warning. "
                + "Options:" + DiagnosticGroups.DIAGNOSTIC_GROUP_NAMES)
        private List<String> jscompWarning = Lists.newArrayList();

        // Used to define the flag, values are stored by the handler.
        @SuppressWarnings("unused")
        @Option(name = "--jscomp_off", handler = WarningGuardOffOptionHandler.class, usage = "Turn off the named class of warnings. Options:"
                + DiagnosticGroups.DIAGNOSTIC_GROUP_NAMES)
        private List<String> jscompOff = Lists.newArrayList();

        @Option(name = "--charset", usage = "Input and output charset for all files. By default, we "
                + "assume UTF-8")
        private String charset = "UTF-8";

        @Option(name = "--extra_annotation_name", usage = "A whitelist of tag names in JSDoc. You may specify multiple")
        private List<String> extraAnnotationName = Lists.newArrayList();

        @Option(name = "--version", handler = BooleanOptionHandler.class, usage = "Prints the compiler version to stderr.")
        private boolean version = false;

        @Argument(usage = "A directory containing a package.json file with a 'main' script,"
                + " or a JS file. May specify multiple."
                + "If none specified, uses the package.json in the current directory")
        private List<String> arguments = Lists.newArrayList();
    }

    private final Flags flags = new Flags();

    private boolean isConfigValid = false;
    private final List<String> entryJsFiles = Lists.newArrayList();
    private final Map<String, SourceFile> externsMap;
    private final Map<String, SourceFile> nodejsMap;

    /**
     * Create a new command-line runner. You should only need to call
     * the constructor if you're extending this class. Otherwise, the main
     * method should instantiate it.
     */
    protected NpmCommandLineRunner(String[] args) throws IOException {
        this(args, System.out, System.err);
    }

    protected NpmCommandLineRunner(String[] args, PrintStream out, PrintStream err) throws IOException {
        super(out, err);
        this.externsMap = CommandLineRunner.getEmbeddedExternsMap();
        this.nodejsMap = CommandLineRunner.getExternsMap(CommandLineRunner.getExternsInputStream("nodejs.zip"),
                NODEJS_LIB_PREFIX);
        initConfigFromFlags(args, err);
    }

    private void initConfigFromFlags(String[] args, PrintStream err) {
        List<String> processedArgs = CommandLineRunner.processArgs(args);

        CmdLineParser parser = new CmdLineParser(flags);
        Flags.guardLevels.clear();
        isConfigValid = true;
        try {
            parser.parseArgument(processedArgs.toArray(new String[] {}));
        } catch (CmdLineException e) {
            err.println(e.getMessage());
            isConfigValid = false;
        }

        if (flags.version) {
            err.println("Closure Node Package Checker (http://code.google.com/closure/compiler)\n" + "Version: "
                    + Compiler.getReleaseVersion() + "\n" + "Built on: " + Compiler.getReleaseDate());
            err.flush();
        }

        if (!isConfigValid || flags.displayHelp) {
            isConfigValid = false;
            parser.printUsage(err);
            return;
        }

        try {
            for (int i = 0; i < getArgumentCount(); i++) {
                entryJsFiles.add(getMainJsFile(i));
            }
        } catch (Exception e) {
            e.printStackTrace(err);
            isConfigValid = false;
            return;
        }

        CodingConvention conv = new ClosureCodingConvention();

        getCommandLineConfig().setCodingConvention(conv).setWarningGuardSpec(Flags.getWarningGuardSpec())
                .setCharset(flags.charset).setAcceptConstKeyword(true).setLanguageIn("ECMASCRIPT5")
                .setSummaryDetailLevel(3);
    }

    @Override
    protected void addWhitelistWarningsGuard(CompilerOptions options, File whitelistFile) {
        options.addWarningsGuard(WhitelistWarningsGuard.fromFile(whitelistFile));
    }

    @Override
    protected int doRun() throws FlagUsageException, IOException {
        List<SourceFile> externs = createExterns();
        Compiler compiler = createCompiler();
        CompilerOptions options = createOptions();
        setRunOptions(options);
        compiler.initOptions(options);

        NodeJSModuleLoader loader = new NodeJSModuleLoader(compiler, getModuleRoot().toString());

        // Hack around the compiler API that makes it difficult
        // to inject CompilerInputs directly. Inect them into a module
        // first, then inject that module.
        JSModule module = new JSModule(Compiler.SINGLETON_MODULE_NAME);

        loader.load(NODEJS_LIB_PREFIX + "events.js");
        loader.load(NODEJS_LIB_PREFIX + "child_process.js");
        loader.load(NODEJS_LIB_PREFIX + "stream.js");
        loader.load(NODEJS_LIB_GLOBALS);

        for (String mainJsFile : entryJsFiles) {
            // Bootstrap the module loading process with entry points.
            loader.load(loader.getCanonicalPath(new File(mainJsFile)));

            if (compiler.hasHaltingErrors()) {
                return Math.min(compiler.getErrors().length, 0x7f);
            }
        }

        for (CompilerInput input : loader.inputsByAddress.values()) {
            module.addAndOverrideModule(input);
        }

        Result result = compiler.compileModules(externs, Lists.newArrayList(module), options);

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

    /** Skip outputs. */
    @Override
    void outputSingleBinary() throws IOException {
    }

    @Override
    protected CompilerOptions createOptions() {
        CompilerOptions options = new CompilerOptions();
        options.setExtraAnnotationNames(flags.extraAnnotationName);
        options.setIdeMode(true);
        WarningLevel.VERBOSE.setOptionsForWarningLevel(options);
        options.closurePass = true;
        options.inferConsts = true;
        options.declaredGlobalExternsOnWindow = false;
        options.setDependencyOptions(new DependencyOptions().setDependencySorting(true));
        return options;
    }

    @Override
    protected Compiler createCompiler() {
        return new Compiler(getErrorPrintStream());
    }

    @Override
    protected List<SourceFile> createExterns() throws FlagUsageException, IOException {
        List<SourceFile> externs = Lists.newArrayList(externsMap.get("es3.js"), externsMap.get("es5.js"),
                externsMap.get("es6.js"), externsMap.get("v8.js"));

        // Attempt to load user-defined externs from the "externs" key in "package.json".
        // Expects the contents of "externs" to be an array.
        Path moduleRoot = getModuleRoot();
        File packageFile = new File(moduleRoot.toString(), "package.json");
        if (packageFile.isFile()) {
            try {
                JsonObject packageJson = getPackageJson(packageFile);
                JsonArray jsonExterns = unsafeGet(packageJson, "externs").getAsJsonArray();
                int len = jsonExterns.size();
                for (int i = 0; i < len; i++) {
                    String path = moduleRoot.resolve(unsafeGet(jsonExterns, i).getAsString()).normalize()
                            .toString();
                    externs.add(SourceFile.fromFile(path));
                }
            } catch (JsonParseException e) {
            } // no one cares
            catch (IOException e) {
            } // no one cares
        }

        return externs;
    }

    private SourceFile getNativeLibrary(String name) {
        return nodejsMap.get(name);
    }

    /**
     * Gets the argument at i.
     * Defaults to the current working dir at i=0, and null at i greater than 0
     */
    private String getArgument(int i) {
        if (i < flags.arguments.size()) {
            return flags.arguments.get(i);
        }
        return i == 0 ? "./" : null;
    }

    private int getArgumentCount() {
        return Math.max(1, flags.arguments.size());
    }

    private Path getModuleRoot() {
        File moduleRoot = new File(getArgument(0));
        if (!moduleRoot.isDirectory()) {
            moduleRoot = moduleRoot.getParentFile();
            if (moduleRoot == null) {
                moduleRoot = new File("./");
            }
        }

        FileSystem fs = FileSystems.getDefault();
        return fs.getPath(moduleRoot.toString()).toAbsolutePath().normalize();
    }

    JsonObject getPackageJson(File packageJsonFile) throws IOException, JsonParseException {
        if (!packageJsonFile.isFile()) {
            throw new IOException("No package.json file at " + packageJsonFile.getAbsoluteFile());
        }

        return new JsonParser().parse(Files.toString(packageJsonFile, Charset.forName(flags.charset)))
                .getAsJsonObject();
    }

    String getMainJsFile(int arg) throws IOException, JsonParseException {
        String argPath = FileSystems.getDefault().getPath(getArgument(arg)).normalize().toString();
        File argFile = new File(argPath).getAbsoluteFile();
        if (!(argFile.isFile() || argFile.isDirectory())) {
            throw new IOException("Nothing at " + argFile);
        }

        if (argFile.isFile()) {
            return argPath;
        }

        JsonObject packageJson = getPackageJson(new File(argFile, "package.json"));

        String main = null;
        try {
            main = unsafeGet(packageJson, "main").getAsString();
        } catch (JsonParseException e) {
        } // Fall through to the default main file.

        File mainFile = tryMainFile(argFile, main);
        if (mainFile == null) {
            throw new IOException("Could not find main JS entry point at " + argFile);
        }

        return FileSystems.getDefault().getPath(mainFile.toString()).normalize().toString();
    }

    /**
     * Simulates the 'main' search algorithm in nodejs
     */
    static File tryMainFile(File dir, String customName) {
        File candidate = null;

        if (customName != null) {
            candidate = new File(dir, customName);
            if (candidate.exists())
                return candidate;

            candidate = new File(dir, customName + ".js");
            if (candidate.exists())
                return candidate;
        }

        candidate = new File(dir, "index");
        if (candidate.exists())
            return candidate;

        candidate = new File(dir, "index.js");
        if (candidate.exists())
            return candidate;

        return null;
    }

    /**
     * Simulates the normal js search algorithm in nodejs
     */
    static File tryFile(File dir, String customName) {
        // Do not try to load other types of requires, like json files.
        if (customName.endsWith(".json")) {
            return new File(dir, customName);
        }

        File candidate = new File(dir, customName);
        if (candidate.exists()) {
            if (candidate.isDirectory()) {
                return tryMainFile(candidate, null);
            }
            return candidate;
        }

        return new File(dir, customName + ".js");
    }

    /**
     * @return Whether the configuration is valid.
     */
    public boolean shouldRunCompiler() {
        return this.isConfigValid;
    }

    /**
     * Runs the Compiler. Exits cleanly in the event of an error.
     */
    public static void main(String[] args) throws IOException {
        NpmCommandLineRunner runner = new NpmCommandLineRunner(args);
        if (runner.shouldRunCompiler()) {
            runner.run();
        } else {
            System.exit(-1);
        }
    }

    /**
     * The NodeJS module loader uses NodeJS lookup semantics.
     *
     * In normal NodeJS operation, the "module root" is always relative to the
     * current file.  require('q') traverses the ancestor tree looking for a
     * node_modules directory, finds node_modules/q/package.json, and uses the
     * main entry point to find the module. It may also look in a global
     * registry.
     *
     * This module loader works similarly, but with one minor tweak.
     * If package.json contains an entry "q" in the map "externsDependencies",
     * we will use the value of that map instead of the normal dependency.
     *
     * To use this module loader, find the entry point of our program,
     * and run ProcessCommonJSModules on it with this module loader.
     * Every time ProcessCommonJSModules loads a file, this loader
     * will dynamically create a new CompilerInput and run it through
     * ProcessCommonJSModules.
     */
    class NodeJSModuleLoader extends ES6ModuleLoader {
        private final Compiler compiler;
        private final Map<String, CompilerInput> inputsByAddress = Maps.newLinkedHashMap();
        private final Path moduleRoot;

        NodeJSModuleLoader(Compiler compiler, String moduleRoot) {
            super(compiler, FileSystems.getDefault().getPath(moduleRoot).toAbsolutePath().normalize().toString());
            this.compiler = compiler;
            this.moduleRoot = getAbsolutePath(moduleRoot);
        }

        /**
         * NodeJS always resolves symlinks, so use the OS-specific canonical
         * path for module addresses.
         */
        @Override
        String locate(String name, CompilerInput referrer) {
            File currentDir = new File(referrer.getName()).getParentFile();
            if (ES6ModuleLoader.isRelativeIdentifier(name)) {
                if (referrer.getName().startsWith(NODEJS_LIB_PREFIX)) {
                    return NODEJS_LIB_PREFIX + name.replace("./", "") + ".js";
                }
                return getCanonicalPath(tryFile(currentDir, name));
            }

            while (currentDir != null) {
                File candidate = resolveTopLevelModuleAt(currentDir, name);
                if (candidate != null && candidate.isFile()) {
                    return getCanonicalPath(candidate);
                }
                currentDir = currentDir.getParentFile();
            }

            if (getNativeLibrary(name + ".js") != null) {
                String location = NODEJS_LIB_PREFIX + name + ".js";

                // globals.js is an internal hack, and shouldn't be
                // loaded by requiring it.
                if (location.equals(NODEJS_LIB_GLOBALS)) {
                    return null;
                }

                return location;
            }
            return null;
        }

        private File resolveTopLevelModuleAt(File currentDir, String name) {
            File packageFile = new File(currentDir, "package.json");
            if (packageFile.isFile()) {
                try {
                    JsonObject packageJson = getPackageJson(packageFile);
                    JsonObject externDependencies = unsafeGet(packageJson, "externDependencies").getAsJsonObject();
                    String myExtern = unsafeGet(externDependencies, name).getAsString();
                    return new File(currentDir, myExtern);
                } catch (JsonParseException e) {
                } // no one cares
                catch (IOException e) {
                } // no one cares
            }

            File nodeModulesFolder = new File(currentDir, "node_modules");
            if (nodeModulesFolder.isDirectory()) {
                File moduleFolder = new File(nodeModulesFolder, name);
                if (moduleFolder.isDirectory()) {
                    packageFile = new File(moduleFolder, "package.json");
                    if (packageFile.isFile()) {
                        String main = null;
                        try {
                            JsonObject packageJson = getPackageJson(packageFile);
                            main = unsafeGet(packageJson, "main").getAsString();
                        } catch (JsonParseException e) {
                        } // no one cares
                        catch (IOException e) {
                        } // no one cares

                        return tryMainFile(moduleFolder, main);
                    }
                }
            }
            return null;
        }

        @Override
        CompilerInput load(String name) {
            CompilerInput input = inputsByAddress.get(name);
            if (input != null)
                return input;

            SourceFile newFile = null;
            if (name.startsWith(NODEJS_LIB_PREFIX)) {
                newFile = getNativeLibrary(name.substring(NODEJS_LIB_PREFIX.length()));
            } else {
                String path = moduleRoot.resolve(name).normalize().toString();
                newFile = SourceFile.fromFile(path);

                if (name.endsWith(".json")) {
                    // Attempt to modify the file's contents to add an exports declaration.
                    // Otherwise the compiler will interpret the JSON object as a block.
                    try {
                        String json = "module.exports = " + newFile.getCode();
                        newFile = SourceFile.fromCode(path, json);
                    } catch (IOException e) {
                    }
                }
            }

            CompilerInput newInput = new CompilerInput(newFile);
            inputsByAddress.put(name, newInput);
            compiler.putCompilerInput(new InputId(name), newInput);

            Node root = newInput.getAstRoot(compiler);
            if (compiler.hasHaltingErrors()) {
                return newInput;
            }

            ProcessCommonJSModules processor = new ProcessCommonJSModules(compiler, this, true);

            // globals.js is evaluated in the global scope,
            // but has some module-relative type names.
            //
            // All other files are evaluated in a file scope.
            if (NODEJS_LIB_GLOBALS.equals(name)) {
                processor.resolveModuleRelativeTypeNames(newInput);
            } else {
                processor.process(newInput);
            }

            return newInput;
        }

        @Override
        String getLoadAddress(CompilerInput input) {
            String nativeLibraryPrefix = NODEJS_LIB_PREFIX;
            if (input.getName().startsWith(nativeLibraryPrefix)) {
                return input.getName();
            }

            return getCanonicalPath(new File(input.getName()));
        }

        private Path getAbsolutePath(String fileName) {
            return FileSystems.getDefault().getPath(fileName).toAbsolutePath().normalize();
        }

        private String getCanonicalPath(File file) {
            try {
                return moduleRoot.relativize(getAbsolutePath(file.getCanonicalPath())).normalize().toString();
            } catch (IOException e) {
                // Just let any IO exceptions propagate up to the CLI.
                throw new RuntimeException(e);
            }
        }
    }

    private JsonElement unsafeGet(JsonObject o, String key) {
        return nullParseCheck(o.get(key));
    }

    private JsonElement unsafeGet(JsonArray o, int key) {
        return nullParseCheck(o.get(key));
    }

    private JsonElement nullParseCheck(JsonElement e) {
        if (e == null) {
            throw new JsonParseException("Couldn't retrieve element.");
        }
        return e;
    }
}