# Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. """Top-level presubmit script for Dart. See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts for more details about the presubmit API built into gcl. """ import imp import os import os.path import scm import subprocess import tempfile import platform def is_cpp_file(path): return path.endswith('.cc') or path.endswith('.h') def _CheckNnbdSdkSync(input_api, output_api): files = [git_file.LocalPath() for git_file in input_api.AffectedTextFiles()] unsynchronized_files = [] for file in files: if file.startswith('sdk/'): nnbd_file = 'sdk_nnbd/' + file[4:] if not nnbd_file in files: unsynchronized_files.append(nnbd_file) if unsynchronized_files: return [ output_api.PresubmitPromptWarning( 'Changes were made to sdk/ that were not made to sdk_nnbd/\n' 'Please update these files as well:\n' '\n' '%s' % ('\n'.join(unsynchronized_files))) ] return [] def _CheckNnbdTestSync(input_api, output_api): """Make sure that any forked SDK tests are kept in sync. If a CL touches a test, the test's counterpart (if it exists at all) should be in the CL too. """ DIRS = ["co19", "corelib", "language", "lib", "standalone"] files = [git_file.LocalPath() for git_file in input_api.AffectedTextFiles()] unsynchronized = [] for file in files: for dir in DIRS: legacy_dir = "tests/{}_2/".format(dir) nnbd_dir = "tests/{}/".format(dir) counterpart = None if file.startswith(legacy_dir): counterpart = file.replace(legacy_dir, nnbd_dir) elif file.startswith(nnbd_dir): counterpart = file.replace(nnbd_dir, legacy_dir) if counterpart: # Changed one file with a potential counterpart. If it exists # on disc, make sure it is also in the CL. if counterpart not in files and os.path.exists(counterpart): unsynchronized.append("- {} -> {}".format( file, counterpart)) break # TODO(rnystrom): Currently, we only warn if a test does exist in both # places on disc but only one is touched by a CL. We don't warn for files # that only exist on one side because the migration isn't complete and the # CL may be migrating. Once the migration is complete, consider making # these checks more rigorous. if unsynchronized: return [ output_api.PresubmitPromptWarning( 'Make sure to keep the forked legacy and NNBD tests in sync.\n' 'You may need to synchronize these files:\n' + '\n'.join(unsynchronized)) ] return [] def _CheckFormat(input_api, identification, extension, windows, hasFormatErrors, should_skip=lambda path: False): local_root = input_api.change.RepositoryRoot() upstream = input_api.change._upstream unformatted_files = [] for git_file in input_api.AffectedTextFiles(): if git_file.LocalPath().startswith("pkg/front_end/testcases/"): continue if should_skip(git_file.LocalPath()): continue filename = git_file.AbsoluteLocalPath() if filename.endswith(extension) and hasFormatErrors(filename=filename): old_version_has_errors = False try: path = git_file.LocalPath() if windows: # Git expects a linux style path. path = path.replace(os.sep, '/') old_contents = scm.GIT.Capture(['show', upstream + ':' + path], cwd=local_root, strip_out=False) if hasFormatErrors(contents=old_contents): old_version_has_errors = True except subprocess.CalledProcessError as e: old_version_has_errors = False if old_version_has_errors: print("WARNING: %s has existing and possibly new %s issues" % (git_file.LocalPath(), identification)) else: unformatted_files.append(filename) return unformatted_files def _CheckDartFormat(input_api, output_api): local_root = input_api.change.RepositoryRoot() upstream = input_api.change._upstream utils = imp.load_source('utils', os.path.join(local_root, 'tools', 'utils.py')) prebuilt_dartfmt = os.path.join(utils.CheckedInSdkPath(), 'bin', 'dartfmt') windows = utils.GuessOS() == 'win32' if windows: prebuilt_dartfmt += '.bat' if not os.path.isfile(prebuilt_dartfmt): print('WARNING: dartfmt not found: %s' % (prebuilt_dartfmt)) return [] def HasFormatErrors(filename=None, contents=None): # Don't look for formatting errors in multitests. Since those are very # sensitive to whitespace, many cannot be formatted with dartfmt without # breaking them. if filename and filename.endswith('_test.dart'): with open(filename) as f: contents = f.read() if '//#' in contents: return False args = [prebuilt_dartfmt, '--set-exit-if-changed'] if not contents: args += [filename, '-n'] process = subprocess.Popen( args, stdout=subprocess.PIPE, stdin=subprocess.PIPE) process.communicate(input=contents) # Check for exit code 1 explicitly to distinguish it from a syntax error # in the file (exit code 65). The repo contains many Dart files that are # known to have syntax errors for testing purposes and which can't be # parsed and formatted. Don't treat those as errors. return process.returncode == 1 unformatted_files = _CheckFormat(input_api, "dartfmt", ".dart", windows, HasFormatErrors) if unformatted_files: lineSep = " \\\n" if windows: lineSep = " ^\n" return [ output_api.PresubmitError( 'File output does not match dartfmt.\n' 'Fix these issues with:\n' '%s -w%s%s' % (prebuilt_dartfmt, lineSep, lineSep.join(unformatted_files))) ] return [] def _CheckStatusFiles(input_api, output_api): local_root = input_api.change.RepositoryRoot() upstream = input_api.change._upstream utils = imp.load_source('utils', os.path.join(local_root, 'tools', 'utils.py')) dart = os.path.join(utils.CheckedInSdkPath(), 'bin', 'dart') lint = os.path.join(local_root, 'pkg', 'status_file', 'bin', 'lint.dart') windows = utils.GuessOS() == 'win32' if windows: dart += '.exe' if not os.path.isfile(dart): print('WARNING: dart not found: %s' % dart) return [] if not os.path.isfile(lint): print('WARNING: Status file linter not found: %s' % lint) return [] def HasFormatErrors(filename=None, contents=None): args = [dart, lint] + (['-t'] if contents else [filename]) process = subprocess.Popen( args, stdout=subprocess.PIPE, stdin=subprocess.PIPE) process.communicate(input=contents) return process.returncode != 0 def should_skip(path): return (path.startswith("pkg/status_file/test/data/") or path.startswith("pkg/front_end/")) unformatted_files = _CheckFormat(input_api, "status file", ".status", windows, HasFormatErrors, should_skip) if unformatted_files: normalize = os.path.join(local_root, 'pkg', 'status_file', 'bin', 'normalize.dart') lineSep = " \\\n" if windows: lineSep = " ^\n" return [ output_api.PresubmitError( 'Status files are not normalized.\n' 'Fix these issues with:\n' '%s %s -w%s%s' % (dart, normalize, lineSep, lineSep.join(unformatted_files))) ] return [] def _CheckValidHostsInDEPS(input_api, output_api): """Checks that DEPS file deps are from allowed_hosts.""" # Run only if DEPS file has been modified to annoy fewer bystanders. if all(f.LocalPath() != 'DEPS' for f in input_api.AffectedFiles()): return [] # Outsource work to gclient verify try: input_api.subprocess.check_output(['gclient', 'verify']) return [] except input_api.subprocess.CalledProcessError, error: return [ output_api.PresubmitError( 'DEPS file must have only dependencies from allowed hosts.', long_text=error.output) ] def _CheckLayering(input_api, output_api): """Run VM layering check. This check validates that sources from one layer do not reference sources from another layer accidentally. """ # Run only if .cc or .h file was modified. if all(not is_cpp_file(f.LocalPath()) for f in input_api.AffectedFiles()): return [] local_root = input_api.change.RepositoryRoot() compiler_layering_check = imp.load_source( 'compiler_layering_check', os.path.join(local_root, 'runtime', 'tools', 'compiler_layering_check.py')) errors = compiler_layering_check.DoCheck(local_root) embedder_layering_check = imp.load_source( 'embedder_layering_check', os.path.join(local_root, 'runtime', 'tools', 'embedder_layering_check.py')) errors += embedder_layering_check.DoCheck(local_root) if errors: return [ output_api.PresubmitError( 'Layering check violation for C++ sources.', long_text='\n'.join(errors)) ] return [] def _CheckClangTidy(input_api, output_api): """Run clang-tidy on VM changes.""" # Only run clang-tidy on linux x64. if platform.system() != 'Linux' or platform.machine() != 'x86_64': return [] # Run only for modified .cc or .h files. files = [] for f in input_api.AffectedFiles(): path = f.LocalPath() if is_cpp_file(path) and os.path.isfile(path): files.append(path) if not files: return [] args = [ 'tools/sdks/dart-sdk/bin/dart', 'runtime/tools/run_clang_tidy.dart', ] args.extend(files) stdout = input_api.subprocess.check_output(args).strip() if not stdout: return [] return [ output_api.PresubmitError( 'The `clang-tidy` linter revealed issues:', long_text=stdout) ] def _CommonChecks(input_api, output_api): results = [] results.extend(_CheckNnbdSdkSync(input_api, output_api)) results.extend(_CheckNnbdTestSync(input_api, output_api)) results.extend(_CheckValidHostsInDEPS(input_api, output_api)) results.extend(_CheckDartFormat(input_api, output_api)) results.extend(_CheckStatusFiles(input_api, output_api)) results.extend(_CheckLayering(input_api, output_api)) results.extend(_CheckClangTidy(input_api, output_api)) results.extend( input_api.canned_checks.CheckPatchFormatted(input_api, output_api)) return results def CheckChangeOnCommit(input_api, output_api): return _CommonChecks(input_api, output_api) def CheckChangeOnUpload(input_api, output_api): return _CommonChecks(input_api, output_api)