Commit 04ada830 authored by dpapad's avatar dpapad Committed by Commit Bot

Closure compiler: Remove unused logic from compiler.py.

Specifically removing logic related to handling <if expr> and
<include> tags, which seems to only be triggered by tests, and
nowhere in prod code.

Bug: 1135226
Change-Id: I7d362420e0d94df3c29e6f646cfe25acc5d23e2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2448658Reviewed-by: default avatarRebekah Potter <rbpotter@chromium.org>
Commit-Queue: dpapad <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#815567}
parent 3cfa3b6b
...@@ -6,14 +6,8 @@ ...@@ -6,14 +6,8 @@
"""Runs Closure compiler on JavaScript files to check for errors and produce """Runs Closure compiler on JavaScript files to check for errors and produce
minified output.""" minified output."""
import argparse
import os import os
import re
import subprocess import subprocess
import sys
import tempfile
import processor
_CURRENT_DIR = os.path.join(os.path.dirname(__file__)) _CURRENT_DIR = os.path.join(os.path.dirname(__file__))
...@@ -34,28 +28,14 @@ class Compiler(object): ...@@ -34,28 +28,14 @@ class Compiler(object):
"-XX:+TieredCompilation", "-XX:+TieredCompilation",
] ]
_POLYMER_EXTERNS = os.path.join(_CURRENT_DIR, "externs", "polymer-1.0.js")
def __init__(self, verbose=False): def __init__(self, verbose=False):
""" """
Args: Args:
verbose: Whether this class should output diagnostic messages. verbose: Whether this class should output diagnostic messages.
""" """
self._compiler_jar = os.path.join(_CURRENT_DIR, "compiler", "compiler.jar") self._compiler_jar = os.path.join(_CURRENT_DIR, "compiler", "compiler.jar")
self._target = None
self._temp_files = []
self._verbose = verbose self._verbose = verbose
def _nuke_temp_files(self):
"""Deletes any temp files this class knows about."""
if not self._temp_files:
return
self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files))
for f in self._temp_files:
os.remove(f)
self._temp_files = []
def _log_debug(self, msg, error=False): def _log_debug(self, msg, error=False):
"""Logs |msg| to stdout if --verbose/-v is passed when invoking this script. """Logs |msg| to stdout if --verbose/-v is passed when invoking this script.
...@@ -65,14 +45,6 @@ class Compiler(object): ...@@ -65,14 +45,6 @@ class Compiler(object):
if self._verbose: if self._verbose:
print "(INFO) %s" % msg print "(INFO) %s" % msg
def _log_error(self, msg):
"""Logs |msg| to stderr regardless of --flags.
Args:
msg: An error message to log.
"""
print >> sys.stderr, "(ERROR) %s" % msg
def run_jar(self, jar, args): def run_jar(self, jar, args):
"""Runs a .jar from the command line with arguments. """Runs a .jar from the command line with arguments.
...@@ -92,223 +64,3 @@ class Compiler(object): ...@@ -92,223 +64,3 @@ class Compiler(object):
process = subprocess.Popen(shell_command, **kwargs) process = subprocess.Popen(shell_command, **kwargs)
_, stderr = process.communicate() _, stderr = process.communicate()
return process.returncode, stderr return process.returncode, stderr
def _get_line_number(self, match):
"""When chrome is built, it preprocesses its JavaScript from:
<include src="blah.js">
alert(1);
to:
/* contents of blah.js inlined */
alert(1);
Because Closure Compiler requires this inlining already be done (as
<include> isn't valid JavaScript), this script creates temporary files to
expand all the <include>s.
When type errors are hit in temporary files, a developer doesn't know the
original source location to fix. This method maps from /tmp/file:300 back to
/original/source/file:100 so fixing errors is faster for developers.
Args:
match: A re.MatchObject from matching against a line number regex.
Returns:
The fixed up /file and :line number.
"""
real_file = self._processor.get_file_from_line(match.group(1))
return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
def _clean_up_error(self, error):
"""Reverse the effects that funky <include> preprocessing steps have on
errors messages.
Args:
error: A Closure compiler error (2 line string with error and source).
Return:
The fixed up error string.
"""
assert self._target
assert self._expanded_file
expanded_file = self._expanded_file
fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error)
return fixed.replace(expanded_file, os.path.abspath(self._target))
def _format_errors(self, errors):
"""Formats Closure compiler errors to easily spot compiler output.
Args:
errors: A list of strings extracted from the Closure compiler's output.
Returns:
A formatted output string.
"""
contents = "\n## ".join("\n\n".join(errors).splitlines())
return "## %s" % contents if contents else ""
def _create_temp_file(self, contents):
"""Creates an owned temporary file with |contents|.
Args:
content: A string of the file contens to write to a temporary file.
Return:
The filepath of the newly created, written, and closed temporary file.
"""
with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
self._temp_files.append(tmp_file.name)
tmp_file.write(contents)
return tmp_file.name
def run(self, sources, out_file, closure_args=None,
custom_sources=False, custom_includes=False):
"""Closure compile |sources| while checking for errors.
Args:
sources: Files to compile. sources[0] is the typically the target file.
sources[1:] are externs and dependencies in topological order. Order
is not guaranteed if custom_sources is True.
out_file: A file where the compiled output is written to.
closure_args: Arguments passed directly to the Closure compiler.
custom_sources: Whether |sources| was customized by the target (e.g. not
in GYP dependency order).
custom_includes: Whether <include>s are processed when |custom_sources|
is True.
Returns:
(found_errors, stderr) A boolean indicating whether errors were found and
the raw Closure compiler stderr (as a string).
"""
is_extern = lambda f: 'externs' in f
externs_and_deps = [self._POLYMER_EXTERNS]
if custom_sources:
if custom_includes:
# TODO(dbeam): this is fairly hacky. Can we just remove custom_sources
# soon when all the things kept on life support using it die?
self._target = sources.pop()
externs_and_deps += sources
else:
self._target = sources[0]
externs_and_deps += sources[1:]
externs = filter(is_extern, externs_and_deps)
deps = filter(lambda f: not is_extern(f), externs_and_deps)
assert externs or deps or self._target
self._log_debug("Externs: %s" % externs)
self._log_debug("Dependencies: %s" % deps)
self._log_debug("Target: %s" % self._target)
js_args = deps + ([self._target] if self._target else [])
process_includes = custom_includes or not custom_sources
if process_includes:
# TODO(dbeam): compiler.jar automatically detects "@externs" in a --js arg
# and moves these files to a different AST tree. However, because we use
# one big funky <include> meta-file, it thinks all the code is one big
# externs. Just use --js when <include> dies.
cwd, tmp_dir = os.getcwd(), tempfile.gettempdir()
rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f)
contents = ['<include src="%s">' % rel_path(f) for f in js_args]
meta_file = self._create_temp_file("\n".join(contents))
self._log_debug("Meta file: %s" % meta_file)
self._processor = processor.Processor(meta_file)
self._expanded_file = self._create_temp_file(self._processor.contents)
self._log_debug("Expanded file: %s" % self._expanded_file)
js_args = [self._expanded_file]
closure_args = closure_args or []
closure_args += ["summary_detail_level=3", "continue_after_errors"]
args = ["--externs=%s" % e for e in externs] + \
["--js=%s" % s for s in js_args] + \
["--%s" % arg for arg in closure_args]
assert out_file
out_dir = os.path.dirname(out_file)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
checks_only = 'checks_only' in closure_args
if not checks_only:
args += ["--js_output_file=%s" % out_file]
self._log_debug("Args: %s" % " ".join(args))
return_code, stderr = self.run_jar(self._compiler_jar, args)
errors = stderr.strip().split("\n\n")
maybe_summary = errors.pop()
summary = re.search("(?P<error_count>\d+).*error.*warning", maybe_summary)
if summary:
self._log_debug("Summary: %s" % maybe_summary)
else:
# Not a summary. Running the jar failed. Bail.
self._log_error(stderr)
self._nuke_temp_files()
sys.exit(1)
if summary.group('error_count') != "0":
if os.path.exists(out_file):
os.remove(out_file)
elif checks_only and return_code == 0:
# Compile succeeded but --checks_only disables --js_output_file from
# actually writing a file. Write a file ourselves so incremental builds
# still work.
with open(out_file, 'w') as f:
f.write('')
if process_includes:
errors = map(self._clean_up_error, errors)
output = self._format_errors(errors)
if errors:
prefix = "\n" if output else ""
self._log_error("Error in: %s%s%s" % (self._target, prefix, output))
elif output:
self._log_debug("Output: %s" % output)
self._nuke_temp_files()
return bool(errors) or return_code > 0, stderr
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Typecheck JavaScript using Closure compiler")
parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
help="Path to a source file to typecheck")
parser.add_argument("--custom_sources", action="store_true",
help="Whether this rules has custom sources.")
parser.add_argument("--custom_includes", action="store_true",
help="If present, <include>s are processed when "
"using --custom_sources.")
parser.add_argument("-o", "--out_file", required=True,
help="A file where the compiled output is written to")
parser.add_argument("-c", "--closure_args", nargs=argparse.ZERO_OR_MORE,
help="Arguments passed directly to the Closure compiler")
parser.add_argument("-v", "--verbose", action="store_true",
help="Show more information as this script runs")
opts = parser.parse_args()
compiler = Compiler(verbose=opts.verbose)
found_errors, stderr = compiler.run(opts.sources, out_file=opts.out_file,
closure_args=opts.closure_args,
custom_sources=opts.custom_sources,
custom_includes=opts.custom_includes)
if found_errors:
if opts.custom_sources:
print stderr
sys.exit(1)
This diff is collapsed.
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Process Chrome resources (HTML/CSS/JS) to handle <include> and <if> tags."""
from collections import defaultdict
import re
import os
class LineNumber(object):
"""A simple wrapper to hold line information (e.g. file.js:32)."""
def __init__(self, source_file, line_number):
"""
Args:
source_file: A file path (as a string).
line_number: The line in |file| (as an integer).
"""
self.file = source_file
self.line_number = int(line_number)
class FileCache(object):
"""An in-memory cache to speed up reading the same files over and over.
Usage:
FileCache.read(path_to_file)
"""
_cache = defaultdict(str)
@classmethod
def read(self, source_file):
"""Read a file and return it as a string.
Args:
source_file: a file path (as a string) to read and return the contents.
Returns:
The contents of |source_file| (as a string).
"""
abs_file = os.path.abspath(source_file)
self._cache[abs_file] = self._cache[abs_file] or open(abs_file, "r").read()
return self._cache[abs_file]
class Processor(object):
"""Processes resource files, inlining the contents of <include> tags, removing
<if> tags, and retaining original line info.
For example
1: /* blah.js */
2: <if expr="is_win">
3: <include src="win.js">
4: </if>
would be turned into:
1: /* blah.js */
2:
3: /* win.js */
4: alert('Ew; Windows.');
5:
"""
_IF_TAGS_REG = "</?if[^>]*?>"
_INCLUDE_REG = "<include[^>]+src=['\"]([^>]*)['\"]>"
def __init__(self, source_file):
"""
Args:
source_file: A file path to process (as a string).
"""
self.included_files = set()
self._index = 0
self._lines = self._get_file(source_file)
# Can't enumerate(self._lines) here because some lines are re-processed.
while self._index < len(self._lines):
current_line = self._lines[self._index]
match = re.search(self._INCLUDE_REG, current_line[2])
if match:
file_dir = os.path.dirname(current_line[0])
file_name = os.path.abspath(os.path.join(file_dir, match.group(1)))
if file_name not in self.included_files:
self._include_file(file_name)
continue # Stay on the same line.
else:
# Found a duplicate <include>. Ignore and insert a blank line to
# preserve line numbers.
self._lines[self._index] = self._lines[self._index][:2] + ("",)
self._index += 1
for i, line in enumerate(self._lines):
self._lines[i] = line[:2] + (re.sub(self._IF_TAGS_REG, "", line[2]),)
self.contents = "\n".join(l[2] for l in self._lines)
# Returns a list of tuples in the format: (file, line number, line contents).
def _get_file(self, source_file):
lines = FileCache.read(source_file).splitlines()
return [(source_file, lnum + 1, line) for lnum, line in enumerate(lines)]
def _include_file(self, source_file):
self.included_files.add(source_file)
f = self._get_file(source_file)
self._lines = self._lines[:self._index] + f + self._lines[self._index + 1:]
def get_file_from_line(self, line_number):
"""Get the original file and line number for an expanded file's line number.
Args:
line_number: A processed file's line number (as an integer or string).
"""
line_number = int(line_number) - 1
return LineNumber(self._lines[line_number][0], self._lines[line_number][1])
#!/usr/bin/env python
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test resources processing, i.e. <if> and <include> tag handling."""
import unittest
from processor import FileCache, Processor, LineNumber
class ProcessorTest(unittest.TestCase):
"""Test <include> tag processing logic."""
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
self.maxDiff = None
def setUp(self):
FileCache._cache["/debug.js"] = """
// Copyright 2002 Older Chromium Author dudes.
function debug(msg) { if (window.DEBUG) alert(msg); }
""".strip()
FileCache._cache["/global.js"] = """
// Copyright 2014 Old Chromium Author dudes.
<include src="/debug.js">
var global = 'type checking!';
""".strip()
FileCache._cache["/checked.js"] = """
// Copyright 2028 Future Chromium Author dudes.
/**
* @fileoverview Coolest app ever.
* @author Douglas Crockford (douglas@crockford.com)
*/
<include src="/global.js">
debug(global);
// Here continues checked.js, a swell file.
""".strip()
FileCache._cache["/double-debug.js"] = """
<include src="/debug.js">
<include src="/debug.js">
""".strip()
self._processor = Processor("/checked.js")
def testInline(self):
self.assertMultiLineEqual("""
// Copyright 2028 Future Chromium Author dudes.
/**
* @fileoverview Coolest app ever.
* @author Douglas Crockford (douglas@crockford.com)
*/
// Copyright 2014 Old Chromium Author dudes.
// Copyright 2002 Older Chromium Author dudes.
function debug(msg) { if (window.DEBUG) alert(msg); }
var global = 'type checking!';
debug(global);
// Here continues checked.js, a swell file.
""".strip(), self._processor.contents)
def assertLineNumber(self, abs_line, expected_line):
actual_line = self._processor.get_file_from_line(abs_line)
self.assertEqual(expected_line.file, actual_line.file)
self.assertEqual(expected_line.line_number, actual_line.line_number)
def testGetFileFromLine(self):
"""Verify that inlined files retain their original line info."""
self.assertLineNumber(1, LineNumber("/checked.js", 1))
self.assertLineNumber(5, LineNumber("/checked.js", 5))
self.assertLineNumber(6, LineNumber("/global.js", 1))
self.assertLineNumber(7, LineNumber("/debug.js", 1))
self.assertLineNumber(8, LineNumber("/debug.js", 2))
self.assertLineNumber(9, LineNumber("/global.js", 3))
self.assertLineNumber(10, LineNumber("/checked.js", 7))
self.assertLineNumber(11, LineNumber("/checked.js", 8))
def testIncludedFiles(self):
"""Verify that files are tracked correctly as they're inlined."""
self.assertEquals(set(["/global.js", "/debug.js"]),
self._processor.included_files)
def testDoubleIncludedSkipped(self):
"""Verify that doubly included files are skipped."""
processor = Processor("/double-debug.js")
self.assertEquals(set(["/debug.js"]), processor.included_files)
self.assertEquals(FileCache.read("/debug.js") + "\n", processor.contents)
class IfStrippingTest(unittest.TestCase):
"""Test that the contents of XML <if> blocks are stripped."""
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
self.maxDiff = None
def setUp(self):
FileCache._cache["/century.js"] = """
function getCurrentCentury() {
<if expr="netscape_os">
alert("Oh wow!");
return "XX";
</if>
return "XXI";
}
""".strip()
self.processor_ = Processor("/century.js")
def testIfStripping(self):
self.assertMultiLineEqual("""
function getCurrentCentury() {
alert("Oh wow!");
return "XX";
return "XXI";
}
""".strip(), self.processor_.contents)
if __name__ == '__main__':
unittest.main()
#!/usr/bin/env python
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import compiler_test
import processor_test
for test_module in [compiler_test, processor_test]:
test_module.unittest.main(test_module)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment