blob: c6a0801cf9d419f1158a691432739a4792e1e459 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Download & run the Closure Compiler.
https://github.com/google/closure-compiler/wiki/Binary-Downloads
"""
import json
import os
import sys
import libdot
# The version we currently run. Pinned to make sure everyone gets consistent
# behavior all the time.
VERSION = "v20240317"
# Full path to Google's closure compiler.
URI = (
"https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/"
f"{VERSION}/closure-compiler-{VERSION}.jar"
)
# Where we cache our copy of closure.
CLOSURE = libdot.BIN_DIR / f".closure-compiler.{VERSION}.jar"
# Where we store externs files.
EXTERNS_DIR = libdot.DIR / "externs"
# The version of externs that we pull in from closure. We normally pin to the
# closure version for stability, but this can be updated independently.
EXTERNS_VERSION = "22994a075cc37a630ca158bbb197efa8fc208eb0"
# The version of Chrome externs that we pull from Chromium directly.
CHROME_EXTERNS_VERSION = "8f892bcb37b3fe5fe7dff5c0150bc3875ea59ac2"
# We pick 50 as the mid point as it allows us to easily add files before and
# after the closure modules.
CLOSURE_ORDER_BASE = 50
# Load the Chrome externs after the closure ones. We'll prob never have 10
# closure modules, but if we do, we can increase that here.
CHROME_ORDER_BASE = CLOSURE_ORDER_BASE + 10
def update_closure_externs():
"""Update our local cache of externs."""
URI_TEMPLATE = (
"https://github.com/google/closure-compiler/raw/"
f"{EXTERNS_VERSION}/contrib/externs/%(name)s.js"
)
# NB: Order here matters when modules are subsets of others. For example,
# 'chrome.js' defines 'var chrome = {}' which 'chrome_extensions.js' needs.
# If we reorder them, closure-compiler might accept it, but then silently
# not check some things. It's kind of buggy that way :(.
# https://github.com/google/closure-compiler/issues/3586
EXTERNS = (
"chrome",
"chrome_extensions",
)
# Delete content we've forked locally. Keep sorted.
DELETE_LINES = {
"chrome_extensions": (
# StorageArea API.
(8945, 9010),
),
}
for i, name in enumerate(EXTERNS, start=CLOSURE_ORDER_BASE):
uri = URI_TEMPLATE % {"name": name}
path = EXTERNS_DIR / f"{i}-closure-{name}-v{EXTERNS_VERSION}.js"
if path.exists():
continue
for oldpath in EXTERNS_DIR.glob(f"*closure-{name}-v*.js"):
oldpath.unlink()
libdot.fetch(uri, path)
deletes = DELETE_LINES.get(name)
if deletes:
lines = path.read_text(encoding="utf-8").splitlines()
for start, end in deletes:
del lines[start : end + 1]
path.write_text("\n".join(lines), encoding="utf-8")
def update_chrome_externs():
"""Update our local cache of externs from Chrome."""
URI_TEMPLATE = (
"https://chromium.googlesource.com/chromium/src/+/"
f"{CHROME_EXTERNS_VERSION}/third_party/closure_compiler/"
"externs/%(name)s.js?format=TEXT"
)
# Order here shouldn't matter as each file shouldn't have overlapping APIs.
EXTERNS = ("terminal_private",)
for i, name in enumerate(EXTERNS, start=CHROME_ORDER_BASE):
uri = URI_TEMPLATE % {"name": name}
path = EXTERNS_DIR / f"{i}-chrome-{name}-v{CHROME_EXTERNS_VERSION}.js"
if path.exists():
continue
for oldpath in EXTERNS_DIR.glob(f"*-chrome-{name}-v*.js"):
oldpath.unlink()
libdot.fetch(uri, path, b64=True)
def update_closure():
"""Update our local cache of Google's closure compiler."""
if CLOSURE.exists():
return
for path in libdot.BIN_DIR.glob(".closure-compiler.*.jar"):
path.unlink()
libdot.fetch(URI, CLOSURE)
def convert_to_kokoro(data):
"""Take closure JSON output and convert it to kokoro comment format.
The |data| input will look like:
[
{
"column": 21,
"context": " * @return {!Promise<FileSystemFileEntry>}\n...",
"description": "FileSystemFileEntry is a reference type with ...",
"key": "JSC_MISSING_NULLABILITY_MODIFIER_JSDOC",
"level": "error",
"line": 174,
"source": "js/lib_fs.js"
},
{
"description": "1 error(s), 0 warning(s)",
"level": "info"
}
]
See convert_eslint_to_kokoro for example return value.
"""
for result in data:
# Ignore info lines.
if result["level"] == "info":
continue
# Ignore generated files not in git.
if libdot.lint.is_generated_path(result["source"]):
continue
# Add leading space prefix to results to get code text.
msg = "[closure] %s: %s\n%s\n\n %s" % (
result["level"],
result["key"],
result["description"],
"\n ".join(result["context"].splitlines()),
)
yield {
"path": os.path.relpath(result["source"], libdot.LIBAPPS_DIR),
"message": msg,
"startLine": result["line"],
"endLine": result.get("endline", result["line"]),
"startCharacter": result["column"],
"endCharacter": result["column"],
}
def setup():
"""Initialize the tool settings."""
update_closure()
update_closure_externs()
update_chrome_externs()
def run(argv=(), **kwargs):
"""Run the tool directly."""
setup()
return libdot.run(
list(argv),
cmd_prefix=["java", "-jar", CLOSURE],
log_prefix=["closure-compiler"],
**kwargs,
)
def perform(argv=(), paths=(), fix=False, gerrit_comments_file=None):
"""Run high level tool logic."""
ret = True
argv = list(argv)
paths = list(paths)
comments_path = libdot.lint.kokoro_comments_path(
gerrit_comments_file, "closure"
)
# Closure doesn't have any automatic fixing logic.
if fix:
return ret
setup()
externs = []
externs_paths = libdot.DIR / "externs"
for extern in externs_paths.glob("*.js"):
externs += ["--externs", os.path.relpath(extern)]
# Set the language first so projects can override it.
argv = (
["--language_in=ECMASCRIPT_2021"] + argv + ["--checks-only"] + externs
)
result = run(argv + paths, check=False)
if result.returncode:
ret = False
# Rerun for Gerrit.
if comments_path:
# Handle relative paths like "foo.json".
dirname = os.path.dirname(comments_path)
if dirname:
os.makedirs(dirname, exist_ok=True)
argv += ["--error_format=JSON"]
result = run(argv + paths, check=False, capture_output=True)
# Save a copy for debugging later.
with open(comments_path + ".in", "wb") as fp:
fp.write(result.stderr)
data = json.loads(result.stderr.decode("utf-8"))
comments = list(convert_to_kokoro(data))
with open(comments_path, "w", encoding="utf-8") as fp:
json.dump(comments, fp, sort_keys=True)
elif comments_path:
# If there were no failures, clear the files to avoid leaving previous
# results laying around & confuse devs.
libdot.unlink(comments_path)
libdot.unlink(comments_path + ".in")
return ret
def main(argv):
"""The main func!"""
libdot.setup_logging()
run(argv)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))