diff --git a/closure/compiler/closure_js_library.bzl b/closure/compiler/closure_js_library.bzl index 6b37bab564..4afe264456 100755 --- a/closure/compiler/closure_js_library.bzl +++ b/closure/compiler/closure_js_library.bzl @@ -24,9 +24,13 @@ load("//closure/private:defs.bzl", "collect_js_srcs", "determine_js_language") +def _determine_check_language(language): + if language == "ANY": + return "ECMASCRIPT3" + return language + def _impl(ctx): srcs, externs = collect_js_srcs(ctx) - direct_srcs, direct_externs = collect_js_srcs(ctx, transitive=False) if ctx.files.exports: for forbid in ['srcs', 'externs', 'deps']: if getattr(ctx.files, forbid): @@ -37,27 +41,32 @@ def _impl(ctx): if ctx.files.srcs and ctx.files.externs: fail("'srcs' may not be specified when 'externs' is set") inputs = [] - args = ["--output_file=%s" % ctx.outputs.provided.path, - "--label=%s" % ctx.label] + args = ["--output=%s" % ctx.outputs.provided.path, + "--label=%s" % ctx.label, + "--language=%s" % _determine_check_language(ctx.attr.language)] if ctx.attr.testonly: args += ["--testonly"] - for direct_src in direct_srcs: + for direct_src in ctx.files.srcs: args += ["--src=%s" % direct_src.path] inputs.append(direct_src) - for direct_extern in direct_externs: + for direct_extern in ctx.files.externs: args += ["--extern=%s" % direct_extern.path] inputs.append(direct_extern) for direct_dep in ctx.attr.deps: args += ["--dep=%s" % direct_dep.js_provided.path] inputs.append(direct_dep.js_provided) + for edep in direct_dep.js_exports: + args += ["--dep=%s" % edep.js_provided.path] + inputs.append(edep.js_provided) + args += ["--jscomp_off=%s" % s for s in ctx.attr.suppress] ctx.action( inputs=inputs, outputs=[ctx.outputs.provided], executable=ctx.executable._jschecker, arguments=args, mnemonic="JSChecker", - progress_message="Sanity checking %d JS files in %s" % ( - len(srcs) + len(externs), ctx.label)) + progress_message="Checking %d JS files in %s" % ( + len(ctx.files.srcs) + len(ctx.files.externs), ctx.label)) return struct(files=set(ctx.files.srcs), js_language=determine_js_language(ctx), js_exports=ctx.attr.exports, @@ -74,8 +83,9 @@ closure_js_library = rule( "deps": JS_DEPS_ATTR, "exports": JS_DEPS_ATTR, "language": attr.string(default=JS_LANGUAGE_DEFAULT), + "suppress": attr.string_list(), "_jschecker": attr.label( - default=Label("//closure/compiler/jschecker"), + default=Label("//java/com/google/javascript/jscomp:jschecker"), executable=True), }, outputs={"provided": "%{name}-provided.txt"}) diff --git a/closure/compiler/jschecker/BUILD b/closure/compiler/jschecker/BUILD deleted file mode 100644 index fbf933bed5..0000000000 --- a/closure/compiler/jschecker/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2016 The Bazel 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(default_visibility = ["//visibility:public"]) - -py_binary( - name = "jschecker", - srcs = ["jschecker.py"], - deps = [ - "@closure_library//:build_source", - "@closure_library//:build_treescan", - ], -) diff --git a/closure/compiler/jschecker/jschecker.py b/closure/compiler/jschecker/jschecker.py deleted file mode 100644 index 113b534e5d..0000000000 --- a/closure/compiler/jschecker/jschecker.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/python2 -# -# Copyright 2016 The Bazel 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. - -"""Sanity checker for closure_js_library() targets. - -This script enforces many of the correctness guarantees promised in the -README.md file. - -For starters, this script enforces that all `closure_js_library()` targets -conform to "strict dependencies." It does this by inspecting the `srcs` in each -target to verify that all `goog.require()` statements are provided by the -`srcs` in the libraries specified in `deps`. It does not perform this check -transitively, i.e. it only scans one level deep. However there is one exception -to this rule. If one of those dependent libraries uses the `exports` attribute, -then we treat whatever is listed there as a direct dependency. - -This whole process happens incrementally. We do this by having each library -rule generate a text file containing the list of things it provides. This -way parent rules don't have to re-scan those sources. - -We also do other little things, like checking to make sure `srcs` don't have -`@externs` comments. We also try to spot some obvious errors, like if -goog.testOnly() is in a library without testonly = True, or if CommonJS or ES6 -module directives are being used when the `depmode = "CLOSURE"`. - -""" - -import codecs -import optparse -import os -import re -import sys - -from external.closure_library.closure.bin.build import source -from external.closure_library.closure.bin.build import treescan - - -def _get_options_parser(): - parser = optparse.OptionParser(__doc__) - parser.add_option('--output_file', - dest='output_file', - action='store', - help='Incremental -provides.txt report output filename.') - parser.add_option('--src', - dest='srcs', - default=[], - action='append', - help=('A .js file specified under srcs. This flag may be ' - 'specified multiple times.')) - parser.add_option('--extern', - dest='externs', - default=[], - action='append', - help=('A .js file specified under externs. This flag may ' - 'be specified multiple times.')) - parser.add_option('--dep', - dest='deps', - default=[], - action='append', - help=('A -provides.txt file from a deps target. This flag ' - 'may be specified multiple times.')) - parser.add_option('--label', - dest='label', - action='store', - help='Name of rule being compiled.') - parser.add_option('--testonly', - dest='testonly', - action='store_true', - help='Indicates a testonly rule is being compiled.') - return parser - - -def _trim_results(results, count=20): - results = list(results) - results.sort() - if len(results) > count: - cut = '[%d results omitted]' % (len(results) - count) - results = results[:count] + [cut] - return results - - -def main(): - options, args = _get_options_parser().parse_args() - if args: - raise Exception('No arguments expected') - required_by = {} - provides = set() - requires = set() - evil_test_files = set() - for src in options.srcs: - content = source.GetFileContents(src) - if not options.testonly and 'goog.setTestOnly(' in content: - evil_test_files.add(src) - sauce = source.Source(content) - provides.update(sauce.provides) - requires.update(sauce.requires) - for require in sauce.requires: - if require not in required_by: - required_by[require] = set() - required_by[require].add(src) - if evil_test_files: - sys.stderr.write( - ('\n' - '\033[31;1mJavaScript sanity checking failed D:\033[0m\n' - '\n' - 'These source files contain goog.setTestOnly():\n' - '\n' - '\t%s\n' - '\n' - 'But \033[35;1m%s\033[0m does not have `testonly = True`.\n' - '\n') % ('\n\t'.join(_trim_results(evil_test_files)), - options.label)) - sys.exit(1) - provided = set() - for dep in options.deps: - provided.update(source.GetFileContents(dep).split()) - missing = requires - provides - provided - if missing: - files = set() - for require in missing: - files.update(required_by[require]) - sys.stderr.write( - ('\n' - '\033[31;1mJavaScript strict dependency checking failed D:\033[0m\n' - '\n' - 'These namespaces were required by \033[35;1m%s\033[0m\n' - '\n' - '\t%s\n' - '\n' - 'In the following files:\n' - '\n' - '\t%s\n' - '\n' - 'But none of the JavaScript libraries listed in `deps` have `srcs`\n' - 'that provide these namepaces. Please note that we only check one\n' - 'level down; we do not check transitive dependencies; this is by\n' - 'design. Please update your `deps` to include a direct reference to\n' - 'whatever library target provides these namespaces.\n' - '\n') % (options.label, - '\n\t'.join(_trim_results(missing)), - '\n\t'.join(_trim_results(files)))) - sys.exit(1) - with codecs.open(options.output_file, 'w', encoding='utf-8') as f: - f.write('\n'.join(sorted(provides))) - if provides: - f.write('\n') - - -if __name__ == '__main__': - main() diff --git a/closure/library/BUILD b/closure/library/BUILD index d468fef450..09d0cc244e 100644 --- a/closure/library/BUILD +++ b/closure/library/BUILD @@ -21,6 +21,13 @@ closure_js_library( name = "library", srcs = ["@closure_library//:js_files"], language = "ECMASCRIPT5_STRICT", + suppress = [ + "duplicateEnumValue", + "illegalPrototypeMember", + "invalidSuppress", + "missingJsDoc", + "missingReturnJsDoc", + ], deps = [":base"], ) @@ -29,6 +36,10 @@ closure_js_library( testonly = True, srcs = ["@closure_library//:js_testing_files"], language = "ECMASCRIPT5_STRICT", + suppress = [ + "invalidSuppress", + "missingJsDoc", + ], deps = [":library"], ) @@ -47,4 +58,5 @@ closure_js_library( name = "base", srcs = ["@closure_library//:closure/goog/base.js"], language = "ECMASCRIPT5_STRICT", + suppress = ["invalidSuppress"], ) diff --git a/closure/library/closure_library.BUILD b/closure/library/closure_library.BUILD index c6e66c2c37..1914c5feb5 100755 --- a/closure/library/closure_library.BUILD +++ b/closure/library/closure_library.BUILD @@ -837,23 +837,14 @@ filegroup( "closure/goog/labs/testing/assertthat.js", "closure/goog/labs/testing/assertthat_test.js", "closure/goog/labs/testing/decoratormatcher.js", - "closure/goog/labs/testing/decoratormatcher_test.js", "closure/goog/labs/testing/dictionarymatcher.js", - "closure/goog/labs/testing/dictionarymatcher_test.js", "closure/goog/labs/testing/environment.js", - "closure/goog/labs/testing/environment_test.js", - "closure/goog/labs/testing/environment_usage_test.js", "closure/goog/labs/testing/json_fuzzing.js", - "closure/goog/labs/testing/json_fuzzing_test.js", "closure/goog/labs/testing/logicmatcher.js", - "closure/goog/labs/testing/logicmatcher_test.js", "closure/goog/labs/testing/matcher.js", "closure/goog/labs/testing/numbermatcher.js", - "closure/goog/labs/testing/numbermatcher_test.js", "closure/goog/labs/testing/objectmatcher.js", - "closure/goog/labs/testing/objectmatcher_test.js", "closure/goog/labs/testing/stringmatcher.js", - "closure/goog/labs/testing/stringmatcher_test.js", "closure/goog/labs/useragent/test_agents.js", "closure/goog/promise/testsuiteadapter.js", "closure/goog/soy/soy_testhelper.js", diff --git a/closure/private/defs.bzl b/closure/private/defs.bzl index 0806488b2b..7153f96e23 100644 --- a/closure/private/defs.bzl +++ b/closure/private/defs.bzl @@ -53,13 +53,12 @@ JS_DEPS_ATTR = attr.label_list( "transitive_js_srcs", "transitive_js_externs"]) -def collect_js_srcs(ctx, transitive=True): +def collect_js_srcs(ctx): srcs = set(order="compile") externs = set(order="compile") for dep in ctx.attr.deps: - if transitive: - srcs += dep.transitive_js_srcs - externs += dep.transitive_js_externs + srcs += dep.transitive_js_srcs + externs += dep.transitive_js_externs for edep in dep.js_exports: srcs += edep.transitive_js_srcs externs += edep.transitive_js_externs diff --git a/closure/repositories.bzl b/closure/repositories.bzl index 8038908b77..ef5ef0ba40 100644 --- a/closure/repositories.bzl +++ b/closure/repositories.bzl @@ -227,33 +227,45 @@ def soyutils_usegoog(): url = "https://bazel-mirror.storage.googleapis.com/repo1.maven.org/maven2/com/google/template/soy/2016-01-12/soy-2016-01-12-soyutils_usegoog.js", ) -def closure_repositories(): - aopalliance() +def closure_repositories_asm(): asm() asm_analysis() asm_commons() asm_util() - args4j() + +def closure_repositories_closure(): closure_compiler() closure_library() closure_linter() closure_stylesheets() - fonts_noto_hinted_deb() - fonts_noto_mono_deb() - guava() - gson() + soy() + soyutils_usegoog() + +def closure_repositories_guice(): + aopalliance() guice() guice_assistedinject() guice_multibindings() - icu4j() - jsr305() - jsr330_inject() + +def closure_repositories_phantomjs(): + fonts_noto_hinted_deb() + fonts_noto_mono_deb() libexpat_amd64_deb() libfontconfig_amd64_deb() libfreetype_amd64_deb() libpng_amd64_deb() phantomjs_linux_x86_64() phantomjs_macosx() + +def closure_repositories(): + args4j() + closure_repositories_asm() + closure_repositories_closure() + closure_repositories_guice() + closure_repositories_phantomjs() + guava() + gson() + icu4j() + jsr305() + jsr330_inject() python_gflags() - soy() - soyutils_usegoog() diff --git a/closure/templates/BUILD b/closure/templates/BUILD index 16322659b3..1ef656f62b 100644 --- a/closure/templates/BUILD +++ b/closure/templates/BUILD @@ -38,6 +38,10 @@ java_library( closure_js_library( name = "soyutils_usegoog", srcs = ["@soyutils_usegoog//file"], + suppress = [ + "mustBePrivate", + "optionalParams", + ], deps = ["//closure/library"], ) diff --git a/java/com/google/javascript/jscomp/BUILD b/java/com/google/javascript/jscomp/BUILD new file mode 100644 index 0000000000..e83c48dcf5 --- /dev/null +++ b/java/com/google/javascript/jscomp/BUILD @@ -0,0 +1,13 @@ +java_binary( + name = "jschecker", + srcs = [ + "JsChecker.java", + "JsCheckerFirstPass.java", + "JsCheckerPassConfig.java", + "JsCheckerSecondPass.java", + "JsCheckerState.java", + ], + main_class = "com.google.javascript.jscomp.JsChecker", + visibility = ["//visibility:public"], + deps = ["@closure_compiler//jar"], +) diff --git a/java/com/google/javascript/jscomp/JsChecker.java b/java/com/google/javascript/jscomp/JsChecker.java new file mode 100644 index 0000000000..00e72f6218 --- /dev/null +++ b/java/com/google/javascript/jscomp/JsChecker.java @@ -0,0 +1,223 @@ +package com.google.javascript.jscomp; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.jscomp.lint.CheckEnums; +import com.google.javascript.jscomp.lint.CheckJSDocStyle; +import com.google.javascript.jscomp.lint.CheckPrototypeProperties; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Program for incrementally checking compiled JavaScript code. + */ +public final class JsChecker { + + static { + DiagnosticGroups.registerGroup("duplicateEnumValue", CheckEnums.DUPLICATE_ENUM_VALUE); + DiagnosticGroups.registerGroup("illegalPrototypeMember", + CheckPrototypeProperties.ILLEGAL_PROTOTYPE_MEMBER); + DiagnosticGroups.registerGroup("invalidSuppress", CheckJSDocStyle.INVALID_SUPPRESS); + DiagnosticGroups.registerGroup("missingJsDoc", CheckJSDocStyle.MISSING_JSDOC); + DiagnosticGroups.registerGroup("missingReturnJsDoc", CheckJSDocStyle.MISSING_RETURN_JSDOC); + DiagnosticGroups.registerGroup("mustBePrivate", + CheckJSDocStyle.MUST_BE_PRIVATE, + CheckJSDocStyle.MUST_HAVE_TRAILING_UNDERSCORE); + DiagnosticGroups.registerGroup("optionalParams", + CheckJSDocStyle.OPTIONAL_PARAM_NOT_MARKED_OPTIONAL, + CheckJSDocStyle.OPTIONAL_TYPE_NOT_USING_OPTIONAL_NAME); + DiagnosticGroups.registerGroup("strictDependencies", + JsCheckerFirstPass.DUPLICATE_PROVIDES, + JsCheckerFirstPass.REDECLARED_PROVIDES, + JsCheckerSecondPass.NOT_PROVIDED); + DiagnosticGroups.registerGroup("strictSetTestOnly", JsCheckerFirstPass.INVALID_SETTESTONLY); + } + + private enum Convention { + CLOSURE(new ClosureCodingConvention()), + GOOGLE(new GoogleCodingConvention()), + JQUERY(new JqueryCodingConvention()); + + final CodingConventions.Proxy convention; + + private Convention(CodingConventions.Proxy convention) { + this.convention = convention; + } + } + + 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 = "//ohmygoth"; + + @Option( + name = "--src", + usage = "JavaScript source files, sans externs.") + private List sources = new ArrayList<>(); + + @Option( + name = "--extern", + usage = "JavaScript @externs source files.") + private List externs = new ArrayList<>(); + + @Option( + name = "--dep", + usage = "foo-provides.txt files from deps targets.") + private List deps = new ArrayList<>(); + + @Option( + name = "--convention", + usage = "Coding convention for linting.") + private Convention convention = Convention.GOOGLE; + + @Option( + name = "--language", + usage = "Language spec of input sources.") + private LanguageMode language = LanguageMode.ECMASCRIPT5_STRICT; + + @Option( + name = "--jscomp_off", + usage = "Means same thing as JSCompiler equivalent.") + private List offs = Lists.newArrayList(); + + @Option( + name = "--jscomp_warning", + usage = "Means same thing as JSCompiler equivalent.") + private List warnings = + Lists.newArrayList( + "checkTypes", + "extraRequire", + "lintChecks"); + + @Option( + name = "--jscomp_error", + usage = "Means same thing as JSCompiler equivalent.") + private List errors = + Lists.newArrayList( + "checkRegExp", + "missingRequire", + "strictDependencies"); + + @Option( + name = "--testonly", + usage = "Indicates a testonly rule is being compiled.") + private boolean testonly; + + @Option( + name = "--output", + usage = "Incremental -provides.txt report output filename.") + private String output = ""; + + @Option( + name = "--help", + usage = "Displays this message on stdout and exit") + private boolean help; + + private boolean run() throws IOException { + JsCheckerState state = new JsCheckerState(label, testonly); + + // read provided files created by this program on deps + for (String dep : deps) { + state.provided.addAll(Files.readAllLines(Paths.get(dep), UTF_8)); + } + + // check syntax and collect state data + Compiler compiler = new Compiler(System.out); + CompilerOptions options = new CompilerOptions(); + options.setLanguage(language); + options.setCodingConvention(convention.convention); + options.setSkipTranspilationAndCrash(true); + options.setIdeMode(true); + options.setWarningsGuard( + new ComposeWarningsGuard(ImmutableList.of(suppressPath("bazel-out/")))); + DiagnosticGroups groups = new DiagnosticGroups(); + for (String error : errors) { + options.setWarningLevel(groups.forName(error), CheckLevel.ERROR); + } + for (String warning : warnings) { + options.setWarningLevel(groups.forName(warning), CheckLevel.WARNING); + } + for (String off : offs) { + options.setWarningLevel(groups.forName(off), CheckLevel.OFF); + } + + if (language == LanguageMode.ECMASCRIPT6_STRICT + || language == LanguageMode.ECMASCRIPT6_TYPED) { + options.setWarningLevel( + DiagnosticGroups.registerGroup("doodle", + CheckJSDocStyle.MISSING_PARAMETER_JSDOC, + CheckJSDocStyle.MISSING_RETURN_JSDOC, + CheckJSDocStyle.OPTIONAL_PARAM_NOT_MARKED_OPTIONAL, + CheckJSDocStyle.OPTIONAL_TYPE_NOT_USING_OPTIONAL_NAME), + CheckLevel.OFF); + } + + // XXX: https://github.com/google/closure-compiler/issues/1769 + options.setWarningLevel(groups.forName("mustBePrivate"), CheckLevel.OFF); + + compiler.setPassConfig(new JsCheckerPassConfig(state, options)); + compiler.disableThreads(); + Result result = compiler.compile(getSourceFiles(externs), getSourceFiles(sources), options); + if (!result.success) { + return false; + } + + // write provided file + if (!output.isEmpty()) { + Files.write(Paths.get(output), Ordering.natural().immutableSortedCopy(state.provides), UTF_8); + } + + return true; + } + + private static WarningsGuard suppressPath(String path) { + return new ShowByPathWarningsGuard(path, ShowByPathWarningsGuard.ShowType.EXCLUDE); + } + + private static ImmutableList getSourceFiles(Iterable filenames) { + ImmutableList.Builder result = new ImmutableList.Builder<>(); + for (String filename : filenames) { + result.add(SourceFile.fromFile(filename)); + } + return result.build(); + } + + public static void main(String[] args) throws IOException { + JsChecker checker = new JsChecker(); + CmdLineParser parser = new CmdLineParser(checker); + parser.setUsageWidth(80); + try { + parser.parseArgument(args); + } catch (CmdLineException e) { + System.err.println(e.getMessage()); + System.err.println(USAGE); + parser.printUsage(System.err); + System.err.println(); + System.exit(1); + } + if (checker.help) { + System.err.println(USAGE); + parser.printUsage(System.out); + System.out.println(); + } else { + if (!checker.run()) { + System.exit(1); + } + } + } +} diff --git a/java/com/google/javascript/jscomp/JsCheckerFirstPass.java b/java/com/google/javascript/jscomp/JsCheckerFirstPass.java new file mode 100644 index 0000000000..e7e3d7541c --- /dev/null +++ b/java/com/google/javascript/jscomp/JsCheckerFirstPass.java @@ -0,0 +1,66 @@ +package com.google.javascript.jscomp; + +import com.google.javascript.jscomp.NodeTraversal.AbstractShallowCallback; +import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.Token; + +final class JsCheckerFirstPass extends AbstractShallowCallback implements HotSwapCompilerPass { + + public static final DiagnosticType INVALID_SETTESTONLY = + DiagnosticType.error( + "CR_INVALID_SETTESTONLY", + "Not allowed here because {0} does not have testonly=1."); + + public static final DiagnosticType DUPLICATE_PROVIDES = + DiagnosticType.error( + "CR_DUPLICATE_PROVIDES", "Namespace provided multiple times by srcs of {0}."); + + public static final DiagnosticType REDECLARED_PROVIDES = + DiagnosticType.error("CR_REDECLARED_PROVIDES", "Namespace already provided by deps of {0}."); + + private final JsCheckerState state; + private final AbstractCompiler compiler; + + JsCheckerFirstPass(JsCheckerState state, AbstractCompiler compiler) { + this.state = state; + this.compiler = compiler; + } + + @Override + public final void process(Node externs, Node root) { + NodeTraversal.traverseEs6(compiler, root, this); + } + + @Override + public final void hotSwapScript(Node scriptRoot, Node originalRoot) { + NodeTraversal.traverseEs6(compiler, scriptRoot, this); + } + + @Override + public final void visit(NodeTraversal t, Node n, Node parent) { + switch (n.getType()) { + case Token.CALL: + Node callee = n.getFirstChild(); + if (!state.testonly && callee.matchesQualifiedName("goog.setTestOnly")) { + t.report(n, INVALID_SETTESTONLY, state.label); + return; + } + Node namespace = n.getLastChild(); + if (namespace.isString() + && (callee.matchesQualifiedName("goog.provide") + || callee.matchesQualifiedName("goog.module"))) { + if (!state.provides.add(namespace.getString())) { + t.report(namespace, DUPLICATE_PROVIDES, state.label); + } + if (state.provided.contains(namespace.getString()) + && state.redeclaredProvides.add(namespace.getString())) { + t.report(namespace, REDECLARED_PROVIDES, state.label); + } + return; + } + break; + default: + break; + } + } +} diff --git a/java/com/google/javascript/jscomp/JsCheckerPassConfig.java b/java/com/google/javascript/jscomp/JsCheckerPassConfig.java new file mode 100644 index 0000000000..c8b20f226f --- /dev/null +++ b/java/com/google/javascript/jscomp/JsCheckerPassConfig.java @@ -0,0 +1,98 @@ +package com.google.javascript.jscomp; + +import com.google.common.collect.ImmutableList; +import com.google.javascript.jscomp.NodeTraversal.Callback; +import com.google.javascript.jscomp.lint.CheckDuplicateCase; +import com.google.javascript.jscomp.lint.CheckEmptyStatements; +import com.google.javascript.jscomp.lint.CheckEnums; +import com.google.javascript.jscomp.lint.CheckInterfaces; +import com.google.javascript.jscomp.lint.CheckJSDocStyle; +import com.google.javascript.jscomp.lint.CheckPrototypeProperties; +import com.google.javascript.jscomp.lint.CheckRequiresAndProvidesSorted; +import com.google.javascript.jscomp.lint.CheckUselessBlocks; + +import java.util.List; + +final class JsCheckerPassConfig extends PassConfig.PassConfigDelegate { + + private final JsCheckerState state; + + JsCheckerPassConfig(JsCheckerState state, CompilerOptions options) { + super(new DefaultPassConfig(options)); + this.state = state; + } + + @Override + protected List getChecks() { + return ImmutableList.of( + earlyLintChecks, + closureGoogScopeAliases, + closureRewriteClass, + lateLintChecks, + checkRequires); + } + + @Override + protected List getOptimizations() { + return ImmutableList.of(); + } + + private final PassFactory earlyLintChecks = + new PassFactory("earlyLintChecks", true) { + @Override + protected CompilerPass create(AbstractCompiler compiler) { + return new CombinedCompilerPass( + compiler, + ImmutableList.of( + new CheckDuplicateCase(compiler), + new CheckEmptyStatements(compiler), + new CheckEnums(compiler), + new CheckJSDocStyle(compiler), + new CheckJSDoc(compiler), + new CheckRequiresAndProvidesSorted(compiler), + new CheckUselessBlocks(compiler), + new ClosureCheckModule(compiler), + new JsCheckerFirstPass(state, compiler))); + } + }; + + private final PassFactory closureGoogScopeAliases = + new PassFactory("closureGoogScopeAliases", true) { + @Override + protected HotSwapCompilerPass create(AbstractCompiler compiler) { + return new ScopedAliases(compiler, null, options.getAliasTransformationHandler()); + } + }; + + private final PassFactory closureRewriteClass = + new PassFactory("closureRewriteClass", true) { + @Override + protected HotSwapCompilerPass create(AbstractCompiler compiler) { + return new ClosureRewriteClass(compiler); + } + }; + + private final PassFactory lateLintChecks = + new PassFactory("lateLintChecks", true) { + @Override + protected CompilerPass create(AbstractCompiler compiler) { + return new CombinedCompilerPass( + compiler, + ImmutableList.of( + new CheckInterfaces(compiler), + new CheckPrototypeProperties(compiler), + new JsCheckerSecondPass(state, compiler))); + } + }; + + // This cannot be part of lintChecks because the callbacks in the CombinedCompilerPass don't + // get access to the externs. + private final PassFactory checkRequires = + new PassFactory("checkRequires", true) { + @Override + protected CompilerPass create(AbstractCompiler compiler) { + return new CheckRequiresForConstructors( + compiler, CheckRequiresForConstructors.Mode.SINGLE_FILE); + } + }; +} diff --git a/java/com/google/javascript/jscomp/JsCheckerSecondPass.java b/java/com/google/javascript/jscomp/JsCheckerSecondPass.java new file mode 100644 index 0000000000..7675ce543c --- /dev/null +++ b/java/com/google/javascript/jscomp/JsCheckerSecondPass.java @@ -0,0 +1,53 @@ +package com.google.javascript.jscomp; + +import com.google.javascript.jscomp.NodeTraversal.AbstractShallowCallback; +import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.Token; + +final class JsCheckerSecondPass extends AbstractShallowCallback implements HotSwapCompilerPass { + + public static final DiagnosticType NOT_PROVIDED = + DiagnosticType.error( + "CR_NOT_PROVIDED", "Namespace not provided by any srcs or deps of {0}"); + + private final JsCheckerState state; + private final AbstractCompiler compiler; + + JsCheckerSecondPass(JsCheckerState state, AbstractCompiler compiler) { + this.state = state; + this.compiler = compiler; + } + + @Override + public void process(Node externs, Node root) { + NodeTraversal.traverseEs6(compiler, root, this); + } + + @Override + public void hotSwapScript(Node scriptRoot, Node originalRoot) { + NodeTraversal.traverseEs6(compiler, scriptRoot, this); + } + + @Override + public void visit(NodeTraversal t, Node n, Node parent) { + switch (n.getType()) { + case Token.CALL: + Node callee = n.getFirstChild(); + Node namespace = n.getLastChild(); + if (!namespace.isString()) { + return; + } + if (callee.matchesQualifiedName("goog.require")) { + if (!state.provided.contains(namespace.getString()) + && !state.provides.contains(namespace.getString()) + && state.notProvidedNamespaces.add(namespace.getString())) { + t.report(namespace, NOT_PROVIDED, state.label); + } + state.provides.add(namespace.getString()); + } + break; + default: + break; + } + } +} diff --git a/java/com/google/javascript/jscomp/JsCheckerState.java b/java/com/google/javascript/jscomp/JsCheckerState.java new file mode 100644 index 0000000000..9fe4a9d8d8 --- /dev/null +++ b/java/com/google/javascript/jscomp/JsCheckerState.java @@ -0,0 +1,33 @@ +package com.google.javascript.jscomp; + +import java.util.HashSet; +import java.util.Set; + +final class JsCheckerState { + + final String label; + final boolean testonly; + + // Assume we're processing //closure/library which has 4788 provides. HashMap has a default load + // factor of 0.75. Therefore redimensioning would never occur for a map with the capacity of 6385 + // (4788/0.75+1). However we're going to pick 7000 to allow for growth in the Closure Library. No + // other project should ever exist with such a large set of provides in one closure_js_library(). + // + // There are actually cooler data structures we could be using here to save space. Like maybe a + // graph of namespace labels represented as an IdentityHashMap of interned strings. But it'd take + // too much braining for too little benefit. + final Set provides = new HashSet<>(7000); + + // In almost all circumstances, the user will be directly depending on the Closure Library, in + // addition to a bunch of other things. So we're going to aim a bit higher. + final Set provided = new HashSet<>(9000); + + // These are used to avoid flooding the user with certain types of error messages. + final Set notProvidedNamespaces = new HashSet<>(); + final Set redeclaredProvides = new HashSet<>(); + + JsCheckerState(String label, boolean testonly) { + this.label = label; + this.testonly = testonly; + } +}