Java tutorial
/* * Copyright 2016 The Closure Rules Authors. All rights reserved. * * 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 static com.google.common.base.Preconditions.checkArgument; import static com.google.javascript.jscomp.JsCheckerHelper.convertPathToModuleName; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.javascript.jscomp.CompilerOptions.IncrementalCheckMode; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.parsing.Config; import io.bazel.rules.closure.BuildInfo.ClosureJsLibrary; import io.bazel.rules.closure.worker.CommandLineProgram; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; /** * Program for incrementally checking JavaScript code. * * <p>This program is invoked once for each {@code closure_js_library} rule. It validates JS files * for most of the really bad syntax errors, but doesn't do robust type checking because it can't * take the full program into consideration. This program also performs linting, which is something * that does not happen on {@code closure_js_binary} rules. * * <p>But most importantly, this program does strict dependency checking. It is able to verify that * required namespaces are provided by direct dependencies rather than transitive dependencies. It * does this in an incremental fashion by producing a txt file containing a sorted list of all * namespaces provided by the srcs listed in a {@code closure_js_library}. These files are then * accessed via the {@code --deps} flag on subsequent invocations for parent rules. */ public final class JsChecker { private static final String USAGE = String.format("Usage:\n java %s [FLAGS]\n", JsChecker.class.getName()); @Option(name = "--label", usage = "Name of rule being compiled.") private String label = "@repo//ohmygoth:some_lib"; @Option(name = "--legacy", usage = "Used for sources defined by legacy rules.") private boolean legacy; @Option(name = "--src", usage = "JavaScript source and externs files.") private List<String> sources = new ArrayList<>(); @Option(name = "--mystery_src", usage = "Transitive JS dependency whose position in the graph we're uncertain.") private List<String> mysterySources = new ArrayList<>(); @Option(name = "--dep", usage = "foo-provides.txt files from deps targets.") private List<String> deps = new ArrayList<>(); @Option(name = "--js_module_root", usage = "Prefixes to disregard in module namespaces, e.g. " + "bazel-out/local-fastbuild/genfiles. These values must be reverse sorted by the number " + "of path components.") private List<String> roots = new ArrayList<>(); @Option(name = "--convention", usage = "Coding convention for linting.") private JsCheckerConvention convention = JsCheckerConvention.CLOSURE; @Option(name = "--suppress", usage = "Diagnostic types to not show as errors or warnings.") private List<String> suppress = new ArrayList<>(); @Option(name = "--testonly", usage = "Indicates a testonly rule is being compiled.") private boolean testonly; @Option(name = "--output", usage = "Path of outputted ClosureJsLibrary.pbtxt file.") private String output = ""; @Option(name = "--output_ijs_file", usage = "Path of the generated .i.js file representing the given sources.") private String outputIjsFile = ""; @Option(name = "--output_errors", usage = "Name of output file for compiler errors in --nofail mode.") private String outputErrors = ""; @Option(name = "--expect_failure", usage = "Invert exit code and disable printing warnings") private boolean expectFailure; @Option(name = "--help", usage = "Displays this message on stdout and exit") private boolean help; private boolean run() throws IOException { final JsCheckerState state = new JsCheckerState(label, legacy, testonly, roots, mysterySources); final Set<String> actuallySuppressed = new HashSet<>(); // read provided files created by this program on deps for (String dep : deps) { state.provided.addAll(JsCheckerHelper.loadClosureJsLibraryInfo(Paths.get(dep)).getNamespaceList()); } Map<String, String> labels = new HashMap<>(); labels.put("", label); Set<String> modules = new LinkedHashSet<>(); for (String source : sources) { for (String module : convertPathToModuleName(source, state.roots).asSet()) { modules.add(module); labels.put(module, label); state.provides.add(module); } } for (String source : mysterySources) { for (String module : convertPathToModuleName(source, state.roots).asSet()) { checkArgument(!module.startsWith("blaze-out/"), "oh no: %s", state.roots); modules.add(module); state.provided.add(module); } } // configure compiler Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); options.setLanguage(LanguageMode.ECMASCRIPT_2017); options.setStrictModeInput(true); options.setIncrementalChecks(IncrementalCheckMode.GENERATE_IJS); options.setCodingConvention(convention.convention); options.setSkipTranspilationAndCrash(true); options.setContinueAfterErrors(true); options.setPrettyPrint(true); options.setPreserveTypeAnnotations(true); options.setPreserveDetailedSourceInfo(true); options.setEmitUseStrict(false); options.setParseJsDocDocumentation(Config.JsDocParsing.INCLUDE_DESCRIPTIONS_NO_WHITESPACE); JsCheckerErrorFormatter errorFormatter = new JsCheckerErrorFormatter(compiler, state.roots, labels); errorFormatter.setColorize(true); JsCheckerErrorManager errorManager = new JsCheckerErrorManager(errorFormatter); compiler.setErrorManager(errorManager); // configure which error messages appear if (!legacy) { for (String error : Iterables.concat(Diagnostics.JSCHECKER_ONLY_ERRORS, Diagnostics.JSCHECKER_EXTRA_ERRORS)) { options.setWarningLevel(Diagnostics.GROUPS.forName(error), CheckLevel.ERROR); } } final Set<DiagnosticType> suppressions = Sets.newHashSetWithExpectedSize(256); for (String code : suppress) { ImmutableSet<DiagnosticType> types = Diagnostics.getDiagnosticTypesForSuppressCode(code); if (types.isEmpty()) { System.err.println("ERROR: Bad --suppress value: " + code); return false; } suppressions.addAll(types); } options.addWarningsGuard(new WarningsGuard() { @Override public CheckLevel level(JSError error) { // TODO(jart): Figure out how to support this. if (error.getType().key.equals("JSC_CONSTANT_WITHOUT_EXPLICIT_TYPE")) { return CheckLevel.OFF; } // Closure Rules will always ignore these checks no matter what. if (Diagnostics.IGNORE_ALWAYS.contains(error.getType())) { return CheckLevel.OFF; } // Disable warnings specific to conventions other than the one we're using. if (!convention.diagnostics.contains(error.getType())) { for (JsCheckerConvention conv : JsCheckerConvention.values()) { if (!conv.equals(convention)) { if (conv.diagnostics.contains(error.getType())) { suppressions.add(error.getType()); return CheckLevel.OFF; } } } } // Disable warnings we've suppressed. Collection<String> groupNames = Diagnostics.DIAGNOSTIC_GROUPS.get(error.getType()); if (suppressions.contains(error.getType())) { actuallySuppressed.add(error.getType().key); actuallySuppressed.addAll(groupNames); return CheckLevel.OFF; } // Ignore linter warnings on generated sources. if (groupNames.contains("lintChecks") && JsCheckerHelper.isGeneratedPath(error.sourceName)) { return CheckLevel.OFF; } return null; } }); // Run the compiler. compiler.setPassConfig(new JsCheckerPassConfig(state, options)); compiler.disableThreads(); compiler.compile(ImmutableList.<SourceFile>of(), getSourceFiles(Iterables.concat(sources, mysterySources)), options); // In order for suppress to be maintainable, we need to make sure the suppress codes relating to // linting were actually suppressed. However we can only offer this safety on the checks over // which JsChecker has sole dominion. Other suppress codes won't actually be suppressed until // they've been propagated up to the closure_js_binary rule. if (!suppress.contains("superfluousSuppress")) { Set<String> useless = Sets.intersection( Sets.difference(ImmutableSet.copyOf(suppress), actuallySuppressed), Diagnostics.JSCHECKER_ONLY_SUPPRESS_CODES); if (!useless.isEmpty()) { errorManager.report(CheckLevel.ERROR, JSError.make(Diagnostics.SUPERFLUOUS_SUPPRESS, label, Joiner.on(", ").join(useless))); } } // TODO: Make compiler.compile() package private so we don't have to do this. errorManager.stderr.clear(); errorManager.generateReport(); // write errors if (!expectFailure) { for (String line : errorManager.stderr) { System.err.println(line); } } if (!outputErrors.isEmpty()) { Files.write(Paths.get(outputErrors), errorManager.stderr, UTF_8); } // write .i.js type summary for this library if (!outputIjsFile.isEmpty()) { Files.write(Paths.get(outputIjsFile), compiler.toSource().getBytes(UTF_8)); } // write file full of information about these sauces if (!output.isEmpty()) { ClosureJsLibrary.Builder info = ClosureJsLibrary.newBuilder().setLabel(label).setLegacy(legacy) .addAllNamespace(state.provides).addAllModule(modules); if (!legacy) { for (DiagnosticType suppression : suppressions) { if (!Diagnostics.JSCHECKER_ONLY_SUPPRESS_CODES.contains(suppression.key)) { info.addSuppress(suppression.key); } } } Files.write(Paths.get(output), info.build().toString().getBytes(UTF_8)); } return errorManager.getErrorCount() == 0; } private static ImmutableList<SourceFile> getSourceFiles(Iterable<String> filenames) throws IOException { ImmutableList.Builder<SourceFile> result = new ImmutableList.Builder<>(); for (String filename : filenames) { if (filename.endsWith(".zip")) { result.addAll(SourceFile.fromZipFile(filename, UTF_8)); } else { result.add(SourceFile.fromFile(filename)); } } return result.build(); } public static final class Program implements CommandLineProgram { @Inject Program() { } @Override public Integer apply(Iterable<String> args) { JsChecker checker = new JsChecker(); CmdLineParser parser = new CmdLineParser(checker); parser.setUsageWidth(80); try { parser.parseArgument(ImmutableList.copyOf(args)); } catch (CmdLineException e) { System.err.println(e.getMessage()); System.err.println(USAGE); parser.printUsage(System.err); System.err.println(); return 1; } if (checker.help) { System.err.println(USAGE); parser.printUsage(System.out); System.err.println(); return 0; } try { boolean success = checker.run(); if (success && checker.expectFailure) { System.err.println("ERROR: Expected failure but did not fail"); } return success == !checker.expectFailure ? 0 : 1; } catch (IOException e) { throw new RuntimeException(e); } } } }