Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(117)

Side by Side Diff: third_party/closure_compiler/checker.py

Issue 476453002: Python readability review for dbeam@. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: remove space Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | third_party/closure_compiler/compile_js.gypi » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Runs Closure compiler on a JavaScript file to check for errors.""" 6 """Runs Closure compiler on a JavaScript file to check for errors."""
7 7
8 import argparse 8 import argparse
9 import os 9 import os
10 import re 10 import re
11 import subprocess 11 import subprocess
12 import sys 12 import sys
13 import tempfile 13 import tempfile
14 14
15 import build.inputs 15 import build.inputs
16 import processor 16 import processor
17 import error_filter 17 import error_filter
18 18
19 19
20 class Checker(object): 20 class Checker(object):
21 """Runs the Closure compiler on a given source file and returns the 21 """Runs the Closure compiler on a given source file to typecheck it."""
22 success/errors."""
23 22
24 _COMMON_CLOSURE_ARGS = [ 23 _COMMON_CLOSURE_ARGS = [
25 "--accept_const_keyword", 24 "--accept_const_keyword",
26 "--jscomp_error=accessControls", 25 "--jscomp_error=accessControls",
27 "--jscomp_error=ambiguousFunctionDecl", 26 "--jscomp_error=ambiguousFunctionDecl",
28 "--jscomp_error=checkStructDictInheritance", 27 "--jscomp_error=checkStructDictInheritance",
29 "--jscomp_error=checkTypes", 28 "--jscomp_error=checkTypes",
30 "--jscomp_error=checkVars", 29 "--jscomp_error=checkVars",
31 "--jscomp_error=constantProperty", 30 "--jscomp_error=constantProperty",
32 "--jscomp_error=deprecated", 31 "--jscomp_error=deprecated",
33 "--jscomp_error=externsValidation", 32 "--jscomp_error=externsValidation",
34 "--jscomp_error=globalThis", 33 "--jscomp_error=globalThis",
35 "--jscomp_error=invalidCasts", 34 "--jscomp_error=invalidCasts",
36 "--jscomp_error=missingProperties", 35 "--jscomp_error=missingProperties",
37 "--jscomp_error=missingReturn", 36 "--jscomp_error=missingReturn",
38 "--jscomp_error=nonStandardJsDocs", 37 "--jscomp_error=nonStandardJsDocs",
39 "--jscomp_error=suspiciousCode", 38 "--jscomp_error=suspiciousCode",
40 "--jscomp_error=undefinedNames", 39 "--jscomp_error=undefinedNames",
41 "--jscomp_error=undefinedVars", 40 "--jscomp_error=undefinedVars",
42 "--jscomp_error=unknownDefines", 41 "--jscomp_error=unknownDefines",
43 "--jscomp_error=uselessCode", 42 "--jscomp_error=uselessCode",
44 "--jscomp_error=visibility", 43 "--jscomp_error=visibility",
45 "--language_in=ECMASCRIPT5_STRICT", 44 "--language_in=ECMASCRIPT5_STRICT",
46 "--summary_detail_level=3", 45 "--summary_detail_level=3",
47 "--compilation_level=SIMPLE_OPTIMIZATIONS", 46 "--compilation_level=SIMPLE_OPTIMIZATIONS",
48 "--source_map_format=V3", 47 "--source_map_format=V3",
49 ] 48 ]
50 49
51 # These are the extra flags used when compiling in 'strict' mode. 50 # These are the extra flags used when compiling in strict mode.
52 # Flags that are normally disabled are turned on for strict mode. 51 # Flags that are normally disabled are turned on for strict mode.
53 _STRICT_CLOSURE_ARGS = [ 52 _STRICT_CLOSURE_ARGS = [
54 "--jscomp_error=reportUnknownTypes", 53 "--jscomp_error=reportUnknownTypes",
55 "--jscomp_error=duplicate", 54 "--jscomp_error=duplicate",
56 "--jscomp_error=misplacedTypeAnnotation", 55 "--jscomp_error=misplacedTypeAnnotation",
57 ] 56 ]
58 57
59 _DISABLED_CLOSURE_ARGS = [ 58 _DISABLED_CLOSURE_ARGS = [
60 # TODO(dbeam): happens when the same file is <include>d multiple times. 59 # TODO(dbeam): happens when the same file is <include>d multiple times.
61 "--jscomp_off=duplicate", 60 "--jscomp_off=duplicate",
62 # TODO(fukino): happens when cr.defineProperty() has a type annotation. 61 # TODO(fukino): happens when cr.defineProperty() has a type annotation.
63 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. 62 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562.
64 "--jscomp_off=misplacedTypeAnnotation", 63 "--jscomp_off=misplacedTypeAnnotation",
65 ] 64 ]
66 65
67 _JAR_COMMAND = [ 66 _JAR_COMMAND = [
68 "java", 67 "java",
69 "-jar", 68 "-jar",
70 "-Xms1024m", 69 "-Xms1024m",
71 "-client", 70 "-client",
72 "-XX:+TieredCompilation" 71 "-XX:+TieredCompilation"
73 ] 72 ]
74 73
75 _found_java = False
76
77 def __init__(self, verbose=False, strict=False): 74 def __init__(self, verbose=False, strict=False):
75 """
76 Args:
77 verbose: Whether this class should output diagnostic messages.
78 strict: Whether the Closure Compiler should be invoked more strictly.
79 """
78 current_dir = os.path.join(os.path.dirname(__file__)) 80 current_dir = os.path.join(os.path.dirname(__file__))
79 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") 81 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar")
80 self._temp_files = [] 82 self._temp_files = []
81 self._verbose = verbose 83 self._verbose = verbose
82 self._strict = strict 84 self._strict = strict
83 self._error_filter = error_filter.PromiseErrorFilter() 85 self._error_filter = error_filter.PromiseErrorFilter()
84 86
85 def _clean_up(self): 87 def _clean_up(self):
88 """Deletes any temp files this class knows about."""
86 if not self._temp_files: 89 if not self._temp_files:
87 return 90 return
88 91
89 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files)) 92 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files))
90 for f in self._temp_files: 93 for f in self._temp_files:
91 os.remove(f) 94 os.remove(f)
92 self._temp_files = [] 95 self._temp_files = []
93 96
94 def _debug(self, msg, error=False): 97 def _log_debug(self, msg, error=False):
98 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script.
99
100 Args:
101 msg: A debug message to log.
102 """
95 if self._verbose: 103 if self._verbose:
96 print "(INFO) %s" % msg 104 print "(INFO) %s" % msg
97 105
98 def _error(self, msg): 106 def _log_error(self, msg):
107 """Logs |msg| to stderr regardless of --flags.
108
109 Args:
110 msg: An error message to log.
111 """
99 print >> sys.stderr, "(ERROR) %s" % msg 112 print >> sys.stderr, "(ERROR) %s" % msg
100 self._clean_up()
101 113
102 def _common_args(self): 114 def _common_args(self):
103 """Returns an array of the common closure compiler args.""" 115 """Returns an array of the common closure compiler args."""
104 if self._strict: 116 if self._strict:
105 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS 117 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS
106 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS 118 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS
107 119
108 def _run_command(self, cmd): 120 def _run_jar(self, jar, args):
109 """Runs a shell command. 121 """Runs a .jar from the command line with arguments.
110 122
111 Args: 123 Args:
112 cmd: A list of tokens to be joined into a shell command. 124 jar: A file path to a .jar file
125 args: A list of command line arguments to be passed when running the .jar.
113 126
114 Return: 127 Return:
115 True if the exit code was 0, else False. 128 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and
129 the stderr collected while running |jar| (as a string).
116 """ 130 """
117 cmd_str = " ".join(cmd) 131 shell_command = " ".join(self._JAR_COMMAND + [jar] + args)
118 self._debug("Running command: %s" % cmd_str) 132 self._log_debug("Running jar: %s" % shell_command)
119 133
120 devnull = open(os.devnull, "w") 134 devnull = open(os.devnull, "w")
121 return subprocess.Popen( 135 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True}
122 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) 136 process = subprocess.Popen(shell_command, **kwargs)
137 _, stderr = process.communicate()
138 return process.returncode, stderr
123 139
124 def _check_java_path(self): 140 def _get_line_number(self, match):
125 """Checks that `java` is on the system path.""" 141 """When chrome is built, it preprocesses its JavaScript from:
126 if not self._found_java:
127 proc = self._run_command(["which", "java"])
128 proc.communicate()
129 if proc.returncode == 0:
130 self._found_java = True
131 else:
132 self._error("Cannot find java (`which java` => %s)" % proc.returncode)
133 142
134 return self._found_java 143 <include src="blah.js">
144 alert(1);
135 145
136 def _run_jar(self, jar, args=None): 146 to:
137 args = args or []
138 self._check_java_path()
139 return self._run_command(self._JAR_COMMAND + [jar] + args)
140 147
141 def _fix_line_number(self, match): 148 /* contents of blah.js inlined */
142 """Changes a line number from /tmp/file:300 to /orig/file:100. 149 alert(1);
150
151 Because Closure Compiler requires this inlining already be done (as
152 <include> isn't valid JavaScript), this script creates temporary files to
153 expand all the <include>s.
154
155 When type errors are hit in temporary files, a developer doesn't know the
156 original source location to fix. This method maps from /tmp/file:300 back to
157 /original/source/file:100 so fixing errors is faster for developers.
143 158
144 Args: 159 Args:
145 match: A re.MatchObject from matching against a line number regex. 160 match: A re.MatchObject from matching against a line number regex.
146 161
147 Returns: 162 Returns:
148 The fixed up /file and :line number. 163 The fixed up /file and :line number.
149 """ 164 """
150 real_file = self._processor.get_file_from_line(match.group(1)) 165 real_file = self._processor.get_file_from_line(match.group(1))
151 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) 166 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
152 167
153 def _fix_up_error(self, error): 168 def _filter_errors(self, errors):
154 """Filter out irrelevant errors or fix line numbers. 169 """Removes some extraneous errors. For example, we ignore:
170
171 Variable x first declared in /tmp/expanded/file
172
173 Because it's just a duplicated error (it'll only ever show up 2+ times).
174 We also ignore Promose-based errors:
175
176 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null)
177 required: (function (Promise<VolumeInfo>): ?|null|undefined)
178
179 as templates don't work with Promises in all cases yet. See
180 https://github.com/google/closure-compiler/issues/715 for details.
155 181
156 Args: 182 Args:
157 error: A Closure compiler error (2 line string with error and source). 183 errors: A list of string errors extracted from Closure Compiler output.
158 184
159 Return: 185 Return:
160 The fixed up error string (blank if it should be ignored). 186 A slimmer, sleeker list of relevant errors (strings).
161 """ 187 """
162 if " first declared in " in error: 188 first_declared_in = lambda e: " first declared in " not in e
163 # Ignore "Variable x first declared in /same/file". 189 return self._error_filter.filter(filter(first_declared_in, errors))
164 return ""
165 190
191 def _fix_up_error(self, error):
192 """Reverse the effects that funky <include> preprocessing steps have on
193 errors messages.
194
195 Args:
196 error: A Closure compiler error (2 line string with error and source).
197
198 Return:
199 The fixed up error string.
200 """
166 expanded_file = self._expanded_file 201 expanded_file = self._expanded_file
167 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error) 202 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error)
168 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) 203 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
169 204
170 def _format_errors(self, errors): 205 def _format_errors(self, errors):
171 """Formats Closure compiler errors to easily spot compiler output.""" 206 """Formats Closure compiler errors to easily spot compiler output.
172 errors = filter(None, errors) 207
208 Args:
209 errors: A list of strings extracted from the Closure compiler's output.
210
211 Returns:
212 A formatted output string.
213 """
173 contents = "\n## ".join("\n\n".join(errors).splitlines()) 214 contents = "\n## ".join("\n\n".join(errors).splitlines())
174 return "## %s" % contents if contents else "" 215 return "## %s" % contents if contents else ""
175 216
176 def _create_temp_file(self, contents): 217 def _create_temp_file(self, contents):
218 """Creates an owned temporary file with |contents|.
219
220 Args:
221 content: A string of the file contens to write to a temporary file.
222
223 Return:
224 The filepath of the newly created, written, and closed temporary file.
225 """
177 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: 226 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
178 self._temp_files.append(tmp_file.name) 227 self._temp_files.append(tmp_file.name)
179 tmp_file.write(contents) 228 tmp_file.write(contents)
180 return tmp_file.name 229 return tmp_file.name
181 230
182 def _run_js_check(self, sources, out_file=None, externs=None): 231 def _run_js_check(self, sources, out_file=None, externs=None):
183 if not self._check_java_path(): 232 """Check |sources| for type errors.
184 return 1, ""
185 233
234 Args:
235 sources: Files to check.
236 externs: @extern files that inform the compiler about custom globals.
237
238 Returns:
239 (errors, stderr) A parsed list of errors (strings) found by the compiler
240 and the raw stderr (as a string).
241 """
186 args = ["--js=%s" % s for s in sources] 242 args = ["--js=%s" % s for s in sources]
187 243
188 if out_file: 244 if out_file:
189 args += ["--js_output_file=%s" % out_file] 245 args += ["--js_output_file=%s" % out_file]
190 args += ["--create_source_map=%s.map" % out_file] 246 args += ["--create_source_map=%s.map" % out_file]
191 247
192 if externs: 248 if externs:
193 args += ["--externs=%s" % e for e in externs] 249 args += ["--externs=%s" % e for e in externs]
250
194 args_file_content = " %s" % " ".join(self._common_args() + args) 251 args_file_content = " %s" % " ".join(self._common_args() + args)
195 self._debug("Args: %s" % args_file_content.strip()) 252 self._log_debug("Args: %s" % args_file_content.strip())
196 253
197 args_file = self._create_temp_file(args_file_content) 254 args_file = self._create_temp_file(args_file_content)
198 self._debug("Args file: %s" % args_file) 255 self._log_debug("Args file: %s" % args_file)
199 256
200 runner_args = ["--compiler-args-file=%s" % args_file] 257 runner_args = ["--compiler-args-file=%s" % args_file]
201 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) 258 _, stderr = self._run_jar(self._runner_jar, runner_args)
202 _, stderr = runner_cmd.communicate()
203 259
204 errors = stderr.strip().split("\n\n") 260 errors = stderr.strip().split("\n\n")
205 self._debug("Summary: %s" % errors.pop()) 261 maybe_summary = errors.pop()
206 262
207 self._clean_up() 263 if re.search(".*error.*warning.*typed", maybe_summary):
264 self._log_debug("Summary: %s" % maybe_summary)
265 else:
266 # Not a summary. Running the jar failed. Bail.
267 self._log_error(stderr)
268 self._clean_up()
269 sys.exit(1)
208 270
209 return errors, stderr 271 return errors, stderr
210 272
211 def check(self, source_file, out_file=None, depends=None, externs=None): 273 def check(self, source_file, out_file=None, depends=None, externs=None):
212 """Closure compile a file and check for errors. 274 """Closure compiler |source_file| while checking for errors.
213 275
214 Args: 276 Args:
215 source_file: A file to check. 277 source_file: A file to check.
216 out_file: A file where the compiled output is written to. 278 out_file: A file where the compiled output is written to.
217 depends: Other files that would be included with a <script> earlier in 279 depends: Files that |source_file| requires to run (e.g. earlier <script>).
218 the page. 280 externs: @extern files that inform the compiler about custom globals.
219 externs: @extern files that inform the compiler about custom globals.
220 281
221 Returns: 282 Returns:
222 (has_errors, output) A boolean indicating if there were errors and the 283 (found_errors, stderr) A boolean indicating whether errors were found and
223 Closure compiler output (as a string). 284 the raw Closure compiler stderr (as a string).
224 """ 285 """
225 depends = depends or [] 286 self._log_debug("FILE: %s" % source_file)
226 externs = externs or set()
227
228 if not self._check_java_path():
229 return 1, ""
230
231 self._debug("FILE: %s" % source_file)
232 287
233 if source_file.endswith("_externs.js"): 288 if source_file.endswith("_externs.js"):
234 self._debug("Skipping externs: %s" % source_file) 289 self._log_debug("Skipping externs: %s" % source_file)
235 return 290 return
236 291
237 self._file_arg = source_file 292 self._file_arg = source_file
238 293
239 tmp_dir = tempfile.gettempdir() 294 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir()
240 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) 295 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f)
241 296
297 depends = depends or []
242 includes = [rel_path(f) for f in depends + [source_file]] 298 includes = [rel_path(f) for f in depends + [source_file]]
243 contents = ['<include src="%s">' % i for i in includes] 299 contents = ['<include src="%s">' % i for i in includes]
244 meta_file = self._create_temp_file("\n".join(contents)) 300 meta_file = self._create_temp_file("\n".join(contents))
245 self._debug("Meta file: %s" % meta_file) 301 self._log_debug("Meta file: %s" % meta_file)
246 302
247 self._processor = processor.Processor(meta_file) 303 self._processor = processor.Processor(meta_file)
248 self._expanded_file = self._create_temp_file(self._processor.contents) 304 self._expanded_file = self._create_temp_file(self._processor.contents)
249 self._debug("Expanded file: %s" % self._expanded_file) 305 self._log_debug("Expanded file: %s" % self._expanded_file)
250 306
251 errors, stderr = self._run_js_check([self._expanded_file], 307 errors, stderr = self._run_js_check([self._expanded_file],
252 out_file=out_file, externs=externs) 308 out_file=out_file, externs=externs)
309 filtered_errors = self._filter_errors(errors)
310 fixed_errors = map(self._fix_up_error, filtered_errors)
311 output = self._format_errors(fixed_errors)
253 312
254 # Filter out false-positive promise chain errors. 313 if fixed_errors:
255 # See https://github.com/google/closure-compiler/issues/715 for details. 314 prefix = "\n" if output else ""
256 errors = self._error_filter.filter(errors); 315 self._log_error("Error in: %s%s%s" % (source_file, prefix, output))
316 elif output:
317 self._log_debug("Output: %s" % output)
257 318
258 output = self._format_errors(map(self._fix_up_error, errors)) 319 self._clean_up()
259 if errors: 320 return bool(fixed_errors), stderr
260 prefix = "\n" if output else ""
261 self._error("Error in: %s%s%s" % (source_file, prefix, output))
262 elif output:
263 self._debug("Output: %s" % output)
264
265 return bool(errors), output
266 321
267 def check_multiple(self, sources): 322 def check_multiple(self, sources):
268 """Closure compile a set of files and check for errors. 323 """Closure compile a set of files and check for errors.
269 324
270 Args: 325 Args:
271 sources: An array of files to check. 326 sources: An array of files to check.
272 327
273 Returns: 328 Returns:
274 (has_errors, output) A boolean indicating if there were errors and the 329 (found_errors, stderr) A boolean indicating whether errors were found and
275 Closure compiler output (as a string). 330 the raw Closure Compiler stderr (as a string).
276 """ 331 """
332 errors, stderr = self._run_js_check(sources, [])
333 self._clean_up()
334 return bool(errors), stderr
277 335
278 errors, stderr = self._run_js_check(sources)
279 return bool(errors), stderr
280 336
281 if __name__ == "__main__": 337 if __name__ == "__main__":
282 parser = argparse.ArgumentParser( 338 parser = argparse.ArgumentParser(
283 description="Typecheck JavaScript using Closure compiler") 339 description="Typecheck JavaScript using Closure compiler")
284 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, 340 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
285 help="Path to a source file to typecheck") 341 help="Path to a source file to typecheck")
286 single_file_group = parser.add_mutually_exclusive_group() 342 single_file_group = parser.add_mutually_exclusive_group()
287 single_file_group.add_argument("--single-file", dest="single_file", 343 single_file_group.add_argument("--single-file", dest="single_file",
288 action="store_true", 344 action="store_true",
289 help="Process each source file individually") 345 help="Process each source file individually")
(...skipping 19 matching lines...) Expand all
309 365
310 if opts.out_file: 366 if opts.out_file:
311 out_dir = os.path.dirname(opts.out_file) 367 out_dir = os.path.dirname(opts.out_file)
312 if not os.path.exists(out_dir): 368 if not os.path.exists(out_dir):
313 os.makedirs(out_dir) 369 os.makedirs(out_dir)
314 370
315 checker = Checker(verbose=opts.verbose, strict=opts.strict) 371 checker = Checker(verbose=opts.verbose, strict=opts.strict)
316 if opts.single_file: 372 if opts.single_file:
317 for source in opts.sources: 373 for source in opts.sources:
318 depends, externs = build.inputs.resolve_recursive_dependencies( 374 depends, externs = build.inputs.resolve_recursive_dependencies(
319 source, 375 source, depends, externs)
320 depends, 376 found_errors, _ = checker.check(source, out_file=opts.out_file,
321 externs) 377 depends=depends, externs=externs)
322 has_errors, _ = checker.check(source, out_file=opts.out_file, 378 if found_errors:
323 depends=depends, externs=externs)
324 if has_errors:
325 sys.exit(1) 379 sys.exit(1)
326
327 else: 380 else:
328 has_errors, errors = checker.check_multiple(opts.sources) 381 found_errors, stderr = checker.check_multiple(opts.sources)
329 if has_errors: 382 if found_errors:
330 print errors 383 print stderr
331 sys.exit(1) 384 sys.exit(1)
332 385
333 if opts.success_stamp: 386 if opts.success_stamp:
334 with open(opts.success_stamp, 'w'): 387 with open(opts.success_stamp, "w"):
335 os.utime(opts.success_stamp, None) 388 os.utime(opts.success_stamp, None)
OLDNEW
« no previous file with comments | « no previous file | third_party/closure_compiler/compile_js.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698