blob: a6284ff93289338fddd20e912c11df2a73b5e90f [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# 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 = '20200927'
# Full path to Google's closure compiler.
URI = ('https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/'
f'v{VERSION}/closure-compiler-v{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 = VERSION
# The version of Chrome externs that we pull from Chromium directly.
CHROME_EXTERNS_VERSION = '18029c0278c7da713fb6b629e84992ceabda7ae2'
# 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'v{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',
)
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)
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)]
argv += [
'--checks-only',
'--language_in=ECMASCRIPT_2018',
] + 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:]))