diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-25 16:09:12 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-25 16:09:12 +0000 |
commit | fd09e19a769b8a5877951b4a4569cfb6b93e9acf (patch) | |
tree | 52cec87e70718b4efea41504bde845addb0022e3 | |
parent | ce869c36f6a5e9cf4577bfc875c79e51609c1b76 (diff) | |
parent | e106ee3301113116bdc4e11cdb9af60ea946d12b (diff) | |
download | repohooks-androidx-transition-release.tar.gz |
Snap for 11610999 from e106ee3301113116bdc4e11cdb9af60ea946d12b to androidx-transition-releaseandroidx-transition-release
Change-Id: I2aea7575f1eb89da922bef17d6c224b8339dbbf9
33 files changed, 4357 insertions, 1427 deletions
@@ -1 +1,2 @@ vapier@google.com +samccone@google.com diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 81f505d..31de3b0 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -2,9 +2,12 @@ # Only list fast unittests here. config_unittest = ./rh/config_unittest.py hooks_unittest = ./rh/hooks_unittest.py +results_unittest = ./rh/results_unittest.py shell_unittest = ./rh/shell_unittest.py +terminal_unittest = ./rh/terminal_unittest.py utils_unittest = ./rh/utils_unittest.py android_test_mapping_format_unittest = ./tools/android_test_mapping_format_unittest.py +clang-format unittest = ./tools/clang-format_unittest.py config_test = ./rh/config_test.py --check-env --commit-id ${PREUPLOAD_COMMIT} --commit-msg ${PREUPLOAD_COMMIT_MESSAGE} --repo-root ${REPO_ROOT} -- ${PREUPLOAD_FILES} [Builtin Hooks] @@ -113,7 +113,11 @@ force your own quote handling. Some variables are available to make it easier to handle OS differences. These are automatically expanded for you: -* `${REPO_ROOT}`: The absolute path of the root of the repo checkout. +* `${REPO_PATH}`: The path to the project relative to the root. +* `${REPO_ROOT}`: The absolute path of the root of the repo checkout. If the + project is in a submanifest, this points to the root of the submanifest. +* `${REPO_OUTER_ROOT}`: The absolute path of the root of the repo checkout. + This always points to the root of the overall repo checkout. * `${BUILD_OS}`: The string `darwin-x86` for macOS and the string `linux-x86` for Linux/x86. @@ -170,6 +174,9 @@ some dog = tool --no-cat-in-commit-message ${PREUPLOAD_COMMIT_MESSAGE} This section allows for turning on common/builtin hooks. There are a bunch of canned hooks already included geared towards AOSP style guidelines. +* `aidl_format`: Run AIDL files (.aidl) through `aidl-format`. +* `android_test_mapping_format`: Validate TEST_MAPPING files in Android source + code. Refer to go/test-mapping for more details. * `bpfmt`: Run Blueprint files (.bp) through `bpfmt`. * `checkpatch`: Run commits through the Linux kernel's `checkpatch.pl` script. * `clang_format`: Run git-clang-format against the commit. The default style is @@ -190,13 +197,14 @@ canned hooks already included geared towards AOSP style guidelines. * `google_java_format`: Run Java code through [`google-java-format`](https://github.com/google/google-java-format) * `jsonlint`: Verify JSON code is sane. +* `ktfmt`: Run Kotlin code through `ktfmt`. Supports an additional option + --include-dirs, which if specified will limit enforcement to only files under + the specified directories. * `pylint`: Alias of `pylint2`. Will change to `pylint3` by end of 2019. * `pylint2`: Run Python code through `pylint` using Python 2. * `pylint3`: Run Python code through `pylint` using Python 3. * `rustfmt`: Run Rust code through `rustfmt`. * `xmllint`: Run XML code through `xmllint`. -* `android_test_mapping_format`: Validate TEST_MAPPING files in Android source - code. Refer to go/test-mapping for more details. Note: Builtin hooks tend to match specific filenames (e.g. `.json`). If no files match in a specific commit, then the hook will be skipped for that commit. @@ -263,6 +271,9 @@ executables can be overridden through `[Tool Paths]`. This is helpful to provide consistent behavior for developers across different OS and Linux distros/versions. The following tools are recognized: +* `aidl-format`: used for the `aidl_format` builtin hook. +* `android-test-mapping-format`: used for the `android_test_mapping_format` + builtin hook. * `bpfmt`: used for the `bpfmt` builtin hook. * `clang-format`: used for the `clang_format` builtin hook. * `cpplint`: used for the `cpplint` builtin hook. @@ -270,10 +281,9 @@ distros/versions. The following tools are recognized: * `gofmt`: used for the `gofmt` builtin hook. * `google-java-format`: used for the `google_java_format` builtin hook. * `google-java-format-diff`: used for the `google_java_format` builtin hook. +* `ktfmt`: used for the `ktfmt` builtin hook. * `pylint`: used for the `pylint` builtin hook. * `rustfmt`: used for the `rustfmt` builtin hook. -* `android-test-mapping-format`: used for the `android_test_mapping_format` - builtin hook. See [Placeholders](#Placeholders) for variables you can expand automatically. diff --git a/pre-upload.py b/pre-upload.py index eaf611e..18bf11f 100755 --- a/pre-upload.py +++ b/pre-upload.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,17 +19,18 @@ Normally this is loaded indirectly by repo itself, but it can be run directly when developing. """ -from __future__ import print_function - import argparse +import concurrent.futures import datetime import os +import signal import sys +from typing import List, Optional # Assert some minimum Python versions as we don't test or support any others. -if sys.version_info < (3, 5): - print('repohooks: error: Python-3.5+ is required', file=sys.stderr) +if sys.version_info < (3, 6): + print('repohooks: error: Python-3.6+ is required', file=sys.stderr) sys.exit(1) @@ -64,6 +64,10 @@ class Output(object): PASSED = COLOR.color(COLOR.GREEN, 'PASSED') FAILED = COLOR.color(COLOR.RED, 'FAILED') WARNING = COLOR.color(COLOR.YELLOW, 'WARNING') + FIXUP = COLOR.color(COLOR.MAGENTA, 'FIXUP') + + # How long a hook is allowed to run before we warn that it is "too slow". + _SLOW_HOOK_DURATION = datetime.timedelta(seconds=30) def __init__(self, project_name): """Create a new Output object for a specified project. @@ -72,58 +76,91 @@ class Output(object): project_name: name of project. """ self.project_name = project_name + self.hooks = None self.num_hooks = None - self.hook_index = 0 + self.num_commits = None + self.commit_index = 0 self.success = True self.start_time = datetime.datetime.now() + self.hook_start_time = None + # Cache number of invisible characters in our banner. + self._banner_esc_chars = len(self.COLOR.color(self.COLOR.YELLOW, '')) - def set_num_hooks(self, num_hooks): - """Keep track of how many hooks we'll be running. + def set_num_commits(self, num_commits: int) -> None: + """Keep track of how many commits we'll be running. Args: - num_hooks: number of hooks to be run. + num_commits: Number of commits to be run. """ - self.num_hooks = num_hooks + self.num_commits = num_commits + self.commit_index = 1 - def commit_start(self, commit, commit_summary): + def commit_start(self, hooks, commit, commit_summary): """Emit status for new commit. Args: + hooks: All the hooks to be run for this commit. commit: commit hash. commit_summary: commit summary. """ - status_line = '[%s %s] %s' % (self.COMMIT, commit[0:12], commit_summary) + status_line = ( + f'[{self.COMMIT} ' + f'{self.commit_index}/{self.num_commits} ' + f'{commit[0:12]}] {commit_summary}' + ) rh.terminal.print_status_line(status_line, print_newline=True) - self.hook_index = 1 - - def hook_start(self, hook_name): - """Emit status before the start of a hook. - - Args: - hook_name: name of the hook. - """ - status_line = '[%s %d/%d] %s' % (self.RUNNING, self.hook_index, - self.num_hooks, hook_name) - self.hook_index += 1 + self.commit_index += 1 + + # Initialize the pending hooks line too. + self.hooks = set(hooks) + self.num_hooks = len(hooks) + self.hook_banner() + + def hook_banner(self): + """Display the banner for current set of hooks.""" + pending = ', '.join(x.name for x in self.hooks) + status_line = ( + f'[{self.RUNNING} ' + f'{self.num_hooks - len(self.hooks)}/{self.num_hooks}] ' + f'{pending}' + ) + if self._banner_esc_chars and sys.stderr.isatty(): + cols = os.get_terminal_size(sys.stderr.fileno()).columns + status_line = status_line[0:cols + self._banner_esc_chars] rh.terminal.print_status_line(status_line) - def hook_error(self, hook_name, error): + def hook_finish(self, hook, duration): + """Finish processing any per-hook state.""" + self.hooks.remove(hook) + if duration >= self._SLOW_HOOK_DURATION: + d = rh.utils.timedelta_str(duration) + self.hook_warning( + hook, + f'This hook took {d} to finish which is fairly slow for ' + 'developers.\nPlease consider moving the check to the ' + 'server/CI system instead.') + + # Show any hooks still pending. + if self.hooks: + self.hook_banner() + + def hook_error(self, hook, error): """Print an error for a single hook. Args: - hook_name: name of the hook. + hook: The hook that generated the output. error: error string. """ - self.error(hook_name, error) + self.error(f'{hook.name} hook', error) - def hook_warning(self, hook_name, warning): + def hook_warning(self, hook, warning): """Print a warning for a single hook. Args: - hook_name: name of the hook. + hook: The hook that generated the output. warning: warning string. """ - status_line = '[%s] %s' % (self.WARNING, hook_name) + status_line = f'[{self.WARNING}] {hook.name}' rh.terminal.print_status_line(status_line, print_newline=True) print(warning, file=sys.stderr) @@ -134,19 +171,34 @@ class Output(object): header: A unique identifier for the source of this error. error: error string. """ - status_line = '[%s] %s' % (self.FAILED, header) + status_line = f'[{self.FAILED}] {header}' rh.terminal.print_status_line(status_line, print_newline=True) print(error, file=sys.stderr) self.success = False + def hook_fixups( + self, + project_results: rh.results.ProjectResults, + hook_results: List[rh.results.HookResult], + ) -> None: + """Display summary of possible fixups for a single hook.""" + for result in (x for x in hook_results if x.fixup_cmd): + cmd = result.fixup_cmd + list(result.files) + for line in ( + f'[{self.FIXUP}] {result.hook} has automated fixups available', + f' cd {rh.shell.quote(project_results.workdir)} && \\', + f' {rh.shell.cmd_to_str(cmd)}', + ): + rh.terminal.print_status_line(line, print_newline=True) + def finish(self): """Print summary for all the hooks.""" - status_line = '[%s] repohooks for %s %s in %s' % ( - self.PASSED if self.success else self.FAILED, - self.project_name, - 'passed' if self.success else 'failed', - rh.utils.timedelta_str(datetime.datetime.now() - self.start_time)) - rh.terminal.print_status_line(status_line, print_newline=True) + header = self.PASSED if self.success else self.FAILED + status = 'passed' if self.success else 'failed' + d = rh.utils.timedelta_str(datetime.datetime.now() - self.start_time) + rh.terminal.print_status_line( + f'[{header}] repohooks for {self.project_name} {status} in {d}', + print_newline=True) def _process_hook_results(results): @@ -170,12 +222,12 @@ def _process_hook_results(results): error_ret = '' warning_ret = '' for result in results: - if result: + if result or result.is_warning(): ret = '' if result.files: - ret += ' FILES: %s' % (result.files,) + ret += f' FILES: {rh.shell.cmd_to_str(result.files)}\n' lines = result.error.splitlines() - ret += '\n'.join(' %s' % (x,) for x in lines) + ret += '\n'.join(f' {x}' for x in lines) if result.is_warning(): has_warning = True warning_ret += ret @@ -187,17 +239,24 @@ def _process_hook_results(results): warning_ret if has_warning else None) -def _get_project_config(): +def _get_project_config(from_git=False): """Returns the configuration for a project. + Args: + from_git: If true, we are called from git directly and repo should not be + used. Expects to be called from within the project root. """ - global_paths = ( - # Load the global config found in the manifest repo. - os.path.join(rh.git.find_repo_root(), '.repo', 'manifests'), - # Load the global config found in the root of the repo checkout. - rh.git.find_repo_root(), - ) + if from_git: + global_paths = (rh.git.find_repo_root(),) + else: + global_paths = ( + # Load the global config found in the manifest repo. + (os.path.join(rh.git.find_repo_root(), '.repo', 'manifests')), + # Load the global config found in the root of the repo checkout. + rh.git.find_repo_root(), + ) + paths = ( # Load the config for this git repo. '.', @@ -205,63 +264,107 @@ def _get_project_config(): return rh.config.PreUploadSettings(paths=paths, global_paths=global_paths) -def _attempt_fixes(fixup_func_list, commit_list): - """Attempts to run |fixup_func_list| given |commit_list|.""" - if len(fixup_func_list) != 1: - # Only single fixes will be attempted, since various fixes might - # interact with each other. - return - - hook_name, commit, fixup_func = fixup_func_list[0] - - if commit != commit_list[0]: - # If the commit is not at the top of the stack, git operations might be - # needed and might leave the working directory in a tricky state if the - # fix is attempted to run automatically (e.g. it might require manual - # merge conflict resolution). Refuse to run the fix in those cases. - return - - prompt = ('An automatic fix can be attempted for the "%s" hook. ' - 'Do you want to run it?' % hook_name) - if not rh.terminal.boolean_prompt(prompt): +def _attempt_fixes(projects_results: List[rh.results.ProjectResults]) -> None: + """Attempts to fix fixable results.""" + # Filter out any result that has a fixup. + fixups = [] + for project_results in projects_results: + fixups.extend((project_results.workdir, x) + for x in project_results.fixups) + if not fixups: return - result = fixup_func() - if result: - print('Attempt to fix "%s" for commit "%s" failed: %s' % - (hook_name, commit, result), - file=sys.stderr) + if len(fixups) > 1: + banner = f'Multiple fixups ({len(fixups)}) are available.' else: - print('Fix successfully applied. Amend the current commit before ' - 'attempting to upload again.\n', file=sys.stderr) - - -def _run_project_hooks_in_cwd(project_name, proj_dir, output, commit_list=None): + banner = 'Automated fixups are available.' + print(Output.COLOR.color(Output.COLOR.MAGENTA, banner), file=sys.stderr) + + # If there's more than one fixup available, ask if they want to blindly run + # them all, or prompt for them one-by-one. + mode = 'some' + if len(fixups) > 1: + while True: + response = rh.terminal.str_prompt( + 'What would you like to do', + ('Run (A)ll', 'Run (S)ome', '(D)ry-run', '(N)othing [default]')) + if not response: + print('', file=sys.stderr) + return + if response.startswith('a') or response.startswith('y'): + mode = 'all' + break + elif response.startswith('s'): + mode = 'some' + break + elif response.startswith('d'): + mode = 'dry-run' + break + elif response.startswith('n'): + print('', file=sys.stderr) + return + + # Walk all the fixups and run them one-by-one. + for workdir, result in fixups: + if mode == 'some': + if not rh.terminal.boolean_prompt( + f'Run {result.hook} fixup for {result.commit}' + ): + continue + + cmd = tuple(result.fixup_cmd) + tuple(result.files) + print( + f'\n[{Output.RUNNING}] cd {rh.shell.quote(workdir)} && ' + f'{rh.shell.cmd_to_str(cmd)}', file=sys.stderr) + if mode == 'dry-run': + continue + + cmd_result = rh.utils.run(cmd, cwd=workdir, check=False) + if cmd_result.returncode: + print(f'[{Output.WARNING}] command exited {cmd_result.returncode}', + file=sys.stderr) + else: + print(f'[{Output.PASSED}] great success', file=sys.stderr) + + print(f'\n[{Output.FIXUP}] Please amend & rebase your tree before ' + 'attempting to upload again.\n', file=sys.stderr) + +def _run_project_hooks_in_cwd( + project_name: str, + proj_dir: str, + output: Output, + jobs: Optional[int] = None, + from_git: bool = False, + commit_list: Optional[List[str]] = None, +) -> rh.results.ProjectResults: """Run the project-specific hooks in the cwd. Args: project_name: The name of this project. proj_dir: The directory for this project (for passing on in metadata). output: Helper for summarizing output/errors to the user. + jobs: How many hooks to run in parallel. + from_git: If true, we are called from git directly and repo should not be + used. commit_list: A list of commits to run hooks against. If None or empty list then we'll automatically get the list of commits that would be uploaded. Returns: - False if any errors were found, else True. + All the results for this project. """ + ret = rh.results.ProjectResults(project_name, proj_dir) + try: - config = _get_project_config() + config = _get_project_config(from_git) except rh.config.ValidationError as e: output.error('Loading config files', str(e)) - return False + return ret._replace(internal_failure=True) # If the repo has no pre-upload hooks enabled, then just return. hooks = list(config.callable_hooks()) if not hooks: - return True - - output.set_num_hooks(len(hooks)) + return ret # Set up the environment like repo would with the forall command. try: @@ -269,13 +372,17 @@ def _run_project_hooks_in_cwd(project_name, proj_dir, output, commit_list=None): upstream_branch = rh.git.get_upstream_branch() except rh.utils.CalledProcessError as e: output.error('Upstream remote/tracking branch lookup', - '%s\nDid you run repo start? Is your HEAD detached?' % - (e,)) - return False + f'{e}\nDid you run repo start? Is your HEAD detached?') + return ret._replace(internal_failure=True) - project = rh.Project(name=project_name, dir=proj_dir, remote=remote) + project = rh.Project(name=project_name, dir=proj_dir) rel_proj_dir = os.path.relpath(proj_dir, rh.git.find_repo_root()) + # Filter out the hooks to process. + hooks = [x for x in hooks if rel_proj_dir not in x.scope] + if not hooks: + return ret + os.environ.update({ 'REPO_LREV': rh.git.get_commit_for_ref(upstream_branch), 'REPO_PATH': rel_proj_dir, @@ -287,56 +394,69 @@ def _run_project_hooks_in_cwd(project_name, proj_dir, output, commit_list=None): if not commit_list: commit_list = rh.git.get_commits( ignore_merged_commits=config.ignore_merged_commits) - - ret = True - fixup_func_list = [] - - for commit in commit_list: - # Mix in some settings for our hooks. - os.environ['PREUPLOAD_COMMIT'] = commit - diff = rh.git.get_affected_files(commit) - desc = rh.git.get_commit_desc(commit) - os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc - - commit_summary = desc.split('\n', 1)[0] - output.commit_start(commit=commit, commit_summary=commit_summary) - - for name, hook, exclusion_scope in hooks: - output.hook_start(name) - if rel_proj_dir in exclusion_scope: - break - hook_results = hook(project, commit, desc, diff) - (error, warning) = _process_hook_results(hook_results) - if error is not None or warning is not None: - if warning is not None: - output.hook_warning(name, warning) - if error is not None: - ret = False - output.hook_error(name, error) - for result in hook_results: - if result.fixup_func: - fixup_func_list.append((name, commit, - result.fixup_func)) - - if fixup_func_list: - _attempt_fixes(fixup_func_list, commit_list) + output.set_num_commits(len(commit_list)) + + def _run_hook(hook, project, commit, desc, diff): + """Run a hook, gather stats, and process its results.""" + start = datetime.datetime.now() + results = hook.hook(project, commit, desc, diff) + (error, warning) = _process_hook_results(results) + duration = datetime.datetime.now() - start + return (hook, results, error, warning, duration) + + with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor: + for commit in commit_list: + # Mix in some settings for our hooks. + os.environ['PREUPLOAD_COMMIT'] = commit + diff = rh.git.get_affected_files(commit) + desc = rh.git.get_commit_desc(commit) + os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc + + commit_summary = desc.split('\n', 1)[0] + output.commit_start(hooks, commit, commit_summary) + + futures = ( + executor.submit(_run_hook, hook, project, commit, desc, diff) + for hook in hooks + ) + future_results = ( + x.result() for x in concurrent.futures.as_completed(futures) + ) + for hook, hook_results, error, warning, duration in future_results: + ret.add_results(hook_results) + if error is not None or warning is not None: + if warning is not None: + output.hook_warning(hook, warning) + if error is not None: + output.hook_error(hook, error) + output.hook_fixups(ret, hook_results) + output.hook_finish(hook, duration) return ret -def _run_project_hooks(project_name, proj_dir=None, commit_list=None): +def _run_project_hooks( + project_name: str, + proj_dir: Optional[str] = None, + jobs: Optional[int] = None, + from_git: bool = False, + commit_list: Optional[List[str]] = None, +) -> rh.results.ProjectResults: """Run the project-specific hooks in |proj_dir|. Args: project_name: The name of project to run hooks for. proj_dir: If non-None, this is the directory the project is in. If None, we'll ask repo. + jobs: How many hooks to run in parallel. + from_git: If true, we are called from git directly and repo should not be + used. commit_list: A list of commits to run hooks against. If None or empty list then we'll automatically get the list of commits that would be uploaded. Returns: - False if any errors were found, else True. + All the results for this project. """ output = Output(project_name) @@ -345,11 +465,11 @@ def _run_project_hooks(project_name, proj_dir=None, commit_list=None): result = rh.utils.run(cmd, capture_output=True) proj_dirs = result.stdout.split() if not proj_dirs: - print('%s cannot be found.' % project_name, file=sys.stderr) + print(f'{project_name} cannot be found.', file=sys.stderr) print('Please specify a valid project.', file=sys.stderr) return False if len(proj_dirs) > 1: - print('%s is associated with multiple directories.' % project_name, + print(f'{project_name} is associated with multiple directories.', file=sys.stderr) print('Please specify a directory to help disambiguate.', file=sys.stderr) @@ -360,13 +480,56 @@ def _run_project_hooks(project_name, proj_dir=None, commit_list=None): try: # Hooks assume they are run from the root of the project. os.chdir(proj_dir) - return _run_project_hooks_in_cwd(project_name, proj_dir, output, - commit_list=commit_list) + return _run_project_hooks_in_cwd( + project_name, proj_dir, output, jobs=jobs, from_git=from_git, + commit_list=commit_list) finally: output.finish() os.chdir(pwd) +def _run_projects_hooks( + project_list: List[str], + worktree_list: List[Optional[str]], + jobs: Optional[int] = None, + from_git: bool = False, + commit_list: Optional[List[str]] = None, +) -> bool: + """Run all the hooks + + Args: + project_list: List of project names. + worktree_list: List of project checkouts. + jobs: How many hooks to run in parallel. + from_git: If true, we are called from git directly and repo should not be + used. + commit_list: A list of commits to run hooks against. If None or empty + list then we'll automatically get the list of commits that would be + uploaded. + + Returns: + True if everything passed, else False. + """ + results = [] + for project, worktree in zip(project_list, worktree_list): + result = _run_project_hooks( + project, + proj_dir=worktree, + jobs=jobs, + from_git=from_git, + commit_list=commit_list, + ) + results.append(result) + if result: + # If a repo had failures, add a blank line to help break up the + # output. If there were no failures, then the output should be + # very minimal, so we don't add it then. + print('', file=sys.stderr) + + _attempt_fixes(results) + return not any(results) + + def main(project_list, worktree_list=None, **_kwargs): """Main function invoked directly by repo. @@ -383,35 +546,40 @@ def main(project_list, worktree_list=None, **_kwargs): the directories automatically. kwargs: Leave this here for forward-compatibility. """ - found_error = False if not worktree_list: worktree_list = [None] * len(project_list) - for project, worktree in zip(project_list, worktree_list): - if not _run_project_hooks(project, proj_dir=worktree): - found_error = True - # If a repo had failures, add a blank line to help break up the - # output. If there were no failures, then the output should be - # very minimal, so we don't add it then. - print('', file=sys.stderr) - - if found_error: + if not _run_projects_hooks(project_list, worktree_list): color = rh.terminal.Color() - print('%s: Preupload failed due to above error(s).\n' - 'For more info, please see:\n%s' % - (color.color(color.RED, 'FATAL'), REPOHOOKS_URL), + print(color.color(color.RED, 'FATAL') + + ': Preupload failed due to above error(s).\n' + f'For more info, see: {REPOHOOKS_URL}', file=sys.stderr) sys.exit(1) -def _identify_project(path): +def _identify_project(path, from_git=False): """Identify the repo project associated with the given path. Returns: A string indicating what project is associated with the path passed in or a blank string upon failure. """ - cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'] - return rh.utils.run(cmd, capture_output=True, cwd=path).stdout.strip() + if from_git: + cmd = ['git', 'rev-parse', '--show-toplevel'] + project_path = rh.utils.run(cmd, capture_output=True).stdout.strip() + cmd = ['git', 'rev-parse', '--show-superproject-working-tree'] + superproject_path = rh.utils.run( + cmd, capture_output=True).stdout.strip() + module_path = project_path[len(superproject_path) + 1:] + cmd = ['git', 'config', '-f', '.gitmodules', + '--name-only', '--get-regexp', r'^submodule\..*\.path$', + f"^{module_path}$"] + module_name = rh.utils.run(cmd, cwd=superproject_path, + capture_output=True).stdout.strip() + return module_name[len('submodule.'):-len(".path")] + else: + cmd = ['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'] + return rh.utils.run(cmd, capture_output=True, cwd=path).stdout.strip() def direct_main(argv): @@ -427,6 +595,8 @@ def direct_main(argv): BadInvocation: On some types of invocation errors. """ parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--git', action='store_true', + help='This hook is called from git instead of repo') parser.add_argument('--dir', default=None, help='The directory that the project lives in. If not ' 'specified, use the git project root based on the cwd.') @@ -435,6 +605,11 @@ def direct_main(argv): 'hooks get run, since some hooks are project-specific.' 'If not specified, `repo` will be used to figure this ' 'out based on the dir.') + parser.add_argument('-j', '--jobs', type=int, + help='Run up to this many hooks in parallel. Setting ' + 'to 1 forces serial execution, and the default ' + 'automatically chooses an appropriate number for the ' + 'current system.') parser.add_argument('commits', nargs='*', help='Check specific commits') opts = parser.parse_args(argv) @@ -448,20 +623,24 @@ def direct_main(argv): parser.error('The current directory is not part of a git project.') opts.dir = os.path.dirname(os.path.abspath(git_dir)) elif not os.path.isdir(opts.dir): - parser.error('Invalid dir: %s' % opts.dir) + parser.error(f'Invalid dir: {opts.dir}') elif not rh.git.is_git_repository(opts.dir): - parser.error('Not a git repository: %s' % opts.dir) + parser.error(f'Not a git repository: {opts.dir}') # Identify the project if it wasn't specified; this _requires_ the repo # tool to be installed and for the project to be part of a repo checkout. if not opts.project: - opts.project = _identify_project(opts.dir) + opts.project = _identify_project(opts.dir, opts.git) if not opts.project: - parser.error("Repo couldn't identify the project of %s" % opts.dir) + parser.error(f"Couldn't identify the project of {opts.dir}") - if _run_project_hooks(opts.project, proj_dir=opts.dir, - commit_list=opts.commits): - return 0 + try: + if _run_projects_hooks([opts.project], [opts.dir], jobs=opts.jobs, + from_git=opts.git, commit_list=opts.commits): + return 0 + except KeyboardInterrupt: + print('Aborting execution early due to user interrupt', file=sys.stderr) + return 128 + signal.SIGINT return 1 diff --git a/rh/__init__.py b/rh/__init__.py index c36cb89..2b1676e 100644 --- a/rh/__init__.py +++ b/rh/__init__.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,10 +14,14 @@ """Common repohook objects/constants.""" -from __future__ import print_function +from typing import NamedTuple -import collections +class Project(NamedTuple): + """The git project that we're testing currently.""" -# An object representing the git project that we're testing currently. -Project = collections.namedtuple('Project', ['name', 'dir', 'remote']) + # The name of the project. + name: str + + # Absolute path to the project checkout. + dir: str diff --git a/rh/config.py b/rh/config.py index b75e03b..6cd218b 100644 --- a/rh/config.py +++ b/rh/config.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,6 @@ """Manage various config files.""" -from __future__ import print_function - import configparser import functools import itertools @@ -66,22 +63,10 @@ class RawConfigParser(configparser.RawConfigParser): return default raise - def get(self, section, option, default=_UNSET): - """Return the value for |option| in |section| (with |default|).""" - try: - return configparser.RawConfigParser.get(self, section, option) - except (configparser.NoSectionError, configparser.NoOptionError): - if default is not _UNSET: - return default - raise - def items(self, section=_UNSET, default=_UNSET): """Return a list of (key, value) tuples for the options in |section|.""" if section is _UNSET: - # Python 3 compat logic. Return a dict of section-to-options. - if sys.version_info.major < 3: - return [(x, self.items(x)) for x in self.sections()] - return super(RawConfigParser, self).items() + return super().items() try: return configparser.RawConfigParser.items(self, section) @@ -90,15 +75,6 @@ class RawConfigParser(configparser.RawConfigParser): return default raise - if sys.version_info.major < 3: - def read_dict(self, dictionary): - """Store |dictionary| into ourselves.""" - for section, settings in dictionary.items(): - for option, value in settings: - if not self.has_section(section): - self.add_section(section) - self.set(section, option, value) - class PreUploadConfig(object): """A single (abstract) config used for `repo upload` hooks.""" @@ -141,7 +117,8 @@ class PreUploadConfig(object): def custom_hook(self, hook): """The command to execute for |hook|.""" - return shlex.split(self.config.get(self.CUSTOM_HOOKS_SECTION, hook, '')) + return shlex.split(self.config.get( + self.CUSTOM_HOOKS_SECTION, hook, fallback='')) @property def builtin_hooks(self): @@ -151,13 +128,13 @@ class PreUploadConfig(object): def builtin_hook_option(self, hook): """The options to pass to |hook|.""" - return shlex.split(self.config.get(self.BUILTIN_HOOKS_OPTIONS_SECTION, - hook, '')) + return shlex.split(self.config.get( + self.BUILTIN_HOOKS_OPTIONS_SECTION, hook, fallback='')) def builtin_hook_exclude_paths(self, hook): """List of paths for which |hook| should not be executed.""" - return shlex.split(self.config.get(self.BUILTIN_HOOKS_EXCLUDE_SECTION, - hook, '')) + return shlex.split(self.config.get( + self.BUILTIN_HOOKS_EXCLUDE_SECTION, hook, fallback='')) @property def tool_paths(self): @@ -189,7 +166,7 @@ class PreUploadConfig(object): """Whether to skip hooks for merged commits.""" return rh.shell.boolean_shell_value( self.config.get(self.OPTIONS_SECTION, - self.OPTION_IGNORE_MERGED_COMMITS, None), + self.OPTION_IGNORE_MERGED_COMMITS, fallback=None), False) def update(self, preupload_config): @@ -203,14 +180,14 @@ class PreUploadConfig(object): # Reject unknown sections. bad_sections = set(config.sections()) - self.VALID_SECTIONS if bad_sections: - raise ValidationError('%s: unknown sections: %s' % - (self.source, bad_sections)) + raise ValidationError( + f'{self.source}: unknown sections: {bad_sections}') # Reject blank custom hooks. for hook in self.custom_hooks: if not config.get(self.CUSTOM_HOOKS_SECTION, hook): - raise ValidationError('%s: custom hook "%s" cannot be blank' % - (self.source, hook)) + raise ValidationError( + f'{self.source}: custom hook "{hook}" cannot be blank') # Reject unknown builtin hooks. valid_builtin_hooks = set(rh.hooks.BUILTIN_HOOKS.keys()) @@ -218,8 +195,8 @@ class PreUploadConfig(object): hooks = set(config.options(self.BUILTIN_HOOKS_SECTION)) bad_hooks = hooks - valid_builtin_hooks if bad_hooks: - raise ValidationError('%s: unknown builtin hooks: %s' % - (self.source, bad_hooks)) + raise ValidationError( + f'{self.source}: unknown builtin hooks: {bad_hooks}') elif config.has_section(self.BUILTIN_HOOKS_OPTIONS_SECTION): raise ValidationError('Builtin hook options specified, but missing ' 'builtin hook settings') @@ -228,24 +205,26 @@ class PreUploadConfig(object): hooks = set(config.options(self.BUILTIN_HOOKS_OPTIONS_SECTION)) bad_hooks = hooks - valid_builtin_hooks if bad_hooks: - raise ValidationError('%s: unknown builtin hook options: %s' % - (self.source, bad_hooks)) + raise ValidationError( + f'{self.source}: unknown builtin hook options: {bad_hooks}') # Verify hooks are valid shell strings. for hook in self.custom_hooks: try: self.custom_hook(hook) except ValueError as e: - raise ValidationError('%s: hook "%s" command line is invalid: ' - '%s' % (self.source, hook, e)) + raise ValidationError( + f'{self.source}: hook "{hook}" command line is invalid: {e}' + ) from e # Verify hook options are valid shell strings. for hook in self.builtin_hooks: try: self.builtin_hook_option(hook) except ValueError as e: - raise ValidationError('%s: hook options "%s" are invalid: %s' % - (self.source, hook, e)) + raise ValidationError( + f'{self.source}: hook options "{hook}" are invalid: {e}' + ) from e # Reject unknown tools. valid_tools = set(rh.hooks.TOOL_PATHS.keys()) @@ -253,16 +232,16 @@ class PreUploadConfig(object): tools = set(config.options(self.TOOL_PATHS_SECTION)) bad_tools = tools - valid_tools if bad_tools: - raise ValidationError('%s: unknown tools: %s' % - (self.source, bad_tools)) + raise ValidationError( + f'{self.source}: unknown tools: {bad_tools}') # Reject unknown options. if config.has_section(self.OPTIONS_SECTION): options = set(config.options(self.OPTIONS_SECTION)) bad_options = options - self.VALID_OPTIONS if bad_options: - raise ValidationError('%s: unknown options: %s' % - (self.source, bad_options)) + raise ValidationError( + f'{self.source}: unknown options: {bad_options}') class PreUploadFile(PreUploadConfig): @@ -282,13 +261,13 @@ class PreUploadFile(PreUploadConfig): Args: path: The config file to load. """ - super(PreUploadFile, self).__init__(source=path) + super().__init__(source=path) self.path = path try: self.config.read(path) except configparser.ParsingError as e: - raise ValidationError('%s: %s' % (path, e)) + raise ValidationError(f'{path}: {e}') from e self._validate() @@ -313,13 +292,13 @@ class LocalPreUploadFile(PreUploadFile): FILENAME = 'PREUPLOAD.cfg' def _validate(self): - super(LocalPreUploadFile, self)._validate() + super()._validate() # Reject Exclude Paths section for local config. if self.config.has_section(self.BUILTIN_HOOKS_EXCLUDE_SECTION): - raise ValidationError('%s: [%s] is not valid in local files' % - (self.path, - self.BUILTIN_HOOKS_EXCLUDE_SECTION)) + raise ValidationError( + f'{self.path}: [{self.BUILTIN_HOOKS_EXCLUDE_SECTION}] is not ' + 'valid in local files') class GlobalPreUploadFile(PreUploadFile): @@ -343,7 +322,7 @@ class PreUploadSettings(PreUploadConfig): paths: The directories to look for config files. global_paths: The directories to look for global config files. """ - super(PreUploadSettings, self).__init__() + super().__init__() self.paths = [] for config in itertools.chain( @@ -354,5 +333,5 @@ class PreUploadSettings(PreUploadConfig): # We validated configs in isolation, now do one final pass altogether. - self.source = '{%s}' % '|'.join(self.paths) + self.source = '{' + '|'.join(self.paths) + '}' self._validate() diff --git a/rh/config_test.py b/rh/config_test.py index 794e50f..df3afb6 100755 --- a/rh/config_test.py +++ b/rh/config_test.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2019 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +15,6 @@ """Integration tests for the config module (via PREUPLOAD.cfg).""" -from __future__ import print_function - import argparse import os import re @@ -30,25 +27,25 @@ REPO_ROOT = os.path.dirname(os.path.dirname(REPOTOOLS)) def assertEqual(msg, exp, actual): """Assert |exp| equals |actual|.""" - assert exp == actual, '%s: expected "%s" but got "%s"' % (msg, exp, actual) + assert exp == actual, f'{msg}: expected "{exp}" but got "{actual}"' def assertEnv(var, value): """Assert |var| is set in the environment as |value|.""" - assert var in os.environ, '$%s missing in environment' % (var,) - assertEqual('env[%s]' % (var,), value, os.environ[var]) + assert var in os.environ, f'${var} missing in environment' + assertEqual(f'env[{var}]', value, os.environ[var]) def check_commit_id(commit): """Check |commit| looks like a git commit id.""" - assert len(commit) == 40, 'commit "%s" must be 40 chars' % (commit,) + assert len(commit) == 40, f'commit "{commit}" must be 40 chars' assert re.match(r'^[a-f0-9]+$', commit), \ - 'commit "%s" must be all hex' % (commit,) + f'commit "{commit}" must be all hex' def check_commit_msg(msg): """Check the ${PREUPLOAD_COMMIT_MESSAGE} setting.""" - assert len(msg) > 1, 'commit message must be at least 2 bytes: %s' + assert len(msg) > 1, f'commit message must be at least 2 bytes: {msg}' def check_repo_root(root): @@ -103,7 +100,7 @@ def main(argv): check_repo_root(opts.repo_root) check_files(opts.files) except AssertionError as e: - print('error: %s' % (e,), file=sys.stderr) + print(f'error: {e}', file=sys.stderr) return 1 return 0 diff --git a/rh/config_unittest.py b/rh/config_unittest.py index 4b27c5a..475dc22 100755 --- a/rh/config_unittest.py +++ b/rh/config_unittest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +15,6 @@ """Unittests for the config module.""" -from __future__ import print_function - import os import shutil import sys @@ -60,7 +57,7 @@ class FileTestCase(unittest.TestCase): Path to the file where the configuration was written. """ path = os.path.join(self.tempdir, filename) - with open(path, 'w') as fp: + with open(path, 'w', encoding='utf-8') as fp: fp.write(data) return path @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,6 @@ """Git helper functions.""" -from __future__ import print_function - import os import re import sys @@ -38,7 +35,7 @@ def get_upstream_remote(): branch = result.stdout.strip() # Then get the remote associated with this branch. - cmd = ['git', 'config', 'branch.%s.remote' % branch] + cmd = ['git', 'config', f'branch.{branch}.remote'] result = rh.utils.run(cmd, capture_output=True) return result.stdout.strip() @@ -55,14 +52,14 @@ def get_upstream_branch(): if not current_branch: raise ValueError('Need to be on a tracking branch') - cfg_option = 'branch.' + current_branch + '.%s' - cmd = ['git', 'config', cfg_option % 'merge'] + cfg_option = 'branch.' + current_branch + '.' + cmd = ['git', 'config', cfg_option + 'merge'] result = rh.utils.run(cmd, capture_output=True) full_upstream = result.stdout.strip() # If remote is not fully qualified, add an implicit namespace. if '/' not in full_upstream: - full_upstream = 'refs/heads/%s' % full_upstream - cmd = ['git', 'config', cfg_option % 'remote'] + full_upstream = f'refs/heads/{full_upstream}' + cmd = ['git', 'config', cfg_option + 'remote'] result = rh.utils.run(cmd, capture_output=True) remote = result.stdout.strip() if not remote or not full_upstream: @@ -80,7 +77,7 @@ def get_commit_for_ref(ref): def get_remote_revision(ref, remote): """Returns the remote revision for this ref.""" - prefix = 'refs/remotes/%s/' % remote + prefix = f'refs/remotes/{remote}/' if ref.startswith(prefix): return ref[len(prefix):] return ref @@ -102,7 +99,7 @@ def get_file_content(commit, path): a full file, you should check that first. One way to detect is that the content will not have any newlines. """ - cmd = ['git', 'show', '%s:%s' % (commit, path)] + cmd = ['git', 'show', f'{commit}:{path}'] return rh.utils.run(cmd, capture_output=True).stdout @@ -150,7 +147,7 @@ def raw_diff(path, target): for line in diff_lines: match = DIFF_RE.match(line) if not match: - raise ValueError('Failed to parse diff output: %s' % line) + raise ValueError(f'Failed to parse diff output: {line}') rawdiff = RawDiffEntry(**match.groupdict()) rawdiff.src_mode = int(rawdiff.src_mode) rawdiff.dst_mode = int(rawdiff.dst_mode) @@ -167,12 +164,12 @@ def get_affected_files(commit): Returns: A list of modified/added (and perhaps deleted) files """ - return raw_diff(os.getcwd(), '%s^-' % commit) + return raw_diff(os.getcwd(), f'{commit}^-') def get_commits(ignore_merged_commits=False): """Returns a list of commits for this review.""" - cmd = ['git', 'rev-list', '%s..' % get_upstream_branch()] + cmd = ['git', 'rev-list', f'{get_upstream_branch()}..'] if ignore_merged_commits: cmd.append('--first-parent') return rh.utils.run(cmd, capture_output=True).stdout.split() @@ -184,17 +181,41 @@ def get_commit_desc(commit): return rh.utils.run(cmd, capture_output=True).stdout -def find_repo_root(path=None): - """Locate the top level of this repo checkout starting at |path|.""" +def find_repo_root(path=None, outer=False): + """Locate the top level of this repo checkout starting at |path|. + + Args: + outer: Whether to find the outermost manifest, or the sub-manifest. + """ if path is None: path = os.getcwd() orig_path = path path = os.path.abspath(path) + + # If we are working on a superproject instead of a repo client, use the + # result from git directly. For regular repo client, this would return + # empty string. + cmd = ['git', 'rev-parse', '--show-superproject-working-tree'] + git_worktree_path = rh.utils.run(cmd, cwd=path, capture_output=True).stdout.strip() + if git_worktree_path: + return git_worktree_path + while not os.path.exists(os.path.join(path, '.repo')): path = os.path.dirname(path) if path == '/': - raise ValueError('Could not locate .repo in %s' % orig_path) + raise ValueError(f'Could not locate .repo in {orig_path}') + + root = path + if not outer and os.path.isdir(os.path.join(root, '.repo', 'submanifests')): + # If there are submanifests, walk backward from path until we find the + # corresponding submanifest root. + abs_orig_path = os.path.abspath(orig_path) + parts = os.path.relpath(abs_orig_path, root).split(os.path.sep) + while parts and not os.path.isdir( + os.path.join(root, '.repo', 'submanifests', *parts, 'manifests')): + parts.pop() + path = os.path.join(root, *parts) return path diff --git a/rh/hooks.py b/rh/hooks.py index a622bf8..6cb92a0 100644 --- a/rh/hooks.py +++ b/rh/hooks.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,15 +14,13 @@ """Functions that implement the actual checks.""" -from __future__ import print_function - -import collections import fnmatch import json import os import platform import re import sys +from typing import Callable, NamedTuple _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: @@ -86,7 +83,7 @@ class Placeholders(object): else: # First scan for exact matches for key, val in replacements.items(): - var = '${%s}' % (key,) + var = '${' + key + '}' if arg == var: if isinstance(val, str): ret.append(val) @@ -101,7 +98,7 @@ class Placeholders(object): if isinstance(val, str): return val return ' '.join(val) - ret.append(re.sub(r'\$\{(%s)\}' % ('|'.join(all_vars),), + ret.append(re.sub(r'\$\{(' + '|'.join(all_vars) + r')\}', replace, arg)) return ret @@ -114,7 +111,7 @@ class Placeholders(object): def get(self, var): """Helper function to get the replacement |var| value.""" - return getattr(self, 'var_%s' % (var,)) + return getattr(self, f'var_{var}') @property def var_PREUPLOAD_COMMIT_MESSAGE(self): @@ -132,11 +129,21 @@ class Placeholders(object): return [x.file for x in self.diff if x.status != 'D'] @property + def var_REPO_PATH(self): + """The path to the project relative to the root""" + return os.environ.get('REPO_PATH', '') + + @property def var_REPO_ROOT(self): - """The root of the repo checkout.""" + """The root of the repo (sub-manifest) checkout.""" return rh.git.find_repo_root() @property + def var_REPO_OUTER_ROOT(self): + """The root of the repo (outer) checkout.""" + return rh.git.find_repo_root(outer=True) + + @property def var_BUILD_OS(self): """The build OS (see _get_build_os_name for details).""" return _get_build_os_name() @@ -236,8 +243,11 @@ class HookOptions(object): return self.expand_vars([tool_path])[0] -# A callable hook. -CallableHook = collections.namedtuple('CallableHook', ('name', 'hook', 'scope')) +class CallableHook(NamedTuple): + """A callable hook.""" + name: str + hook: Callable + scope: ExclusionScope def _run(cmd, **kwargs): @@ -307,26 +317,11 @@ def _get_build_os_name(): return 'linux-x86' -def _fixup_func_caller(cmd, **kwargs): - """Wraps |cmd| around a callable automated fixup. - - For hooks that support automatically fixing errors after running (e.g. code - formatters), this function provides a way to run |cmd| as the |fixup_func| - parameter in HookCommandResult. - """ - def wrapper(): - result = _run(cmd, **kwargs) - if result.returncode not in (None, 0): - return result.stdout - return None - return wrapper - - -def _check_cmd(hook_name, project, commit, cmd, fixup_func=None, **kwargs): +def _check_cmd(hook_name, project, commit, cmd, fixup_cmd=None, **kwargs): """Runs |cmd| and returns its result as a HookCommandResult.""" return [rh.results.HookCommandResult(hook_name, project, commit, _run(cmd, **kwargs), - fixup_func=fixup_func)] + fixup_cmd=fixup_cmd)] # Where helper programs exist. @@ -350,15 +345,23 @@ def check_bpfmt(project, commit, _desc, diff, options=None): return None bpfmt = options.tool_path('bpfmt') - cmd = [bpfmt, '-l'] + options.args((), filtered) + bpfmt_options = options.args((), filtered) + cmd = [bpfmt, '-d'] + bpfmt_options + fixup_cmd = [bpfmt, '-w'] + if '-s' in bpfmt_options: + fixup_cmd.append('-s') + fixup_cmd.append('--') + ret = [] for d in filtered: data = rh.git.get_file_content(commit, d.file) result = _run(cmd, input=data) if result.stdout: ret.append(rh.results.HookResult( - 'bpfmt', project, commit, error=result.stdout, - files=(d.file,))) + 'bpfmt', project, commit, + error=result.stdout, + files=(d.file,), + fixup_cmd=fixup_cmd)) return ret @@ -380,9 +383,9 @@ def check_clang_format(project, commit, _desc, diff, options=None): git_clang_format] + options.args(('--style', 'file', '--commit', commit), diff)) cmd = [tool] + tool_args - fixup_func = _fixup_func_caller([tool, '--fix'] + tool_args) + fixup_cmd = [tool, '--fix'] + tool_args return _check_cmd('clang-format', project, commit, cmd, - fixup_func=fixup_func) + fixup_cmd=fixup_cmd) def check_google_java_format(project, commit, _desc, _diff, options=None): @@ -395,19 +398,50 @@ def check_google_java_format(project, commit, _desc, _diff, options=None): '--google-java-format-diff', google_java_format_diff, '--commit', commit] + options.args() cmd = [tool] + tool_args - fixup_func = _fixup_func_caller([tool, '--fix'] + tool_args) + fixup_cmd = [tool, '--fix'] + tool_args return _check_cmd('google-java-format', project, commit, cmd, - fixup_func=fixup_func) + fixup_cmd=fixup_cmd) + + +def check_ktfmt(project, commit, _desc, diff, options=None): + """Checks that kotlin files are formatted with ktfmt.""" + + include_dir_args = [x for x in options.args() + if x.startswith('--include-dirs=')] + include_dirs = [x[len('--include-dirs='):].split(',') + for x in include_dir_args] + patterns = [fr'^{x}/.*\.kt$' for dir_list in include_dirs + for x in dir_list] + if not patterns: + patterns = [r'\.kt$'] + + filtered = _filter_diff(diff, patterns) + + if not filtered: + return None + + args = [x for x in options.args() if x not in include_dir_args] + + ktfmt = options.tool_path('ktfmt') + cmd = [ktfmt, '--dry-run'] + args + HookOptions.expand_vars( + ('${PREUPLOAD_FILES}',), filtered) + result = _run(cmd) + if result.stdout: + fixup_cmd = [ktfmt] + args + return [rh.results.HookResult( + 'ktfmt', project, commit, error='Formatting errors detected', + files=[x.file for x in filtered], fixup_cmd=fixup_cmd)] + return None def check_commit_msg_bug_field(project, commit, desc, _diff, options=None): """Check the commit message for a 'Bug:' line.""" field = 'Bug' - regex = r'^%s: (None|[0-9]+(, [0-9]+)*)$' % (field,) + regex = fr'^{field}: (None|[0-9]+(, [0-9]+)*)$' check_re = re.compile(regex) if options.args(): - raise ValueError('commit msg %s check takes no options' % (field,)) + raise ValueError(f'commit msg {field} check takes no options') found = [] for line in desc.splitlines(): @@ -415,23 +449,25 @@ def check_commit_msg_bug_field(project, commit, desc, _diff, options=None): found.append(line) if not found: - error = ('Commit message is missing a "%s:" line. It must match the\n' - 'following case-sensitive regex:\n\n %s') % (field, regex) + error = ( + f'Commit message is missing a "{field}:" line. It must match the\n' + f'following case-sensitive regex:\n\n {regex}' + ) else: return None - return [rh.results.HookResult('commit msg: "%s:" check' % (field,), + return [rh.results.HookResult(f'commit msg: "{field}:" check', project, commit, error=error)] def check_commit_msg_changeid_field(project, commit, desc, _diff, options=None): """Check the commit message for a 'Change-Id:' line.""" field = 'Change-Id' - regex = r'^%s: I[a-f0-9]+$' % (field,) + regex = fr'^{field}: I[a-f0-9]+$' check_re = re.compile(regex) if options.args(): - raise ValueError('commit msg %s check takes no options' % (field,)) + raise ValueError(f'commit msg {field} check takes no options') found = [] for line in desc.splitlines(): @@ -439,15 +475,17 @@ def check_commit_msg_changeid_field(project, commit, desc, _diff, options=None): found.append(line) if not found: - error = ('Commit message is missing a "%s:" line. It must match the\n' - 'following case-sensitive regex:\n\n %s') % (field, regex) + error = ( + f'Commit message is missing a "{field}:" line. It must match the\n' + f'following case-sensitive regex:\n\n {regex}' + ) elif len(found) > 1: - error = ('Commit message has too many "%s:" lines. There can be only ' - 'one.') % (field,) + error = (f'Commit message has too many "{field}:" lines. There can be ' + 'only one.') else: return None - return [rh.results.HookResult('commit msg: "%s:" check' % (field,), + return [rh.results.HookResult(f'commit msg: "{field}:" check', project, commit, error=error)] @@ -540,11 +578,11 @@ high-quality Test: descriptions. def check_commit_msg_test_field(project, commit, desc, _diff, options=None): """Check the commit message for a 'Test:' line.""" field = 'Test' - regex = r'^%s: .*$' % (field,) + regex = fr'^{field}: .*$' check_re = re.compile(regex) if options.args(): - raise ValueError('commit msg %s check takes no options' % (field,)) + raise ValueError(f'commit msg {field} check takes no options') found = [] for line in desc.splitlines(): @@ -556,7 +594,7 @@ def check_commit_msg_test_field(project, commit, desc, _diff, options=None): else: return None - return [rh.results.HookResult('commit msg: "%s:" check' % (field,), + return [rh.results.HookResult(f'commit msg: "{field}:" check', project, commit, error=error)] @@ -585,7 +623,7 @@ release notes, you need to include a starting and closing quote. Multi-line Relnote example: Relnote: "Added a new API `Class#getSize` to get the size of the class. -This is useful if you need to know the size of the class." + This is useful if you need to know the size of the class." Single-line Relnote example: @@ -616,22 +654,23 @@ def check_commit_msg_relnote_field_format(project, commit, desc, _diff, quotes are escaped with a backslash. """ field = 'Relnote' - regex_relnote = r'^%s:.*$' % (field,) + regex_relnote = fr'^{field}:.*$' check_re_relnote = re.compile(regex_relnote, re.IGNORECASE) if options.args(): - raise ValueError('commit msg %s check takes no options' % (field,)) + raise ValueError(f'commit msg {field} check takes no options') # Check 1: Check for possible misspellings of the `Relnote:` field. # Regex for misspelled fields. - possible_field_misspells = {'Relnotes', 'ReleaseNote', - 'Rel-note', 'Rel note', - 'rel-notes', 'releasenotes', - 'release-note', 'release-notes'} - regex_field_misspells = r'^(%s): .*$' % ( - '|'.join(possible_field_misspells), - ) + possible_field_misspells = { + 'Relnotes', 'ReleaseNote', + 'Rel-note', 'Rel note', + 'rel-notes', 'releasenotes', + 'release-note', 'release-notes', + } + re_possible_field_misspells = '|'.join(possible_field_misspells) + regex_field_misspells = fr'^({re_possible_field_misspells}): .*$' check_re_field_misspells = re.compile(regex_field_misspells, re.IGNORECASE) ret = [] @@ -639,9 +678,9 @@ def check_commit_msg_relnote_field_format(project, commit, desc, _diff, if check_re_field_misspells.match(line): error = RELNOTE_MISSPELL_MSG % (regex_relnote, ) ret.append( - rh.results.HookResult(('commit msg: "%s:" ' - 'tag spelling error') % (field,), - project, commit, error=error)) + rh.results.HookResult( + f'commit msg: "{field}:" tag spelling error', + project, commit, error=error)) # Check 2: Check that multiline Relnotes are quoted. @@ -664,14 +703,12 @@ def check_commit_msg_relnote_field_format(project, commit, desc, _diff, if (not check_re_other_fields.findall(next_line) and not check_re_empty_string.match(next_line)): ret.append( - rh.results.HookResult(('commit msg: "%s:" ' - 'tag missing quotes') % (field,), - project, commit, - error=RELNOTE_MISSING_QUOTES_MSG)) + rh.results.HookResult( + f'commit msg: "{field}:" tag missing quotes', + project, commit, error=RELNOTE_MISSING_QUOTES_MSG)) break # Check 3: Check that multiline Relnotes contain matching quotes. - first_quote_found = False second_quote_found = False for cur_line in desc_lines: @@ -688,22 +725,26 @@ def check_commit_msg_relnote_field_format(project, commit, desc, _diff, # Check that the `Relnote:` tag exists and it contains a starting quote. if check_re_relnote.match(cur_line) and contains_quote: first_quote_found = True + # A single-line Relnote containing a start and ending triple quote + # is valid. + if cur_line.count('"""') == 2: + second_quote_found = True + break # A single-line Relnote containing a start and ending quote - # is valid as well. + # is valid. if cur_line.count('"') - cur_line.count('\\"') == 2: second_quote_found = True break if first_quote_found != second_quote_found: ret.append( - rh.results.HookResult(('commit msg: "%s:" ' - 'tag missing closing quote') % (field,), - project, commit, - error=RELNOTE_MISSING_QUOTES_MSG)) + rh.results.HookResult( + f'commit msg: "{field}:" tag missing closing quote', + project, commit, error=RELNOTE_MISSING_QUOTES_MSG)) # Check 4: Check that non-starting or non-ending quotes are escaped with a # backslash. line_needs_checking = False - uses_invalide_quotes = False + uses_invalid_quotes = False for cur_line in desc_lines: if check_re_other_fields.findall(cur_line): line_needs_checking = False @@ -711,32 +752,36 @@ def check_commit_msg_relnote_field_format(project, commit, desc, _diff, # Determine if we are parsing the base `Relnote:` line. if on_relnote_line and '"' in cur_line: line_needs_checking = True + # We don't think anyone will type '"""' and then forget to + # escape it, so we're not checking for this. + if '"""' in cur_line: + break if line_needs_checking: - stripped_line = re.sub('^%s:' % field, '', cur_line, + stripped_line = re.sub(fr'^{field}:', '', cur_line, flags=re.IGNORECASE).strip() for i, character in enumerate(stripped_line): - # Case 1: Valid quote at the beginning of the - # base `Relnote:` line. - if on_relnote_line and i == 0: - continue - # Case 2: Invalid quote at the beginning of following lines. - if not on_relnote_line and i == 0 and character == '"': - uses_invalide_quotes = True - break + if i == 0: + # Case 1: Valid quote at the beginning of the + # base `Relnote:` line. + if on_relnote_line: + continue + # Case 2: Invalid quote at the beginning of following + # lines, where we are not terminating the release note. + if character == '"' and stripped_line != '"': + uses_invalid_quotes = True + break # Case 3: Check all other cases. if (character == '"' and 0 < i < len(stripped_line) - 1 - and stripped_line[i-1] != "\"" + and stripped_line[i-1] != '"' and stripped_line[i-1] != "\\"): - uses_invalide_quotes = True + uses_invalid_quotes = True break - if uses_invalide_quotes: - ret.append(rh.results.HookResult(('commit msg: "%s:" ' - 'tag using unescaped ' - 'quotes') % (field,), - project, commit, - error=RELNOTE_INVALID_QUOTES_MSG)) + if uses_invalid_quotes: + ret.append(rh.results.HookResult( + f'commit msg: "{field}:" tag using unescaped quotes', + project, commit, error=RELNOTE_INVALID_QUOTES_MSG)) return ret @@ -766,11 +811,11 @@ def check_commit_msg_relnote_for_current_txt(project, commit, desc, diff, options=None): """Check changes to current.txt contain the 'Relnote:' stanza.""" field = 'Relnote' - regex = r'^%s: .+$' % (field,) + regex = fr'^{field}: .+$' check_re = re.compile(regex, re.IGNORECASE) if options.args(): - raise ValueError('commit msg %s check takes no options' % (field,)) + raise ValueError(f'commit msg {field} check takes no options') filtered = _filter_diff( diff, @@ -791,7 +836,7 @@ def check_commit_msg_relnote_for_current_txt(project, commit, desc, diff, else: return None - return [rh.results.HookResult('commit msg: "%s:" check' % (field,), + return [rh.results.HookResult(f'commit msg: "{field}:" check', project, commit, error=error)] @@ -815,16 +860,17 @@ def check_gofmt(project, commit, _desc, diff, options=None): return None gofmt = options.tool_path('gofmt') - cmd = [gofmt, '-l'] + options.args((), filtered) + cmd = [gofmt, '-l'] + options.args() + fixup_cmd = [gofmt, '-w'] + options.args() + ret = [] for d in filtered: data = rh.git.get_file_content(commit, d.file) result = _run(cmd, input=data) if result.stdout: - fixup_func = _fixup_func_caller([gofmt, '-w', d.file]) ret.append(rh.results.HookResult( 'gofmt', project, commit, error=result.stdout, - files=(d.file,), fixup_func=fixup_func)) + files=(d.file,), fixup_cmd=fixup_cmd)) return ret @@ -900,11 +946,9 @@ def check_rustfmt(project, commit, _desc, diff, options=None): # TODO(b/164111102): rustfmt stable does not support --check on stdin. # If no error is reported, compare stdin with stdout. if data != result.stdout: - msg = ('To fix, please run: %s' % - rh.shell.cmd_to_str(cmd + [d.file])) ret.append(rh.results.HookResult( - 'rustfmt', project, commit, error=msg, - files=(d.file,))) + 'rustfmt', project, commit, error='Files not formatted', + files=(d.file,), fixup_cmd=cmd)) return ret @@ -941,7 +985,7 @@ def check_xmllint(project, commit, _desc, diff, options=None): 'xsl', # Extensible Stylesheet Language. )) - filtered = _filter_diff(diff, [r'\.(%s)$' % '|'.join(extensions)]) + filtered = _filter_diff(diff, [r'\.(' + '|'.join(extensions) + r')$']) if not filtered: return None @@ -967,9 +1011,32 @@ def check_android_test_mapping(project, commit, _desc, diff, options=None): return _check_cmd('android-test-mapping-format', project, commit, cmd) +def check_aidl_format(project, commit, _desc, diff, options=None): + """Checks that AIDL files are formatted with aidl-format.""" + # All *.aidl files except for those under aidl_api directory. + filtered = _filter_diff(diff, [r'\.aidl$'], [r'(^|/)aidl_api/']) + if not filtered: + return None + aidl_format = options.tool_path('aidl-format') + clang_format = options.tool_path('clang-format') + diff_cmd = [aidl_format, '-d', '--clang-format-path', clang_format] + \ + options.args((), filtered) + ret = [] + for d in filtered: + data = rh.git.get_file_content(commit, d.file) + result = _run(diff_cmd, input=data) + if result.stdout: + fixup_cmd = [aidl_format, '-w', '--clang-format-path', clang_format] + ret.append(rh.results.HookResult( + 'aidl-format', project, commit, error=result.stdout, + files=(d.file,), fixup_cmd=fixup_cmd)) + return ret + + # Hooks that projects can opt into. # Note: Make sure to keep the top level README.md up to date when adding more! BUILTIN_HOOKS = { + 'aidl_format': check_aidl_format, 'android_test_mapping_format': check_android_test_mapping, 'bpfmt': check_bpfmt, 'checkpatch': check_checkpatch, @@ -977,14 +1044,15 @@ BUILTIN_HOOKS = { 'commit_msg_bug_field': check_commit_msg_bug_field, 'commit_msg_changeid_field': check_commit_msg_changeid_field, 'commit_msg_prebuilt_apk_fields': check_commit_msg_prebuilt_apk_fields, - 'commit_msg_test_field': check_commit_msg_test_field, 'commit_msg_relnote_field_format': check_commit_msg_relnote_field_format, 'commit_msg_relnote_for_current_txt': check_commit_msg_relnote_for_current_txt, + 'commit_msg_test_field': check_commit_msg_test_field, 'cpplint': check_cpplint, 'gofmt': check_gofmt, 'google_java_format': check_google_java_format, 'jsonlint': check_json, + 'ktfmt': check_ktfmt, 'pylint': check_pylint2, 'pylint2': check_pylint2, 'pylint3': check_pylint3, @@ -995,6 +1063,7 @@ BUILTIN_HOOKS = { # Additional tools that the hooks can call with their default values. # Note: Make sure to keep the top level README.md up to date when adding more! TOOL_PATHS = { + 'aidl-format': 'aidl-format', 'android-test-mapping-format': os.path.join(TOOLS_DIR, 'android_test_mapping_format.py'), 'bpfmt': 'bpfmt', @@ -1004,6 +1073,7 @@ TOOL_PATHS = { 'gofmt': 'gofmt', 'google-java-format': 'google-java-format', 'google-java-format-diff': 'google-java-format-diff.py', + 'ktfmt': 'ktfmt', 'pylint': 'pylint', 'rustfmt': 'rustfmt', } diff --git a/rh/hooks_unittest.py b/rh/hooks_unittest.py index 74971b8..003057e 100755 --- a/rh/hooks_unittest.py +++ b/rh/hooks_unittest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +15,6 @@ """Unittests for the hooks module.""" -from __future__ import print_function - import os import sys import unittest @@ -36,6 +33,11 @@ import rh.config import rh.hooks +# pylint: disable=unused-argument +def mock_find_repo_root(path=None, outer=False): + return '/ ${BUILD_OS}' if outer else '/ ${BUILD_OS}/sub' + + class HooksDocsTests(unittest.TestCase): """Make sure all hook features are documented. @@ -51,7 +53,7 @@ class HooksDocsTests(unittest.TestCase): """Extract the |section| text out of the readme.""" ret = [] in_section = False - with open(self.readme) as fp: + with open(self.readme, encoding='utf-8') as fp: for line in fp: if not in_section: # Look for the section like "## [Tool Paths]". @@ -69,22 +71,22 @@ class HooksDocsTests(unittest.TestCase): """Verify builtin hooks are documented.""" data = self._grab_section('[Builtin Hooks]') for hook in rh.hooks.BUILTIN_HOOKS: - self.assertIn('* `%s`:' % (hook,), data, - msg='README.md missing docs for hook "%s"' % (hook,)) + self.assertIn(f'* `{hook}`:', data, + msg=f'README.md missing docs for hook "{hook}"') def testToolPaths(self): """Verify tools are documented.""" data = self._grab_section('[Tool Paths]') for tool in rh.hooks.TOOL_PATHS: - self.assertIn('* `%s`:' % (tool,), data, - msg='README.md missing docs for tool "%s"' % (tool,)) + self.assertIn(f'* `{tool}`:', data, + msg=f'README.md missing docs for tool "{tool}"') def testPlaceholders(self): """Verify placeholder replacement vars are documented.""" data = self._grab_section('Placeholders') for var in rh.hooks.Placeholders.vars(): - self.assertIn('* `${%s}`:' % (var,), data, - msg='README.md missing docs for var "%s"' % (var,)) + self.assertIn('* `${' + var + '}`:', data, + msg=f'README.md missing docs for var "{var}"') class PlaceholderTests(unittest.TestCase): @@ -110,7 +112,8 @@ class PlaceholderTests(unittest.TestCase): self.assertGreater(len(ret), 4) self.assertIn('PREUPLOAD_COMMIT', ret) - @mock.patch.object(rh.git, 'find_repo_root', return_value='/ ${BUILD_OS}') + @mock.patch.object(rh.git, 'find_repo_root', + side_effect=mock_find_repo_root) def testExpandVars(self, _m): """Verify the replacement actually works.""" input_args = [ @@ -118,6 +121,8 @@ class PlaceholderTests(unittest.TestCase): # We also make sure that things in ${REPO_ROOT} are not double # expanded (which is why the return includes ${BUILD_OS}). '${REPO_ROOT}/some/prog/REPO_ROOT/ok', + # Verify that ${REPO_OUTER_ROOT} is expanded. + '${REPO_OUTER_ROOT}/some/prog/REPO_OUTER_ROOT/ok', # Verify lists are merged rather than inserted. '${PREUPLOAD_FILES}', # Verify each file is preceded with '--file=' prefix. @@ -134,7 +139,8 @@ class PlaceholderTests(unittest.TestCase): ] output_args = self.replacer.expand_vars(input_args) exp_args = [ - '/ ${BUILD_OS}/some/prog/REPO_ROOT/ok', + '/ ${BUILD_OS}/sub/some/prog/REPO_ROOT/ok', + '/ ${BUILD_OS}/some/prog/REPO_OUTER_ROOT/ok', 'path1/file1', 'path2/file2', '--file=path1/file1', @@ -152,8 +158,8 @@ class PlaceholderTests(unittest.TestCase): def testTheTester(self): """Make sure we have a test for every variable.""" for var in self.replacer.vars(): - self.assertIn('test%s' % (var,), dir(self), - msg='Missing unittest for variable %s' % (var,)) + self.assertIn(f'test{var}', dir(self), + msg=f'Missing unittest for variable {var}') def testPREUPLOAD_COMMIT_MESSAGE(self): """Verify handling of PREUPLOAD_COMMIT_MESSAGE.""" @@ -170,10 +176,26 @@ class PlaceholderTests(unittest.TestCase): self.assertEqual(self.replacer.get('PREUPLOAD_FILES'), ['path1/file1', 'path2/file2']) - @mock.patch.object(rh.git, 'find_repo_root', return_value='/repo!') + @mock.patch.object(rh.git, 'find_repo_root') + def testREPO_OUTER_ROOT(self, m): + """Verify handling of REPO_OUTER_ROOT.""" + m.side_effect = mock_find_repo_root + self.assertEqual(self.replacer.get('REPO_OUTER_ROOT'), + mock_find_repo_root(path=None, outer=True)) + + @mock.patch.object(rh.git, 'find_repo_root') def testREPO_ROOT(self, m): """Verify handling of REPO_ROOT.""" - self.assertEqual(self.replacer.get('REPO_ROOT'), m.return_value) + m.side_effect = mock_find_repo_root + self.assertEqual(self.replacer.get('REPO_ROOT'), + mock_find_repo_root(path=None, outer=False)) + + def testREPO_PATH(self): + """Verify handling of REPO_PATH.""" + os.environ['REPO_PATH'] = '' + self.assertEqual(self.replacer.get('REPO_PATH'), '') + os.environ['REPO_PATH'] = 'foo/bar' + self.assertEqual(self.replacer.get('REPO_PATH'), 'foo/bar') @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os') def testBUILD_OS(self, m): @@ -215,7 +237,7 @@ class HookOptionsTests(unittest.TestCase): # At least one replacement. Most real testing is in PlaceholderTests. args = ['who', 'goes', 'there ?', '${BUILD_OS} is great'] - exp_args = ['who', 'goes', 'there ?', '%s is great' % (m.return_value,)] + exp_args = ['who', 'goes', 'there ?', f'{m.return_value} is great'] self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args)) def testArgs(self): @@ -270,6 +292,20 @@ class UtilsTests(unittest.TestCase): self.assertTrue(isinstance(ret, str)) self.assertNotEqual(ret, '') + def testSortedToolPaths(self): + """Check TOOL_PATHS is sorted.""" + # This assumes dictionary key ordering matches insertion/definition + # order which Python 3.7+ has codified. + # https://docs.python.org/3.7/library/stdtypes.html#dict + self.assertEqual(list(rh.hooks.TOOL_PATHS), sorted(rh.hooks.TOOL_PATHS)) + + def testSortedBuiltinHooks(self): + """Check BUILTIN_HOOKS is sorted.""" + # This assumes dictionary key ordering matches insertion/definition + # order which Python 3.7+ has codified. + # https://docs.python.org/3.7/library/stdtypes.html#dict + self.assertEqual( + list(rh.hooks.BUILTIN_HOOKS), sorted(rh.hooks.BUILTIN_HOOKS)) @mock.patch.object(rh.utils, 'run') @@ -278,8 +314,7 @@ class BuiltinHooksTests(unittest.TestCase): """Verify the builtin hooks.""" def setUp(self): - self.project = rh.Project(name='project-name', dir='/.../repo/dir', - remote='remote') + self.project = rh.Project(name='project-name', dir='/.../repo/dir') self.options = rh.hooks.HookOptions('hook name', [], {}) def _test_commit_messages(self, func, accept, msgs, files=None): @@ -299,10 +334,10 @@ class BuiltinHooksTests(unittest.TestCase): ret = func(self.project, 'commit', desc, diff, options=self.options) if accept: self.assertFalse( - bool(ret), msg='Should have accepted: {{{%s}}}' % (desc,)) + bool(ret), msg='Should have accepted: {{{' + desc + '}}}') else: self.assertTrue( - bool(ret), msg='Should have rejected: {{{%s}}}' % (desc,)) + bool(ret), msg='Should have rejected: {{{' + desc + '}}}') def _test_file_filter(self, mock_check, func, files): """Helper for testing hooks that filter by files and run external tools. @@ -325,8 +360,8 @@ class BuiltinHooksTests(unittest.TestCase): def testTheTester(self, _mock_check, _mock_run): """Make sure we have a test for every hook.""" for hook in rh.hooks.BUILTIN_HOOKS: - self.assertIn('test_%s' % (hook,), dir(self), - msg='Missing unittest for builtin hook %s' % (hook,)) + self.assertIn(f'test_{hook}', dir(self), + msg=f'Missing unittest for builtin hook {hook}') def test_bpfmt(self, mock_check, _mock_run): """Verify the bpfmt builtin hook.""" @@ -341,6 +376,8 @@ class BuiltinHooksTests(unittest.TestCase): ret = rh.hooks.check_bpfmt( self.project, 'commit', 'desc', diff, options=self.options) self.assertIsNotNone(ret) + for result in ret: + self.assertIsNotNone(result.fixup_cmd) def test_checkpatch(self, mock_check, _mock_run): """Verify the checkpatch builtin hook.""" @@ -567,6 +604,33 @@ class BuiltinHooksTests(unittest.TestCase): 'a correctly formatted second line."\n\n' 'Bug: 1234' 'Here is some extra "quoted" content.'), + ('subj\n\nRelnote: """This is a release note.\n\n' + 'This relnote contains an empty line.\n' + 'Then a non-empty line.\n\n' + 'And another empty line."""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note.\n\n' + 'This relnote contains an empty line.\n' + 'Then an acceptable "quoted" line.\n\n' + 'And another empty line."""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note."""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note.\n' + 'It has a second line."""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note.\n' + 'It has a second line, but does not end here.\n' + '"""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note.\n' + '"It" has a second line, but does not end here.\n' + '"""\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: "This is a release note.\n' + 'It has a second line, but does not end here.\n' + '"\n\n' + 'Bug: 1234'), )) # Check some bad messages. @@ -607,6 +671,13 @@ class BuiltinHooksTests(unittest.TestCase): 'It contains a correct second line.\n' 'But incorrect "quotes" on the third line."\n' 'Bug: 1234'), + ('subj\n\nRelnote: """This is a release note.\n' + 'It has a second line, but no closing triple quote.\n\n' + 'Bug: 1234'), + ('subj\n\nRelnote: "This is a release note.\n' + '"It" has a second line, but does not end here.\n' + '"\n\n' + 'Bug: 1234'), )) def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run): @@ -745,6 +816,28 @@ class BuiltinHooksTests(unittest.TestCase): # TODO: Actually pass some valid/invalid json data down. + def test_ktfmt(self, mock_check, _mock_run): + """Verify the ktfmt builtin hook.""" + # First call should do nothing as there are no files to check. + ret = rh.hooks.check_ktfmt( + self.project, 'commit', 'desc', (), options=self.options) + self.assertIsNone(ret) + self.assertFalse(mock_check.called) + # Check that .kt files are included by default. + diff = [rh.git.RawDiffEntry(file='foo.kt'), + rh.git.RawDiffEntry(file='bar.java'), + rh.git.RawDiffEntry(file='baz/blah.kt')] + ret = rh.hooks.check_ktfmt( + self.project, 'commit', 'desc', diff, options=self.options) + self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt']) + diff = [rh.git.RawDiffEntry(file='foo/f1.kt'), + rh.git.RawDiffEntry(file='bar/f2.kt'), + rh.git.RawDiffEntry(file='baz/f2.kt')] + ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff, + options=rh.hooks.HookOptions('hook name', [ + '--include-dirs=foo,baz'], {})) + self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt']) + def test_pylint(self, mock_check, _mock_run): """Verify the pylint builtin hook.""" self._test_file_filter(mock_check, rh.hooks.check_pylint2, @@ -792,6 +885,20 @@ class BuiltinHooksTests(unittest.TestCase): self.project, 'commit', 'desc', diff, options=self.options) self.assertIsNotNone(ret) + def test_aidl_format(self, mock_check, _mock_run): + """Verify the aidl_format builtin hook.""" + # First call should do nothing as there are no files to check. + ret = rh.hooks.check_aidl_format( + self.project, 'commit', 'desc', (), options=self.options) + self.assertIsNone(ret) + self.assertFalse(mock_check.called) + + # Second call will have some results. + diff = [rh.git.RawDiffEntry(file='IFoo.go')] + ret = rh.hooks.check_gofmt( + self.project, 'commit', 'desc', diff, options=self.options) + self.assertIsNotNone(ret) + if __name__ == '__main__': unittest.main() diff --git a/rh/results.py b/rh/results.py index bdac83c..65e0052 100644 --- a/rh/results.py +++ b/rh/results.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,10 +14,9 @@ """Common errors thrown when repo preupload checks fail.""" -from __future__ import print_function - import os import sys +from typing import List, NamedTuple, Optional _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: @@ -29,7 +27,8 @@ del _path class HookResult(object): """A single hook result.""" - def __init__(self, hook, project, commit, error, files=(), fixup_func=None): + def __init__(self, hook, project, commit, error, files=(), + fixup_cmd: Optional[List[str]] = None): """Initialize. Args: @@ -39,27 +38,23 @@ class HookResult(object): error: A string representation of the hook's result. Empty on success. files: The list of files that were involved in the hook execution. - fixup_func: A callable that will attempt to automatically fix errors - found in the hook's execution. Returns an non-empty string if - this, too, fails. Can be None if the hook does not support - automatically fixing errors. + fixup_cmd: A command that can automatically fix errors found in the + hook's execution. Can be None if the hook does not support + automatic fixups. """ self.hook = hook self.project = project self.commit = commit self.error = error self.files = files - self.fixup_func = fixup_func + self.fixup_cmd = fixup_cmd def __bool__(self): + """Whether this result is an error.""" return bool(self.error) - # pylint: disable=nonzero-method - def __nonzero__(self): - """Python 2/3 glue.""" - return self.__bool__() - def is_warning(self): + """Whether this result is a non-fatal warning.""" return False @@ -67,14 +62,44 @@ class HookCommandResult(HookResult): """A single hook result based on a CompletedProcess.""" def __init__(self, hook, project, commit, result, files=(), - fixup_func=None): + fixup_cmd=None): HookResult.__init__(self, hook, project, commit, result.stderr if result.stderr else result.stdout, - files=files, fixup_func=fixup_func) + files=files, fixup_cmd=fixup_cmd) self.result = result def __bool__(self): - return self.result.returncode not in (None, 0) + """Whether this result is an error.""" + return self.result.returncode not in (None, 0, 77) def is_warning(self): + """Whether this result is a non-fatal warning.""" return self.result.returncode == 77 + + +class ProjectResults(NamedTuple): + """All results for a single project.""" + + project: str + workdir: str + + # All the results from running all the hooks. + results: List[HookResult] = [] + + # Whether there were any non-hook related errors. For example, trying to + # parse the project configuration. + internal_failure: bool = False + + def add_results(self, results: Optional[List[HookResult]]) -> None: + """Add |results| to our results.""" + if results: + self.results.extend(results) + + @property + def fixups(self): + """Yield results that have a fixup available.""" + yield from (x for x in self.results if x and x.fixup_cmd) + + def __bool__(self): + """Whether there are any errors in this set of results.""" + return self.internal_failure or any(self.results) diff --git a/rh/results_unittest.py b/rh/results_unittest.py new file mode 100755 index 0000000..93d909e --- /dev/null +++ b/rh/results_unittest.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# Copyright 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittests for the results module.""" + +import os +import sys +import unittest + +_path = os.path.realpath(__file__ + '/../..') +if sys.path[0] != _path: + sys.path.insert(0, _path) +del _path + +# We have to import our local modules after the sys.path tweak. We can't use +# relative imports because this is an executable program, not a module. +# pylint: disable=wrong-import-position +import rh +import rh.results +import rh.utils + + +COMPLETED_PROCESS_PASS = rh.utils.CompletedProcess(returncode=0) +COMPLETED_PROCESS_FAIL = rh.utils.CompletedProcess(returncode=1) +COMPLETED_PROCESS_WARN = rh.utils.CompletedProcess(returncode=77) + + +class HookResultTests(unittest.TestCase): + """Verify behavior of HookResult object.""" + + def test_error_warning(self): + """Check error & warning handling.""" + # No errors. + result = rh.results.HookResult('hook', 'project', 'HEAD', False) + self.assertFalse(result) + self.assertFalse(result.is_warning()) + + # An error. + result = rh.results.HookResult('hook', 'project', 'HEAD', True) + self.assertTrue(result) + self.assertFalse(result.is_warning()) + + +class HookCommandResultTests(unittest.TestCase): + """Verify behavior of HookCommandResult object.""" + + def test_error_warning(self): + """Check error & warning handling.""" + # No errors. + result = rh.results.HookCommandResult( + 'hook', 'project', 'HEAD', COMPLETED_PROCESS_PASS) + self.assertFalse(result) + self.assertFalse(result.is_warning()) + + # An error. + result = rh.results.HookCommandResult( + 'hook', 'project', 'HEAD', COMPLETED_PROCESS_FAIL) + self.assertTrue(result) + self.assertFalse(result.is_warning()) + + # A warning. + result = rh.results.HookCommandResult( + 'hook', 'project', 'HEAD', COMPLETED_PROCESS_WARN) + self.assertFalse(result) + self.assertTrue(result.is_warning()) + + +class ProjectResultsTests(unittest.TestCase): + """Verify behavior of ProjectResults object.""" + + def test_error_warning(self): + """Check error & warning handling.""" + # No errors. + result = rh.results.ProjectResults('project', 'workdir') + self.assertFalse(result) + + # Warnings are not errors. + result.add_results([ + rh.results.HookResult('hook', 'project', 'HEAD', False), + rh.results.HookCommandResult( + 'hook', 'project', 'HEAD', COMPLETED_PROCESS_WARN), + ]) + self.assertFalse(result) + + # Errors are errors. + result.add_results([ + rh.results.HookResult('hook', 'project', 'HEAD', True), + ]) + self.assertTrue(result) + + +if __name__ == '__main__': + unittest.main() diff --git a/rh/shell.py b/rh/shell.py index 4c6c45c..bc66f37 100644 --- a/rh/shell.py +++ b/rh/shell.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +14,8 @@ """Functions for working with shell code.""" -from __future__ import print_function - import os +import pathlib import sys _path = os.path.realpath(__file__ + '/../..') @@ -41,7 +39,7 @@ _SHELL_QUOTABLE_CHARS = frozenset('[|&;()<> \t!{}[]=*?~$"\'\\#^') _SHELL_ESCAPE_CHARS = r'\"`$' -def shell_quote(s): +def quote(s): """Quote |s| in a way that is safe for use in a shell. We aim to be safe, but also to produce "nice" output. That means we don't @@ -68,29 +66,34 @@ def shell_quote(s): Returns: A safely (possibly quoted) string. """ + # If callers pass down bad types, don't blow up. if isinstance(s, bytes): s = s.encode('utf-8') + elif isinstance(s, pathlib.PurePath): + return str(s) + elif not isinstance(s, str): + return repr(s) # See if no quoting is needed so we can return the string as-is. for c in s: if c in _SHELL_QUOTABLE_CHARS: break else: - return s if s else u"''" + return s if s else "''" # See if we can use single quotes first. Output is nicer. if "'" not in s: - return u"'%s'" % s + return f"'{s}'" # Have to use double quotes. Escape the few chars that still expand when # used inside of double quotes. for c in _SHELL_ESCAPE_CHARS: if c in s: - s = s.replace(c, r'\%s' % c) - return u'"%s"' % s + s = s.replace(c, fr'\{c}') + return f'"{s}"' -def shell_unquote(s): +def unquote(s): """Do the opposite of ShellQuote. This function assumes that the input is a valid escaped string. @@ -145,7 +148,7 @@ def cmd_to_str(cmd): String representing full command. """ # Use str before repr to translate unicode strings to regular strings. - return ' '.join(shell_quote(arg) for arg in cmd) + return ' '.join(quote(arg) for arg in cmd) def boolean_shell_value(sval, default): @@ -160,4 +163,4 @@ def boolean_shell_value(sval, default): if s in ('no', 'n', '0', 'false'): return False - raise ValueError('Could not decode as a boolean value: %r' % (sval,)) + raise ValueError(f'Could not decode as a boolean value: {sval!r}') diff --git a/rh/shell_unittest.py b/rh/shell_unittest.py index 47182a5..fec8710 100755 --- a/rh/shell_unittest.py +++ b/rh/shell_unittest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +15,9 @@ """Unittests for the shell module.""" -from __future__ import print_function - import difflib import os +from pathlib import Path import sys import unittest @@ -43,8 +41,8 @@ class DiffTestCase(unittest.TestCase): def _assertEqual(self, func, test_input, test_output, result): """Like assertEqual but with built in diff support.""" diff = '\n'.join(list(self.differ.compare([test_output], [result]))) - msg = ('Expected %s to translate %r to %r, but got %r\n%s' % - (func, test_input, test_output, result, diff)) + msg = (f'Expected {func} to translate {test_input!r} to ' + f'{test_output!r}, but got {result!r}\n{diff}') self.assertEqual(test_output, result, msg) def _testData(self, functor, tests, check_type=True): @@ -61,15 +59,15 @@ class DiffTestCase(unittest.TestCase): class ShellQuoteTest(DiffTestCase): - """Test the shell_quote & shell_unquote functions.""" + """Test the quote & unquote functions.""" def testShellQuote(self): """Basic ShellQuote tests.""" # Dict of expected output strings to input lists. tests_quote = { "''": '', - 'a': u'a', - "'a b c'": u'a b c', + 'a': 'a', + "'a b c'": 'a b c', "'a\tb'": 'a\tb', "'/a$file'": '/a$file', "'/a#file'": '/a#file', @@ -88,15 +86,27 @@ class ShellQuoteTest(DiffTestCase): } def aux(s): - return rh.shell.shell_unquote(rh.shell.shell_quote(s)) + return rh.shell.unquote(rh.shell.quote(s)) - self._testData(rh.shell.shell_quote, tests_quote) - self._testData(rh.shell.shell_unquote, tests_unquote) + self._testData(rh.shell.quote, tests_quote) + self._testData(rh.shell.unquote, tests_unquote) # Test that the operations are reversible. self._testData(aux, {k: k for k in tests_quote.values()}, False) self._testData(aux, {k: k for k in tests_quote}, False) + def testPathlib(self): + """Verify pathlib is handled.""" + self.assertEqual(rh.shell.quote(Path('/')), '/') + + def testBadInputs(self): + """Verify bad inputs do not crash.""" + for arg, exp in ( + (1234, '1234'), + (Exception('hi'), "Exception('hi')"), + ): + self.assertEqual(rh.shell.quote(arg), exp) + class CmdToStrTest(DiffTestCase): """Test the cmd_to_str function.""" @@ -108,7 +118,7 @@ class CmdToStrTest(DiffTestCase): r"'a b' c": ['a b', 'c'], r'''a "b'c"''': ['a', "b'c"], r'''a "/'\$b" 'a b c' "xy'z"''': - [u'a', "/'$b", 'a b c', "xy'z"], + ['a', "/'$b", 'a b c', "xy'z"], '': [], } self._testData(rh.shell.cmd_to_str, tests) diff --git a/rh/signals.py b/rh/signals.py index 45d4e8a..c8a8d81 100644 --- a/rh/signals.py +++ b/rh/signals.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,6 @@ """Signal related functionality.""" -from __future__ import print_function - import os import signal import sys diff --git a/rh/terminal.py b/rh/terminal.py index c549f12..a6f31d9 100644 --- a/rh/terminal.py +++ b/rh/terminal.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +17,9 @@ This module handles terminal interaction including ANSI color codes. """ -from __future__ import print_function - import os import sys +from typing import List, Optional _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: @@ -32,6 +30,12 @@ del _path import rh.shell +# This will erase all content in the current line after the cursor. This is +# useful for partial updates & progress messages as the terminal can display +# it better. +CSI_ERASE_LINE_AFTER = '\x1b[K' + + class Color(object): """Conditionally wraps text in ANSI color escape sequences.""" @@ -39,7 +43,7 @@ class Color(object): BOLD = -1 COLOR_START = '\033[1;%dm' BOLD_START = '\033[1m' - RESET = '\033[0m' + RESET = '\033[m' def __init__(self, enabled=None): """Create a new Color object, optionally disabling color output. @@ -54,7 +58,7 @@ class Color(object): """Returns a start color code. Args: - color: Color to use, .e.g BLACK, RED, etc. + color: Color to use, e.g. BLACK, RED, etc... Returns: If color is enabled, returns an ANSI sequence to start the given @@ -102,25 +106,10 @@ class Color(object): self._enabled = not rh.shell.boolean_shell_value( os.environ['NOCOLOR'], False) else: - self._enabled = is_tty(sys.stderr) + self._enabled = sys.stderr.isatty() return self._enabled -def is_tty(fh): - """Returns whether the specified file handle is a TTY. - - Args: - fh: File handle to check. - - Returns: - True if |fh| is a TTY - """ - try: - return os.isatty(fh.fileno()) - except IOError: - return False - - def print_status_line(line, print_newline=False): """Clears the current terminal line, and prints |line|. @@ -128,8 +117,8 @@ def print_status_line(line, print_newline=False): line: String to print. print_newline: Print a newline at the end, if sys.stderr is a TTY. """ - if is_tty(sys.stderr): - output = '\r' + line + '\x1B[K' + if sys.stderr.isatty(): + output = '\r' + line + CSI_ERASE_LINE_AFTER if print_newline: output += '\n' else: @@ -139,16 +128,32 @@ def print_status_line(line, print_newline=False): sys.stderr.flush() -def get_input(prompt): - """Python 2/3 glue for raw_input/input differences.""" +def str_prompt( + prompt: str, + choices: List[str], + lower: bool = True, +) -> Optional[str]: + """Helper function for processing user input. + + Args: + prompt: The question to present to the user. + lower: Whether to lowercase the response. + + Returns: + The string the user entered, or None if EOF (e.g. Ctrl+D). + """ + prompt = f'{prompt} ({"/".join(choices)})? ' try: - # pylint: disable=raw_input-builtin - return raw_input(prompt) - except NameError: - # Python 3 renamed raw_input() to input(), which is safe to call since - # it does not evaluate the input. - # pylint: disable=bad-builtin,input-builtin - return input(prompt) + result = input(prompt) + return result.lower() if lower else result + except EOFError: + # If the user hits Ctrl+D, or stdin is disabled, use the default. + print() + return None + except KeyboardInterrupt: + # If the user hits Ctrl+C, just exit the process. + print() + raise def boolean_prompt(prompt='Do you want to continue?', default=True, @@ -168,31 +173,20 @@ def boolean_prompt(prompt='Do you want to continue?', default=True, true_value, false_value = true_value.lower(), false_value.lower() true_text, false_text = true_value, false_value if true_value == false_value: - raise ValueError('true_value and false_value must differ: got %r' - % true_value) + raise ValueError( + f'true_value and false_value must differ: got {true_value!r}') if default: true_text = true_text[0].upper() + true_text[1:] else: false_text = false_text[0].upper() + false_text[1:] - prompt = ('\n%s (%s/%s)? ' % (prompt, true_text, false_text)) - if prolog: - prompt = ('\n%s\n%s' % (prolog, prompt)) + prompt = f'\n{prolog}\n{prompt}' + prompt = '\n' + prompt while True: - try: - response = get_input(prompt).lower() - except EOFError: - # If the user hits CTRL+D, or stdin is disabled, use the default. - print() - response = None - except KeyboardInterrupt: - # If the user hits CTRL+C, just exit the process. - print() - raise - + response = str_prompt(prompt, choices=(true_text, false_text)) if not response: return default if true_value.startswith(response): diff --git a/rh/terminal_unittest.py b/rh/terminal_unittest.py new file mode 100755 index 0000000..b76b907 --- /dev/null +++ b/rh/terminal_unittest.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# Copyright 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittests for the terminal module.""" + +import contextlib +import io +import os +import sys +import unittest + +_path = os.path.realpath(__file__ + '/../..') +if sys.path[0] != _path: + sys.path.insert(0, _path) +del _path + +# We have to import our local modules after the sys.path tweak. We can't use +# relative imports because this is an executable program, not a module. +# pylint: disable=wrong-import-position +import rh.terminal + + +class ColorTests(unittest.TestCase): + """Verify behavior of Color class.""" + + def setUp(self): + os.environ.pop('NOCOLOR', None) + + def test_enabled_auto_tty(self): + """Test automatic enable behavior based on tty.""" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + c = rh.terminal.Color() + self.assertFalse(c.enabled) + + stderr.isatty = lambda: True + c = rh.terminal.Color() + self.assertTrue(c.enabled) + + def test_enabled_auto_env(self): + """Test automatic enable behavior based on $NOCOLOR.""" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + os.environ['NOCOLOR'] = 'yes' + c = rh.terminal.Color() + self.assertFalse(c.enabled) + + os.environ['NOCOLOR'] = 'no' + c = rh.terminal.Color() + self.assertTrue(c.enabled) + + def test_enabled_override(self): + """Test explicit enable behavior.""" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + stderr.isatty = lambda: True + os.environ['NOCOLOR'] = 'no' + c = rh.terminal.Color() + self.assertTrue(c.enabled) + c = rh.terminal.Color(False) + self.assertFalse(c.enabled) + + stderr.isatty = lambda: False + os.environ['NOCOLOR'] = 'yes' + c = rh.terminal.Color() + self.assertFalse(c.enabled) + c = rh.terminal.Color(True) + self.assertTrue(c.enabled) + + def test_output_disabled(self): + """Test output when coloring is disabled.""" + c = rh.terminal.Color(False) + self.assertEqual(c.start(rh.terminal.Color.BLACK), '') + self.assertEqual(c.color(rh.terminal.Color.BLACK, 'foo'), 'foo') + self.assertEqual(c.stop(), '') + + def test_output_enabled(self): + """Test output when coloring is enabled.""" + c = rh.terminal.Color(True) + self.assertEqual(c.start(rh.terminal.Color.BLACK), '\x1b[1;30m') + self.assertEqual(c.color(rh.terminal.Color.BLACK, 'foo'), + '\x1b[1;30mfoo\x1b[m') + self.assertEqual(c.stop(), '\x1b[m') + + +class PrintStatusLine(unittest.TestCase): + """Verify behavior of print_status_line.""" + + def test_terminal(self): + """Check tty behavior.""" + stderr = io.StringIO() + stderr.isatty = lambda: True + with contextlib.redirect_stderr(stderr): + rh.terminal.print_status_line('foo') + rh.terminal.print_status_line('bar', print_newline=True) + csi = rh.terminal.CSI_ERASE_LINE_AFTER + self.assertEqual(stderr.getvalue(), f'\rfoo{csi}\rbar{csi}\n') + + def test_no_terminal(self): + """Check tty-less behavior.""" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + rh.terminal.print_status_line('foo') + rh.terminal.print_status_line('bar', print_newline=True) + self.assertEqual(stderr.getvalue(), 'foo\nbar\n') + + +@contextlib.contextmanager +def redirect_stdin(new_target): + """Temporarily switch sys.stdin to |new_target|.""" + old = sys.stdin + try: + sys.stdin = new_target + yield + finally: + sys.stdin = old + + +class StringPromptTests(unittest.TestCase): + """Verify behavior of str_prompt.""" + + def setUp(self): + self.stdin = io.StringIO() + + def set_stdin(self, value: str) -> None: + """Set stdin wrapper to a string.""" + self.stdin.seek(0) + self.stdin.write(value) + self.stdin.truncate() + self.stdin.seek(0) + + def test_defaults(self): + """Test default behavior.""" + stdout = io.StringIO() + with redirect_stdin(self.stdin), contextlib.redirect_stdout(stdout): + # Test EOF behavior. + self.assertIsNone(rh.terminal.str_prompt('foo', ('a', 'b'))) + + # Test enter behavior. + self.set_stdin('\n') + self.assertEqual(rh.terminal.str_prompt('foo', ('a', 'b')), '') + + # Lowercase inputs. + self.set_stdin('Ok') + self.assertEqual(rh.terminal.str_prompt('foo', ('a', 'b')), 'ok') + + # Don't lowercase inputs. + self.set_stdin('Ok') + self.assertEqual( + rh.terminal.str_prompt('foo', ('a', 'b'), lower=False), 'Ok') + + +class BooleanPromptTests(unittest.TestCase): + """Verify behavior of boolean_prompt.""" + + def setUp(self): + self.stdin = io.StringIO() + + def set_stdin(self, value: str) -> None: + """Set stdin wrapper to a string.""" + self.stdin.seek(0) + self.stdin.write(value) + self.stdin.truncate() + self.stdin.seek(0) + + def test_defaults(self): + """Test default behavior.""" + stdout = io.StringIO() + with redirect_stdin(self.stdin), contextlib.redirect_stdout(stdout): + # Default values. Will loop to EOF when it doesn't match anything. + for v in ('', '\n', 'oops'): + self.set_stdin(v) + self.assertTrue(rh.terminal.boolean_prompt()) + + # False values. + for v in ('n', 'N', 'no', 'NO'): + self.set_stdin(v) + self.assertFalse(rh.terminal.boolean_prompt()) + + # True values. + for v in ('y', 'Y', 'ye', 'yes', 'YES'): + self.set_stdin(v) + self.assertTrue(rh.terminal.boolean_prompt()) + + +if __name__ == '__main__': + unittest.main() diff --git a/rh/utils.py b/rh/utils.py index 5c3e753..4f1a063 100644 --- a/rh/utils.py +++ b/rh/utils.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,6 @@ """Various utility functions.""" -from __future__ import print_function - import errno import functools import os @@ -45,32 +42,23 @@ def timedelta_str(delta): total = delta.total_seconds() hours, rem = divmod(total, 3600) mins, secs = divmod(rem, 60) - ret = '%i.%03is' % (secs, delta.microseconds // 1000) + ret = f'{int(secs)}.{delta.microseconds // 1000:03}s' if mins: - ret = '%im%s' % (mins, ret) + ret = f'{int(mins)}m{ret}' if hours: - ret = '%ih%s' % (hours, ret) + ret = f'{int(hours)}h{ret}' return ret -class CompletedProcess(getattr(subprocess, 'CompletedProcess', object)): +class CompletedProcess(subprocess.CompletedProcess): """An object to store various attributes of a child process. This is akin to subprocess.CompletedProcess. """ - # The linter is confused by the getattr usage above. - # TODO(vapier): Drop this once we're Python 3-only and we drop getattr. - # pylint: disable=bad-option-value,super-on-old-class def __init__(self, args=None, returncode=None, stdout=None, stderr=None): - if sys.version_info.major < 3: - self.args = args - self.stdout = stdout - self.stderr = stderr - self.returncode = returncode - else: - super(CompletedProcess, self).__init__( - args=args, returncode=returncode, stdout=stdout, stderr=stderr) + super().__init__( + args=args, returncode=returncode, stdout=stdout, stderr=stderr) @property def cmd(self): @@ -93,25 +81,27 @@ class CalledProcessError(subprocess.CalledProcessError): returncode: The exit code of the process. cmd: The command that triggered this exception. msg: Short explanation of the error. - exception: The underlying Exception if available. """ - def __init__(self, returncode, cmd, stdout=None, stderr=None, msg=None, - exception=None): - if exception is not None and not isinstance(exception, Exception): - raise TypeError('exception must be an exception instance; got %r' - % (exception,)) + def __init__(self, returncode, cmd, stdout=None, stderr=None, msg=None): + super().__init__(returncode, cmd, stdout, stderr=stderr) - super(CalledProcessError, self).__init__(returncode, cmd, stdout) - # The parent class will set |output|, so delete it. + # The parent class will set |output|, so delete it. If Python ever drops + # this output/stdout compat logic, we can drop this to match. del self.output - # TODO(vapier): When we're Python 3-only, delete this assignment as the - # parent handles it for us. - self.stdout = stdout - # TODO(vapier): When we're Python 3-only, move stderr to the init above. - self.stderr = stderr + self._stdout = stdout + self.msg = msg - self.exception = exception + + @property + def stdout(self): + """Override parent's usage of .output""" + return self._stdout + + @stdout.setter + def stdout(self, value): + """Override parent's usage of .output""" + self._stdout = value @property def cmdstr(self): @@ -129,7 +119,7 @@ class CalledProcessError(subprocess.CalledProcessError): A summary string for this result. """ items = [ - 'return code: %s; command: %s' % (self.returncode, self.cmdstr), + f'return code: {self.returncode}; command: {self.cmdstr}', ] if stderr and self.stderr: items.append(self.stderr) @@ -183,20 +173,16 @@ def _kill_child_process(proc, int_timeout, kill_timeout, cmd, original_handler, # Still doesn't want to die. Too bad, so sad, time to die. proc.kill() except EnvironmentError as e: - print('Ignoring unhandled exception in _kill_child_process: %s' % e, + print(f'Ignoring unhandled exception in _kill_child_process: {e}', file=sys.stderr) - # Ensure our child process has been reaped. - kwargs = {} - if sys.version_info.major >= 3: - # ... but don't wait forever. - kwargs['timeout'] = 60 - proc.wait_lock_breaker(**kwargs) + # Ensure our child process has been reaped, but don't wait forever. + proc.wait_lock_breaker(timeout=60) if not rh.signals.relay_signal(original_handler, signum, frame): # Mock up our own, matching exit code for signaling. raise TerminateCalledProcessError( - signum << 8, cmd, msg='Received signal %i' % signum) + signum << 8, cmd, msg=f'Received signal {signum}') class _Popen(subprocess.Popen): @@ -212,7 +198,7 @@ class _Popen(subprocess.Popen): process has knowingly been waitpid'd already. """ - # pylint: disable=arguments-differ + # pylint: disable=arguments-differ,arguments-renamed def send_signal(self, signum): if self.returncode is not None: # The original implementation in Popen allows signaling whatever @@ -261,11 +247,10 @@ class _Popen(subprocess.Popen): # We use the keyword arg |input| which trips up pylint checks. -# pylint: disable=redefined-builtin,input-builtin +# pylint: disable=redefined-builtin def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, shell=False, env=None, extra_env=None, combine_stdout_stderr=False, - check=True, int_timeout=1, kill_timeout=1, capture_output=False, - close_fds=True): + check=True, int_timeout=1, kill_timeout=1, capture_output=False): """Runs a command. Args: @@ -291,7 +276,6 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, kill_timeout: If we're interrupted, how long (in seconds) should we give the invoked process to shutdown from a SIGTERM before we SIGKILL it. capture_output: Set |redirect_stdout| and |redirect_stderr| to True. - close_fds: Whether to close all fds before running |cmd|. Returns: A CompletedProcess object. @@ -313,13 +297,8 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, kill_timeout = float(kill_timeout) def _get_tempfile(): - kwargs = {} - if sys.version_info.major < 3: - kwargs['bufsize'] = 0 - else: - kwargs['buffering'] = 0 try: - return tempfile.TemporaryFile(**kwargs) + return tempfile.TemporaryFile(buffering=0) except EnvironmentError as e: if e.errno != errno.ENOENT: raise @@ -328,7 +307,7 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, # issue in this particular case since our usage gurantees deletion, # and since this is primarily triggered during hard cgroups # shutdown. - return tempfile.TemporaryFile(dir='/tmp', **kwargs) + return tempfile.TemporaryFile(dir='/tmp', buffering=0) # Modify defaults based on parameters. # Note that tempfiles must be unbuffered else attempts to read @@ -373,29 +352,44 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, env = env.copy() if env is not None else os.environ.copy() env.update(extra_env if extra_env else {}) + def ensure_text(s): + """Make sure |s| is a string if it's bytes.""" + if isinstance(s, bytes): + s = s.decode('utf-8', 'replace') + return s + result.args = cmd proc = None try: proc = _Popen(cmd, cwd=cwd, stdin=stdin, stdout=popen_stdout, stderr=popen_stderr, shell=False, env=env, - close_fds=close_fds) + close_fds=True) old_sigint = signal.getsignal(signal.SIGINT) handler = functools.partial(_kill_child_process, proc, int_timeout, kill_timeout, cmd, old_sigint) - signal.signal(signal.SIGINT, handler) + # We have to ignore ValueError in case we're run from a thread. + try: + signal.signal(signal.SIGINT, handler) + except ValueError: + old_sigint = None old_sigterm = signal.getsignal(signal.SIGTERM) handler = functools.partial(_kill_child_process, proc, int_timeout, kill_timeout, cmd, old_sigterm) - signal.signal(signal.SIGTERM, handler) + try: + signal.signal(signal.SIGTERM, handler) + except ValueError: + old_sigterm = None try: (result.stdout, result.stderr) = proc.communicate(input) finally: - signal.signal(signal.SIGINT, old_sigint) - signal.signal(signal.SIGTERM, old_sigterm) + if old_sigint is not None: + signal.signal(signal.SIGINT, old_sigint) + if old_sigterm is not None: + signal.signal(signal.SIGTERM, old_sigterm) if popen_stdout: # The linter is confused by how stdout is a file & an int. @@ -414,23 +408,30 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, result.returncode = proc.returncode if check and proc.returncode: - msg = 'cwd=%s' % cwd + msg = f'cwd={cwd}' if extra_env: - msg += ', extra env=%s' % extra_env + msg += f', extra env={extra_env}' raise CalledProcessError( - result.returncode, result.cmd, stdout=result.stdout, - stderr=result.stderr, msg=msg) + result.returncode, result.cmd, msg=msg, + stdout=ensure_text(result.stdout), + stderr=ensure_text(result.stderr)) except OSError as e: + # Avoid leaking tempfiles. + if popen_stdout is not None and not isinstance(popen_stdout, int): + popen_stdout.close() + if popen_stderr is not None and not isinstance(popen_stderr, int): + popen_stderr.close() + estr = str(e) if e.errno == errno.EACCES: estr += '; does the program need `chmod a+x`?' if not check: - result = CompletedProcess( - args=cmd, stderr=estr.encode('utf-8'), returncode=255) + result = CompletedProcess(args=cmd, stderr=estr, returncode=255) else: raise CalledProcessError( - result.returncode, result.cmd, stdout=result.stdout, - stderr=result.stderr, msg=estr, exception=e) + result.returncode, result.cmd, msg=estr, + stdout=ensure_text(result.stdout), + stderr=ensure_text(result.stderr)) from e finally: if proc is not None: # Ensure the process is dead. @@ -440,10 +441,8 @@ def run(cmd, redirect_stdout=False, redirect_stderr=False, cwd=None, input=None, None, None) # Make sure output is returned as a string rather than bytes. - if result.stdout is not None: - result.stdout = result.stdout.decode('utf-8', 'replace') - if result.stderr is not None: - result.stderr = result.stderr.decode('utf-8', 'replace') + result.stdout = ensure_text(result.stdout) + result.stderr = ensure_text(result.stderr) return result -# pylint: enable=redefined-builtin,input-builtin +# pylint: enable=redefined-builtin diff --git a/rh/utils_unittest.py b/rh/utils_unittest.py index bddb0e7..bf720a7 100755 --- a/rh/utils_unittest.py +++ b/rh/utils_unittest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2019 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +15,9 @@ """Unittests for the utils module.""" -from __future__ import print_function - import datetime import os +from pathlib import Path import sys import unittest @@ -100,34 +98,40 @@ class CalledProcessErrorTests(unittest.TestCase): def test_basic(self): """Basic test we can create a normal instance.""" rh.utils.CalledProcessError(0, ['mycmd']) - rh.utils.CalledProcessError(1, ['mycmd'], exception=Exception('bad')) def test_stringify(self): """Check stringify() handling.""" # We don't assert much so we leave flexibility in changing format. err = rh.utils.CalledProcessError(0, ['mycmd']) self.assertIn('mycmd', err.stringify()) - err = rh.utils.CalledProcessError( - 0, ['mycmd'], exception=Exception('bad')) - self.assertIn('mycmd', err.stringify()) def test_str(self): """Check str() handling.""" # We don't assert much so we leave flexibility in changing format. err = rh.utils.CalledProcessError(0, ['mycmd']) self.assertIn('mycmd', str(err)) - err = rh.utils.CalledProcessError( - 0, ['mycmd'], exception=Exception('bad')) - self.assertIn('mycmd', str(err)) def test_repr(self): """Check repr() handling.""" # We don't assert much so we leave flexibility in changing format. err = rh.utils.CalledProcessError(0, ['mycmd']) self.assertNotEqual('', repr(err)) - err = rh.utils.CalledProcessError( - 0, ['mycmd'], exception=Exception('bad')) - self.assertNotEqual('', repr(err)) + + def test_output(self): + """Make sure .output is removed and .stdout works.""" + e = rh.utils.CalledProcessError( + 0, ['true'], stdout='STDOUT', stderr='STDERR') + with self.assertRaises(AttributeError): + assert e.output is None + assert e.stdout == 'STDOUT' + assert e.stderr == 'STDERR' + + e.stdout = 'STDout' + e.stderr = 'STDerr' + with self.assertRaises(AttributeError): + assert e.output is None + assert e.stdout == 'STDout' + assert e.stderr == 'STDerr' class RunCommandTests(unittest.TestCase): @@ -155,15 +159,76 @@ class RunCommandTests(unittest.TestCase): def test_stdout_utf8(self): """Verify reading UTF-8 data works.""" ret = rh.utils.run(['printf', r'\xc3\x9f'], redirect_stdout=True) - self.assertEqual(u'ß', ret.stdout) + self.assertEqual('ß', ret.stdout) self.assertIsNone(ret.stderr) def test_stdin_utf8(self): """Verify writing UTF-8 data works.""" - ret = rh.utils.run(['cat'], redirect_stdout=True, input=u'ß') - self.assertEqual(u'ß', ret.stdout) + ret = rh.utils.run(['cat'], redirect_stdout=True, input='ß') + self.assertEqual('ß', ret.stdout) self.assertIsNone(ret.stderr) + def test_check_false(self): + """Verify handling of check=False.""" + ret = rh.utils.run(['false'], check=False) + self.assertNotEqual(0, ret.returncode) + self.assertIn('false', str(ret)) + + ret = rh.utils.run(['true'], check=False) + self.assertEqual(0, ret.returncode) + self.assertIn('true', str(ret)) + + def test_check_true(self): + """Verify handling of check=True.""" + with self.assertRaises(rh.utils.CalledProcessError) as e: + rh.utils.run(['false'], check=True) + err = e.exception + self.assertNotEqual(0, err.returncode) + self.assertIn('false', str(err)) + + ret = rh.utils.run(['true'], check=True) + self.assertEqual(0, ret.returncode) + self.assertIn('true', str(ret)) + + def test_check_false_output(self): + """Verify handling of output capturing w/check=False.""" + with self.assertRaises(rh.utils.CalledProcessError) as e: + rh.utils.run(['sh', '-c', 'echo out; echo err >&2; false'], + check=True, capture_output=True) + err = e.exception + self.assertNotEqual(0, err.returncode) + self.assertIn('false', str(err)) + + def test_check_true_missing_prog_output(self): + """Verify handling of output capturing w/missing progs.""" + with self.assertRaises(rh.utils.CalledProcessError) as e: + rh.utils.run(['./!~a/b/c/d/'], check=True, capture_output=True) + err = e.exception + self.assertNotEqual(0, err.returncode) + self.assertIn('a/b/c/d', str(err)) + + def test_check_false_missing_prog_output(self): + """Verify handling of output capturing w/missing progs.""" + ret = rh.utils.run(['./!~a/b/c/d/'], check=False, capture_output=True) + self.assertNotEqual(0, ret.returncode) + self.assertIn('a/b/c/d', str(ret)) + + def test_check_false_missing_prog_combined_output(self): + """Verify handling of combined output capturing w/missing progs.""" + with self.assertRaises(rh.utils.CalledProcessError) as e: + rh.utils.run(['./!~a/b/c/d/'], check=True, + combine_stdout_stderr=True) + err = e.exception + self.assertNotEqual(0, err.returncode) + self.assertIn('a/b/c/d', str(err)) + + def test_pathlib(self): + """Verify pathlib arguments work.""" + result = rh.utils.run(['true', Path('/')]) + # Verify stringify behavior. + str(result) + self.assertEqual(result.cmdstr, 'true /') + if __name__ == '__main__': unittest.main() diff --git a/tools/android_test_mapping_format.py b/tools/android_test_mapping_format.py index 47e09c5..7780859 100755 --- a/tools/android_test_mapping_format.py +++ b/tools/android_test_mapping_format.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2018 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Validate TEST_MAPPING files in Android source code. +"""Validates TEST_MAPPING files in Android source code. The goal of this script is to validate the format of TEST_MAPPING files: 1. It must be a valid json file. @@ -23,13 +22,12 @@ The goal of this script is to validate the format of TEST_MAPPING files: import TEST_MAPPING files. """ -from __future__ import print_function - import argparse import json import os import re import sys +from typing import Any, Dict _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: @@ -41,14 +39,16 @@ del _path # pylint: disable=wrong-import-position import rh.git -IMPORTS = 'imports' -NAME = 'name' -OPTIONS = 'options' -PATH = 'path' -HOST = 'host' -PREFERRED_TARGETS = 'preferred_targets' -FILE_PATTERNS = 'file_patterns' -TEST_MAPPING_URL = ( +_IMPORTS = 'imports' +_NAME = 'name' +_OPTIONS = 'options' +_PATH = 'path' +_HOST = 'host' +_PREFERRED_TARGETS = 'preferred_targets' +_FILE_PATTERNS = 'file_patterns' +_INVALID_IMPORT_CONFIG = 'Invalid import config in TEST_MAPPING file' +_INVALID_TEST_CONFIG = 'Invalid test config in TEST_MAPPING file' +_TEST_MAPPING_URL = ( 'https://source.android.com/compatibility/tests/development/' 'test-mapping') @@ -56,13 +56,6 @@ TEST_MAPPING_URL = ( _COMMENTS_RE = re.compile(r'^\s*//') -if sys.version_info.major < 3: - # pylint: disable=basestring-builtin,undefined-variable - string_types = basestring -else: - string_types = str - - class Error(Exception): """Base exception for all custom exceptions in this module.""" @@ -71,8 +64,8 @@ class InvalidTestMappingError(Error): """Exception to raise when detecting an invalid TEST_MAPPING file.""" -def filter_comments(json_data): - """Remove '//'-format comments in TEST_MAPPING file to valid format. +def _filter_comments(json_data: str) -> str: + """Removes '//'-format comments in TEST_MAPPING file to valid format. Args: json_data: TEST_MAPPING file content (as a string). @@ -80,12 +73,12 @@ def filter_comments(json_data): Returns: Valid json string without comments. """ - return ''.join('\n' if _COMMENTS_RE.match(x) else x for x in - json_data.splitlines()) + return ''.join( + '\n' if _COMMENTS_RE.match(x) else x for x in json_data.splitlines()) -def _validate_import(entry, test_mapping_file): - """Validate an import setting. +def _validate_import(entry: Dict[str, Any], test_mapping_file: str): + """Validates an import setting. Args: entry: A dictionary of an import setting. @@ -96,85 +89,84 @@ def _validate_import(entry, test_mapping_file): """ if len(entry) != 1: raise InvalidTestMappingError( - 'Invalid import config in test mapping file %s. each import can ' - 'only have one `path` setting. Failed entry: %s' % - (test_mapping_file, entry)) - if list(entry.keys())[0] != PATH: + f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Each import can ' + f'only have one `path` setting. Failed entry: {entry}') + if _PATH not in entry: raise InvalidTestMappingError( - 'Invalid import config in test mapping file %s. import can only ' - 'have one `path` setting. Failed entry: %s' % - (test_mapping_file, entry)) + f'{_INVALID_IMPORT_CONFIG} {test_mapping_file}. Import can ' + f'only have one `path` setting. Failed entry: {entry}') -def _validate_test(test, test_mapping_file): - """Validate a test declaration. +def _validate_test(test: Dict[str, Any], test_mapping_file: str) -> bool: + """Returns whether a test declaration is valid. Args: - entry: A dictionary of a test declaration. + test: A dictionary of a test declaration. test_mapping_file: Path to the TEST_MAPPING file to be validated. Raises: InvalidTestMappingError: if the a test declaration is invalid. """ - if NAME not in test: + if _NAME not in test: raise InvalidTestMappingError( - 'Invalid test config in test mapping file %s. test config must ' - 'a `name` setting. Failed test config: %s' % - (test_mapping_file, test)) - if not isinstance(test.get(HOST, False), bool): - raise InvalidTestMappingError( - 'Invalid test config in test mapping file %s. `host` setting in ' - 'test config can only have boolean value of `true` or `false`. ' - 'Failed test config: %s' % (test_mapping_file, test)) - preferred_targets = test.get(PREFERRED_TARGETS, []) - if (not isinstance(preferred_targets, list) or - any(not isinstance(t, string_types) for t in preferred_targets)): - raise InvalidTestMappingError( - 'Invalid test config in test mapping file %s. `preferred_targets` ' - 'setting in test config can only be a list of strings. Failed test ' - 'config: %s' % (test_mapping_file, test)) - file_patterns = test.get(FILE_PATTERNS, []) - if (not isinstance(file_patterns, list) or - any(not isinstance(p, string_types) for p in file_patterns)): + + f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Test config must ' + f'have a `name` setting. Failed test config: {test}') + + if not isinstance(test.get(_HOST, False), bool): raise InvalidTestMappingError( - 'Invalid test config in test mapping file %s. `file_patterns` ' - 'setting in test config can only be a list of strings. Failed test ' - 'config: %s' % (test_mapping_file, test)) - for option in test.get(OPTIONS, []): + f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `host` setting in ' + f'test config can only have boolean value of `true` or `false`. ' + f'Failed test config: {test}') + + for key in (_PREFERRED_TARGETS, _FILE_PATTERNS): + value = test.get(key, []) + if (not isinstance(value, list) or + any(not isinstance(t, str) for t in value)): + raise InvalidTestMappingError( + f'{_INVALID_TEST_CONFIG} {test_mapping_file}. `{key}` setting ' + f'in test config can only be a list of strings. ' + f'Failed test config: {test}') + + for option in test.get(_OPTIONS, []): + if not isinstance(option, dict): + raise InvalidTestMappingError( + f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Option setting ' + f'in test config can only be a dictionary of key-val setting. ' + f'Failed entry: {option}') if len(option) != 1: raise InvalidTestMappingError( - 'Invalid option setting in test mapping file %s. each option ' - 'setting can only have one key-val setting. Failed entry: %s' % - (test_mapping_file, option)) + f'{_INVALID_TEST_CONFIG} {test_mapping_file}. Each option ' + f'setting can only have one key-val setting. ' + f'Failed entry: {option}') -def _load_file(test_mapping_file): - """Load a TEST_MAPPING file as a json file.""" +def process_file(test_mapping_file: str): + """Validates a TEST_MAPPING file content.""" try: - return json.loads(filter_comments(test_mapping_file)) - except ValueError as e: + test_mapping_data = json.loads(_filter_comments(test_mapping_file)) + except ValueError as exception: # The file is not a valid JSON file. print( - 'Failed to parse JSON file %s, error: %s' % (test_mapping_file, e), + f'Invalid JSON data in TEST_MAPPING file ' + f'Failed to parse JSON data: {test_mapping_file}, ' + f'error: {exception}', file=sys.stderr) raise - -def process_file(test_mapping_file): - """Validate a TEST_MAPPING file.""" - test_mapping = _load_file(test_mapping_file) - # Validate imports. - for import_entry in test_mapping.get(IMPORTS, []): - _validate_import(import_entry, test_mapping_file) - # Validate tests. - all_tests = [test for group, tests in test_mapping.items() - if group != IMPORTS for test in tests] - for test in all_tests: - _validate_test(test, test_mapping_file) + for group, value in test_mapping_data.items(): + if group == _IMPORTS: + # Validate imports. + for test in value: + _validate_import(test, test_mapping_file) + else: + # Validate tests. + for test in value: + _validate_test(test, test_mapping_file) def get_parser(): - """Return a command line parser.""" + """Returns a command line parser.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--commit', type=str, help='Specify the commit to validate.') @@ -184,6 +176,7 @@ def get_parser(): def main(argv): + """Main function.""" parser = get_parser() opts = parser.parse_args(argv) try: @@ -191,12 +184,13 @@ def main(argv): if opts.commit: json_data = rh.git.get_file_content(opts.commit, filename) else: - with open(os.path.join(opts.project_dir, filename)) as f: - json_data = f.read() + with open(os.path.join(opts.project_dir, filename), + encoding='utf-8') as file: + json_data = file.read() process_file(json_data) except: - print('Visit %s for details about the format of TEST_MAPPING ' - 'file.' % TEST_MAPPING_URL, file=sys.stderr) + print(f'Visit {_TEST_MAPPING_URL} for details about the format of ' + 'TEST_MAPPING file.', file=sys.stderr) raise diff --git a/tools/android_test_mapping_format_unittest.py b/tools/android_test_mapping_format_unittest.py index 9191a25..cf3c3ca 100755 --- a/tools/android_test_mapping_format_unittest.py +++ b/tools/android_test_mapping_format_unittest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2018 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Unittests for android_test_mapping_format.""" + import os import shutil import tempfile @@ -22,7 +23,7 @@ import unittest import android_test_mapping_format -VALID_TEST_MAPPING = r""" +_VALID_TEST_MAPPING = r""" { "presubmit": [ { @@ -53,11 +54,11 @@ VALID_TEST_MAPPING = r""" } """ -BAD_JSON = """ +_BAD_JSON = """ {wrong format} """ -BAD_TEST_WRONG_KEY = """ +_BAD_TEST_WRONG_KEY = """ { "presubmit": [ { @@ -67,7 +68,7 @@ BAD_TEST_WRONG_KEY = """ } """ -BAD_TEST_WRONG_HOST_VALUE = """ +_BAD_TEST_WRONG_HOST_VALUE = """ { "presubmit": [ { @@ -79,7 +80,7 @@ BAD_TEST_WRONG_HOST_VALUE = """ """ -BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST = """ +_BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST = """ { "presubmit": [ { @@ -90,7 +91,7 @@ BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST = """ } """ -BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE = """ +_BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE = """ { "presubmit": [ { @@ -101,7 +102,7 @@ BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE = """ } """ -BAD_TEST_WRONG_OPTION = """ +_BAD_TEST_WRONG_OPTION = """ { "presubmit": [ { @@ -117,7 +118,7 @@ BAD_TEST_WRONG_OPTION = """ } """ -BAD_IMPORT_WRONG_KEY = """ +_BAD_IMPORT_WRONG_KEY = """ { "imports": [ { @@ -127,7 +128,7 @@ BAD_IMPORT_WRONG_KEY = """ } """ -BAD_IMPORT_WRONG_IMPORT_VALUE = """ +_BAD_IMPORT_WRONG_IMPORT_VALUE = """ { "imports": [ { @@ -138,7 +139,7 @@ BAD_IMPORT_WRONG_IMPORT_VALUE = """ } """ -BAD_FILE_PATTERNS = """ +_BAD_FILE_PATTERNS = """ { "presubmit": [ { @@ -149,7 +150,7 @@ BAD_FILE_PATTERNS = """ } """ -TEST_MAPPING_WITH_SUPPORTED_COMMENTS = r""" +_TEST_MAPPING_WITH_SUPPORTED_COMMENTS = r""" // supported comment { // supported comment!@#$%^&*()_ @@ -172,7 +173,7 @@ TEST_MAPPING_WITH_SUPPORTED_COMMENTS = r""" } """ -TEST_MAPPING_WITH_NON_SUPPORTED_COMMENTS = """ +_TEST_MAPPING_WITH_NON_SUPPORTED_COMMENTS = """ { #non-supported comments // supported comments "presubmit": [#non-supported comments @@ -197,112 +198,112 @@ class AndroidTestMappingFormatTests(unittest.TestCase): def test_valid_test_mapping(self): """Verify that the check doesn't raise any error for valid test mapping. """ - with open(self.test_mapping_file, 'w') as f: - f.write(VALID_TEST_MAPPING) - with open(self.test_mapping_file, 'r') as f: - android_test_mapping_format.process_file(f.read()) + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_VALID_TEST_MAPPING) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: + android_test_mapping_format.process_file(file.read()) def test_invalid_test_mapping_bad_json(self): """Verify that TEST_MAPPING file with bad json can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_JSON) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_JSON) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( ValueError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_test_key(self): """Verify that test config using wrong key can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_TEST_WRONG_KEY) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_TEST_WRONG_KEY) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_test_value(self): """Verify that test config using wrong host value can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_TEST_WRONG_HOST_VALUE) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_TEST_WRONG_HOST_VALUE) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_preferred_targets_value(self): """Verify invalid preferred_targets are rejected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_NONE_LIST) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE) - with open(self.test_mapping_file, 'r') as f: + file.read()) + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_TEST_WRONG_PREFERRED_TARGETS_VALUE_WRONG_TYPE) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_test_option(self): """Verify that test config using wrong option can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_TEST_WRONG_OPTION) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_TEST_WRONG_OPTION) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_import_key(self): """Verify that import setting using wrong key can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_IMPORT_WRONG_KEY) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_IMPORT_WRONG_KEY) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_wrong_import_value(self): """Verify that import setting using wrong value can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_IMPORT_WRONG_IMPORT_VALUE) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_IMPORT_WRONG_IMPORT_VALUE) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_invalid_test_mapping_file_patterns_value(self): """Verify that file_patterns using wrong value can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(BAD_FILE_PATTERNS) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_BAD_FILE_PATTERNS) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( android_test_mapping_format.InvalidTestMappingError, android_test_mapping_format.process_file, - f.read()) + file.read()) def test_valid_test_mapping_file_with_supported_comments(self): """Verify that '//'-format comment can be filtered.""" - with open(self.test_mapping_file, 'w') as f: - f.write(TEST_MAPPING_WITH_SUPPORTED_COMMENTS) - with open(self.test_mapping_file, 'r') as f: - android_test_mapping_format.process_file(f.read()) + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_TEST_MAPPING_WITH_SUPPORTED_COMMENTS) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: + android_test_mapping_format.process_file(file.read()) def test_valid_test_mapping_file_with_non_supported_comments(self): """Verify that non-supported comment can be detected.""" - with open(self.test_mapping_file, 'w') as f: - f.write(TEST_MAPPING_WITH_NON_SUPPORTED_COMMENTS) - with open(self.test_mapping_file, 'r') as f: + with open(self.test_mapping_file, 'w', encoding='utf-8') as file: + file.write(_TEST_MAPPING_WITH_NON_SUPPORTED_COMMENTS) + with open(self.test_mapping_file, 'r', encoding='utf-8') as file: self.assertRaises( ValueError, android_test_mapping_format.process_file, - f.read()) + file.read()) if __name__ == '__main__': diff --git a/tools/checkpatch.pl b/tools/checkpatch.pl index 2d42eb9..b01c36a 100755 --- a/tools/checkpatch.pl +++ b/tools/checkpatch.pl @@ -1,9 +1,11 @@ #!/usr/bin/env perl +# SPDX-License-Identifier: GPL-2.0 +# # (c) 2001, Dave Jones. (the file handling bit) # (c) 2005, Joel Schopp <jschopp@austin.ibm.com> (the ugly bit) # (c) 2007,2008, Andy Whitcroft <apw@uk.ibm.com> (new conditions, test suite) # (c) 2008-2010 Andy Whitcroft <apw@canonical.com> -# Licensed under the terms of the GNU GPL License version 2 +# (c) 2010-2018 Joe Perches <joe@perches.com> use strict; use warnings; @@ -11,6 +13,7 @@ use POSIX; use File::Basename; use Cwd 'abs_path'; use Term::ANSIColor qw(:constants); +use Encode qw(decode encode); my $P = $0; my $D = dirname(abs_path($P)); @@ -20,6 +23,9 @@ my $V = '0.32'; use Getopt::Long qw(:config no_auto_abbrev); my $quiet = 0; +my $verbose = 0; +my %verbose_messages = (); +my %verbose_emitted = (); my $tree = 1; my $chk_signoff = 1; my $chk_patch = 1; @@ -40,6 +46,8 @@ my $list_types = 0; my $fix = 0; my $fix_inplace = 0; my $root; +my $gitroot = $ENV{'GIT_DIR'}; +$gitroot = ".git" if !defined($gitroot); my %debug; my %camelcase = (); my %use_type = (); @@ -48,17 +56,23 @@ my %ignore_type = (); my @ignore = (); my $help = 0; my $configuration_file = ".checkpatch.conf"; -my $max_line_length = 80; +my $max_line_length = 100; my $ignore_perl_version = 0; my $minimum_perl_version = 5.10.0; my $min_conf_desc_length = 4; my $spelling_file = "$D/spelling.txt"; my $codespell = 0; my $codespellfile = "/usr/share/codespell/dictionary.txt"; +my $user_codespellfile = ""; my $conststructsfile = "$D/const_structs.checkpatch"; -my $typedefsfile = ""; +my $docsfile = "$D/../Documentation/dev-tools/checkpatch.rst"; +my $typedefsfile; my $color = "auto"; -my $allow_c99_comments = 1; +my $allow_c99_comments = 1; # Can be overridden by --ignore C99_COMMENT_TOLERANCE +# git output parsing needs US English output, so first set backtick child process LANGUAGE +my $git_command ='export LANGUAGE=en_US.UTF-8; git'; +my $tabsize = 8; +my ${CONFIG_} = "CONFIG_"; sub help { my ($exitcode) = @_; @@ -69,6 +83,7 @@ Version: $V Options: -q, --quiet quiet + -v, --verbose verbose mode --no-tree run without a kernel tree --no-signoff do not check for 'Signed-off-by' line --patch treat FILE as patchfile (default) @@ -91,8 +106,11 @@ Options: --types TYPE(,TYPE2...) show only these comma separated message types --ignore TYPE(,TYPE2...) ignore various comma separated message types --show-types show the specific message type in the output - --max-line-length=n set the maximum line length, if exceeded, warn + --max-line-length=n set the maximum line length, (default $max_line_length) + if exceeded, warn on patches + requires --strict for use with --file --min-conf-desc-length=n set the min description length, if shorter, warn + --tab-size=n set the number of spaces for tab (default $tabsize) --root=PATH PATH to the kernel tree root --no-summary suppress the per-file summary --mailback only produce a report in case of warnings/errors @@ -113,11 +131,13 @@ Options: --ignore-perl-version override checking of perl version. expect runtime errors. --codespell Use the codespell dictionary for spelling/typos - (default:/usr/share/codespell/dictionary.txt) + (default:$codespellfile) --codespellfile Use this codespell dictionary --typedefsfile Read additional types from this file --color[=WHEN] Use colors 'always', 'never', or only when output is a terminal ('auto'). Default is 'auto'. + --kconfig-prefix=WORD use WORD as a prefix for Kconfig symbols (default + ${CONFIG_}) -h, --help, --version display this help and exit When FILE is - read standard input. @@ -144,15 +164,51 @@ sub list_types { my $text = <$script>; close($script); - my @types = (); + my %types = (); # Also catch when type or level is passed through a variable - for ($text =~ /(?:(?:\bCHK|\bWARN|\bERROR|&\{\$msg_level})\s*\(|\$msg_type\s*=)\s*"([^"]+)"/g) { - push (@types, $_); + while ($text =~ /(?:(\bCHK|\bWARN|\bERROR|&\{\$msg_level})\s*\(|\$msg_type\s*=)\s*"([^"]+)"/g) { + if (defined($1)) { + if (exists($types{$2})) { + $types{$2} .= ",$1" if ($types{$2} ne $1); + } else { + $types{$2} = $1; + } + } else { + $types{$2} = "UNDETERMINED"; + } } - @types = sort(uniq(@types)); + print("#\tMessage type\n\n"); - foreach my $type (@types) { + if ($color) { + print(" ( Color coding: "); + print(RED . "ERROR" . RESET); + print(" | "); + print(YELLOW . "WARNING" . RESET); + print(" | "); + print(GREEN . "CHECK" . RESET); + print(" | "); + print("Multiple levels / Undetermined"); + print(" )\n\n"); + } + + foreach my $type (sort keys %types) { + my $orig_type = $type; + if ($color) { + my $level = $types{$type}; + if ($level eq "ERROR") { + $type = RED . $type . RESET; + } elsif ($level eq "WARN") { + $type = YELLOW . $type . RESET; + } elsif ($level eq "CHK") { + $type = GREEN . $type . RESET; + } + } print(++$count . "\t" . $type . "\n"); + if ($verbose && exists($verbose_messages{$orig_type})) { + my $message = $verbose_messages{$orig_type}; + $message =~ s/\n/\n\t/g; + print("\t" . $message . "\n\n"); + } } exit($exitcode); @@ -184,6 +240,46 @@ if (-f $conf) { unshift(@ARGV, @conf_args) if @conf_args; } +sub load_docs { + open(my $docs, '<', "$docsfile") + or warn "$P: Can't read the documentation file $docsfile $!\n"; + + my $type = ''; + my $desc = ''; + my $in_desc = 0; + + while (<$docs>) { + chomp; + my $line = $_; + $line =~ s/\s+$//; + + if ($line =~ /^\s*\*\*(.+)\*\*$/) { + if ($desc ne '') { + $verbose_messages{$type} = trim($desc); + } + $type = $1; + $desc = ''; + $in_desc = 1; + } elsif ($in_desc) { + if ($line =~ /^(?:\s{4,}|$)/) { + $line =~ s/^\s{4}//; + $desc .= $line; + $desc .= "\n"; + } else { + $verbose_messages{$type} = trim($desc); + $type = ''; + $desc = ''; + $in_desc = 0; + } + } + } + + if ($desc ne '') { + $verbose_messages{$type} = trim($desc); + } + close($docs); +} + # Perl's Getopt::Long allows options to take optional arguments after a space. # Prevent --color by itself from consuming other arguments foreach (@ARGV) { @@ -194,6 +290,7 @@ foreach (@ARGV) { GetOptions( 'q|quiet+' => \$quiet, + 'v|verbose!' => \$verbose, 'tree!' => \$tree, 'signoff!' => \$chk_signoff, 'patch!' => \$chk_patch, @@ -210,6 +307,7 @@ GetOptions( 'list-types!' => \$list_types, 'max-line-length=i' => \$max_line_length, 'min-conf-desc-length=i' => \$min_conf_desc_length, + 'tab-size=i' => \$tabsize, 'root=s' => \$root, 'summary!' => \$summary, 'mailback!' => \$mailback, @@ -220,17 +318,57 @@ GetOptions( 'debug=s' => \%debug, 'test-only=s' => \$tst_only, 'codespell!' => \$codespell, - 'codespellfile=s' => \$codespellfile, + 'codespellfile=s' => \$user_codespellfile, 'typedefsfile=s' => \$typedefsfile, 'color=s' => \$color, 'no-color' => \$color, #keep old behaviors of -nocolor 'nocolor' => \$color, #keep old behaviors of -nocolor + 'kconfig-prefix=s' => \${CONFIG_}, 'h|help' => \$help, 'version' => \$help -) or help(1); +) or $help = 2; + +if ($user_codespellfile) { + # Use the user provided codespell file unconditionally + $codespellfile = $user_codespellfile; +} elsif (!(-f $codespellfile)) { + # If /usr/share/codespell/dictionary.txt is not present, try to find it + # under codespell's install directory: <codespell_root>/data/dictionary.txt + if (($codespell || $help) && which("codespell") ne "" && which("python") ne "") { + my $python_codespell_dict = << "EOF"; + +import os.path as op +import codespell_lib +codespell_dir = op.dirname(codespell_lib.__file__) +codespell_file = op.join(codespell_dir, 'data', 'dictionary.txt') +print(codespell_file, end='') +EOF + + my $codespell_dict = `python -c "$python_codespell_dict" 2> /dev/null`; + $codespellfile = $codespell_dict if (-f $codespell_dict); + } +} -help(0) if ($help); +# $help is 1 if either -h, --help or --version is passed as option - exitcode: 0 +# $help is 2 if invalid option is passed - exitcode: 1 +help($help - 1) if ($help); + +die "$P: --git cannot be used with --file or --fix\n" if ($git && ($file || $fix)); +die "$P: --verbose cannot be used with --terse\n" if ($verbose && $terse); + +if ($color =~ /^[01]$/) { + $color = !$color; +} elsif ($color =~ /^always$/i) { + $color = 1; +} elsif ($color =~ /^never$/i) { + $color = 0; +} elsif ($color =~ /^auto$/i) { + $color = (-t STDOUT); +} else { + die "$P: Invalid color mode: $color\n"; +} +load_docs() if ($verbose); list_types(0) if ($list_types); $fix = 1 if ($fix_inplace); @@ -238,11 +376,11 @@ $check_orig = $check; my $exit = 0; +my $perl_version_ok = 1; if ($^V && $^V lt $minimum_perl_version) { + $perl_version_ok = 0; printf "$P: requires at least perl version %vd\n", $minimum_perl_version; - if (!$ignore_perl_version) { - exit(1); - } + exit(1) if (!$ignore_perl_version); } #if no filenames are given, push '-' to read patch from stdin @@ -250,17 +388,8 @@ if ($#ARGV < 0) { push(@ARGV, '-'); } -if ($color =~ /^[01]$/) { - $color = !$color; -} elsif ($color =~ /^always$/i) { - $color = 1; -} elsif ($color =~ /^never$/i) { - $color = 0; -} elsif ($color =~ /^auto$/i) { - $color = (-t STDOUT); -} else { - die "Invalid color mode: $color\n"; -} +# skip TAB size 1 to avoid additional checks on $tabsize - 1 +die "$P: Invalid TAB size: $tabsize\n" if ($tabsize < 2); sub hash_save_array_words { my ($hashRef, $arrayRef) = @_; @@ -344,9 +473,10 @@ our $Sparse = qr{ __force| __iomem| __must_check| - __init_refok| __kprobes| __ref| + __refconst| + __refdata| __rcu| __private }x; @@ -360,6 +490,7 @@ our $InitAttribute = qr{$InitAttributeData|$InitAttributeConst|$InitAttributeIni # We need \b after 'init' otherwise 'initconst' will cause a false positive in a check our $Attribute = qr{ const| + volatile| __percpu| __nocast| __safe| @@ -376,12 +507,14 @@ our $Attribute = qr{ __noclone| __deprecated| __read_mostly| + __ro_after_init| __kprobes| $InitAttribute| ____cacheline_aligned| ____cacheline_aligned_in_smp| ____cacheline_internodealigned_in_smp| - __weak + __weak| + __alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) }x; our $Modifier; our $Inline = qr{inline|__always_inline|noinline|__inline|__inline__}; @@ -393,7 +526,7 @@ our $Binary = qr{(?i)0b[01]+$Int_type?}; our $Hex = qr{(?i)0x[0-9a-f]+$Int_type?}; our $Int = qr{[0-9]+$Int_type?}; our $Octal = qr{0[0-7]+$Int_type?}; -our $String = qr{"[X\t]*"}; +our $String = qr{(?:\b[Lu])?"[X\t]*"}; our $Float_hex = qr{(?i)0x[0-9a-f]+p-?[0-9]+[fl]?}; our $Float_dec = qr{(?i)(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)(?:e-?[0-9]+)?[fl]?}; our $Float_int = qr{(?i)[0-9]+e-?[0-9]+[fl]?}; @@ -461,8 +594,19 @@ our $logFunctions = qr{(?x: seq_vprintf|seq_printf|seq_puts )}; +our $allocFunctions = qr{(?x: + (?:(?:devm_)? + (?:kv|k|v)[czm]alloc(?:_array)?(?:_node)? | + kstrdup(?:_const)? | + kmemdup(?:_nul)?) | + (?:\w+)?alloc_skb(?:_ip_align)? | + # dev_alloc_skb/netdev_alloc_skb, et al + dma_alloc_coherent +)}; + our $signature_tags = qr{(?xi: Signed-off-by:| + Co-developed-by:| Acked-by:| Tested-by:| Reviewed-by:| @@ -472,6 +616,88 @@ our $signature_tags = qr{(?xi: Cc: )}; +our $tracing_logging_tags = qr{(?xi: + [=-]*> | + <[=-]* | + \[ | + \] | + start | + called | + entered | + entry | + enter | + in | + inside | + here | + begin | + exit | + end | + done | + leave | + completed | + out | + return | + [\.\!:\s]* +)}; + +sub edit_distance_min { + my (@arr) = @_; + my $len = scalar @arr; + if ((scalar @arr) < 1) { + # if underflow, return + return; + } + my $min = $arr[0]; + for my $i (0 .. ($len-1)) { + if ($arr[$i] < $min) { + $min = $arr[$i]; + } + } + return $min; +} + +sub get_edit_distance { + my ($str1, $str2) = @_; + $str1 = lc($str1); + $str2 = lc($str2); + $str1 =~ s/-//g; + $str2 =~ s/-//g; + my $len1 = length($str1); + my $len2 = length($str2); + # two dimensional array storing minimum edit distance + my @distance; + for my $i (0 .. $len1) { + for my $j (0 .. $len2) { + if ($i == 0) { + $distance[$i][$j] = $j; + } elsif ($j == 0) { + $distance[$i][$j] = $i; + } elsif (substr($str1, $i-1, 1) eq substr($str2, $j-1, 1)) { + $distance[$i][$j] = $distance[$i - 1][$j - 1]; + } else { + my $dist1 = $distance[$i][$j - 1]; #insert distance + my $dist2 = $distance[$i - 1][$j]; # remove + my $dist3 = $distance[$i - 1][$j - 1]; #replace + $distance[$i][$j] = 1 + edit_distance_min($dist1, $dist2, $dist3); + } + } + } + return $distance[$len1][$len2]; +} + +sub find_standard_signature { + my ($sign_off) = @_; + my @standard_signature_tags = ( + 'Signed-off-by:', 'Co-developed-by:', 'Acked-by:', 'Tested-by:', + 'Reviewed-by:', 'Reported-by:', 'Suggested-by:' + ); + foreach my $signature (@standard_signature_tags) { + return $signature if (get_edit_distance($sign_off, $signature) <= 2); + } + + return ""; +} + our @typeListMisordered = ( qr{char\s+(?:un)?signed}, qr{int\s+(?:(?:un)?signed\s+)?short\s}, @@ -560,6 +786,8 @@ our @mode_permission_funcs = ( ["__ATTR", 2], ); +my $word_pattern = '\b[A-Z]?[a-z]{2,}\b'; + #Create a search pattern for all these functions to speed up a loop below our $mode_perms_search = ""; foreach my $entry (@mode_permission_funcs) { @@ -568,6 +796,27 @@ foreach my $entry (@mode_permission_funcs) { } $mode_perms_search = "(?:${mode_perms_search})"; +our %deprecated_apis = ( + "synchronize_rcu_bh" => "synchronize_rcu", + "synchronize_rcu_bh_expedited" => "synchronize_rcu_expedited", + "call_rcu_bh" => "call_rcu", + "rcu_barrier_bh" => "rcu_barrier", + "synchronize_sched" => "synchronize_rcu", + "synchronize_sched_expedited" => "synchronize_rcu_expedited", + "call_rcu_sched" => "call_rcu", + "rcu_barrier_sched" => "rcu_barrier", + "get_state_synchronize_sched" => "get_state_synchronize_rcu", + "cond_synchronize_sched" => "cond_synchronize_rcu", +); + +#Create a search pattern for all these strings to speed up a loop below +our $deprecated_apis_search = ""; +foreach my $entry (keys %deprecated_apis) { + $deprecated_apis_search .= '|' if ($deprecated_apis_search ne ""); + $deprecated_apis_search .= $entry; +} +$deprecated_apis_search = "(?:${deprecated_apis_search})"; + our $mode_perms_world_writable = qr{ S_IWUGO | S_IWOTH | @@ -707,7 +956,7 @@ sub read_words { next; } - $$wordsRef .= '|' if ($$wordsRef ne ""); + $$wordsRef .= '|' if (defined $$wordsRef); $$wordsRef .= $line; } close($file); @@ -717,16 +966,18 @@ sub read_words { return 0; } -my $const_structs = ""; -read_words(\$const_structs, $conststructsfile) - or warn "No structs that should be const will be found - file '$conststructsfile': $!\n"; +my $const_structs; +if (show_type("CONST_STRUCT")) { + read_words(\$const_structs, $conststructsfile) + or warn "No structs that should be const will be found - file '$conststructsfile': $!\n"; +} -my $typeOtherTypedefs = ""; -if (length($typedefsfile)) { +if (defined($typedefsfile)) { + my $typeOtherTypedefs; read_words(\$typeOtherTypedefs, $typedefsfile) or warn "No additional types will be considered - file '$typedefsfile': $!\n"; + $typeTypedefs .= '|' . $typeOtherTypedefs if (defined $typeOtherTypedefs); } -$typeTypedefs .= '|' . $typeOtherTypedefs if ($typeOtherTypedefs ne ""); sub build_types { my $mods = "(?x: \n" . join("|\n ", (@modifierList, @modifierListFile)) . "\n)"; @@ -765,12 +1016,12 @@ sub build_types { }x; $Type = qr{ $NonptrType - (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+)? + (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+){0,4} (?:\s+$Inline|\s+$Modifier)* }x; $TypeMisordered = qr{ $NonptrTypeMisordered - (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+)? + (?:(?:\s|\*|\[\])+\s*const|(?:\s|\*\s*(?:const\s*)?|\[\])+|(?:\s*\[\s*\])+){0,4} (?:\s+$Inline|\s+$Modifier)* }x; $Declare = qr{(?:$Storage\s+(?:$Inline\s+)?)?$Type}; @@ -791,10 +1042,16 @@ our $FuncArg = qr{$Typecast{0,1}($LvalOrFunc|$Constant|$String)}; our $declaration_macros = qr{(?x: (?:$Storage\s+)?(?:[A-Z_][A-Z0-9]*_){0,2}(?:DEFINE|DECLARE)(?:_[A-Z0-9]+){1,6}\s*\(| (?:$Storage\s+)?[HLP]?LIST_HEAD\s*\(| - (?:$Storage\s+)?${Type}\s+uninitialized_var\s*\(| (?:SKCIPHER_REQUEST|SHASH_DESC|AHASH_REQUEST)_ON_STACK\s*\( )}; +our %allow_repeated_words = ( + add => '', + added => '', + bad => '', + be => '', +); + sub deparenthesize { my ($string) = @_; return "" if (!defined($string)); @@ -835,14 +1092,29 @@ sub seed_camelcase_file { } } +our %maintained_status = (); + sub is_maintained_obsolete { my ($filename) = @_; return 0 if (!$tree || !(-e "$root/scripts/get_maintainer.pl")); - my $status = `perl $root/scripts/get_maintainer.pl --status --nom --nol --nogit --nogit-fallback -f $filename 2>&1`; + if (!exists($maintained_status{$filename})) { + $maintained_status{$filename} = `perl $root/scripts/get_maintainer.pl --status --nom --nol --nogit --nogit-fallback -f $filename 2>&1`; + } + + return $maintained_status{$filename} =~ /obsolete/i; +} + +sub is_SPDX_License_valid { + my ($license) = @_; + + return 1 if (!$tree || which("python3") eq "" || !(-x "$root/scripts/spdxcheck.py") || !(-e "$gitroot")); - return $status =~ /obsolete/i; + my $root_path = abs_path($root); + my $status = `cd "$root_path"; echo "$license" | scripts/spdxcheck.py -`; + return 0 if ($status ne ""); + return 1; } my $camelcase_seeded = 0; @@ -855,8 +1127,8 @@ sub seed_camelcase_includes { $camelcase_seeded = 1; - if (-e ".git") { - my $git_last_include_commit = `git log --no-merges --pretty=format:"%h%n" -1 -- include`; + if (-e "$gitroot") { + my $git_last_include_commit = `${git_command} log --no-merges --pretty=format:"%h%n" -1 -- include`; chomp $git_last_include_commit; $camelcase_cache = ".checkpatch-camelcase.git.$git_last_include_commit"; } else { @@ -883,8 +1155,8 @@ sub seed_camelcase_includes { return; } - if (-e ".git") { - $files = `git ls-files "include/*.h"`; + if (-e "$gitroot") { + $files = `${git_command} ls-files "include/*.h"`; @include_files = split('\n', $files); } @@ -903,18 +1175,28 @@ sub seed_camelcase_includes { } } +sub git_is_single_file { + my ($filename) = @_; + + return 0 if ((which("git") eq "") || !(-e "$gitroot")); + + my $output = `${git_command} ls-files -- $filename 2>/dev/null`; + my $count = $output =~ tr/\n//; + return $count eq 1 && $output =~ m{^${filename}$}; +} + sub git_commit_info { my ($commit, $id, $desc) = @_; - return ($id, $desc) if ((which("git") eq "") || !(-e ".git")); + return ($id, $desc) if ((which("git") eq "") || !(-e "$gitroot")); - my $output = `git log --no-color --format='%H %s' -1 $commit 2>&1`; + my $output = `${git_command} log --no-color --format='%H %s' -1 $commit 2>&1`; $output =~ s/^\s*//gm; my @lines = split("\n", $output); return ($id, $desc) if ($#lines < 0); - if ($lines[0] =~ /^error: short SHA1 $commit is ambiguous\./) { + if ($lines[0] =~ /^error: short SHA1 $commit is ambiguous/) { # Maybe one day convert this block of bash into something that returns # all matching commit ids, but it's very slow... # @@ -924,7 +1206,8 @@ sub git_commit_info { # git log --format='%H %s' -1 $line | # echo "commit $(cut -c 1-12,41-)" # done - } elsif ($lines[0] =~ /^fatal: ambiguous argument '$commit': unknown revision or path not in the working tree\./) { + } elsif ($lines[0] =~ /^fatal: ambiguous argument '$commit': unknown revision or path not in the working tree\./ || + $lines[0] =~ /^fatal: bad object $commit/) { $id = undef; } else { $id = substr($lines[0], 0, 12); @@ -945,7 +1228,7 @@ my $fixlinenr = -1; # If input is git commits, extract all commits from the commit expressions. # For example, HEAD-3 means we need check 'HEAD, HEAD~1, HEAD~2'. -die "$P: No git repository found\n" if ($git && !-e ".git"); +die "$P: No git repository found\n" if ($git && !-e "$gitroot"); if ($git) { my @commits = (); @@ -958,7 +1241,7 @@ if ($git) { } else { $git_range = "-1 $commit_expr"; } - my $lines = `git log --no-color --no-merges --pretty=format:'%H %s' $git_range`; + my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%H %s' $git_range`; foreach my $line (split(/\n/, $lines)) { $line =~ /^([0-9a-fA-F]{40,40}) (.*)$/; next if (!defined($1) || !defined($2)); @@ -973,8 +1256,12 @@ if ($git) { } my $vname; +$allow_c99_comments = !defined $ignore_type{"C99_COMMENT_TOLERANCE"}; for my $filename (@ARGV) { my $FILE; + my $is_git_file = git_is_single_file($filename); + my $oldfile = $file; + $file = 1 if ($is_git_file); if ($git) { open($FILE, '-|', "git format-patch -M --stdout -1 $filename") || die "$P: $filename: git format-patch failed - $!\n"; @@ -997,6 +1284,7 @@ for my $filename (@ARGV) { while (<$FILE>) { chomp; push(@rawlines, $_); + $vname = qq("$1") if ($filename eq '-' && $_ =~ m/^Subject:\s+(.+)/i); } close($FILE); @@ -1018,17 +1306,18 @@ for my $filename (@ARGV) { @modifierListFile = (); @typeListFile = (); build_types(); + $file = $oldfile if ($is_git_file); } if (!$quiet) { hash_show_words(\%use_type, "Used"); hash_show_words(\%ignore_type, "Ignored"); - if ($^V lt 5.10.0) { + if (!$perl_version_ok) { print << "EOM" NOTE: perl $^V is not modern enough to detect all possible issues. - An upgrade to at least perl v5.10.0 is suggested. + An upgrade to at least perl $minimum_perl_version is suggested. EOM } if ($exit) { @@ -1063,6 +1352,8 @@ sub parse_email { my ($formatted_email) = @_; my $name = ""; + my $quoted = ""; + my $name_comment = ""; my $address = ""; my $comment = ""; @@ -1093,42 +1384,76 @@ sub parse_email { } } - $name = trim($name); - $name =~ s/^\"|\"$//g; + # Extract comments from names excluding quoted parts + # "John D. (Doe)" - Do not extract + if ($name =~ s/\"(.+)\"//) { + $quoted = $1; + } + while ($name =~ s/\s*($balanced_parens)\s*/ /) { + $name_comment .= trim($1); + } + $name =~ s/^[ \"]+|[ \"]+$//g; + $name = trim("$quoted $name"); + $address = trim($address); $address =~ s/^\<|\>$//g; + $comment = trim($comment); if ($name =~ /[^\w \-]/i) { ##has "must quote" chars $name =~ s/(?<!\\)"/\\"/g; ##escape quotes $name = "\"$name\""; } - return ($name, $address, $comment); + return ($name, $name_comment, $address, $comment); } sub format_email { - my ($name, $address) = @_; + my ($name, $name_comment, $address, $comment) = @_; my $formatted_email; - $name = trim($name); - $name =~ s/^\"|\"$//g; + $name =~ s/^[ \"]+|[ \"]+$//g; $address = trim($address); + $address =~ s/(?:\.|\,|\")+$//; ##trailing commas, dots or quotes if ($name =~ /[^\w \-]/i) { ##has "must quote" chars $name =~ s/(?<!\\)"/\\"/g; ##escape quotes $name = "\"$name\""; } + $name_comment = trim($name_comment); + $name_comment = " $name_comment" if ($name_comment ne ""); + $comment = trim($comment); + $comment = " $comment" if ($comment ne ""); + if ("$name" eq "") { $formatted_email = "$address"; } else { - $formatted_email = "$name <$address>"; + $formatted_email = "$name$name_comment <$address>"; } - + $formatted_email .= "$comment"; return $formatted_email; } +sub reformat_email { + my ($email) = @_; + + my ($email_name, $name_comment, $email_address, $comment) = parse_email($email); + return format_email($email_name, $name_comment, $email_address, $comment); +} + +sub same_email_addresses { + my ($email1, $email2) = @_; + + my ($email1_name, $name1_comment, $email1_address, $comment1) = parse_email($email1); + my ($email2_name, $name2_comment, $email2_address, $comment2) = parse_email($email2); + + return $email1_name eq $email2_name && + $email1_address eq $email2_address && + $name1_comment eq $name2_comment && + $comment1 eq $comment2; +} + sub which { my ($bin) = @_; @@ -1162,7 +1487,7 @@ sub expand_tabs { if ($c eq "\t") { $res .= ' '; $n++; - for (; ($n % 8) != 0; $n++) { + for (; ($n % $tabsize) != 0; $n++) { $res .= ' '; } next; @@ -1591,8 +1916,16 @@ sub ctx_statement_level { sub ctx_locate_comment { my ($first_line, $end_line) = @_; + # If c99 comment on the current line, or the line before or after + my ($current_comment) = ($rawlines[$end_line - 1] =~ m@^\+.*(//.*$)@); + return $current_comment if (defined $current_comment); + ($current_comment) = ($rawlines[$end_line - 2] =~ m@^[\+ ].*(//.*$)@); + return $current_comment if (defined $current_comment); + ($current_comment) = ($rawlines[$end_line] =~ m@^[\+ ].*(//.*$)@); + return $current_comment if (defined $current_comment); + # Catch a comment on the end of the line itself. - my ($current_comment) = ($rawlines[$end_line - 1] =~ m@.*(/\*.*\*/)\s*(?:\\\s*)?$@); + ($current_comment) = ($rawlines[$end_line - 1] =~ m@.*(/\*.*\*/)\s*(?:\\\s*)?$@); return $current_comment if (defined $current_comment); # Look through the context and try and figure out if there is a @@ -1986,7 +2319,16 @@ sub report { splice(@lines, 1, 1); $output = join("\n", @lines); } - $output = (split('\n', $output))[0] . "\n" if ($terse); + + if ($terse) { + $output = (split('\n', $output))[0] . "\n"; + } + + if ($verbose && exists($verbose_messages{$type}) && + !exists($verbose_emitted{$type})) { + $output .= $verbose_messages{$type} . "\n\n"; + $verbose_emitted{$type} = 1; + } push(our @report, $output); @@ -2175,7 +2517,7 @@ sub string_find_replace { sub tabify { my ($leading) = @_; - my $source_indent = 8; + my $source_indent = $tabsize; my $max_spaces_before_tab = $source_indent - 1; my $spaces_to_tab = " " x $source_indent; @@ -2217,6 +2559,28 @@ sub pos_last_openparen { return length(expand_tabs(substr($line, 0, $last_openparen))) + 1; } +sub get_raw_comment { + my ($line, $rawline) = @_; + my $comment = ''; + + for my $i (0 .. (length($line) - 1)) { + if (substr($line, $i, 1) eq "$;") { + $comment .= substr($rawline, $i, 1); + } + } + + return $comment; +} + +sub exclude_global_initialisers { + my ($realfile) = @_; + + # Do not check for BPF programs (tools/testing/selftests/bpf/progs/*.c, samples/bpf/*_kern.c, *.bpf.c). + return $realfile =~ m@^tools/testing/selftests/bpf/progs/.*\.c$@ || + $realfile =~ m@^samples/bpf/.*_kern\.c$@ || + $realfile =~ m@/bpf/.*\.bpf\.c$@; +} + sub process { my $filename = shift; @@ -2233,16 +2597,24 @@ sub process { our $clean = 1; my $signoff = 0; + my $author = ''; + my $authorsignoff = 0; + my $author_sob = ''; my $is_patch = 0; + my $is_binding_patch = -1; my $in_header_lines = $file ? 0 : 1; my $in_commit_log = 0; #Scanning lines before patch + my $has_patch_separator = 0; #Found a --- line my $has_commit_log = 0; #Encountered lines before patch + my $commit_log_lines = 0; #Number of commit log lines my $commit_log_possible_stack_dump = 0; my $commit_log_long_line = 0; my $commit_log_has_diff = 0; my $reported_maintainer_file = 0; my $non_utf8_charset = 0; + my $last_git_commit_id_linenr = -1; + my $last_blank_line = 0; my $last_coalesced_string_linenr = -1; @@ -2293,7 +2665,7 @@ sub process { if ($rawline=~/^\+\+\+\s+(\S+)/) { $setup_docs = 0; - if ($1 =~ m@Documentation/admin-guide/kernel-parameters.rst$@) { + if ($1 =~ m@Documentation/admin-guide/kernel-parameters.txt$@) { $setup_docs = 1; } #next; @@ -2374,6 +2746,15 @@ sub process { $sline =~ s/$;/ /g; #with comments as spaces my $rawline = $rawlines[$linenr - 1]; + my $raw_comment = get_raw_comment($line, $rawline); + +# check if it's a mode change, rename or start of a patch + if (!$in_commit_log && + ($line =~ /^ mode change [0-7]+ => [0-7]+ \S+\s*$/ || + ($line =~ /^rename (?:from|to) \S+\s*$/ || + $line =~ /^diff --git a\/[\w\/\.\_\-]+ b\/\S+\s*$/))) { + $is_patch = 1; + } #extract the line range in the file after the patch is applied if (!$in_commit_log && @@ -2475,6 +2856,19 @@ sub process { $check = $check_orig; } $checklicenseline = 1; + + if ($realfile !~ /^MAINTAINERS/) { + my $last_binding_patch = $is_binding_patch; + + $is_binding_patch = () = $realfile =~ m@^(?:Documentation/devicetree/|include/dt-bindings/)@; + + if (($last_binding_patch != -1) && + ($last_binding_patch ^ $is_binding_patch)) { + WARN("DT_SPLIT_BINDING_PATCH", + "DT binding docs and includes should be a separate patch. See: Documentation/devicetree/bindings/submitting-patches.rst\n"); + } + } + next; } @@ -2486,10 +2880,22 @@ sub process { $cnt_lines++ if ($realcnt != 0); +# Verify the existence of a commit log if appropriate +# 2 is used because a $signature is counted in $commit_log_lines + if ($in_commit_log) { + if ($line !~ /^\s*$/) { + $commit_log_lines++; #could be a $signature + } + } elsif ($has_commit_log && $commit_log_lines < 2) { + WARN("COMMIT_MESSAGE", + "Missing commit description - Add an appropriate one\n"); + $commit_log_lines = 2; #warn only once + } + # Check if the commit log has what seems like a diff which can confuse patch if ($in_commit_log && !$commit_log_has_diff && - (($line =~ m@^\s+diff\b.*a/[\w/]+@ && - $line =~ m@^\s+diff\b.*a/([\w/]+)\s+b/$1\b@) || + (($line =~ m@^\s+diff\b.*a/([\w/]+)@ && + $line =~ m@^\s+diff\b.*a/[\w/]+\s+b/$1\b@) || $line =~ m@^\s*(?:\-\-\-\s+a/|\+\+\+\s+b/)@ || $line =~ m/^\s*\@\@ \-\d+,\d+ \+\d+,\d+ \@\@/)) { ERROR("DIFF_IN_COMMIT_MSG", @@ -2507,10 +2913,61 @@ sub process { } } +# Check the patch for a From: + if (decode("MIME-Header", $line) =~ /^From:\s*(.*)/) { + $author = $1; + my $curline = $linenr; + while(defined($rawlines[$curline]) && ($rawlines[$curline++] =~ /^[ \t]\s*(.*)/)) { + $author .= $1; + } + $author = encode("utf8", $author) if ($line =~ /=\?utf-8\?/i); + $author =~ s/"//g; + $author = reformat_email($author); + } + # Check the patch for a signoff: - if ($line =~ /^\s*signed-off-by:/i) { + if ($line =~ /^\s*signed-off-by:\s*(.*)/i) { $signoff++; $in_commit_log = 0; + if ($author ne '' && $authorsignoff != 1) { + if (same_email_addresses($1, $author)) { + $authorsignoff = 1; + } else { + my $ctx = $1; + my ($email_name, $email_comment, $email_address, $comment1) = parse_email($ctx); + my ($author_name, $author_comment, $author_address, $comment2) = parse_email($author); + + if (lc $email_address eq lc $author_address && $email_name eq $author_name) { + $author_sob = $ctx; + $authorsignoff = 2; + } elsif (lc $email_address eq lc $author_address) { + $author_sob = $ctx; + $authorsignoff = 3; + } elsif ($email_name eq $author_name) { + $author_sob = $ctx; + $authorsignoff = 4; + + my $address1 = $email_address; + my $address2 = $author_address; + + if ($address1 =~ /(\S+)\+\S+(\@.*)/) { + $address1 = "$1$2"; + } + if ($address2 =~ /(\S+)\+\S+(\@.*)/) { + $address2 = "$1$2"; + } + if ($address1 eq $address2) { + $authorsignoff = 5; + } + } + } + } + } + +# Check for patch separator + if ($line =~ /^---$/) { + $has_patch_separator = 1; + $in_commit_log = 0; } # Check if MAINTAINERS is being updated. If so, there's probably no need to @@ -2529,8 +2986,17 @@ sub process { my $ucfirst_sign_off = ucfirst(lc($sign_off)); if ($sign_off !~ /$signature_tags/) { - WARN("BAD_SIGN_OFF", - "Non-standard signature: $sign_off\n" . $herecurr); + my $suggested_signature = find_standard_signature($sign_off); + if ($suggested_signature eq "") { + WARN("BAD_SIGN_OFF", + "Non-standard signature: $sign_off\n" . $herecurr); + } else { + if (WARN("BAD_SIGN_OFF", + "Non-standard signature: '$sign_off' - perhaps '$suggested_signature'?\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/$sign_off/$suggested_signature/; + } + } } if (defined $space_before && $space_before ne "") { if (WARN("BAD_SIGN_OFF", @@ -2558,8 +3024,8 @@ sub process { } } - my ($email_name, $email_address, $comment) = parse_email($email); - my $suggested_email = format_email(($email_name, $email_address)); + my ($email_name, $name_comment, $email_address, $comment) = parse_email($email); + my $suggested_email = format_email(($email_name, $name_comment, $email_address, $comment)); if ($suggested_email eq "") { ERROR("BAD_SIGN_OFF", "Unrecognized email address: '$email'\n" . $herecurr); @@ -2569,11 +3035,77 @@ sub process { $dequoted =~ s/" </ </; # Don't force email to have quotes # Allow just an angle bracketed address - if ("$dequoted$comment" ne $email && - "<$email_address>$comment" ne $email && - "$suggested_email$comment" ne $email) { + if (!same_email_addresses($email, $suggested_email)) { + if (WARN("BAD_SIGN_OFF", + "email address '$email' might be better as '$suggested_email'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email\E/$suggested_email/; + } + } + + # Address part shouldn't have comments + my $stripped_address = $email_address; + $stripped_address =~ s/\([^\(\)]*\)//g; + if ($email_address ne $stripped_address) { + if (WARN("BAD_SIGN_OFF", + "address part of email should not have comments: '$email_address'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email_address\E/$stripped_address/; + } + } + + # Only one name comment should be allowed + my $comment_count = () = $name_comment =~ /\([^\)]+\)/g; + if ($comment_count > 1) { WARN("BAD_SIGN_OFF", - "email address '$email' might be better as '$suggested_email$comment'\n" . $herecurr); + "Use a single name comment in email: '$email'\n" . $herecurr); + } + + + # stable@vger.kernel.org or stable@kernel.org shouldn't + # have an email name. In addition comments should strictly + # begin with a # + if ($email =~ /^.*stable\@(?:vger\.)?kernel\.org/i) { + if (($comment ne "" && $comment !~ /^#.+/) || + ($email_name ne "")) { + my $cur_name = $email_name; + my $new_comment = $comment; + $cur_name =~ s/[a-zA-Z\s\-\"]+//g; + + # Remove brackets enclosing comment text + # and # from start of comments to get comment text + $new_comment =~ s/^\((.*)\)$/$1/; + $new_comment =~ s/^\[(.*)\]$/$1/; + $new_comment =~ s/^[\s\#]+|\s+$//g; + + $new_comment = trim("$new_comment $cur_name") if ($cur_name ne $new_comment); + $new_comment = " # $new_comment" if ($new_comment ne ""); + my $new_email = "$email_address$new_comment"; + + if (WARN("BAD_STABLE_ADDRESS_STYLE", + "Invalid email format for stable: '$email', prefer '$new_email'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email\E/$new_email/; + } + } + } elsif ($comment ne "" && $comment !~ /^(?:#.+|\(.+\))$/) { + my $new_comment = $comment; + + # Extract comment text from within brackets or + # c89 style /*...*/ comments + $new_comment =~ s/^\[(.*)\]$/$1/; + $new_comment =~ s/^\/\*(.*)\*\/$/$1/; + + $new_comment = trim($new_comment); + $new_comment =~ s/^[^\w]$//; # Single lettered comment with non word character is usually a typo + $new_comment = "($new_comment)" if ($new_comment ne ""); + my $new_email = format_email($email_name, $name_comment, $email_address, $new_comment); + + if (WARN("BAD_SIGN_OFF", + "Unexpected content after email: '$email', should be: '$new_email'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\Q$email\E/$new_email/; + } } } @@ -2587,6 +3119,24 @@ sub process { } else { $signatures{$sig_nospace} = 1; } + +# Check Co-developed-by: immediately followed by Signed-off-by: with same name and email + if ($sign_off =~ /^co-developed-by:$/i) { + if ($email eq $author) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: should not be used to attribute nominal patch author '$author'\n" . "$here\n" . $rawline); + } + if (!defined $lines[$linenr]) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: must be immediately followed by Signed-off-by:\n" . "$here\n" . $rawline); + } elsif ($rawlines[$linenr] !~ /^\s*signed-off-by:\s*(.*)/i) { + WARN("BAD_SIGN_OFF", + "Co-developed-by: must be immediately followed by Signed-off-by:\n" . "$here\n" . $rawline . "\n" .$rawlines[$linenr]); + } elsif ($1 ne $email) { + WARN("BAD_SIGN_OFF", + "Co-developed-by and Signed-off-by: name/email do not match \n" . "$here\n" . $rawline . "\n" .$rawlines[$linenr]); + } + } } # Check email subject for common tools that don't need to be mentioned @@ -2596,16 +3146,13 @@ sub process { "A patch subject line should describe the change not the tool that found it\n" . $herecurr); } -# Check for old stable address - if ($line =~ /^\s*cc:\s*.*<?\bstable\@kernel\.org\b>?.*$/i) { - ERROR("STABLE_ADDRESS", - "The 'stable' address should be 'stable\@vger.kernel.org'\n" . $herecurr); - } - -# Check for unwanted Gerrit info - if ($in_commit_log && $line =~ /^\s*change-id:/i) { - ERROR("GERRIT_CHANGE_ID", - "Remove Gerrit Change-Id's before submitting upstream.\n" . $herecurr); +# Check for Gerrit Change-Ids not in any patch context + if ($realfile eq '' && !$has_patch_separator && $line =~ /^\s*change-id:/i) { + if (ERROR("GERRIT_CHANGE_ID", + "Remove Gerrit Change-Id's before submitting upstream\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } } # Check if the commit log is in a possible stack dump @@ -2613,8 +3160,10 @@ sub process { ($line =~ /^\s*(?:WARNING:|BUG:)/ || $line =~ /^\s*\[\s*\d+\.\d{6,6}\s*\]/ || # timestamp - $line =~ /^\s*\[\<[0-9a-fA-F]{8,}\>\]/)) { - # stack dump address + $line =~ /^\s*\[\<[0-9a-fA-F]{8,}\>\]/) || + $line =~ /^(?:\s+\w+:\s+[0-9a-fA-F]+){3,3}/ || + $line =~ /^\s*\#\d+\s*\[[0-9a-fA-F]+\]\s*\w+ at [0-9a-fA-F]+/) { + # stack dump address styles $commit_log_possible_stack_dump = 1; } @@ -2623,10 +3172,10 @@ sub process { length($line) > 75 && !($line =~ /^\s*[a-zA-Z0-9_\/\.]+\s+\|\s+\d+/ || # file delta changes - $line =~ /^\s*(?:[\w\.\-]+\/)++[\w\.\-]+:/ || + $line =~ /^\s*(?:[\w\.\-\+]*\/)++[\w\.\-\+]+:/ || # filename then : - $line =~ /^\s*(?:Fixes:|Link:)/i || - # A Fixes: or Link: line + $line =~ /^\s*(?:Fixes:|Link:|$signature_tags)/i || + # A Fixes: or Link: line or signature tag line $commit_log_possible_stack_dump)) { WARN("COMMIT_LOG_LONG_LINE", "Possible unwrapped commit description (prefer a maximum 75 chars per line)\n" . $herecurr); @@ -2639,11 +3188,30 @@ sub process { $commit_log_possible_stack_dump = 0; } +# Check for lines starting with a # + if ($in_commit_log && $line =~ /^#/) { + if (WARN("COMMIT_COMMENT_SYMBOL", + "Commit log lines starting with '#' are dropped by git as comments\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^/ /; + } + } + # Check for git id commit length and improperly formed commit descriptions - if ($in_commit_log && !$commit_log_possible_stack_dump && - $line !~ /^\s*(?:Link|Patchwork|http|https|BugLink):/i && +# A correctly formed commit description is: +# commit <SHA-1 hash length 12+ chars> ("Complete commit subject") +# with the commit subject '("' prefix and '")' suffix +# This is a fairly compilicated block as it tests for what appears to be +# bare SHA-1 hash with minimum length of 5. It also avoids several types of +# possible SHA-1 matches. +# A commit match can span multiple lines so this block attempts to find a +# complete typical commit on a maximum of 3 lines + if ($perl_version_ok && + $in_commit_log && !$commit_log_possible_stack_dump && + $line !~ /^\s*(?:Link|Patchwork|http|https|BugLink|base-commit):/i && $line !~ /^This reverts commit [0-9a-f]{7,40}/ && - ($line =~ /\bcommit\s+[0-9a-f]{5,}\b/i || + (($line =~ /\bcommit\s+[0-9a-f]{5,}\b/i || + ($line =~ /\bcommit\s*$/i && defined($rawlines[$linenr]) && $rawlines[$linenr] =~ /^\s*[0-9a-f]{5,}\b/i)) || ($line =~ /(?:\s|^)[0-9a-f]{12,40}(?:[\s"'\(\[]|$)/i && $line !~ /[\<\[][0-9a-f]{12,40}[\>\]]/i && $line !~ /\bfixes:\s*[0-9a-f]{12,40}/i))) { @@ -2653,49 +3221,56 @@ sub process { my $long = 0; my $case = 1; my $space = 1; - my $hasdesc = 0; - my $hasparens = 0; my $id = '0123456789ab'; my $orig_desc = "commit description"; my $description = ""; + my $herectx = $herecurr; + my $has_parens = 0; + my $has_quotes = 0; + + my $input = $line; + if ($line =~ /(?:\bcommit\s+[0-9a-f]{5,}|\bcommit\s*$)/i) { + for (my $n = 0; $n < 2; $n++) { + if ($input =~ /\bcommit\s+[0-9a-f]{5,}\s*($balanced_parens)/i) { + $orig_desc = $1; + $has_parens = 1; + # Always strip leading/trailing parens then double quotes if existing + $orig_desc = substr($orig_desc, 1, -1); + if ($orig_desc =~ /^".*"$/) { + $orig_desc = substr($orig_desc, 1, -1); + $has_quotes = 1; + } + last; + } + last if ($#lines < $linenr + $n); + $input .= " " . trim($rawlines[$linenr + $n]); + $herectx .= "$rawlines[$linenr + $n]\n"; + } + $herectx = $herecurr if (!$has_parens); + } - if ($line =~ /\b(c)ommit\s+([0-9a-f]{5,})\b/i) { + if ($input =~ /\b(c)ommit\s+([0-9a-f]{5,})\b/i) { $init_char = $1; $orig_commit = lc($2); - } elsif ($line =~ /\b([0-9a-f]{12,40})\b/i) { + $short = 0 if ($input =~ /\bcommit\s+[0-9a-f]{12,40}/i); + $long = 1 if ($input =~ /\bcommit\s+[0-9a-f]{41,}/i); + $space = 0 if ($input =~ /\bcommit [0-9a-f]/i); + $case = 0 if ($input =~ /\b[Cc]ommit\s+[0-9a-f]{5,40}[^A-F]/); + } elsif ($input =~ /\b([0-9a-f]{12,40})\b/i) { $orig_commit = lc($1); } - $short = 0 if ($line =~ /\bcommit\s+[0-9a-f]{12,40}/i); - $long = 1 if ($line =~ /\bcommit\s+[0-9a-f]{41,}/i); - $space = 0 if ($line =~ /\bcommit [0-9a-f]/i); - $case = 0 if ($line =~ /\b[Cc]ommit\s+[0-9a-f]{5,40}[^A-F]/); - if ($line =~ /\bcommit\s+[0-9a-f]{5,}\s+\("([^"]+)"\)/i) { - $orig_desc = $1; - $hasparens = 1; - } elsif ($line =~ /\bcommit\s+[0-9a-f]{5,}\s*$/i && - defined $rawlines[$linenr] && - $rawlines[$linenr] =~ /^\s*\("([^"]+)"\)/) { - $orig_desc = $1; - $hasparens = 1; - } elsif ($line =~ /\bcommit\s+[0-9a-f]{5,}\s+\("[^"]+$/i && - defined $rawlines[$linenr] && - $rawlines[$linenr] =~ /^\s*[^"]+"\)/) { - $line =~ /\bcommit\s+[0-9a-f]{5,}\s+\("([^"]+)$/i; - $orig_desc = $1; - $rawlines[$linenr] =~ /^\s*([^"]+)"\)/; - $orig_desc .= " " . $1; - $hasparens = 1; - } - ($id, $description) = git_commit_info($orig_commit, $id, $orig_desc); if (defined($id) && - ($short || $long || $space || $case || ($orig_desc ne $description) || !$hasparens)) { + ($short || $long || $space || $case || ($orig_desc ne $description) || !$has_quotes) && + $last_git_commit_id_linenr != $linenr - 1) { ERROR("GIT_COMMIT_ID", - "Please use git commit description style 'commit <12+ chars of sha1> (\"<title line>\")' - ie: '${init_char}ommit $id (\"$description\")'\n" . $herecurr); + "Please use git commit description style 'commit <12+ chars of sha1> (\"<title line>\")' - ie: '${init_char}ommit $id (\"$description\")'\n" . $herectx); } + #don't report the next line if this line ends in commit and the sha1 hash is the next line + $last_git_commit_id_linenr = $linenr if ($line =~ /\bcommit\s*$/i); } # Check for added, moved or deleted files @@ -2710,6 +3285,14 @@ sub process { "added, moved or deleted file(s), does MAINTAINERS need updating?\n" . $herecurr); } +# Check for adding new DT bindings not in schema format + if (!$in_commit_log && + ($line =~ /^new file mode\s*\d+\s*$/) && + ($realfile =~ m@^Documentation/devicetree/bindings/.*\.txt$@)) { + WARN("DT_SCHEMA_BINDING_PATCH", + "DT bindings should be in DT schema format. See: Documentation/devicetree/bindings/writing-schema.rst\n"); + } + # Check for wrappage within a valid hunk of the file if ($realcnt != 0 && $line !~ m{^(?:\+|-| |\\ No newline|$)}) { ERROR("CORRUPTED_PATCH", @@ -2771,21 +3354,89 @@ sub process { # Check for various typo / spelling mistakes if (defined($misspellings) && ($in_commit_log || $line =~ /^(?:\+|Subject:)/i)) { - while ($rawline =~ /(?:^|[^a-z@])($misspellings)(?:\b|$|[^a-z@])/gi) { + while ($rawline =~ /(?:^|[^\w\-'`])($misspellings)(?:[^\w\-'`]|$)/gi) { my $typo = $1; + my $blank = copy_spacing($rawline); + my $ptr = substr($blank, 0, $-[1]) . "^" x length($typo); + my $hereptr = "$hereline$ptr\n"; my $typo_fix = $spelling_fix{lc($typo)}; $typo_fix = ucfirst($typo_fix) if ($typo =~ /^[A-Z]/); $typo_fix = uc($typo_fix) if ($typo =~ /^[A-Z]+$/); my $msg_level = \&WARN; $msg_level = \&CHK if ($file); if (&{$msg_level}("TYPO_SPELLING", - "'$typo' may be misspelled - perhaps '$typo_fix'?\n" . $herecurr) && + "'$typo' may be misspelled - perhaps '$typo_fix'?\n" . $hereptr) && $fix) { $fixed[$fixlinenr] =~ s/(^|[^A-Za-z@])($typo)($|[^A-Za-z@])/$1$typo_fix$3/; } } } +# check for invalid commit id + if ($in_commit_log && $line =~ /(^fixes:|\bcommit)\s+([0-9a-f]{6,40})\b/i) { + my $id; + my $description; + ($id, $description) = git_commit_info($2, undef, undef); + if (!defined($id)) { + WARN("UNKNOWN_COMMIT_ID", + "Unknown commit id '$2', maybe rebased or not pulled?\n" . $herecurr); + } + } + +# check for repeated words separated by a single space +# avoid false positive from list command eg, '-rw-r--r-- 1 root root' + if (($rawline =~ /^\+/ || $in_commit_log) && + $rawline !~ /[bcCdDlMnpPs\?-][rwxsStT-]{9}/) { + pos($rawline) = 1 if (!$in_commit_log); + while ($rawline =~ /\b($word_pattern) (?=($word_pattern))/g) { + + my $first = $1; + my $second = $2; + my $start_pos = $-[1]; + my $end_pos = $+[2]; + if ($first =~ /(?:struct|union|enum)/) { + pos($rawline) += length($first) + length($second) + 1; + next; + } + + next if (lc($first) ne lc($second)); + next if ($first eq 'long'); + + # check for character before and after the word matches + my $start_char = ''; + my $end_char = ''; + $start_char = substr($rawline, $start_pos - 1, 1) if ($start_pos > ($in_commit_log ? 0 : 1)); + $end_char = substr($rawline, $end_pos, 1) if ($end_pos < length($rawline)); + + next if ($start_char =~ /^\S$/); + next if (index(" \t.,;?!", $end_char) == -1); + + # avoid repeating hex occurrences like 'ff ff fe 09 ...' + if ($first =~ /\b[0-9a-f]{2,}\b/i) { + next if (!exists($allow_repeated_words{lc($first)})); + } + + if (WARN("REPEATED_WORD", + "Possible repeated word: '$first'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b$first $second\b/$first/; + } + } + + # if it's a repeated word on consecutive lines in a comment block + if ($prevline =~ /$;+\s*$/ && + $prevrawline =~ /($word_pattern)\s*$/) { + my $last_word = $1; + if ($rawline =~ /^\+\s*\*\s*$last_word /) { + if (WARN("REPEATED_WORD", + "Possible repeated word: '$last_word'\n" . $hereprev) && + $fix) { + $fixed[$fixlinenr] =~ s/(\+\s*\*\s*)$last_word /$1/; + } + } + } + } + # ignore non-hunk lines and lines being removed next if (!$hunk_line || $line =~ /^-/); @@ -2828,69 +3479,87 @@ sub process { # Kconfig supports named choices), so use a word boundary # (\b) rather than a whitespace character (\s) $line =~ /^\+\s*(?:config|menuconfig|choice)\b/) { - my $length = 0; - my $cnt = $realcnt; - my $ln = $linenr + 1; - my $f; - my $is_start = 0; - my $is_end = 0; - for (; $cnt > 0 && defined $lines[$ln - 1]; $ln++) { - $f = $lines[$ln - 1]; - $cnt-- if ($lines[$ln - 1] !~ /^-/); - $is_end = $lines[$ln - 1] =~ /^\+/; + my $ln = $linenr; + my $needs_help = 0; + my $has_help = 0; + my $help_length = 0; + while (defined $lines[$ln]) { + my $f = $lines[$ln++]; next if ($f =~ /^-/); - last if (!$file && $f =~ /^\@\@/); - - if ($lines[$ln - 1] =~ /^\+\s*(?:bool|tristate|prompt)\s*["']/) { - $is_start = 1; - } elsif ($lines[$ln - 1] =~ /^\+\s*(?:help|---help---)\s*$/) { - if ($lines[$ln - 1] =~ "---help---") { - WARN("CONFIG_DESCRIPTION", - "prefer 'help' over '---help---' for new help texts\n" . $herecurr); - } - $length = -1; + last if ($f !~ /^[\+ ]/); # !patch context + + if ($f =~ /^\+\s*(?:bool|tristate|prompt)\s*["']/) { + $needs_help = 1; + next; + } + if ($f =~ /^\+\s*help\s*$/) { + $has_help = 1; + next; } - $f =~ s/^.//; - $f =~ s/#.*//; - $f =~ s/^\s+//; - next if ($f =~ /^$/); + $f =~ s/^.//; # strip patch context [+ ] + $f =~ s/#.*//; # strip # directives + $f =~ s/^\s+//; # strip leading blanks + next if ($f =~ /^$/); # skip blank lines + # At the end of this Kconfig block: # This only checks context lines in the patch # and so hopefully shouldn't trigger false # positives, even though some of these are # common words in help texts - if ($f =~ /^\s*(?:config|menuconfig|choice|endchoice| - if|endif|menu|endmenu|source)\b/x) { - $is_end = 1; + if ($f =~ /^(?:config|menuconfig|choice|endchoice| + if|endif|menu|endmenu|source)\b/x) { last; } - $length++; + $help_length++ if ($has_help); } - if ($is_start && $is_end && $length < $min_conf_desc_length) { + if ($needs_help && + $help_length < $min_conf_desc_length) { + my $stat_real = get_stat_real($linenr, $ln - 1); WARN("CONFIG_DESCRIPTION", - "please write a paragraph that describes the config symbol fully\n" . $herecurr); + "please write a help paragraph that fully describes the config symbol\n" . "$here\n$stat_real\n"); } - #print "is_start<$is_start> is_end<$is_end> length<$length>\n"; } -# check for MAINTAINERS entries that don't have the right form - if ($realfile =~ /^MAINTAINERS$/ && - $rawline =~ /^\+[A-Z]:/ && - $rawline !~ /^\+[A-Z]:\t\S/) { - if (WARN("MAINTAINERS_STYLE", - "MAINTAINERS entries use one tab after TYPE:\n" . $herecurr) && - $fix) { - $fixed[$fixlinenr] =~ s/^(\+[A-Z]):\s*/$1:\t/; +# check MAINTAINERS entries + if ($realfile =~ /^MAINTAINERS$/) { +# check MAINTAINERS entries for the right form + if ($rawline =~ /^\+[A-Z]:/ && + $rawline !~ /^\+[A-Z]:\t\S/) { + if (WARN("MAINTAINERS_STYLE", + "MAINTAINERS entries use one tab after TYPE:\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/^(\+[A-Z]):\s*/$1:\t/; + } + } +# check MAINTAINERS entries for the right ordering too + my $preferred_order = 'MRLSWQBCPTFXNK'; + if ($rawline =~ /^\+[A-Z]:/ && + $prevrawline =~ /^[\+ ][A-Z]:/) { + $rawline =~ /^\+([A-Z]):\s*(.*)/; + my $cur = $1; + my $curval = $2; + $prevrawline =~ /^[\+ ]([A-Z]):\s*(.*)/; + my $prev = $1; + my $prevval = $2; + my $curindex = index($preferred_order, $cur); + my $previndex = index($preferred_order, $prev); + if ($curindex < 0) { + WARN("MAINTAINERS_STYLE", + "Unknown MAINTAINERS entry type: '$cur'\n" . $herecurr); + } else { + if ($previndex >= 0 && $curindex < $previndex) { + WARN("MAINTAINERS_STYLE", + "Misordered MAINTAINERS entry - list '$cur:' before '$prev:'\n" . $hereprev); + } elsif ((($prev eq 'F' && $cur eq 'F') || + ($prev eq 'X' && $cur eq 'X')) && + ($prevval cmp $curval) > 0) { + WARN("MAINTAINERS_STYLE", + "Misordered MAINTAINERS entry - list file patterns in alphabetic order\n" . $hereprev); + } + } } - } - -# discourage the use of boolean for type definition attributes of Kconfig options - if ($realfile =~ /Kconfig/ && - $line =~ /^\+\s*\bboolean\b/) { - WARN("CONFIG_TYPE_BOOLEAN", - "Use of boolean is deprecated, please use bool instead.\n" . $herecurr); } if (($realfile =~ /Makefile.*/ || $realfile =~ /Kbuild.*/) && @@ -2915,7 +3584,7 @@ sub process { my @compats = $rawline =~ /\"([a-zA-Z0-9\-\,\.\+_]+)\"/g; my $dt_path = $root . "/Documentation/devicetree/bindings/"; - my $vp_file = $dt_path . "vendor-prefixes.txt"; + my $vp_file = $dt_path . "vendor-prefixes.yaml"; foreach my $compat (@compats) { my $compat2 = $compat; @@ -2930,7 +3599,7 @@ sub process { next if $compat !~ /^([a-zA-Z0-9\-]+)\,/; my $vendor = $1; - `grep -Eq "^$vendor\\b" $vp_file`; + `grep -Eq "\\"\\^\Q$vendor\E,\\.\\*\\":" $vp_file`; if ( $? >> 8 ) { WARN("UNDOCUMENTED_DT_STRING", "DT compatible string vendor \"$vendor\" appears un-documented -- check $vp_file\n" . $herecurr); @@ -2948,23 +3617,62 @@ sub process { $comment = '/*'; } elsif ($realfile =~ /\.(c|dts|dtsi)$/) { $comment = '//'; - } elsif (($checklicenseline == 2) || $realfile =~ /\.(sh|pl|py|awk|tc)$/) { + } elsif (($checklicenseline == 2) || $realfile =~ /\.(sh|pl|py|awk|tc|yaml)$/) { $comment = '#'; } elsif ($realfile =~ /\.rst$/) { $comment = '..'; } +# check SPDX comment style for .[chsS] files + if ($realfile =~ /\.[chsS]$/ && + $rawline =~ /SPDX-License-Identifier:/ && + $rawline !~ m@^\+\s*\Q$comment\E\s*@) { + WARN("SPDX_LICENSE_TAG", + "Improper SPDX comment style for '$realfile', please use '$comment' instead\n" . $herecurr); + } + if ($comment !~ /^$/ && - $rawline !~ /^\+\Q$comment\E SPDX-License-Identifier: /) { + $rawline !~ m@^\+\Q$comment\E SPDX-License-Identifier: @) { WARN("SPDX_LICENSE_TAG", "Missing or malformed SPDX-License-Identifier tag in line $checklicenseline\n" . $herecurr); + } elsif ($rawline =~ /(SPDX-License-Identifier: .*)/) { + my $spdx_license = $1; + if (!is_SPDX_License_valid($spdx_license)) { + WARN("SPDX_LICENSE_TAG", + "'$spdx_license' is not supported in LICENSES/...\n" . $herecurr); + } + if ($realfile =~ m@^Documentation/devicetree/bindings/@ && + not $spdx_license =~ /GPL-2\.0.*BSD-2-Clause/) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + if (&{$msg_level}("SPDX_LICENSE_TAG", + + "DT binding documents should be licensed (GPL-2.0-only OR BSD-2-Clause)\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/SPDX-License-Identifier: .*/SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)/; + } + } } } } +# check for embedded filenames + if ($rawline =~ /^\+.*\Q$realfile\E/) { + WARN("EMBEDDED_FILENAME", + "It's generally not useful to have the filename in the file\n" . $herecurr); + } + # check we are in a valid source file if not then ignore this hunk next if ($realfile !~ /\.(h|c|s|S|sh|dtsi|dts)$/); +# check for using SPDX-License-Identifier on the wrong line number + if ($realline != $checklicenseline && + $rawline =~ /\bSPDX-License-Identifier:/ && + substr($line, @-, @+ - @-) eq "$;" x (@+ - @-)) { + WARN("SPDX_LICENSE_TAG", + "Misplaced SPDX-License-Identifier tag - use line $checklicenseline instead\n" . $herecurr); + } + # line length limit (with some exclusions) # # There are a few types of lines that may extend beyond $max_line_length: @@ -3022,22 +3730,34 @@ sub process { if ($msg_type ne "" && (show_type("LONG_LINE") || show_type($msg_type))) { - WARN($msg_type, - "line over $max_line_length characters\n" . $herecurr); + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + &{$msg_level}($msg_type, + "line length of $length exceeds $max_line_length columns\n" . $herecurr); } } # check for adding lines without a newline. if ($line =~ /^\+/ && defined $lines[$linenr] && $lines[$linenr] =~ /^\\ No newline at end of file/) { - WARN("MISSING_EOF_NEWLINE", - "adding a line without newline at end of file\n" . $herecurr); + if (WARN("MISSING_EOF_NEWLINE", + "adding a line without newline at end of file\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr+1, "No newline at end of file"); + } + } + +# check for .L prefix local symbols in .S files + if ($realfile =~ /\.S$/ && + $line =~ /^\+\s*(?:[A-Z]+_)?SYM_[A-Z]+_(?:START|END)(?:_[A-Z_]+)?\s*\(\s*\.L/) { + WARN("AVOID_L_PREFIX", + "Avoid using '.L' prefixed local symbol names for denoting a range of code via 'SYM_*_START/END' annotations; see Documentation/asm-annotations.rst\n" . $herecurr); } # check we are in a valid source file C or perl if not then ignore this hunk next if ($realfile !~ /\.(h|c|pl|dtsi|dts)$/); # at the beginning of a line any tabs must come first and anything -# more than 8 must use tabs. +# more than $tabsize must use tabs. if ($rawline =~ /^\+\s* \t\s*\S/ || $rawline =~ /^\+\s* \s*/) { my $herevet = "$here\n" . cat_vet($rawline) . "\n"; @@ -3056,7 +3776,7 @@ sub process { "please, no space before tabs\n" . $herevet) && $fix) { while ($fixed[$fixlinenr] =~ - s/(^\+.*) {8,8}\t/$1\t\t/) {} + s/(^\+.*) {$tabsize,$tabsize}\t/$1\t\t/) {} while ($fixed[$fixlinenr] =~ s/(^\+.*) +\t/$1\t/) {} } @@ -3064,31 +3784,45 @@ sub process { # check for assignments on the start of a line if ($sline =~ /^\+\s+($Assignment)[^=]/) { - CHK("ASSIGNMENT_CONTINUATIONS", - "Assignment operator '$1' should be on the previous line\n" . $hereprev); + my $operator = $1; + if (CHK("ASSIGNMENT_CONTINUATIONS", + "Assignment operator '$1' should be on the previous line\n" . $hereprev) && + $fix && $prevrawline =~ /^\+/) { + # add assignment operator to the previous line, remove from current line + $fixed[$fixlinenr - 1] .= " $operator"; + $fixed[$fixlinenr] =~ s/\Q$operator\E\s*//; + } } # check for && or || at the start of a line if ($rawline =~ /^\+\s*(&&|\|\|)/) { - CHK("LOGICAL_CONTINUATIONS", - "Logical continuations should be on the previous line\n" . $hereprev); + my $operator = $1; + if (CHK("LOGICAL_CONTINUATIONS", + "Logical continuations should be on the previous line\n" . $hereprev) && + $fix && $prevrawline =~ /^\+/) { + # insert logical operator at last non-comment, non-whitepsace char on previous line + $prevline =~ /[\s$;]*$/; + my $line_end = substr($prevrawline, $-[0]); + $fixed[$fixlinenr - 1] =~ s/\Q$line_end\E$/ $operator$line_end/; + $fixed[$fixlinenr] =~ s/\Q$operator\E\s*//; + } } # check indentation starts on a tab stop - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $sline =~ /^\+\t+( +)(?:$c90_Keywords\b|\{\s*$|\}\s*(?:else\b|while\b|\s*$)|$Declare\s*$Ident\s*[;=])/) { my $indent = length($1); - if ($indent % 8) { + if ($indent % $tabsize) { if (WARN("TABSTOP", "Statements should start on a tabstop\n" . $herecurr) && $fix) { - $fixed[$fixlinenr] =~ s@(^\+\t+) +@$1 . "\t" x ($indent/8)@e; + $fixed[$fixlinenr] =~ s@(^\+\t+) +@$1 . "\t" x ($indent/$tabsize)@e; } } } # check multi-line statement indentation matches previous line - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $prevline =~ /^\+([ \t]*)((?:$c90_Keywords(?:\s+if)\s*)|(?:$Declare\s*)?(?:$Ident|\(\s*\*\s*$Ident\s*\))\s*|(?:\*\s*)*$Lval\s*=\s*$Ident\s*)\(.*(\&\&|\|\||,)\s*$/) { $prevline =~ /^\+(\t*)(.*)$/; my $oldindent = $1; @@ -3100,8 +3834,8 @@ sub process { my $newindent = $2; my $goodtabindent = $oldindent . - "\t" x ($pos / 8) . - " " x ($pos % 8); + "\t" x ($pos / $tabsize) . + " " x ($pos % $tabsize); my $goodspaceindent = $oldindent . " " x $pos; if ($newindent ne $goodtabindent && @@ -3139,7 +3873,7 @@ sub process { if ($realfile =~ m@^(drivers/net/|net/)@ && $prevrawline =~ /^\+[ \t]*\/\*[ \t]*$/ && $rawline =~ /^\+[ \t]*\*/ && - $realline > 2) { + $realline > 3) { # Do not warn about the initial copyright comment block after SPDX-License-Identifier WARN("NETWORKING_BLOCK_COMMENT_STYLE", "networking block comments don't use an empty /* line, use /* Comment...\n" . $hereprev); } @@ -3221,43 +3955,48 @@ sub process { } # check for missing blank lines after declarations - if ($sline =~ /^\+\s+\S/ && #Not at char 1 - # actual declarations - ($prevline =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || +# (declarations must have the same indentation and not be at the start of line) + if (($prevline =~ /\+(\s+)\S/) && $sline =~ /^\+$1\S/) { + # use temporaries + my $sl = $sline; + my $pl = $prevline; + # remove $Attribute/$Sparse uses to simplify comparisons + $sl =~ s/\b(?:$Attribute|$Sparse)\b//g; + $pl =~ s/\b(?:$Attribute|$Sparse)\b//g; + if (($pl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || # function pointer declarations - $prevline =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + $pl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || # foo bar; where foo is some local typedef or #define - $prevline =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || + $pl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || # known declaration macros - $prevline =~ /^\+\s+$declaration_macros/) && + $pl =~ /^\+\s+$declaration_macros/) && # for "else if" which can look like "$Ident $Ident" - !($prevline =~ /^\+\s+$c90_Keywords\b/ || + !($pl =~ /^\+\s+$c90_Keywords\b/ || # other possible extensions of declaration lines - $prevline =~ /(?:$Compare|$Assignment|$Operators)\s*$/ || + $pl =~ /(?:$Compare|$Assignment|$Operators)\s*$/ || # not starting a section or a macro "\" extended line - $prevline =~ /(?:\{\s*|\\)$/) && + $pl =~ /(?:\{\s*|\\)$/) && # looks like a declaration - !($sline =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || + !($sl =~ /^\+\s+$Declare\s*$Ident\s*[=,;:\[]/ || # function pointer declarations - $sline =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || + $sl =~ /^\+\s+$Declare\s*\(\s*\*\s*$Ident\s*\)\s*[=,;:\[\(]/ || # foo bar; where foo is some local typedef or #define - $sline =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || + $sl =~ /^\+\s+$Ident(?:\s+|\s*\*\s*)$Ident\s*[=,;\[]/ || # known declaration macros - $sline =~ /^\+\s+$declaration_macros/ || + $sl =~ /^\+\s+$declaration_macros/ || # start of struct or union or enum - $sline =~ /^\+\s+(?:union|struct|enum|typedef)\b/ || + $sl =~ /^\+\s+(?:static\s+)?(?:const\s+)?(?:union|struct|enum|typedef)\b/ || # start or end of block or continuation of declaration - $sline =~ /^\+\s+(?:$|[\{\}\.\#\"\?\:\(\[])/ || + $sl =~ /^\+\s+(?:$|[\{\}\.\#\"\?\:\(\[])/ || # bitfield continuation - $sline =~ /^\+\s+$Ident\s*:\s*\d+\s*[,;]/ || + $sl =~ /^\+\s+$Ident\s*:\s*\d+\s*[,;]/ || # other possible extensions of declaration lines - $sline =~ /^\+\s+\(?\s*(?:$Compare|$Assignment|$Operators)/) && - # indentation of previous and current line are the same - (($prevline =~ /\+(\s+)\S/) && $sline =~ /^\+$1\S/)) { - if (WARN("LINE_SPACING", - "Missing a blank line after declarations\n" . $hereprev) && - $fix) { - fix_insert_line($fixlinenr, "\+"); + $sl =~ /^\+\s+\(?\s*(?:$Compare|$Assignment|$Operators)/)) { + if (WARN("LINE_SPACING", + "Missing a blank line after declarations\n" . $hereprev) && + $fix) { + fix_insert_line($fixlinenr, "\+"); + } } } @@ -3310,12 +4049,16 @@ sub process { } # check indentation of a line with a break; -# if the previous line is a goto or return and is indented the same # of tabs +# if the previous line is a goto, return or break +# and is indented the same # of tabs if ($sline =~ /^\+([\t]+)break\s*;\s*$/) { my $tabs = $1; - if ($prevline =~ /^\+$tabs(?:goto|return)\b/) { - WARN("UNNECESSARY_BREAK", - "break is not useful after a goto or return\n" . $hereprev); + if ($prevline =~ /^\+$tabs(goto|return|break)\b/) { + if (WARN("UNNECESSARY_BREAK", + "break is not useful after a $1\n" . $hereprev) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } } } @@ -3572,11 +4315,11 @@ sub process { #print "line<$line> prevline<$prevline> indent<$indent> sindent<$sindent> check<$check> continuation<$continuation> s<$s> cond_lines<$cond_lines> stat_real<$stat_real> stat<$stat>\n"; if ($check && $s ne '' && - (($sindent % 8) != 0 || + (($sindent % $tabsize) != 0 || ($sindent < $indent) || ($sindent == $indent && ($s !~ /^\s*(?:\}|\{|else\b)/)) || - ($sindent > $indent + 8))) { + ($sindent > $indent + $tabsize))) { WARN("SUSPECT_CODE_INDENT", "suspect code indent for conditional statements ($indent, $sindent)\n" . $herecurr . "$stat_real\n"); } @@ -3598,6 +4341,17 @@ sub process { #ignore lines not being added next if ($line =~ /^[^\+]/); +# check for self assignments used to avoid compiler warnings +# e.g.: int foo = foo, *bar = NULL; +# struct foo bar = *(&(bar)); + if ($line =~ /^\+\s*(?:$Declare)?([A-Za-z_][A-Za-z\d_]*)\s*=/) { + my $var = $1; + if ($line =~ /^\+\s*(?:$Declare)?$var\s*=\s*(?:$var|\*\s*\(?\s*&\s*\(?\s*$var\s*\)?\s*\)?)\s*[;,]/) { + WARN("SELF_ASSIGNMENT", + "Do not use self-assignments to avoid compiler warnings\n" . $herecurr); + } + } + # check for dereferences that span multiple lines if ($prevline =~ /^\+.*$Lval\s*(?:\.|->)\s*$/ && $line =~ /^\+\s*(?!\#\s*(?!define\s+|if))\s*$Lval/) { @@ -3713,13 +4467,13 @@ sub process { if (defined $realline_next && exists $lines[$realline_next - 1] && !defined $suppress_export{$realline_next} && - ($lines[$realline_next - 1] =~ /EXPORT_SYMBOL.*\((.*)\)/ || - $lines[$realline_next - 1] =~ /EXPORT_UNUSED_SYMBOL.*\((.*)\)/)) { + ($lines[$realline_next - 1] =~ /EXPORT_SYMBOL.*\((.*)\)/)) { # Handle definitions which produce identifiers with # a prefix: # XXX(foo); # EXPORT_SYMBOL(something_foo); my $name = $1; + $name =~ s/^\s*($Ident).*/$1/; if ($stat =~ /^(?:.\s*}\s*\n)?.([A-Z_]+)\s*\(\s*($Ident)/ && $name =~ /^${Ident}_$2/) { #print "FOO C name<$name>\n"; @@ -3741,8 +4495,7 @@ sub process { } if (!defined $suppress_export{$linenr} && $prevline =~ /^.\s*$/ && - ($line =~ /EXPORT_SYMBOL.*\((.*)\)/ || - $line =~ /EXPORT_UNUSED_SYMBOL.*\((.*)\)/)) { + ($line =~ /EXPORT_SYMBOL.*\((.*)\)/)) { #print "FOO B <$lines[$linenr - 1]>\n"; $suppress_export{$linenr} = 2; } @@ -3753,7 +4506,8 @@ sub process { } # check for global initialisers. - if ($line =~ /^\+$Type\s*$Ident(?:\s+$Modifier)*\s*=\s*($zero_initializer)\s*;/) { + if ($line =~ /^\+$Type\s*$Ident(?:\s+$Modifier)*\s*=\s*($zero_initializer)\s*;/ && + !exclude_global_initialisers($realfile)) { if (ERROR("GLOBAL_INITIALISERS", "do not initialise globals to $1\n" . $herecurr) && $fix) { @@ -3777,19 +4531,48 @@ sub process { "type '$tmp' should be specified in [[un]signed] [short|int|long|long long] order\n" . $herecurr); } +# check for unnecessary <signed> int declarations of short/long/long long + while ($sline =~ m{\b($TypeMisordered(\s*\*)*|$C90_int_types)\b}g) { + my $type = trim($1); + next if ($type !~ /\bint\b/); + next if ($type !~ /\b(?:short|long\s+long|long)\b/); + my $new_type = $type; + $new_type =~ s/\b\s*int\s*\b/ /; + $new_type =~ s/\b\s*(?:un)?signed\b\s*/ /; + $new_type =~ s/^const\s+//; + $new_type = "unsigned $new_type" if ($type =~ /\bunsigned\b/); + $new_type = "const $new_type" if ($type =~ /^const\b/); + $new_type =~ s/\s+/ /g; + $new_type = trim($new_type); + if (WARN("UNNECESSARY_INT", + "Prefer '$new_type' over '$type' as the int is unnecessary\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\b\Q$type\E\b/$new_type/; + } + } + # check for static const char * arrays. if ($line =~ /\bstatic\s+const\s+char\s*\*\s*(\w+)\s*\[\s*\]\s*=\s*/) { WARN("STATIC_CONST_CHAR_ARRAY", "static const char * array should probably be static const char * const\n" . $herecurr); - } + } + +# check for initialized const char arrays that should be static const + if ($line =~ /^\+\s*const\s+(char|unsigned\s+char|_*u8|(?:[us]_)?int8_t)\s+\w+\s*\[\s*(?:\w+\s*)?\]\s*=\s*"/) { + if (WARN("STATIC_CONST_CHAR_ARRAY", + "const array should probably be static const\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/(^.\s*)const\b/${1}static const/; + } + } # check for static char foo[] = "bar" declarations. if ($line =~ /\bstatic\s+char\s+(\w+)\s*\[\s*\]\s*=\s*"/) { WARN("STATIC_CONST_CHAR_ARRAY", "static char array declaration should probably be static const char\n" . $herecurr); - } + } # check for const <foo> const where <foo> is not a pointer or array type if ($sline =~ /\bconst\s+($BasicType)\s+const\b/) { @@ -3803,12 +4586,24 @@ sub process { } } +# check for const static or static <non ptr type> const declarations +# prefer 'static const <foo>' over 'const static <foo>' and 'static <foo> const' + if ($sline =~ /^\+\s*const\s+static\s+($Type)\b/ || + $sline =~ /^\+\s*static\s+($BasicType)\s+const\b/) { + if (WARN("STATIC_CONST", + "Move const after static - use 'static const $1'\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bconst\s+static\b/static const/; + $fixed[$fixlinenr] =~ s/\bstatic\s+($BasicType)\s+const\b/static const $1/; + } + } + # check for non-global char *foo[] = {"bar", ...} declarations. if ($line =~ /^.\s+(?:static\s+|const\s+)?char\s+\*\s*\w+\s*\[\s*\]\s*=\s*\{/) { WARN("STATIC_CONST_CHAR_ARRAY", "char * array declaration might be better as static const\n" . $herecurr); - } + } # check for sizeof(foo)/sizeof(foo[0]) that could be ARRAY_SIZE(foo) if ($line =~ m@\bsizeof\s*\(\s*($Lval)\s*\)@) { @@ -3824,7 +4619,7 @@ sub process { } # check for function declarations without arguments like "int foo()" - if ($line =~ /(\b$Type\s+$Ident)\s*\(\s*\)/) { + if ($line =~ /(\b$Type\s*$Ident)\s*\(\s*\)/) { if (ERROR("FUNCTION_WITHOUT_ARGS", "Bad function definition - $1() should probably be $1(void)\n" . $herecurr) && $fix) { @@ -3925,25 +4720,23 @@ sub process { "printk() should include KERN_<LEVEL> facility level\n" . $herecurr); } - if ($line =~ /\bprintk\s*\(\s*KERN_([A-Z]+)/) { - my $orig = $1; +# prefer variants of (subsystem|netdev|dev|pr)_<level> to printk(KERN_<LEVEL> + if ($line =~ /\b(printk(_once|_ratelimited)?)\s*\(\s*KERN_([A-Z]+)/) { + my $printk = $1; + my $modifier = $2; + my $orig = $3; + $modifier = "" if (!defined($modifier)); my $level = lc($orig); $level = "warn" if ($level eq "warning"); my $level2 = $level; $level2 = "dbg" if ($level eq "debug"); + $level .= $modifier; + $level2 .= $modifier; WARN("PREFER_PR_LEVEL", - "Prefer [subsystem eg: netdev]_$level2([subsystem]dev, ... then dev_$level2(dev, ... then pr_$level(... to printk(KERN_$orig ...\n" . $herecurr); - } - - if ($line =~ /\bpr_warning\s*\(/) { - if (WARN("PREFER_PR_LEVEL", - "Prefer pr_warn(... to pr_warning(...\n" . $herecurr) && - $fix) { - $fixed[$fixlinenr] =~ - s/\bpr_warning\b/pr_warn/; - } + "Prefer [subsystem eg: netdev]_$level2([subsystem]dev, ... then dev_$level2(dev, ... then pr_$level(... to $printk(KERN_$orig ...\n" . $herecurr); } +# prefer dev_<level> to dev_printk(KERN_<LEVEL> if ($line =~ /\bdev_printk\s*\(\s*KERN_([A-Z]+)/) { my $orig = $1; my $level = lc($orig); @@ -3953,6 +4746,12 @@ sub process { "Prefer dev_$level(... to dev_printk(KERN_$orig, ...\n" . $herecurr); } +# trace_printk should not be used in production code. + if ($line =~ /\b(trace_printk|trace_puts|ftrace_vprintk)\s*\(/) { + WARN("TRACE_PRINTK", + "Do not use $1() in production code (this can be ignored if built only with a debug config option)\n" . $herecurr); + } + # ENOSYS means "bad syscall nr" and nothing else. This will have a small # number of false positives, but assembly files are not checked, so at # least the arch entry code will not trigger this warning. @@ -3961,9 +4760,20 @@ sub process { "ENOSYS means 'invalid syscall nr' and nothing else\n" . $herecurr); } +# ENOTSUPP is not a standard error code and should be avoided in new patches. +# Folks usually mean EOPNOTSUPP (also called ENOTSUP), when they type ENOTSUPP. +# Similarly to ENOSYS warning a small number of false positives is expected. + if (!$file && $line =~ /\bENOTSUPP\b/) { + if (WARN("ENOTSUPP", + "ENOTSUPP is not a SUSV4 error code, prefer EOPNOTSUPP\n" . $herecurr) && + $fix) { + $fixed[$fixlinenr] =~ s/\bENOTSUPP\b/EOPNOTSUPP/; + } + } + # function brace can't be on same line, except for #defines of do while, # or if closed on same line - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $sline =~ /$Type\s*$Ident\s*$balanced_parens\s*\{/ && $sline !~ /\#\s*define\b.*do\s*\{/ && $sline !~ /}/) { @@ -3972,7 +4782,7 @@ sub process { $fix) { fix_delete_line($fixlinenr, $rawline); my $fixed_line = $rawline; - $fixed_line =~ /(^..*$Type\s*$Ident\(.*\)\s*){(.*)$/; + $fixed_line =~ /(^..*$Type\s*$Ident\(.*\)\s*)\{(.*)$/; my $line1 = $1; my $line2 = $2; fix_insert_line($fixlinenr, ltrim($line1)); @@ -4383,7 +5193,7 @@ sub process { # A colon needs no spaces before when it is # terminating a case value or a label. } elsif ($opv eq ':C' || $opv eq ':L') { - if ($ctx =~ /Wx./) { + if ($ctx =~ /Wx./ and $realfile !~ m@.*\.lds\.h$@) { if (ERROR("SPACING", "space prohibited before that '$op' $at\n" . $hereptr)) { $good = rtrim($fix_elements[$n]) . trim($fix_elements[$n + 1]); @@ -4401,7 +5211,7 @@ sub process { ($op eq '>' && $ca =~ /<\S+\@\S+$/)) { - $ok = 1; + $ok = 1; } # for asm volatile statements @@ -4467,7 +5277,7 @@ sub process { ## $line !~ /^.\s*$Type\s+$Ident(?:\s*=[^,{]*)?\s*,\s*$Type\s*$Ident.*/) { ## ## # Remove any bracketed sections to ensure we do not -## # falsly report the parameters of functions. +## # falsely report the parameters of functions. ## my $ln = $line; ## while ($ln =~ s/\([^\(\)]*\)//g) { ## } @@ -4479,11 +5289,11 @@ sub process { #need space before brace following if, while, etc if (($line =~ /\(.*\)\{/ && $line !~ /\($Type\)\{/) || - $line =~ /do\{/) { + $line =~ /\b(?:else|do)\{/) { if (ERROR("SPACING", "space required before the open brace '{'\n" . $herecurr) && $fix) { - $fixed[$fixlinenr] =~ s/^(\+.*(?:do|\)))\{/$1 {/; + $fixed[$fixlinenr] =~ s/^(\+.*(?:do|else|\)))\{/$1 {/; } } @@ -4497,7 +5307,7 @@ sub process { # closing brace should have a space following it when it has anything # on the line - if ($line =~ /}(?!(?:,|;|\)))\S/) { + if ($line =~ /}(?!(?:,|;|\)|\}))\S/) { if (ERROR("SPACING", "space required after that close brace '}'\n" . $herecurr) && $fix) { @@ -4574,7 +5384,7 @@ sub process { # check for unnecessary parentheses around comparisons in if uses # when !drivers/staging or command-line uses --strict if (($realfile !~ m@^(?:drivers/staging/)@ || $check_orig) && - $^V && $^V ge 5.10.0 && defined($stat) && + $perl_version_ok && defined($stat) && $stat =~ /(^.\s*if\s*($balanced_parens))/) { my $if_stat = $1; my $test = substr($2, 1, -1); @@ -4597,9 +5407,13 @@ sub process { } } -#goto labels aren't indented, allow a single space however - if ($line=~/^.\s+[A-Za-z\d_]+:(?![0-9]+)/ and - !($line=~/^. [A-Za-z\d_]+:/) and !($line=~/^.\s+default:/)) { +# check that goto labels aren't indented (allow a single space indentation) +# and ignore bitfield definitions like foo:1 +# Strictly, labels can have whitespace after the identifier and before the : +# but this is not allowed here as many ?: uses would appear to be labels + if ($sline =~ /^.\s+[A-Za-z_][A-Za-z\d_]*:(?!\s*\d+)/ && + $sline !~ /^. [A-Za-z\d_][A-Za-z\d_]*:/ && + $sline !~ /^.\s+default:/) { if (WARN("INDENTED_LABEL", "labels should not be indented\n" . $herecurr) && $fix) { @@ -4608,10 +5422,21 @@ sub process { } } +# check if a statement with a comma should be two statements like: +# foo = bar(), /* comma should be semicolon */ +# bar = baz(); + if (defined($stat) && + $stat =~ /^\+\s*(?:$Lval\s*$Assignment\s*)?$FuncArg\s*,\s*(?:$Lval\s*$Assignment\s*)?$FuncArg\s*;\s*$/) { + my $cnt = statement_rawlines($stat); + my $herectx = get_stat_here($linenr, $cnt, $here); + WARN("SUSPECT_COMMA_SEMICOLON", + "Possible comma where semicolon could be used\n" . $herectx); + } + # return is not a function if (defined($stat) && $stat =~ /^.\s*return(\s*)\(/s) { my $spacing = $1; - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $stat =~ /^.\s*return\s*($balanced_parens)\s*;\s*$/) { my $value = $1; $value = deparenthesize($value); @@ -4635,10 +5460,10 @@ sub process { $lines[$linenr - 3] !~ /^[ +]\s*$Ident\s*:/) { WARN("RETURN_VOID", "void function return statements are not generally useful\n" . $hereprev); - } + } # if statements using unnecessary parentheses - ie: if ((foo == bar)) - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $line =~ /\bif\s*((?:\(\s*){2,})/) { my $openparens = $1; my $count = $openparens =~ tr@\(@\(@; @@ -4655,7 +5480,7 @@ sub process { # avoid cases like "foo + BAR < baz" # only fix matches surrounded by parentheses to avoid incorrect # conversions like "FOO < baz() + 5" being "misfixed" to "baz() > FOO + 5" - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $line =~ /^\+(.*)\b($Constant|[A-Z_][A-Z0-9_]*)\s*($Compare)\s*($LvalOrFunc)/) { my $lead = $1; my $const = $2; @@ -4683,7 +5508,7 @@ sub process { # Return of what appears to be an errno should normally be negative if ($sline =~ /\breturn(?:\s*\(+\s*|\s+)(E[A-Z]+)(?:\s*\)+\s*|\s*)[;:,]/) { my $name = $1; - if ($name ne 'EOF' && $name ne 'ERROR') { + if ($name ne 'EOF' && $name ne 'ERROR' && $name !~ /^EPOLL/) { WARN("USE_NEGATIVE_ERRNO", "return of an errno should typically be negative (ie: return -$1)\n" . $herecurr); } @@ -4728,15 +5553,37 @@ sub process { my ($s, $c) = ($stat, $cond); if ($c =~ /\bif\s*\(.*[^<>!=]=[^=].*/s) { - ERROR("ASSIGN_IN_IF", - "do not use assignment in if condition\n" . $herecurr); + if (ERROR("ASSIGN_IN_IF", + "do not use assignment in if condition\n" . $herecurr) && + $fix && $perl_version_ok) { + if ($rawline =~ /^\+(\s+)if\s*\(\s*(\!)?\s*\(\s*(($Lval)\s*=\s*$LvalOrFunc)\s*\)\s*(?:($Compare)\s*($FuncArg))?\s*\)\s*(\{)?\s*$/) { + my $space = $1; + my $not = $2; + my $statement = $3; + my $assigned = $4; + my $test = $8; + my $against = $9; + my $brace = $15; + fix_delete_line($fixlinenr, $rawline); + fix_insert_line($fixlinenr, "$space$statement;"); + my $newline = "${space}if ("; + $newline .= '!' if defined($not); + $newline .= '(' if (defined $not && defined($test) && defined($against)); + $newline .= "$assigned"; + $newline .= " $test $against" if (defined($test) && defined($against)); + $newline .= ')' if (defined $not && defined($test) && defined($against)); + $newline .= ')'; + $newline .= " {" if (defined($brace)); + fix_insert_line($fixlinenr + 1, $newline); + } + } } # Find out what is on the end of the line after the # conditional. substr($s, 0, length($c), ''); $s =~ s/\n.*//g; - $s =~ s/$;//g; # Remove any comments + $s =~ s/$;//g; # Remove any comments if (length($c) && $s !~ /^\s*{?\s*\\*\s*$/ && $c !~ /}\s*while\s*/) { @@ -4775,7 +5622,7 @@ sub process { # if and else should not have general statements after it if ($line =~ /^.\s*(?:}\s*)?else\b(.*)/) { my $s = $1; - $s =~ s/$;//g; # Remove any comments + $s =~ s/$;//g; # Remove any comments if ($s !~ /^\s*(?:\sif|(?:{|)\s*\\?\s*$)/) { ERROR("TRAILING_STATEMENTS", "trailing statements should be on next line\n" . $herecurr); @@ -4847,24 +5694,16 @@ sub process { while ($line =~ m{($Constant|$Lval)}g) { my $var = $1; -#gcc binary extension - if ($var =~ /^$Binary$/) { - if (WARN("GCC_BINARY_CONSTANT", - "Avoid gcc v4.3+ binary constant extension: <$var>\n" . $herecurr) && - $fix) { - my $hexval = sprintf("0x%x", oct($var)); - $fixed[$fixlinenr] =~ - s/\b$var\b/$hexval/; - } - } - #CamelCase if ($var !~ /^$Constant$/ && $var =~ /[A-Z][a-z]|[a-z][A-Z]/ && +#Ignore some autogenerated defines and enum values + $var !~ /^(?:[A-Z]+_){1,5}[A-Z]{1,3}[a-z]/ && #Ignore Page<foo> variants $var !~ /^(?:Clear|Set|TestClear|TestSet|)Page[A-Z]/ && -#Ignore SI style variants like nS, mV and dB (ie: max_uV, regulator_min_uA_show) - $var !~ /^(?:[a-z_]*?)_?[a-z][A-Z](?:_[a-z_]+)?$/ && +#Ignore SI style variants like nS, mV and dB +#(ie: max_uV, regulator_min_uA_show, RANGE_mA_VALUE) + $var !~ /^(?:[a-z0-9_]*|[A-Z0-9_]*)?_?[a-z][A-Z](?:_[a-z0-9_]+|_[A-Z0-9_]+)?$/ && #Ignore some three character SI units explicitly, like MiB and KHz $var !~ /^(?:[a-z_]*?)_?(?:[KMGT]iB|[KMGT]?Hz)(?:_[a-z_]+)?$/) { while ($var =~ m{($Ident)}g) { @@ -4945,6 +5784,7 @@ sub process { if (defined $define_args && $define_args ne "") { $define_args = substr($define_args, 1, length($define_args) - 2); $define_args =~ s/\s*//g; + $define_args =~ s/\\\+?//g; @def_args = split(",", $define_args); } @@ -4954,13 +5794,13 @@ sub process { $dstat =~ s/\s*$//s; # Flatten any parentheses and braces - while ($dstat =~ s/\([^\(\)]*\)/1/ || - $dstat =~ s/\{[^\{\}]*\}/1/ || - $dstat =~ s/.\[[^\[\]]*\]/1/) + while ($dstat =~ s/\([^\(\)]*\)/1u/ || + $dstat =~ s/\{[^\{\}]*\}/1u/ || + $dstat =~ s/.\[[^\[\]]*\]/1u/) { } - # Flatten any obvious string concatentation. + # Flatten any obvious string concatenation. while ($dstat =~ s/($String)\s*$Ident/$1/ || $dstat =~ s/$Ident\s*($String)/$1/) { @@ -4997,6 +5837,7 @@ sub process { $dstat !~ /^\.$Ident\s*=/ && # .foo = $dstat !~ /^(?:\#\s*$Ident|\#\s*$Constant)\s*$/ && # stringification #foo $dstat !~ /^do\s*$Constant\s*while\s*$Constant;?$/ && # do {...} while (...); // do {...} while (...) + $dstat !~ /^while\s*$Constant\s*$Constant\s*$/ && # while (...) {...} $dstat !~ /^for\s*$Constant$/ && # for (...) $dstat !~ /^for\s*$Constant\s+(?:$Ident|-?$Constant)$/ && # for (...) bar() $dstat !~ /^do\s*{/ && # do {... @@ -5038,7 +5879,7 @@ sub process { next if ($arg =~ /\.\.\./); next if ($arg =~ /^type$/i); my $tmp_stmt = $define_stmt; - $tmp_stmt =~ s/\b(typeof|__typeof__|__builtin\w+|typecheck\s*\(\s*$Type\s*,|\#+)\s*\(*\s*$arg\s*\)*\b//g; + $tmp_stmt =~ s/\b(__must_be_array|offsetof|sizeof|sizeof_field|__stringify|typeof|__typeof__|__builtin\w+|typecheck\s*\(\s*$Type\s*,|\#+)\s*\(*\s*$arg\s*\)*\b//g; $tmp_stmt =~ s/\#+\s*$arg\b//g; $tmp_stmt =~ s/\b$arg\s*\#\#//g; my $use_cnt = () = $tmp_stmt =~ /\b$arg\b/g; @@ -5080,7 +5921,7 @@ sub process { # do {} while (0) macro tests: # single-statement macros do not need to be enclosed in do while (0) loop, # macro should not end with a semicolon - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $realfile !~ m@/vmlinux.lds.h$@ && $line =~ /^.\s*\#\s*define\s+$Ident(\()?/) { my $ln = $linenr; @@ -5121,16 +5962,6 @@ sub process { } } -# make sure symbols are always wrapped with VMLINUX_SYMBOL() ... -# all assignments may have only one of the following with an assignment: -# . -# ALIGN(...) -# VMLINUX_SYMBOL(...) - if ($realfile eq 'vmlinux.lds.h' && $line =~ /(?:(?:^|\s)$Ident\s*=|=\s*$Ident(?:\s|$))/) { - WARN("MISSING_VMLINUX_SYMBOL", - "vmlinux.lds.h needs VMLINUX_SYMBOL() around C-visible symbols\n" . $herecurr); - } - # check for redundant bracing round if etc if ($line =~ /(^.*)\bif\b/ && $1 !~ /else\s*$/) { my ($level, $endln, @chunks) = @@ -5325,6 +6156,17 @@ sub process { "Prefer using '\"%s...\", __func__' to using '$context_function', this function's name, in a string\n" . $herecurr); } +# check for unnecessary function tracing like uses +# This does not use $logFunctions because there are many instances like +# 'dprintk(FOO, "%s()\n", __func__);' which do not match $logFunctions + if ($rawline =~ /^\+.*\([^"]*"$tracing_logging_tags{0,3}%s(?:\s*\(\s*\)\s*)?$tracing_logging_tags{0,3}(?:\\n)?"\s*,\s*__func__\s*\)\s*;/) { + if (WARN("TRACING_LOGGING", + "Unnecessary ftrace-like logging - prefer using ftrace\n" . $herecurr) && + $fix) { + fix_delete_line($fixlinenr, $rawline); + } + } + # check for spaces before a quoted newline if ($rawline =~ /^.*\".*\s\\n/) { if (WARN("QUOTED_WHITESPACE_BEFORE_NEWLINE", @@ -5336,15 +6178,29 @@ sub process { } # concatenated string without spaces between elements - if ($line =~ /$String[A-Z_]/ || $line =~ /[A-Za-z0-9_]$String/) { - CHK("CONCATENATED_STRING", - "Concatenated strings should use spaces between elements\n" . $herecurr); + if ($line =~ /$String[A-Z_]/ || + ($line =~ /([A-Za-z0-9_]+)$String/ && $1 !~ /^[Lu]$/)) { + if (CHK("CONCATENATED_STRING", + "Concatenated strings should use spaces between elements\n" . $herecurr) && + $fix) { + while ($line =~ /($String)/g) { + my $extracted_string = substr($rawline, $-[0], $+[0] - $-[0]); + $fixed[$fixlinenr] =~ s/\Q$extracted_string\E([A-Za-z0-9_])/$extracted_string $1/; + $fixed[$fixlinenr] =~ s/([A-Za-z0-9_])\Q$extracted_string\E/$1 $extracted_string/; + } + } } # uncoalesced string fragments - if ($line =~ /$String\s*"/) { - WARN("STRING_FRAGMENTS", - "Consecutive strings are generally better as a single string\n" . $herecurr); + if ($line =~ /$String\s*[Lu]?"/) { + if (WARN("STRING_FRAGMENTS", + "Consecutive strings are generally better as a single string\n" . $herecurr) && + $fix) { + while ($line =~ /($String)(?=\s*")/g) { + my $extracted_string = substr($rawline, $-[0], $+[0] - $-[0]); + $fixed[$fixlinenr] =~ s/\Q$extracted_string\E\s*"/substr($extracted_string, 0, -1)/e; + } + } } # check for non-standard and hex prefixed decimal printf formats @@ -5380,9 +6236,14 @@ sub process { # warn about #if 0 if ($line =~ /^.\s*\#\s*if\s+0\b/) { - CHK("REDUNDANT_CODE", - "if this code is redundant consider removing it\n" . - $herecurr); + WARN("IF_0", + "Consider removing the code enclosed by this #if 0 and its #endif\n" . $herecurr); + } + +# warn about #if 1 + if ($line =~ /^.\s*\#\s*if\s+1\b/) { + WARN("IF_1", + "Consider removing the #if 1 and its #endif\n" . $herecurr); } # check for needless "if (<foo>) fn(<foo>)" uses @@ -5429,7 +6290,8 @@ sub process { my ($s, $c) = ctx_statement_block($linenr - 3, $realcnt, 0); # print("line: <$line>\nprevline: <$prevline>\ns: <$s>\nc: <$c>\n\n\n"); - if ($s =~ /(?:^|\n)[ \+]\s*(?:$Type\s*)?\Q$testval\E\s*=\s*(?:\([^\)]*\)\s*)?\s*(?:devm_)?(?:[kv][czm]alloc(?:_node|_array)?\b|kstrdup|kmemdup|(?:dev_)?alloc_skb)/) { + if ($s =~ /(?:^|\n)[ \+]\s*(?:$Type\s*)?\Q$testval\E\s*=\s*(?:\([^\)]*\)\s*)?\s*$allocFunctions\s*\(/ && + $s !~ /\b__GFP_NOWARN\b/ ) { WARN("OOM_MESSAGE", "Possible unnecessary 'out of memory' message\n" . $hereprev); } @@ -5452,8 +6314,30 @@ sub process { "Avoid logging continuation uses where feasible\n" . $herecurr); } +# check for unnecessary use of %h[xudi] and %hh[xudi] in logging functions + if (defined $stat && + $line =~ /\b$logFunctions\s*\(/ && + index($stat, '"') >= 0) { + my $lc = $stat =~ tr@\n@@; + $lc = $lc + $linenr; + my $stat_real = get_stat_real($linenr, $lc); + pos($stat_real) = index($stat_real, '"'); + while ($stat_real =~ /[^\"%]*(%[\#\d\.\*\-]*(h+)[idux])/g) { + my $pspec = $1; + my $h = $2; + my $lineoff = substr($stat_real, 0, $-[1]) =~ tr@\n@@; + if (WARN("UNNECESSARY_MODIFIER", + "Integer promotion: Using '$h' in '$pspec' is unnecessary\n" . "$here\n$stat_real\n") && + $fix && $fixed[$fixlinenr + $lineoff] =~ /^\+/) { + my $nspec = $pspec; + $nspec =~ s/h//g; + $fixed[$fixlinenr + $lineoff] =~ s/\Q$pspec\E/$nspec/; + } + } + } + # check for mask then right shift without a parentheses - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $line =~ /$LvalOrFunc\s*\&\s*($LvalOrFunc)\s*>>/ && $4 !~ /^\&/) { # $LvalOrFunc may be &foo, ignore if so WARN("MASK_THEN_SHIFT", @@ -5461,7 +6345,7 @@ sub process { } # check for pointer comparisons to NULL - if ($^V && $^V ge 5.10.0) { + if ($perl_version_ok) { while ($line =~ /\b$LvalOrFunc\s*(==|\!=)\s*NULL\b/g) { my $val = $1; my $equal = "!"; @@ -5550,7 +6434,7 @@ sub process { # ignore udelay's < 10, however if (! ($delay < 10) ) { CHK("USLEEP_RANGE", - "usleep_range is preferred over udelay; see Documentation/timers/timers-howto.txt\n" . $herecurr); + "usleep_range is preferred over udelay; see Documentation/timers/timers-howto.rst\n" . $herecurr); } if ($delay > 2000) { WARN("LONG_UDELAY", @@ -5562,7 +6446,7 @@ sub process { if ($line =~ /\bmsleep\s*\((\d+)\);/) { if ($1 < 20) { WARN("MSLEEP", - "msleep < 20ms can sleep for up to 20ms; see Documentation/timers/timers-howto.txt\n" . $herecurr); + "msleep < 20ms can sleep for up to 20ms; see Documentation/timers/timers-howto.rst\n" . $herecurr); } } @@ -5610,8 +6494,7 @@ sub process { my $barriers = qr{ mb| rmb| - wmb| - read_barrier_depends + wmb }x; my $barrier_stems = qr{ mb__before_atomic| @@ -5652,10 +6535,12 @@ sub process { } } -# check for smp_read_barrier_depends and read_barrier_depends - if (!$file && $line =~ /\b(smp_|)read_barrier_depends\s*\(/) { - WARN("READ_BARRIER_DEPENDS", - "$1read_barrier_depends should only be used in READ_ONCE or DEC Alpha code\n" . $herecurr); +# check for data_race without a comment. + if ($line =~ /\bdata_race\s*\(/) { + if (!ctx_has_comment($first_line, $linenr)) { + WARN("DATA_RACE", + "data_race without comment\n" . $herecurr); + } } # check of hardware specific defines @@ -5697,43 +6582,73 @@ sub process { } } -# Check for __attribute__ packed, prefer __packed - if ($realfile !~ m@\binclude/uapi/@ && - $line =~ /\b__attribute__\s*\(\s*\(.*\bpacked\b/) { - WARN("PREFER_PACKED", - "__packed is preferred over __attribute__((packed))\n" . $herecurr); - } - -# Check for __attribute__ aligned, prefer __aligned - if ($realfile !~ m@\binclude/uapi/@ && - $line =~ /\b__attribute__\s*\(\s*\(.*aligned/) { - WARN("PREFER_ALIGNED", - "__aligned(size) is preferred over __attribute__((aligned(size)))\n" . $herecurr); - } - -# Check for __attribute__ format(printf, prefer __printf +# Check for compiler attributes if ($realfile !~ m@\binclude/uapi/@ && - $line =~ /\b__attribute__\s*\(\s*\(\s*format\s*\(\s*printf/) { - if (WARN("PREFER_PRINTF", - "__printf(string-index, first-to-check) is preferred over __attribute__((format(printf, string-index, first-to-check)))\n" . $herecurr) && - $fix) { - $fixed[$fixlinenr] =~ s/\b__attribute__\s*\(\s*\(\s*format\s*\(\s*printf\s*,\s*(.*)\)\s*\)\s*\)/"__printf(" . trim($1) . ")"/ex; - + $rawline =~ /\b__attribute__\s*\(\s*($balanced_parens)\s*\)/) { + my $attr = $1; + $attr =~ s/\s*\(\s*(.*)\)\s*/$1/; + + my %attr_list = ( + "alias" => "__alias", + "aligned" => "__aligned", + "always_inline" => "__always_inline", + "assume_aligned" => "__assume_aligned", + "cold" => "__cold", + "const" => "__attribute_const__", + "copy" => "__copy", + "designated_init" => "__designated_init", + "externally_visible" => "__visible", + "format" => "printf|scanf", + "gnu_inline" => "__gnu_inline", + "malloc" => "__malloc", + "mode" => "__mode", + "no_caller_saved_registers" => "__no_caller_saved_registers", + "noclone" => "__noclone", + "noinline" => "noinline", + "nonstring" => "__nonstring", + "noreturn" => "__noreturn", + "packed" => "__packed", + "pure" => "__pure", + "section" => "__section", + "used" => "__used", + "weak" => "__weak" + ); + + while ($attr =~ /\s*(\w+)\s*(${balanced_parens})?/g) { + my $orig_attr = $1; + my $params = ''; + $params = $2 if defined($2); + my $curr_attr = $orig_attr; + $curr_attr =~ s/^[\s_]+|[\s_]+$//g; + if (exists($attr_list{$curr_attr})) { + my $new = $attr_list{$curr_attr}; + if ($curr_attr eq "format" && $params) { + $params =~ /^\s*\(\s*(\w+)\s*,\s*(.*)/; + $new = "__$1\($2"; + } else { + $new = "$new$params"; + } + if (WARN("PREFER_DEFINED_ATTRIBUTE_MACRO", + "Prefer $new over __attribute__(($orig_attr$params))\n" . $herecurr) && + $fix) { + my $remove = "\Q$orig_attr\E" . '\s*' . "\Q$params\E" . '(?:\s*,\s*)?'; + $fixed[$fixlinenr] =~ s/$remove//; + $fixed[$fixlinenr] =~ s/\b__attribute__/$new __attribute__/; + $fixed[$fixlinenr] =~ s/\}\Q$new\E/} $new/; + $fixed[$fixlinenr] =~ s/ __attribute__\s*\(\s*\(\s*\)\s*\)//; + } + } } - } -# Check for __attribute__ format(scanf, prefer __scanf - if ($realfile !~ m@\binclude/uapi/@ && - $line =~ /\b__attribute__\s*\(\s*\(\s*format\s*\(\s*scanf\b/) { - if (WARN("PREFER_SCANF", - "__scanf(string-index, first-to-check) is preferred over __attribute__((format(scanf, string-index, first-to-check)))\n" . $herecurr) && - $fix) { - $fixed[$fixlinenr] =~ s/\b__attribute__\s*\(\s*\(\s*format\s*\(\s*scanf\s*,\s*(.*)\)\s*\)\s*\)/"__scanf(" . trim($1) . ")"/ex; + # Check for __attribute__ unused, prefer __always_unused or __maybe_unused + if ($attr =~ /^_*unused/) { + WARN("PREFER_DEFINED_ATTRIBUTE_MACRO", + "__always_unused or __maybe_unused is preferred over __attribute__((__unused__))\n" . $herecurr); } } # Check for __attribute__ weak, or __weak declarations (may have link issues) - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $line =~ /(?:$Declare|$DeclareMisordered)\s*$Ident\s*$balanced_parens\s*(?:$Attribute)?\s*;/ && ($line =~ /\b__attribute__\s*\(\s*\(.*\bweak\b/ || $line =~ /\b__weak\b/)) { @@ -5764,18 +6679,18 @@ sub process { if ($line =~ /(\(\s*$C90_int_types\s*\)\s*)($Constant)\b/) { my $cast = $1; my $const = $2; + my $suffix = ""; + my $newconst = $const; + $newconst =~ s/${Int_type}$//; + $suffix .= 'U' if ($cast =~ /\bunsigned\b/); + if ($cast =~ /\blong\s+long\b/) { + $suffix .= 'LL'; + } elsif ($cast =~ /\blong\b/) { + $suffix .= 'L'; + } if (WARN("TYPECAST_INT_CONSTANT", - "Unnecessary typecast of c90 int constant\n" . $herecurr) && + "Unnecessary typecast of c90 int constant - '$cast$const' could be '$const$suffix'\n" . $herecurr) && $fix) { - my $suffix = ""; - my $newconst = $const; - $newconst =~ s/${Int_type}$//; - $suffix .= 'U' if ($cast =~ /\bunsigned\b/); - if ($cast =~ /\blong\s+long\b/) { - $suffix .= 'LL'; - } elsif ($cast =~ /\blong\b/) { - $suffix .= 'L'; - } $fixed[$fixlinenr] =~ s/\Q$cast\E$const\b/$newconst$suffix/; } } @@ -5815,25 +6730,31 @@ sub process { } # check for vsprintf extension %p<foo> misuses - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+(?![^\{]*\{\s*).*\b(\w+)\s*\(.*$String\s*,/s && $1 !~ /^_*volatile_*$/) { - my $specifier; - my $extension; - my $bad_specifier = ""; my $stat_real; my $lc = $stat =~ tr@\n@@; $lc = $lc + $linenr; for (my $count = $linenr; $count <= $lc; $count++) { + my $specifier; + my $extension; + my $qualifier; + my $bad_specifier = ""; my $fmt = get_quoted_string($lines[$count - 1], raw_line($count, 0)); $fmt =~ s/%%//g; - while ($fmt =~ /(\%[\*\d\.]*p(\w))/g) { + while ($fmt =~ /(\%[\*\d\.]*p(\w)(\w*))/g) { $specifier = $1; $extension = $2; - if ($extension !~ /[SsBKRraEhMmIiUDdgVCbGNOx]/) { + $qualifier = $3; + if ($extension !~ /[4SsBKRraEehMmIiUDdgVCbGNOxtf]/ || + ($extension eq "f" && + defined $qualifier && $qualifier !~ /^w/) || + ($extension eq "4" && + defined $qualifier && $qualifier !~ /^cc/)) { $bad_specifier = $specifier; last; } @@ -5850,7 +6771,6 @@ sub process { my $ext_type = "Invalid"; my $use = ""; if ($bad_specifier =~ /p[Ff]/) { - $ext_type = "Deprecated"; $use = " - use %pS instead"; $use =~ s/pS/ps/ if ($bad_specifier =~ /pf/); } @@ -5862,7 +6782,7 @@ sub process { } # Check for misused memsets - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+(?:.*?)\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*$FuncArg\s*\)/) { @@ -5880,7 +6800,7 @@ sub process { } # Check for memcpy(foo, bar, ETH_ALEN) that could be ether_addr_copy(foo, bar) -# if ($^V && $^V ge 5.10.0 && +# if ($perl_version_ok && # defined $stat && # $stat =~ /^\+(?:.*?)\bmemcpy\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { # if (WARN("PREFER_ETHER_ADDR_COPY", @@ -5891,7 +6811,7 @@ sub process { # } # Check for memcmp(foo, bar, ETH_ALEN) that could be ether_addr_equal*(foo, bar) -# if ($^V && $^V ge 5.10.0 && +# if ($perl_version_ok && # defined $stat && # $stat =~ /^\+(?:.*?)\bmemcmp\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { # WARN("PREFER_ETHER_ADDR_EQUAL", @@ -5900,7 +6820,7 @@ sub process { # check for memset(foo, 0x0, ETH_ALEN) that could be eth_zero_addr # check for memset(foo, 0xFF, ETH_ALEN) that could be eth_broadcast_addr -# if ($^V && $^V ge 5.10.0 && +# if ($perl_version_ok && # defined $stat && # $stat =~ /^\+(?:.*?)\bmemset\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\,\s*ETH_ALEN\s*\)/) { # @@ -5921,8 +6841,14 @@ sub process { # } # } +# strlcpy uses that should likely be strscpy + if ($line =~ /\bstrlcpy\s*\(/) { + WARN("STRLCPY", + "Prefer strscpy over strlcpy - see: https://lore.kernel.org/r/CAHk-=wgfRnXz0W3D37d01q3JFkr_i_uTL=V6A6G1oUZcprmknw\@mail.gmail.com/\n" . $herecurr); + } + # typecasts on min/max could be min_t/max_t - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+(?:.*?)\b(min|max)\s*\(\s*$FuncArg\s*,\s*$FuncArg\s*\)/) { if (defined $2 || defined $7) { @@ -5946,23 +6872,23 @@ sub process { } # check usleep_range arguments - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+(?:.*?)\busleep_range\s*\(\s*($FuncArg)\s*,\s*($FuncArg)\s*\)/) { my $min = $1; my $max = $7; if ($min eq $max) { WARN("USLEEP_RANGE", - "usleep_range should not use min == max args; see Documentation/timers/timers-howto.txt\n" . "$here\n$stat\n"); + "usleep_range should not use min == max args; see Documentation/timers/timers-howto.rst\n" . "$here\n$stat\n"); } elsif ($min =~ /^\d+$/ && $max =~ /^\d+$/ && $min > $max) { WARN("USLEEP_RANGE", - "usleep_range args reversed, use min then max; see Documentation/timers/timers-howto.txt\n" . "$here\n$stat\n"); + "usleep_range args reversed, use min then max; see Documentation/timers/timers-howto.rst\n" . "$here\n$stat\n"); } } # check for naked sscanf - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $line =~ /\bsscanf\b/ && ($stat !~ /$Ident\s*=\s*sscanf\s*$balanced_parens/ && @@ -5976,7 +6902,7 @@ sub process { } # check for simple sscanf that should be kstrto<foo> - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $line =~ /\bsscanf\b/) { my $lc = $stat =~ tr@\n@@; @@ -6014,8 +6940,7 @@ sub process { if (defined $cond) { substr($s, 0, length($cond), ''); } - if ($s =~ /^\s*;/ && - $function_name ne 'uninitialized_var') + if ($s =~ /^\s*;/) { WARN("AVOID_EXTERNS", "externs should be avoided in .c files\n" . $herecurr); @@ -6048,7 +6973,7 @@ sub process { } # check for function definitions - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^.\s*(?:$Storage\s+)?$Type\s*($Ident)\s*$balanced_parens\s*{/s) { $context_function = $1; @@ -6076,26 +7001,26 @@ sub process { if (!grep(/$name/, @setup_docs)) { CHK("UNDOCUMENTED_SETUP", - "__setup appears un-documented -- check Documentation/admin-guide/kernel-parameters.rst\n" . $herecurr); + "__setup appears un-documented -- check Documentation/admin-guide/kernel-parameters.txt\n" . $herecurr); } } -# check for pointless casting of kmalloc return - if ($line =~ /\*\s*\)\s*[kv][czm]alloc(_node){0,1}\b/) { +# check for pointless casting of alloc functions + if ($line =~ /\*\s*\)\s*$allocFunctions\b/) { WARN("UNNECESSARY_CASTS", "unnecessary cast may hide bugs, see http://c-faq.com/malloc/mallocnocast.html\n" . $herecurr); } # alloc style # p = alloc(sizeof(struct foo), ...) should be p = alloc(sizeof(*p), ...) - if ($^V && $^V ge 5.10.0 && - $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*([kv][mz]alloc(?:_node)?)\s*\(\s*(sizeof\s*\(\s*struct\s+$Lval\s*\))/) { + if ($perl_version_ok && + $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*((?:kv|k|v)[mz]alloc(?:_node)?)\s*\(\s*(sizeof\s*\(\s*struct\s+$Lval\s*\))/) { CHK("ALLOC_SIZEOF_STRUCT", "Prefer $3(sizeof(*$1)...) over $3($4...)\n" . $herecurr); } # check for k[mz]alloc with multiplies that could be kmalloc_array/kcalloc - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+\s*($Lval)\s*\=\s*(?:$balanced_parens)?\s*(k[mz]alloc)\s*\(\s*($FuncArg)\s*\*\s*($FuncArg)\s*,/) { my $oldfunc = $3; @@ -6124,14 +7049,15 @@ sub process { } # check for krealloc arg reuse - if ($^V && $^V ge 5.10.0 && - $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*krealloc\s*\(\s*\1\s*,/) { + if ($perl_version_ok && + $line =~ /\b($Lval)\s*\=\s*(?:$balanced_parens)?\s*krealloc\s*\(\s*($Lval)\s*,/ && + $1 eq $3) { WARN("KREALLOC_ARG_REUSE", "Reusing the krealloc arg is almost always a bug\n" . $herecurr); } # check for alloc argument mismatch - if ($line =~ /\b(kcalloc|kmalloc_array)\s*\(\s*sizeof\b/) { + if ($line =~ /\b((?:devm_)?(?:kcalloc|kmalloc_array))\s*\(\s*sizeof\b/) { WARN("ALLOC_ARRAY_ARGS", "$1 uses number as first arg, sizeof is generally wrong\n" . $herecurr); } @@ -6157,43 +7083,46 @@ sub process { } } +# check for IS_ENABLED() without CONFIG_<FOO> ($rawline for comments too) + if ($rawline =~ /\bIS_ENABLED\s*\(\s*(\w+)\s*\)/ && $1 !~ /^${CONFIG_}/) { + WARN("IS_ENABLED_CONFIG", + "IS_ENABLED($1) is normally used as IS_ENABLED(${CONFIG_}$1)\n" . $herecurr); + } + # check for #if defined CONFIG_<FOO> || defined CONFIG_<FOO>_MODULE - if ($line =~ /^\+\s*#\s*if\s+defined(?:\s*\(?\s*|\s+)(CONFIG_[A-Z_]+)\s*\)?\s*\|\|\s*defined(?:\s*\(?\s*|\s+)\1_MODULE\s*\)?\s*$/) { + if ($line =~ /^\+\s*#\s*if\s+defined(?:\s*\(?\s*|\s+)(${CONFIG_}[A-Z_]+)\s*\)?\s*\|\|\s*defined(?:\s*\(?\s*|\s+)\1_MODULE\s*\)?\s*$/) { my $config = $1; if (WARN("PREFER_IS_ENABLED", - "Prefer IS_ENABLED(<FOO>) to CONFIG_<FOO> || CONFIG_<FOO>_MODULE\n" . $herecurr) && + "Prefer IS_ENABLED(<FOO>) to ${CONFIG_}<FOO> || ${CONFIG_}<FOO>_MODULE\n" . $herecurr) && $fix) { $fixed[$fixlinenr] = "\+#if IS_ENABLED($config)"; } } -# check for case / default statements not preceded by break/fallthrough/switch - if ($line =~ /^.\s*(?:case\s+(?:$Ident|$Constant)\s*|default):/) { - my $has_break = 0; - my $has_statement = 0; - my $count = 0; - my $prevline = $linenr; - while ($prevline > 1 && ($file || $count < 3) && !$has_break) { - $prevline--; - my $rline = $rawlines[$prevline - 1]; - my $fline = $lines[$prevline - 1]; - last if ($fline =~ /^\@\@/); - next if ($fline =~ /^\-/); - next if ($fline =~ /^.(?:\s*(?:case\s+(?:$Ident|$Constant)[\s$;]*|default):[\s$;]*)*$/); - $has_break = 1 if ($rline =~ /fall[\s_-]*(through|thru)/i); - next if ($fline =~ /^.[\s$;]*$/); - $has_statement = 1; - $count++; - $has_break = 1 if ($fline =~ /\bswitch\b|\b(?:break\s*;[\s$;]*$|exit\s*\(\b|return\b|goto\b|continue\b)/); - } - if (!$has_break && $has_statement) { - WARN("MISSING_BREAK", - "Possible switch case/default not preceded by break or fallthrough comment\n" . $herecurr); +# check for /* fallthrough */ like comment, prefer fallthrough; + my @fallthroughs = ( + 'fallthrough', + '@fallthrough@', + 'lint -fallthrough[ \t]*', + 'intentional(?:ly)?[ \t]*fall(?:(?:s | |-)[Tt]|t)hr(?:ough|u|ew)', + '(?:else,?\s*)?FALL(?:S | |-)?THR(?:OUGH|U|EW)[ \t.!]*(?:-[^\n\r]*)?', + 'Fall(?:(?:s | |-)[Tt]|t)hr(?:ough|u|ew)[ \t.!]*(?:-[^\n\r]*)?', + 'fall(?:s | |-)?thr(?:ough|u|ew)[ \t.!]*(?:-[^\n\r]*)?', + ); + if ($raw_comment ne '') { + foreach my $ft (@fallthroughs) { + if ($raw_comment =~ /$ft/) { + my $msg_level = \&WARN; + $msg_level = \&CHK if ($file); + &{$msg_level}("PREFER_FALLTHROUGH", + "Prefer 'fallthrough;' over fallthrough comment\n" . $herecurr); + last; + } } } # check for switch/default statements without a break; - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /^\+[$;\s]*(?:case[$;\s]+\w+[$;\s]*:[$;\s]*|)*[$;\s]*\bdefault[$;\s]*:[$;\s]*;/g) { my $cnt = statement_rawlines($stat); @@ -6251,12 +7180,6 @@ sub process { } } -# check for bool bitfields - if ($sline =~ /^.\s+bool\s*$Ident\s*:\s*\d+\s*;/) { - WARN("BOOL_BITFIELD", - "Avoid using bool as bitfield. Prefer bool bitfields as unsigned int or u<8|16|32>\n" . $herecurr); - } - # check for semaphores initialized locked if ($line =~ /^.\s*sema_init.+,\W?0\W?\)/) { WARN("CONSIDER_COMPLETION", @@ -6275,9 +7198,24 @@ sub process { "please use device_initcall() or more appropriate function instead of __initcall() (see include/linux/init.h)\n" . $herecurr); } +# check for spin_is_locked(), suggest lockdep instead + if ($line =~ /\bspin_is_locked\(/) { + WARN("USE_LOCKDEP", + "Where possible, use lockdep_assert_held instead of assertions based on spin_is_locked\n" . $herecurr); + } + +# check for deprecated apis + if ($line =~ /\b($deprecated_apis_search)\b\s*\(/) { + my $deprecated_api = $1; + my $new_api = $deprecated_apis{$deprecated_api}; + WARN("DEPRECATED_API", + "Deprecated use of '$deprecated_api', prefer '$new_api' instead\n" . $herecurr); + } + # check for various structs that are normally const (ops, kgdb, device_tree) # and avoid what seem like struct definitions 'struct foo {' - if ($line !~ /\bconst\b/ && + if (defined($const_structs) && + $line !~ /\bconst\b/ && $line =~ /\bstruct\s+($const_structs)\b(?!\s*\{)/) { WARN("CONST_STRUCT", "struct $1 should normally be const\n" . $herecurr); @@ -6285,12 +7223,14 @@ sub process { # use of NR_CPUS is usually wrong # ignore definitions of NR_CPUS and usage to define arrays as likely right +# ignore designated initializers using NR_CPUS if ($line =~ /\bNR_CPUS\b/ && $line !~ /^.\s*\s*#\s*if\b.*\bNR_CPUS\b/ && $line !~ /^.\s*\s*#\s*define\b.*\bNR_CPUS\b/ && $line !~ /^.\s*$Declare\s.*\[[^\]]*NR_CPUS[^\]]*\]/ && $line !~ /\[[^\]]*\.\.\.[^\]]*NR_CPUS[^\]]*\]/ && - $line !~ /\[[^\]]*NR_CPUS[^\]]*\.\.\.[^\]]*\]/) + $line !~ /\[[^\]]*NR_CPUS[^\]]*\.\.\.[^\]]*\]/ && + $line !~ /^.\s*\.\w+\s*=\s*.*\bNR_CPUS\b/) { WARN("NR_CPUS", "usage of NR_CPUS is often wrong - consider using cpu_possible(), num_possible_cpus(), for_each_possible_cpu(), etc\n" . $herecurr); @@ -6303,12 +7243,29 @@ sub process { } # likely/unlikely comparisons similar to "(likely(foo) > 0)" - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && $line =~ /\b((?:un)?likely)\s*\(\s*$FuncArg\s*\)\s*$Compare/) { WARN("LIKELY_MISUSE", "Using $1 should generally have parentheses around the comparison\n" . $herecurr); } +# return sysfs_emit(foo, fmt, ...) fmt without newline + if ($line =~ /\breturn\s+sysfs_emit\s*\(\s*$FuncArg\s*,\s*($String)/ && + substr($rawline, $-[6], $+[6] - $-[6]) !~ /\\n"$/) { + my $offset = $+[6] - 1; + if (WARN("SYSFS_EMIT", + "return sysfs_emit(...) formats should include a terminating newline\n" . $herecurr) && + $fix) { + substr($fixed[$fixlinenr], $offset, 0) = '\\n'; + } + } + +# nested likely/unlikely calls + if ($line =~ /\b(?:(?:un)?likely)\s*\(\s*!?\s*(IS_ERR(?:_OR_NULL|_VALUE)?|WARN)/) { + WARN("LIKELY_MISUSE", + "nested (un)?likely() calls, $1 already uses unlikely() internally\n" . $herecurr); + } + # whine mightly about in_atomic if ($line =~ /\bin_atomic\s*\(/) { if ($realfile =~ m@^drivers/@) { @@ -6320,12 +7277,6 @@ sub process { } } -# check for mutex_trylock_recursive usage - if ($line =~ /mutex_trylock_recursive/) { - ERROR("LOCKING", - "recursive locking is bad, do not use this ever.\n" . $herecurr); - } - # check for lockdep_set_novalidate_class if ($line =~ /^.\s*lockdep_set_novalidate_class\s*\(/ || $line =~ /__lockdep_no_validate__\s*\)/ ) { @@ -6346,7 +7297,7 @@ sub process { # check for DEVICE_ATTR uses that could be DEVICE_ATTR_<FOO> # and whether or not function naming is typical and if # DEVICE_ATTR permissions uses are unusual too - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $stat =~ /\bDEVICE_ATTR\s*\(\s*(\w+)\s*,\s*\(?\s*(\s*(?:${multi_mode_perms_string_search}|0[0-7]{3,3})\s*)\s*\)?\s*,\s*(\w+)\s*,\s*(\w+)\s*\)/) { my $var = $1; @@ -6406,7 +7357,7 @@ sub process { # specific definition of not visible in sysfs. # o Ignore proc_create*(...) uses with a decimal 0 permission as that means # use the default permissions - if ($^V && $^V ge 5.10.0 && + if ($perl_version_ok && defined $stat && $line =~ /$mode_perms_search/) { foreach my $entry (@mode_permission_funcs) { @@ -6468,6 +7419,12 @@ sub process { "unknown module license " . $extracted_string . "\n" . $herecurr); } } + +# check for sysctl duplicate constants + if ($line =~ /\.extra[12]\s*=\s*&(zero|one|int_max)\b/) { + WARN("DUPLICATED_SYSCTL_CONST", + "duplicated sysctl range checking value '$1', consider using the shared one in include/linux/sysctl.h\n" . $herecurr); + } } # If we have no input at all, then there is nothing to report on @@ -6482,7 +7439,7 @@ sub process { exit(0); } - # This is not a patch, and we are are in 'no-patch' mode so + # This is not a patch, and we are in 'no-patch' mode so # just keep quiet. if (!$chk_patch && !$is_patch) { exit(0); @@ -6492,9 +7449,38 @@ sub process { ERROR("NOT_UNIFIED_DIFF", "Does not appear to be a unified-diff format patch\n"); } - if ($is_patch && $has_commit_log && $chk_signoff && $signoff == 0) { - ERROR("MISSING_SIGN_OFF", - "Missing Signed-off-by: line(s)\n"); + if ($is_patch && $has_commit_log && $chk_signoff) { + if ($signoff == 0) { + ERROR("MISSING_SIGN_OFF", + "Missing Signed-off-by: line(s)\n"); + } elsif ($authorsignoff != 1) { + # authorsignoff values: + # 0 -> missing sign off + # 1 -> sign off identical + # 2 -> names and addresses match, comments mismatch + # 3 -> addresses match, names different + # 4 -> names match, addresses different + # 5 -> names match, addresses excluding subaddress details (refer RFC 5233) match + + my $sob_msg = "'From: $author' != 'Signed-off-by: $author_sob'"; + + if ($authorsignoff == 0) { + ERROR("NO_AUTHOR_SIGN_OFF", + "Missing Signed-off-by: line by nominal patch author '$author'\n"); + } elsif ($authorsignoff == 2) { + CHK("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email comments mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 3) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email name mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 4) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email address mismatch: $sob_msg\n"); + } elsif ($authorsignoff == 5) { + WARN("FROM_SIGN_OFF_MISMATCH", + "From:/Signed-off-by: email subaddress mismatch: $sob_msg\n"); + } + } } print report_dump(); diff --git a/tools/checkpatch.pl-update b/tools/checkpatch.pl-update index 2462038..58d1f40 100755 --- a/tools/checkpatch.pl-update +++ b/tools/checkpatch.pl-update @@ -66,6 +66,10 @@ download() { # Then any data it uses. url="${CGIT_URL}/scripts/spelling.txt?h=v${version}" wget "${url}" -O spelling.txt + + # Then any data it uses. + url="${CGIT_URL}/scripts/const_structs.checkpatch?h=v${version}" + wget "${url}" -O const_structs.checkpatch } main() { diff --git a/tools/clang-format.py b/tools/clang-format.py index 958f543..1d5f1ac 100755 --- a/tools/clang-format.py +++ b/tools/clang-format.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +15,6 @@ """Wrapper to run git-clang-format and parse its output.""" -from __future__ import print_function - import argparse import os import sys @@ -29,7 +26,7 @@ del _path # We have to import our local modules after the sys.path tweak. We can't use # relative imports because this is an executable program, not a module. -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position,import-error import rh.shell import rh.utils @@ -78,16 +75,39 @@ def main(argv): if opts.extensions: cmd.extend(['--extensions', opts.extensions]) if not opts.working_tree: - cmd.extend(['%s^' % opts.commit, opts.commit]) + cmd.extend([f'{opts.commit}^', opts.commit]) cmd.extend(['--'] + opts.files) # Fail gracefully if clang-format itself aborts/fails. - try: - result = rh.utils.run(cmd, capture_output=True) - except rh.utils.CalledProcessError as e: - print('clang-format failed:\n%s' % (e,), file=sys.stderr) - print('\nPlease report this to the clang team.', file=sys.stderr) - return 1 + result = rh.utils.run(cmd, capture_output=True, check=False) + # Newer versions of git-clang-format will exit 1 when it worked. Assume a + # real failure is any exit code above 1, or any time stderr is used, or if + # it exited 1 and produce useful format diffs to stdout. If it exited 0, + # then assume all is well and we'll attempt to parse its output below. + ret_code = None + if (result.returncode > 1 or result.stderr or + (result.stdout and result.returncode)): + # Apply fix if the flag is set and clang-format shows it is fixible. + if opts.fix and result.stdout and result.returncode: + result = rh.utils.run(['git', 'apply'], input=result.stdout, + check=False) + ret_code = result.returncode + if ret_code: + print('Error: Unable to automatically fix things.\n' + ' Make sure your checkout is clean first.\n' + ' If you have multiple commits, you might have to ' + 'manually rebase your tree first.', + file=sys.stderr) + + else: # Regular clang-format aborts/fails. + print(f'clang-format failed:\ncmd: {result.cmdstr}\n' + f'stdout:\n{result.stdout}\n', file=sys.stderr) + if result.returncode > 1 or result.stderr: + print('\nPlease report this to the clang team.\n', + f'stderr:\n{result.stderr}', file=sys.stderr) + ret_code = 1 + + return ret_code stdout = result.stdout if stdout.rstrip('\n') == 'no modified files to format': @@ -113,9 +133,9 @@ def main(argv): else: print('The following files have formatting errors:') for filename in diff_filenames: - print('\t%s' % filename) - print('You can try to fix this by running:\n%s --fix %s' % - (sys.argv[0], rh.shell.cmd_to_str(argv))) + print(f'\t{filename}') + print('You can try to fix this by running:\n' + f'{sys.argv[0]} --fix {rh.shell.cmd_to_str(argv)}') return 1 return 0 diff --git a/tools/clang-format_unittest.py b/tools/clang-format_unittest.py new file mode 100755 index 0000000..8dfb5cf --- /dev/null +++ b/tools/clang-format_unittest.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# Copyright 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittests for clang-format.""" + +import contextlib +from pathlib import Path +import sys +import tempfile +import unittest + + +DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(DIR.parent)) + +# We have to import our local modules after the sys.path tweak. We can't use +# relative imports because this is an executable program, not a module. +# pylint: disable=wrong-import-position,import-error +import rh.utils + + +CLANG_FORMAT = DIR / 'clang-format.py' + + +@contextlib.contextmanager +def git_clang_format(data: str): + """Create a fake git-clang-format script.""" + with tempfile.TemporaryDirectory(prefix='repohooks-tests') as tempdir: + tempdir = Path(tempdir) + script = tempdir / 'git-clang-format-fake.sh' + script.write_text(f'#!/bin/sh\n{data}', encoding='utf-8') + script.chmod(0o755) + yield script + + +def run_clang_format(script, args, **kwargs): + """Helper to run clang-format.py with fake git-clang-format script.""" + kwargs.setdefault('capture_output', True) + return rh.utils.run( + [CLANG_FORMAT, '--git-clang-format', script] + args, **kwargs) + + +class GitClangFormatExit(unittest.TestCase): + """Test git-clang-format parsing.""" + + def test_diff_exit_0_no_output(self): + """Test exit 0 w/no output.""" + with git_clang_format('exit 0') as script: + result = run_clang_format(script, ['--working-tree']) + self.assertEqual(result.stdout, '') + + def test_diff_exit_0_stderr(self): + """Test exit 0 w/stderr output.""" + with git_clang_format('echo bad >&2; exit 0') as script: + with self.assertRaises(rh.utils.CalledProcessError) as e: + run_clang_format(script, ['--working-tree']) + self.assertIn('clang-format failed', e.exception.stderr) + + def test_diff_exit_1_no_output(self): + """Test exit 1 w/no output.""" + with git_clang_format('exit 1') as script: + result = run_clang_format(script, ['--working-tree']) + self.assertEqual(result.stdout, '') + + def test_diff_exit_1_output(self): + """Test exit 1 with output.""" + with git_clang_format('echo bad; exit 1') as script: + with self.assertRaises(rh.utils.CalledProcessError) as e: + run_clang_format(script, ['--working-tree']) + self.assertIn('clang-format failed', e.exception.stderr) + + def test_diff_exit_1_stderr(self): + """Test exit 1 w/stderr.""" + with git_clang_format('echo bad >&2; exit 1') as script: + with self.assertRaises(rh.utils.CalledProcessError) as e: + run_clang_format(script, ['--working-tree']) + self.assertIn('clang-format failed', e.exception.stderr) + + def test_diff_exit_2(self): + """Test exit 2.""" + with git_clang_format('exit 2') as script: + with self.assertRaises(rh.utils.CalledProcessError) as e: + run_clang_format(script, ['--working-tree']) + self.assertIn('clang-format failed', e.exception.stderr) + + def test_fix_exit_1_output(self): + """Test fix with incorrect patch syntax.""" + with git_clang_format('echo bad patch; exit 1') as script: + with self.assertRaises(rh.utils.CalledProcessError) as e: + run_clang_format(script, ['--working-tree', '--fix']) + self.assertIn('Error: Unable to automatically fix things', + e.exception.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/const_structs.checkpatch b/tools/const_structs.checkpatch new file mode 100644 index 0000000..1eeb7b4 --- /dev/null +++ b/tools/const_structs.checkpatch @@ -0,0 +1,95 @@ +acpi_dock_ops +address_space_operations +backlight_ops +block_device_operations +clk_ops +comedi_lrange +component_ops +dentry_operations +dev_pm_ops +dma_map_ops +driver_info +drm_connector_funcs +drm_encoder_funcs +drm_encoder_helper_funcs +dvb_frontend_ops +dvb_tuner_ops +ethtool_ops +extent_io_ops +fb_ops +file_lock_operations +file_operations +hv_ops +hwmon_ops +ib_device_ops +ide_dma_ops +ide_port_ops +ieee80211_ops +iio_buffer_setup_ops +inode_operations +intel_dvo_dev_ops +irq_domain_ops +item_operations +iwl_cfg +iwl_ops +kernel_param_ops +kgdb_arch +kgdb_io +kset_uevent_ops +lock_manager_operations +machine_desc +microcode_ops +mlxsw_reg_info +mtd_ooblayout_ops +mtrr_ops +nand_controller_ops +neigh_ops +net_device_ops +nft_expr_ops +nlmsvc_binding +nvkm_device_chip +of_device_id +pci_raw_ops +phy_ops +pinconf_ops +pinctrl_ops +pinmux_ops +pipe_buf_operations +platform_hibernation_ops +platform_suspend_ops +proc_ops +proto_ops +pwm_ops +regmap_access_table +regulator_ops +reset_control_ops +rpc_pipe_ops +rtc_class_ops +sd_desc +sdhci_ops +seq_operations +sirfsoc_padmux +snd_ac97_build_ops +snd_pcm_ops +snd_rawmidi_ops +snd_soc_component_driver +snd_soc_dai_ops +snd_soc_ops +soc_pcmcia_socket_ops +stacktrace_ops +sysfs_ops +tty_operations +uart_ops +usb_mon_operations +v4l2_ctrl_ops +v4l2_ioctl_ops +v4l2_subdev_core_ops +v4l2_subdev_internal_ops +v4l2_subdev_ops +v4l2_subdev_pad_ops +v4l2_subdev_video_ops +vb2_ops +vm_operations_struct +wacom_features +watchdog_ops +wd_ops diff --git a/tools/cpplint.py b/tools/cpplint.py index e99d661..c5db879 100755 --- a/tools/cpplint.py +++ b/tools/cpplint.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # pylint: skip-file # # Copyright (c) 2009 Google Inc. All rights reserved. @@ -45,30 +45,49 @@ same line, but it is far from perfect (in either direction). import codecs import copy import getopt +import glob +import itertools import math # for log import os import re import sre_compile import string import sys -import unicodedata import sysconfig +import unicodedata +import xml.etree.ElementTree + +# if empty, use defaults +_valid_extensions = set([]) + +__VERSION__ = '1.5.5' try: xrange # Python 2 except NameError: + # -- pylint: disable=redefined-builtin xrange = range # Python 3 _USAGE = """ -Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] +Syntax: cpplint.py [--verbose=#] [--output=emacs|eclipse|vs7|junit|sed|gsed] + [--filter=-x,+y,...] [--counting=total|toplevel|detailed] [--root=subdir] + [--repository=path] [--linelength=digits] [--headers=x,y,...] + [--recursive] + [--exclude=path] + [--extensions=hpp,cpp,...] + [--includeorder=default|standardcfirst] [--quiet] + [--version] <file> [file] ... + Style checker for C/C++ source files. + This is a fork of the Google style checker with minor extensions. + The style guidelines this tries to follow are those in - https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml + https://google.github.io/styleguide/cppguide.html Every problem is given a confidence score from 1-5, with 5 meaning we are certain of the problem, and 1 meaning it could be a legitimate construct. @@ -79,17 +98,27 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] suppresses errors of all categories on that line. The files passed in will be linted; at least one file must be provided. - Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the - extensions with the --extensions flag. + Default linted extensions are %s. + Other file types will be ignored. + Change the extensions with the --extensions flag. Flags: - output=vs7 + output=emacs|eclipse|vs7|junit|sed|gsed By default, the output is formatted to ease emacs parsing. Visual Studio - compatible output (vs7) may also be used. Other formats are unsupported. + compatible output (vs7) may also be used. Further support exists for + eclipse (eclipse), and JUnit (junit). XML parsers such as those used + in Jenkins and Bamboo may also be used. + The sed format outputs sed commands that should fix some of the errors. + Note that this requires gnu sed. If that is installed as gsed on your + system (common e.g. on macOS with homebrew) you can use the gsed output + format. Sed commands are written to stdout, not stderr, so you should be + able to pipe output straight to a shell to run the fixes. verbose=# Specify a number 0-5 to restrict errors to certain verbosity levels. + Errors with lower verbosity levels have lower confidence and are more + likely to be false positives. quiet Don't print anything if no errors are found. @@ -99,11 +128,11 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] error messages whose category names pass the filters will be printed. (Category names are printed with the message and look like "[whitespace/indent]".) Filters are evaluated left to right. - "-FOO" and "FOO" means "do not print categories that start with FOO". + "-FOO" means "do not print categories that start with FOO". "+FOO" means "do print categories that start with FOO". Examples: --filter=-whitespace,+whitespace/braces - --filter=whitespace,runtime/printf,+runtime/printf_format + --filter=-whitespace,-runtime/printf,+runtime/printf_format --filter=-,+build/include_what_you_use To see a list of all the categories used in cpplint, pass no arg: @@ -116,17 +145,41 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] also be printed. If 'detailed' is provided, then a count is provided for each category like 'build/class'. + repository=path + The top level directory of the repository, used to derive the header + guard CPP variable. By default, this is determined by searching for a + path that contains .git, .hg, or .svn. When this flag is specified, the + given path is used instead. This option allows the header guard CPP + variable to remain consistent even if members of a team have different + repository root directories (such as when checking out a subdirectory + with SVN). In addition, users of non-mainstream version control systems + can use this flag to ensure readable header guard CPP variables. + + Examples: + Assuming that Alice checks out ProjectName and Bob checks out + ProjectName/trunk and trunk contains src/chrome/ui/browser.h, then + with no --repository flag, the header guard CPP variable will be: + + Alice => TRUNK_SRC_CHROME_BROWSER_UI_BROWSER_H_ + Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ + + If Alice uses the --repository=trunk flag and Bob omits the flag or + uses --repository=. then the header guard CPP variable will be: + + Alice => SRC_CHROME_BROWSER_UI_BROWSER_H_ + Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ + root=subdir The root directory used for deriving header guard CPP variable. - By default, the header guard CPP variable is calculated as the relative - path to the directory that contains .git, .hg, or .svn. When this flag - is specified, the relative path is calculated from the specified - directory. If the specified directory does not exist, this flag is - ignored. + This directory is relative to the top level directory of the repository + which by default is determined by searching for a directory that contains + .git, .hg, or .svn but can also be controlled with the --repository flag. + If the specified directory does not exist, this flag is ignored. Examples: - Assuming that top/src/.git exists (and cwd=top/src), the header guard - CPP variables for top/src/chrome/browser/ui/browser.h are: + Assuming that src is the top level directory of the repository (and + cwd=top/src), the header guard CPP variables for + src/chrome/browser/ui/browser.h are: No flag => CHROME_BROWSER_UI_BROWSER_H_ --root=chrome => BROWSER_UI_BROWSER_H_ @@ -140,17 +193,45 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] Examples: --linelength=120 + recursive + Search for files to lint recursively. Each directory given in the list + of files to be linted is replaced by all files that descend from that + directory. Files with extensions not in the valid extensions list are + excluded. + + exclude=path + Exclude the given path from the list of files to be linted. Relative + paths are evaluated relative to the current directory and shell globbing + is performed. This flag can be provided multiple times to exclude + multiple files. + + Examples: + --exclude=one.cc + --exclude=src/*.cc + --exclude=src/*.cc --exclude=test/*.cc + extensions=extension,extension,... The allowed file extensions that cpplint will check Examples: - --extensions=hpp,cpp + --extensions=%s + + includeorder=default|standardcfirst + For the build/include_order rule, the default is to blindly assume angle + bracket includes with file extension are c-system-headers (default), + even knowing this will have false classifications. + The default is established at google. + standardcfirst means to instead use an allow-list of known c headers and + treat all others as separate group of "other system headers". The C headers + included are those of the C-standard lib and closely related ones. headers=x,y,... The header extensions that cpplint will treat as .h in checks. Values are automatically added to --extensions list. + (by default, only files with extensions %s will be assumed to be headers) Examples: + --headers=%s --headers=hpp,hxx --headers=hpp @@ -175,7 +256,7 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] "exclude_files" allows to specify a regular expression to be matched against a file name. If the expression matches, the file is skipped and not run - through liner. + through the linter. "linelength" allows to specify the allowed line length for the project. @@ -190,7 +271,7 @@ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] Example file: filter=-build/include_order,+build/include_alpha - exclude_files=.*\.cc + exclude_files=.*\\.cc The above example disables build/include_order warning and enables build/include_alpha as well as excludes all .cc from being @@ -213,9 +294,12 @@ _ERROR_CATEGORIES = [ 'build/forward_decl', 'build/header_guard', 'build/include', + 'build/include_subdir', 'build/include_alpha', 'build/include_order', 'build/include_what_you_use', + 'build/namespaces_headers', + 'build/namespaces_literals', 'build/namespaces', 'build/printf_format', 'build/storage_class', @@ -271,6 +355,13 @@ _ERROR_CATEGORIES = [ 'whitespace/todo', ] +# keywords to use with --outputs which generate stdout for machine processing +_MACHINE_OUTPUTS = [ + 'junit', + 'sed', + 'gsed' +] + # These error categories are no longer enforced by cpplint, but for backwards- # compatibility they may still appear in NOLINT comments. _LEGACY_ERROR_CATEGORIES = [ @@ -406,6 +497,18 @@ _CPP_HEADERS = frozenset([ 'utility', 'valarray', 'vector', + # 17.6.1.2 C++14 headers + 'shared_mutex', + # 17.6.1.2 C++17 headers + 'any', + 'charconv', + 'codecvt', + 'execution', + 'filesystem', + 'memory_resource', + 'optional', + 'string_view', + 'variant', # 17.6.1.2 C++ headers for C library facilities 'cassert', 'ccomplex', @@ -435,6 +538,186 @@ _CPP_HEADERS = frozenset([ 'cwctype', ]) +# C headers +_C_HEADERS = frozenset([ + # System C headers + 'assert.h', + 'complex.h', + 'ctype.h', + 'errno.h', + 'fenv.h', + 'float.h', + 'inttypes.h', + 'iso646.h', + 'limits.h', + 'locale.h', + 'math.h', + 'setjmp.h', + 'signal.h', + 'stdalign.h', + 'stdarg.h', + 'stdatomic.h', + 'stdbool.h', + 'stddef.h', + 'stdint.h', + 'stdio.h', + 'stdlib.h', + 'stdnoreturn.h', + 'string.h', + 'tgmath.h', + 'threads.h', + 'time.h', + 'uchar.h', + 'wchar.h', + 'wctype.h', + # additional POSIX C headers + 'aio.h', + 'arpa/inet.h', + 'cpio.h', + 'dirent.h', + 'dlfcn.h', + 'fcntl.h', + 'fmtmsg.h', + 'fnmatch.h', + 'ftw.h', + 'glob.h', + 'grp.h', + 'iconv.h', + 'langinfo.h', + 'libgen.h', + 'monetary.h', + 'mqueue.h', + 'ndbm.h', + 'net/if.h', + 'netdb.h', + 'netinet/in.h', + 'netinet/tcp.h', + 'nl_types.h', + 'poll.h', + 'pthread.h', + 'pwd.h', + 'regex.h', + 'sched.h', + 'search.h', + 'semaphore.h', + 'setjmp.h', + 'signal.h', + 'spawn.h', + 'strings.h', + 'stropts.h', + 'syslog.h', + 'tar.h', + 'termios.h', + 'trace.h', + 'ulimit.h', + 'unistd.h', + 'utime.h', + 'utmpx.h', + 'wordexp.h', + # additional GNUlib headers + 'a.out.h', + 'aliases.h', + 'alloca.h', + 'ar.h', + 'argp.h', + 'argz.h', + 'byteswap.h', + 'crypt.h', + 'endian.h', + 'envz.h', + 'err.h', + 'error.h', + 'execinfo.h', + 'fpu_control.h', + 'fstab.h', + 'fts.h', + 'getopt.h', + 'gshadow.h', + 'ieee754.h', + 'ifaddrs.h', + 'libintl.h', + 'mcheck.h', + 'mntent.h', + 'obstack.h', + 'paths.h', + 'printf.h', + 'pty.h', + 'resolv.h', + 'shadow.h', + 'sysexits.h', + 'ttyent.h', + # Additional linux glibc headers + 'dlfcn.h', + 'elf.h', + 'features.h', + 'gconv.h', + 'gnu-versions.h', + 'lastlog.h', + 'libio.h', + 'link.h', + 'malloc.h', + 'memory.h', + 'netash/ash.h', + 'netatalk/at.h', + 'netax25/ax25.h', + 'neteconet/ec.h', + 'netipx/ipx.h', + 'netiucv/iucv.h', + 'netpacket/packet.h', + 'netrom/netrom.h', + 'netrose/rose.h', + 'nfs/nfs.h', + 'nl_types.h', + 'nss.h', + 're_comp.h', + 'regexp.h', + 'sched.h', + 'sgtty.h', + 'stab.h', + 'stdc-predef.h', + 'stdio_ext.h', + 'syscall.h', + 'termio.h', + 'thread_db.h', + 'ucontext.h', + 'ustat.h', + 'utmp.h', + 'values.h', + 'wait.h', + 'xlocale.h', + # Hardware specific headers + 'arm_neon.h', + 'emmintrin.h', + 'xmmintin.h', + ]) + +# Folders of C libraries so commonly used in C++, +# that they have parity with standard C libraries. +C_STANDARD_HEADER_FOLDERS = frozenset([ + # standard C library + "sys", + # glibc for linux + "arpa", + "asm-generic", + "bits", + "gnu", + "net", + "netinet", + "protocols", + "rpc", + "rpcsvc", + "scsi", + # linux kernel header + "drm", + "linux", + "misc", + "mtd", + "rdma", + "sound", + "video", + "xen", + ]) + # Type names _TYPES = re.compile( r'^(?:' @@ -458,7 +741,8 @@ _THIRD_PARTY_HEADERS_PATTERN = re.compile( r'^(?:[^/]*[A-Z][^/]*\.h|lua\.h|lauxlib\.h|lualib\.h)$') # Pattern for matching FileInfo.BaseName() against test file name -_TEST_FILE_SUFFIX = r'(_test|_unittest|_regtest)$' +_test_suffixes = ['_test', '_regtest', '_unittest'] +_TEST_FILE_SUFFIX = '(' + '|'.join(_test_suffixes) + r')$' # Pattern that matches only complete whitespace, possibly across multiple lines. _EMPTY_CONDITIONAL_BODY_PATTERN = re.compile(r'^\s*$', re.DOTALL) @@ -472,7 +756,7 @@ _CHECK_MACROS = [ ] # Replacement macros for CHECK/DCHECK/EXPECT_TRUE/EXPECT_FALSE -_CHECK_REPLACEMENT = dict([(m, {}) for m in _CHECK_MACROS]) +_CHECK_REPLACEMENT = dict([(macro_var, {}) for macro_var in _CHECK_MACROS]) for op, replacement in [('==', 'EQ'), ('!=', 'NE'), ('>=', 'GE'), ('>', 'GT'), @@ -520,9 +804,10 @@ _ALT_TOKEN_REPLACEMENT_PATTERN = re.compile( # _IncludeState.CheckNextIncludeOrder(). _C_SYS_HEADER = 1 _CPP_SYS_HEADER = 2 -_LIKELY_MY_HEADER = 3 -_POSSIBLE_MY_HEADER = 4 -_OTHER_HEADER = 5 +_OTHER_SYS_HEADER = 3 +_LIKELY_MY_HEADER = 4 +_POSSIBLE_MY_HEADER = 5 +_OTHER_HEADER = 6 # These constants define the current inline assembly state _NO_ASM = 0 # Outside of inline assembly block @@ -542,6 +827,22 @@ _SEARCH_C_FILE = re.compile(r'\b(?:LINT_C_FILE|' # Match string that indicates we're working on a Linux Kernel file. _SEARCH_KERNEL_FILE = re.compile(r'\b(?:LINT_KERNEL_FILE)') +# Commands for sed to fix the problem +_SED_FIXUPS = { + 'Remove spaces around =': r's/ = /=/', + 'Remove spaces around !=': r's/ != /!=/', + 'Remove space before ( in if (': r's/if (/if(/', + 'Remove space before ( in for (': r's/for (/for(/', + 'Remove space before ( in while (': r's/while (/while(/', + 'Remove space before ( in switch (': r's/switch (/switch(/', + 'Should have a space between // and comment': r's/\/\//\/\/ /', + 'Missing space before {': r's/\([^ ]\){/\1 {/', + 'Tab found, replace by spaces': r's/\t/ /g', + 'Line ends in whitespace. Consider deleting these extra spaces.': r's/\s*$//', + 'You don\'t need a ; after a }': r's/};/}/', + 'Missing space after ,': r's/,\([^ ]\)/, \1/g', +} + _regexp_compile_cache = {} # {str, set(int)}: a map from error categories to sets of linenumbers @@ -553,17 +854,55 @@ _error_suppressions = {} _root = None _root_debug = False +# The top level repository directory. If set, _root is calculated relative to +# this directory instead of the directory containing version control artifacts. +# This is set by the --repository flag. +_repository = None + +# Files to exclude from linting. This is set by the --exclude flag. +_excludes = None + +# Whether to supress all PrintInfo messages, UNRELATED to --quiet flag +_quiet = False + # The allowed line length of files. # This is set by --linelength flag. _line_length = 80 -# The allowed extensions for file names -# This is set by --extensions flag. -_valid_extensions = set(['cc', 'h', 'cpp', 'cu', 'cuh']) +# This allows to use different include order rule than default +_include_order = "default" + +try: + unicode +except NameError: + # -- pylint: disable=redefined-builtin + basestring = unicode = str + +try: + long +except NameError: + # -- pylint: disable=redefined-builtin + long = int + +if sys.version_info < (3,): + # -- pylint: disable=no-member + # BINARY_TYPE = str + itervalues = dict.itervalues + iteritems = dict.iteritems +else: + # BINARY_TYPE = bytes + itervalues = dict.values + iteritems = dict.items + +def unicode_escape_decode(x): + if sys.version_info < (3,): + return codecs.unicode_escape_decode(x)[0] + else: + return x # Treat all headers starting with 'h' equally: .h, .hpp, .hxx etc. # This is set by --headers flag. -_hpp_headers = set(['h']) +_hpp_headers = set([]) # {str, bool}: a map from error categories to booleans which indicate if the # category should be suppressed for every line. @@ -572,14 +911,47 @@ _global_error_suppressions = {} def ProcessHppHeadersOption(val): global _hpp_headers try: - _hpp_headers = set(val.split(',')) - # Automatically append to extensions list so it does not have to be set 2 times - _valid_extensions.update(_hpp_headers) + _hpp_headers = {ext.strip() for ext in val.split(',')} except ValueError: PrintUsage('Header extensions must be comma separated list.') +def ProcessIncludeOrderOption(val): + if val is None or val == "default": + pass + elif val == "standardcfirst": + global _include_order + _include_order = val + else: + PrintUsage('Invalid includeorder value %s. Expected default|standardcfirst') + def IsHeaderExtension(file_extension): - return file_extension in _hpp_headers + return file_extension in GetHeaderExtensions() + +def GetHeaderExtensions(): + if _hpp_headers: + return _hpp_headers + if _valid_extensions: + return {h for h in _valid_extensions if 'h' in h} + return set(['h', 'hh', 'hpp', 'hxx', 'h++', 'cuh']) + +# The allowed extensions for file names +# This is set by --extensions flag +def GetAllExtensions(): + return GetHeaderExtensions().union(_valid_extensions or set( + ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu'])) + +def ProcessExtensionsOption(val): + global _valid_extensions + try: + extensions = [ext.strip() for ext in val.split(',')] + _valid_extensions = set(extensions) + except ValueError: + PrintUsage('Extensions should be a comma-separated list of values;' + 'for example: extensions=hpp,cpp\n' + 'This could not be parsed: "%s"' % (val,)) + +def GetNonHeaderExtensions(): + return GetAllExtensions().difference(GetHeaderExtensions()) def ParseNolintSuppressions(filename, raw_line, linenum, error): """Updates the global list of line error-suppressions. @@ -692,7 +1064,7 @@ def Search(pattern, s): def _IsSourceExtension(s): """File extension (excluding dot) matches a source file extension.""" - return s in ('c', 'cc', 'cpp', 'cxx') + return s in GetNonHeaderExtensions() class _IncludeState(object): @@ -713,11 +1085,13 @@ class _IncludeState(object): _MY_H_SECTION = 1 _C_SECTION = 2 _CPP_SECTION = 3 - _OTHER_H_SECTION = 4 + _OTHER_SYS_SECTION = 4 + _OTHER_H_SECTION = 5 _TYPE_NAMES = { _C_SYS_HEADER: 'C system header', _CPP_SYS_HEADER: 'C++ system header', + _OTHER_SYS_HEADER: 'other system header', _LIKELY_MY_HEADER: 'header this file implements', _POSSIBLE_MY_HEADER: 'header this file may implement', _OTHER_HEADER: 'other header', @@ -727,11 +1101,14 @@ class _IncludeState(object): _MY_H_SECTION: 'a header this file implements', _C_SECTION: 'C system header', _CPP_SECTION: 'C++ system header', + _OTHER_SYS_SECTION: 'other system header', _OTHER_H_SECTION: 'other header', } def __init__(self): self.include_list = [[]] + self._section = None + self._last_header = None self.ResetSection('') def FindHeader(self, header): @@ -838,6 +1215,12 @@ class _IncludeState(object): else: self._last_header = '' return error_message + elif header_type == _OTHER_SYS_HEADER: + if self._section <= self._OTHER_SYS_SECTION: + self._section = self._OTHER_SYS_SECTION + else: + self._last_header = '' + return error_message elif header_type == _LIKELY_MY_HEADER: if self._section <= self._MY_H_SECTION: self._section = self._MY_H_SECTION @@ -876,9 +1259,18 @@ class _CppLintState(object): # output format: # "emacs" - format that emacs can parse (default) + # "eclipse" - format that eclipse can parse # "vs7" - format that Microsoft Visual Studio 7 can parse + # "junit" - format that Jenkins, Bamboo, etc can parse + # "sed" - returns a gnu sed command to fix the problem + # "gsed" - like sed, but names the command gsed, e.g. for macOS homebrew users self.output_format = 'emacs' + # For JUnit output, save errors and failures until the end so that they + # can be written into the XML + self._junit_errors = [] + self._junit_failures = [] + def SetOutputFormat(self, output_format): """Sets the output format for errors.""" self.output_format = output_format @@ -953,10 +1345,71 @@ class _CppLintState(object): def PrintErrorCounts(self): """Print a summary of errors by category, and the total.""" - for category, count in self.errors_by_category.iteritems(): - sys.stderr.write('Category \'%s\' errors found: %d\n' % + for category, count in sorted(iteritems(self.errors_by_category)): + self.PrintInfo('Category \'%s\' errors found: %d\n' % (category, count)) - sys.stdout.write('Total errors found: %d\n' % self.error_count) + if self.error_count > 0: + self.PrintInfo('Total errors found: %d\n' % self.error_count) + + def PrintInfo(self, message): + # _quiet does not represent --quiet flag. + # Hide infos from stdout to keep stdout pure for machine consumption + if not _quiet and self.output_format not in _MACHINE_OUTPUTS: + sys.stdout.write(message) + + def PrintError(self, message): + if self.output_format == 'junit': + self._junit_errors.append(message) + else: + sys.stderr.write(message) + + def AddJUnitFailure(self, filename, linenum, message, category, confidence): + self._junit_failures.append((filename, linenum, message, category, + confidence)) + + def FormatJUnitXML(self): + num_errors = len(self._junit_errors) + num_failures = len(self._junit_failures) + + testsuite = xml.etree.ElementTree.Element('testsuite') + testsuite.attrib['errors'] = str(num_errors) + testsuite.attrib['failures'] = str(num_failures) + testsuite.attrib['name'] = 'cpplint' + + if num_errors == 0 and num_failures == 0: + testsuite.attrib['tests'] = str(1) + xml.etree.ElementTree.SubElement(testsuite, 'testcase', name='passed') + + else: + testsuite.attrib['tests'] = str(num_errors + num_failures) + if num_errors > 0: + testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase') + testcase.attrib['name'] = 'errors' + error = xml.etree.ElementTree.SubElement(testcase, 'error') + error.text = '\n'.join(self._junit_errors) + if num_failures > 0: + # Group failures by file + failed_file_order = [] + failures_by_file = {} + for failure in self._junit_failures: + failed_file = failure[0] + if failed_file not in failed_file_order: + failed_file_order.append(failed_file) + failures_by_file[failed_file] = [] + failures_by_file[failed_file].append(failure) + # Create a testcase for each file + for failed_file in failed_file_order: + failures = failures_by_file[failed_file] + testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase') + testcase.attrib['name'] = failed_file + failure = xml.etree.ElementTree.SubElement(testcase, 'failure') + template = '{0}: {1} [{2}] [{3}]' + texts = [template.format(f[1], f[2], f[3], f[4]) for f in failures] + failure.text = '\n'.join(texts) + + xml_decl = '<?xml version="1.0" encoding="UTF-8" ?>\n' + return xml_decl + xml.etree.ElementTree.tostring(testsuite, 'utf-8').decode('utf-8') + _cpplint_state = _CppLintState() @@ -1110,12 +1563,12 @@ class FileInfo(object): return os.path.abspath(self._filename).replace('\\', '/') def RepositoryName(self): - """FullName after removing the local path to the repository. + r"""FullName after removing the local path to the repository. If we have a real absolute path name here we can try to do something smart: detecting the root of the checkout and truncating /path/to/checkout from the name so that we get header guards that don't include things like - "C:\Documents and Settings\..." or "/home/username/..." in them and thus + "C:\\Documents and Settings\\..." or "/home/username/..." in them and thus people on different computers who have checked the source out to different locations won't see bogus errors. """ @@ -1124,6 +1577,20 @@ class FileInfo(object): if os.path.exists(fullname): project_dir = os.path.dirname(fullname) + # If the user specified a repository path, it exists, and the file is + # contained in it, use the specified repository path + if _repository: + repo = FileInfo(_repository).FullName() + root_dir = project_dir + while os.path.exists(root_dir): + # allow case insensitive compare on Windows + if os.path.normcase(root_dir) == os.path.normcase(repo): + return os.path.relpath(fullname, root_dir).replace('\\', '/') + one_up_dir = os.path.dirname(root_dir) + if one_up_dir == root_dir: + break + root_dir = one_up_dir + if os.path.exists(os.path.join(project_dir, ".svn")): # If there's a .svn file in the current directory, we recursively look # up the directory tree for the top of the SVN checkout @@ -1174,7 +1641,7 @@ class FileInfo(object): return self.Split()[1] def Extension(self): - """File extension - text following the final period.""" + """File extension - text following the final period, includes that period.""" return self.Split()[2] def NoExtension(self): @@ -1239,15 +1706,25 @@ def Error(filename, linenum, category, confidence, message): if _ShouldPrintError(category, confidence, linenum): _cpplint_state.IncrementErrorCount(category) if _cpplint_state.output_format == 'vs7': - sys.stderr.write('%s(%s): error cpplint: [%s] %s [%d]\n' % ( + _cpplint_state.PrintError('%s(%s): error cpplint: [%s] %s [%d]\n' % ( filename, linenum, category, message, confidence)) elif _cpplint_state.output_format == 'eclipse': sys.stderr.write('%s:%s: warning: %s [%s] [%d]\n' % ( filename, linenum, message, category, confidence)) + elif _cpplint_state.output_format == 'junit': + _cpplint_state.AddJUnitFailure(filename, linenum, message, category, + confidence) + elif _cpplint_state.output_format in ['sed', 'gsed']: + if message in _SED_FIXUPS: + sys.stdout.write(_cpplint_state.output_format + " -i '%s%s' %s # %s [%s] [%d]\n" % ( + linenum, _SED_FIXUPS[message], filename, message, category, confidence)) + else: + sys.stderr.write('# %s:%s: "%s" [%s] [%d]\n' % ( + filename, linenum, message, category, confidence)) else: - sys.stderr.write('%s:%s: %s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - + final_message = '%s:%s: %s [%s] [%d]\n' % ( + filename, linenum, message, category, confidence) + sys.stderr.write(final_message) # Matches standard C++ escape sequences per 2.13.2.3 of the C++ standard. _RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile( @@ -1794,10 +2271,10 @@ def PathSplitToList(path): lst = [] while True: (head, tail) = os.path.split(path) - if head == path: # absolute paths end + if head == path: # absolute paths end lst.append(head) break - if tail == path: # relative paths end + if tail == path: # relative paths end lst.append(tail) break @@ -1832,7 +2309,7 @@ def GetHeaderGuardCPPVariable(filename): def FixupPathFromRoot(): if _root_debug: sys.stderr.write("\n_root fixup, _root = '%s', repository name = '%s'\n" - %(_root, fileinfo.RepositoryName())) + % (_root, fileinfo.RepositoryName())) # Process the file path with the --root flag if it was set. if not _root: @@ -1854,27 +2331,28 @@ def GetHeaderGuardCPPVariable(filename): if _root_debug: sys.stderr.write(("_root lstrip (maybe_path=%s, file_path_from_root=%s," + - " _root=%s)\n") %(maybe_path, file_path_from_root, _root)) + " _root=%s)\n") % (maybe_path, file_path_from_root, _root)) if maybe_path: return os.path.join(*maybe_path) # --root=.. , will prepend the outer directory to the header guard full_path = fileinfo.FullName() - root_abspath = os.path.abspath(_root) + # adapt slashes for windows + root_abspath = os.path.abspath(_root).replace('\\', '/') maybe_path = StripListPrefix(PathSplitToList(full_path), PathSplitToList(root_abspath)) if _root_debug: sys.stderr.write(("_root prepend (maybe_path=%s, full_path=%s, " + - "root_abspath=%s)\n") %(maybe_path, full_path, root_abspath)) + "root_abspath=%s)\n") % (maybe_path, full_path, root_abspath)) if maybe_path: return os.path.join(*maybe_path) if _root_debug: - sys.stderr.write("_root ignore, returning %s\n" %(file_path_from_root)) + sys.stderr.write("_root ignore, returning %s\n" % (file_path_from_root)) # --root=FAKE_DIR is ignored return file_path_from_root @@ -1906,6 +2384,11 @@ def CheckForHeaderGuard(filename, clean_lines, error): if Search(r'//\s*NOLINT\(build/header_guard\)', i): return + # Allow pragma once instead of header guards + for i in raw_lines: + if Search(r'^\s*#pragma\s+once', i): + return + cppvar = GetHeaderGuardCPPVariable(filename) ifndef = '' @@ -1982,28 +2465,36 @@ def CheckForHeaderGuard(filename, clean_lines, error): def CheckHeaderFileIncluded(filename, include_state, error): - """Logs an error if a .cc file does not include its header.""" + """Logs an error if a source file does not include its header.""" # Do not check test files fileinfo = FileInfo(filename) if Search(_TEST_FILE_SUFFIX, fileinfo.BaseName()): return - headerfile = filename[0:len(filename) - len(fileinfo.Extension())] + '.h' - if not os.path.exists(headerfile): - return - headername = FileInfo(headerfile).RepositoryName() - first_include = 0 - for section_list in include_state.include_list: - for f in section_list: - if headername in f[0] or f[0] in headername: - return - if not first_include: - first_include = f[1] + for ext in GetHeaderExtensions(): + basefilename = filename[0:len(filename) - len(fileinfo.Extension())] + headerfile = basefilename + '.' + ext + if not os.path.exists(headerfile): + continue + headername = FileInfo(headerfile).RepositoryName() + first_include = None + include_uses_unix_dir_aliases = False + for section_list in include_state.include_list: + for f in section_list: + include_text = f[0] + if "./" in include_text: + include_uses_unix_dir_aliases = True + if headername in include_text or include_text in headername: + return + if not first_include: + first_include = f[1] + + message = '%s should include its header file %s' % (fileinfo.RepositoryName(), headername) + if include_uses_unix_dir_aliases: + message += ". Relative paths like . and .. are not allowed." - error(filename, first_include, 'build/include', 5, - '%s should include its header file %s' % (fileinfo.RepositoryName(), - headername)) + error(filename, first_include, 'build/include', 5, message) def CheckForBadCharacters(filename, lines, error): @@ -2024,7 +2515,7 @@ def CheckForBadCharacters(filename, lines, error): error: The function to call with any errors found. """ for linenum, line in enumerate(lines): - if u'\ufffd' in line: + if unicode_escape_decode('\ufffd') in line: error(filename, linenum, 'readability/utf8', 5, 'Line contains invalid UTF-8 (or Unicode replacement character).') if '\0' in line: @@ -2653,8 +3144,8 @@ class NestingState(object): # class LOCKABLE API Object { # }; class_decl_match = Match( - r'^(\s*(?:template\s*<[\w\s<>,:]*>\s*)?' - r'(class|struct)\s+(?:[A-Z_]+\s+)*(\w+(?:::\w+)*))' + r'^(\s*(?:template\s*<[\w\s<>,:=]*>\s*)?' + r'(class|struct)\s+(?:[a-zA-Z0-9_]+\s+)*(\w+(?:::\w+)*))' r'(.*)$', line) if (class_decl_match and (not self.stack or self.stack[-1].open_parentheses == 0)): @@ -2902,6 +3393,7 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, constructor_args[i] = constructor_arg i += 1 + variadic_args = [arg for arg in constructor_args if '&&...' in arg] defaulted_args = [arg for arg in constructor_args if '=' in arg] noarg_constructor = (not constructor_args or # empty arg list # 'void' arg specifier @@ -2912,20 +3404,24 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, # all but at most one arg defaulted (len(constructor_args) >= 1 and not noarg_constructor and - len(defaulted_args) >= len(constructor_args) - 1)) + len(defaulted_args) >= len(constructor_args) - 1) or + # variadic arguments with zero or one argument + (len(constructor_args) <= 2 and + len(variadic_args) >= 1)) initializer_list_constructor = bool( onearg_constructor and Search(r'\bstd\s*::\s*initializer_list\b', constructor_args[0])) copy_constructor = bool( onearg_constructor and - Match(r'(const\s+)?%s(\s*<[^>]*>)?(\s+const)?\s*(?:<\w+>\s*)?&' + Match(r'((const\s+(volatile\s+)?)?|(volatile\s+(const\s+)?))?' + r'%s(\s*<[^>]*>)?(\s+const)?\s*(?:<\w+>\s*)?&' % re.escape(base_classname), constructor_args[0].strip())) if (not is_marked_explicit and onearg_constructor and not initializer_list_constructor and not copy_constructor): - if defaulted_args: + if defaulted_args or variadic_args: error(filename, linenum, 'runtime/explicit', 5, 'Constructors callable with one argument ' 'should be marked explicit.') @@ -2977,7 +3473,7 @@ def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error): # Note that we assume the contents of [] to be short enough that # they'll never need to wrap. if ( # Ignore control structures. - not Search(r'\b(if|for|while|switch|return|new|delete|catch|sizeof)\b', + not Search(r'\b(if|elif|for|while|switch|return|new|delete|catch|sizeof)\b', fncall) and # Ignore pointers/references to functions. not Search(r' \([^)]+\)\([^)]*(\)|,$)', fncall) and @@ -3090,7 +3586,7 @@ def CheckForFunctionLengths(filename, clean_lines, linenum, if Search(r'(;|})', start_line): # Declarations and trivial functions body_found = True break # ... ignore - elif Search(r'{', start_line): + if Search(r'{', start_line): body_found = True function = Search(r'((\w|:)*)\(', line).group(1) if Match(r'TEST', function): # Handle TEST... macros @@ -3283,9 +3779,10 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): # get rid of comments and strings line = clean_lines.elided[linenum] - # You shouldn't have spaces before your brackets, except maybe after - # 'delete []', 'return []() {};', or 'auto [abc, ...] = ...;'. - if Search(r'\w\s+\[', line) and not Search(r'(?:auto&?|delete|return)\s+\[', line): + # You shouldn't have spaces before your brackets, except for C++11 attributes + # or maybe after 'delete []', 'return []() {};', or 'auto [abc, ...] = ...;'. + if (Search(r'\w\s+\[(?!\[)', line) and + not Search(r'(?:auto&?|delete|return)\s+\[', line)): error(filename, linenum, 'whitespace/braces', 5, 'Extra space before [') @@ -3655,7 +4152,6 @@ def IsDecltype(clean_lines, linenum, column): return True return False - def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error): """Checks for additional blank line issues related to sections. @@ -3804,11 +4300,11 @@ def CheckBraces(filename, clean_lines, linenum, error): # its line, and the line after that should have an indent level equal to or # lower than the if. We also check for ambiguous if/else nesting without # braces. - if_else_match = Search(r'\b(if\s*\(|else\b)', line) + if_else_match = Search(r'\b(if\s*(|constexpr)\s*\(|else\b)', line) if if_else_match and not Match(r'\s*#', line): if_indent = GetIndentLevel(line) endline, endlinenum, endpos = line, linenum, if_else_match.end() - if_match = Search(r'\bif\s*\(', line) + if_match = Search(r'\bif\s*(|constexpr)\s*\(', line) if if_match: # This could be a multiline if condition, so find the end first. pos = if_match.end() - 1 @@ -4073,12 +4569,12 @@ def CheckEmptyBlockBody(filename, clean_lines, linenum, error): return if closing_linenum > opening_linenum: # Opening line after the {. Ignore comments here since we checked above. - body = list(opening_line[opening_pos+1:]) + bodylist = list(opening_line[opening_pos+1:]) # All lines until closing line, excluding closing line, with comments. - body.extend(clean_lines.raw_lines[opening_linenum+1:closing_linenum]) + bodylist.extend(clean_lines.raw_lines[opening_linenum+1:closing_linenum]) # Closing line before the }. Won't (and can't) have comments. - body.append(clean_lines.elided[closing_linenum][:closing_pos-1]) - body = '\n'.join(body) + bodylist.append(clean_lines.elided[closing_linenum][:closing_pos-1]) + body = '\n'.join(bodylist) else: # If statement has brackets and fits on a single line. body = opening_line[opening_pos+1:closing_pos-1] @@ -4302,7 +4798,7 @@ def GetLineWidth(line): is_low_surrogate = 0xDC00 <= ord(uc) <= 0xDFFF if not is_wide_build and is_low_surrogate: width -= 1 - + width += 1 return width else: @@ -4350,7 +4846,7 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, # if(match($0, " <<")) complain = 0; # if(match(prev, " +for \\(")) complain = 0; # if(prevodd && match(prevprev, " +for \\(")) complain = 0; - scope_or_label_pattern = r'\s*\w+\s*:\s*\\?$' + scope_or_label_pattern = r'\s*(?:public|private|protected|signals)(?:\s+(?:slots\s*)?)?:\s*\\?$' classinfo = nesting_state.InnermostClass() initial_spaces = 0 cleansed_line = clean_lines.elided[linenum] @@ -4390,16 +4886,23 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, # # The "$Id:...$" comment may also get very long without it being the # developers fault. + # + # Doxygen documentation copying can get pretty long when using an overloaded + # function declaration if (not line.startswith('#include') and not is_header_guard and not Match(r'^\s*//.*http(s?)://\S*$', line) and not Match(r'^\s*//\s*[^\s]*$', line) and - not Match(r'^// \$Id:.*#[0-9]+ \$$', line)): + not Match(r'^// \$Id:.*#[0-9]+ \$$', line) and + not Match(r'^\s*/// [@\\](copydoc|copydetails|copybrief) .*$', line)): line_width = GetLineWidth(line) if line_width > _line_length: error(filename, linenum, 'whitespace/line_length', 2, 'Lines should be <= %i characters long' % _line_length) if (cleansed_line.count(';') > 1 and + # allow simple single line lambdas + not Match(r'^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}\n\r]*\}', + line) and # for loops are allowed two ;'s (and may run over two lines). cleansed_line.find('for') == -1 and (GetPreviousNonBlankLine(clean_lines, linenum)[0].find('for') == -1 or @@ -4456,21 +4959,25 @@ def _DropCommonSuffixes(filename): Returns: The filename with the common suffix removed. """ - for suffix in ('test.cc', 'regtest.cc', 'unittest.cc', - 'inl.h', 'impl.h', 'internal.h'): + for suffix in itertools.chain( + ('%s.%s' % (test_suffix.lstrip('_'), ext) + for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions())), + ('%s.%s' % (suffix, ext) + for suffix, ext in itertools.product(['inl', 'imp', 'internal'], GetHeaderExtensions()))): if (filename.endswith(suffix) and len(filename) > len(suffix) and filename[-len(suffix) - 1] in ('-', '_')): return filename[:-len(suffix) - 1] return os.path.splitext(filename)[0] -def _ClassifyInclude(fileinfo, include, is_system): +def _ClassifyInclude(fileinfo, include, used_angle_brackets, include_order="default"): """Figures out what kind of header 'include' is. Args: fileinfo: The current file cpplint is running over. A FileInfo instance. include: The path to a #included file. - is_system: True if the #include used <> rather than "". + used_angle_brackets: True if the #include used <> rather than "". + include_order: "default" or other value allowed in program arguments Returns: One of the _XXX_HEADER constants. @@ -4480,6 +4987,8 @@ def _ClassifyInclude(fileinfo, include, is_system): _C_SYS_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'string', True) _CPP_SYS_HEADER + >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', True, "standardcfirst") + _OTHER_SYS_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', False) _LIKELY_MY_HEADER >>> _ClassifyInclude(FileInfo('foo/foo_unknown_extension.cc'), @@ -4490,13 +4999,23 @@ def _ClassifyInclude(fileinfo, include, is_system): """ # This is a list of all standard c++ header files, except # those already checked for above. - is_cpp_h = include in _CPP_HEADERS + is_cpp_header = include in _CPP_HEADERS + + # Mark include as C header if in list or in a known folder for standard-ish C headers. + is_std_c_header = (include_order == "default") or (include in _C_HEADERS + # additional linux glibc header folders + or Search(r'(?:%s)\/.*\.h' % "|".join(C_STANDARD_HEADER_FOLDERS), include)) + + # Headers with C++ extensions shouldn't be considered C system headers + is_system = used_angle_brackets and not os.path.splitext(include)[1] in ['.hpp', '.hxx', '.h++'] if is_system: - if is_cpp_h: + if is_cpp_header: return _CPP_SYS_HEADER - else: + if is_std_c_header: return _C_SYS_HEADER + else: + return _OTHER_SYS_HEADER # If the target file and the include we're checking share a # basename when we drop common extensions, and the include @@ -4504,9 +5023,11 @@ def _ClassifyInclude(fileinfo, include, is_system): target_dir, target_base = ( os.path.split(_DropCommonSuffixes(fileinfo.RepositoryName()))) include_dir, include_base = os.path.split(_DropCommonSuffixes(include)) + target_dir_pub = os.path.normpath(target_dir + '/../public') + target_dir_pub = target_dir_pub.replace('\\', '/') if target_base == include_base and ( include_dir == target_dir or - include_dir == os.path.normpath(target_dir + '/../public')): + include_dir == target_dir_pub): return _LIKELY_MY_HEADER # If the target and include share some initial basename @@ -4550,7 +5071,7 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): # naming convention but not the include convention. match = Match(r'#include\s*"([^/]+\.h)"', line) if match and not _THIRD_PARTY_HEADERS_PATTERN.match(match.group(1)): - error(filename, linenum, 'build/include', 4, + error(filename, linenum, 'build/include_subdir', 4, 'Include the directory when naming .h files') # we shouldn't include a file more than once. actually, there are a @@ -4559,17 +5080,34 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): match = _RE_PATTERN_INCLUDE.search(line) if match: include = match.group(2) - is_system = (match.group(1) == '<') + used_angle_brackets = (match.group(1) == '<') duplicate_line = include_state.FindHeader(include) if duplicate_line >= 0: error(filename, linenum, 'build/include', 4, '"%s" already included at %s:%s' % (include, filename, duplicate_line)) - elif (include.endswith('.cc') and + return + + for extension in GetNonHeaderExtensions(): + if (include.endswith('.' + extension) and os.path.dirname(fileinfo.RepositoryName()) != os.path.dirname(include)): - error(filename, linenum, 'build/include', 4, - 'Do not include .cc files from other packages') - elif not _THIRD_PARTY_HEADERS_PATTERN.match(include): + error(filename, linenum, 'build/include', 4, + 'Do not include .' + extension + ' files from other packages') + return + + # We DO want to include a 3rd party looking header if it matches the + # filename. Otherwise we get an erroneous error "...should include its + # header" error later. + third_src_header = False + for ext in GetHeaderExtensions(): + basefilename = filename[0:len(filename) - len(fileinfo.Extension())] + headerfile = basefilename + '.' + ext + headername = FileInfo(headerfile).RepositoryName() + if headername in include or include in headername: + third_src_header = True + break + + if third_src_header or not _THIRD_PARTY_HEADERS_PATTERN.match(include): include_state.include_list[-1].append((include, linenum)) # We want to ensure that headers appear in the right order: @@ -4584,7 +5122,7 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): # track of the highest type seen, and complains if we see a # lower type after that. error_message = include_state.CheckNextIncludeOrder( - _ClassifyInclude(fileinfo, include, is_system)) + _ClassifyInclude(fileinfo, include, used_angle_brackets, _include_order)) if error_message: error(filename, linenum, 'build/include_order', 4, '%s. Should be: %s.h, c system, c++ system, other.' % @@ -4623,7 +5161,7 @@ def _GetTextInside(text, start_pattern): # Give opening punctuations to get the matching close-punctuations. matching_punctuation = {'(': ')', '{': '}', '[': ']'} - closing_punctuation = set(matching_punctuation.itervalues()) + closing_punctuation = set(itervalues(matching_punctuation)) # Find the position to start extracting text. match = re.search(start_pattern, text, re.M) @@ -4717,8 +5255,6 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, if match: include_state.ResetSection(match.group(1)) - # Make Windows paths like Unix. - fullname = os.path.abspath(filename).replace('\\', '/') # Perform other checks now that we are sure that this is not an include line CheckCasts(filename, clean_lines, linenum, error) @@ -4786,9 +5322,14 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, % (match.group(1), match.group(2))) if Search(r'\busing namespace\b', line): - error(filename, linenum, 'build/namespaces', 5, - 'Do not use namespace using-directives. ' - 'Use using-declarations instead.') + if Search(r'\bliterals\b', line): + error(filename, linenum, 'build/namespaces_literals', 5, + 'Do not use namespace using-directives. ' + 'Use using-declarations instead.') + else: + error(filename, linenum, 'build/namespaces', 5, + 'Do not use namespace using-directives. ' + 'Use using-declarations instead.') # Detect variable-length arrays. match = Match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line) @@ -4835,7 +5376,7 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, if (IsHeaderExtension(file_extension) and Search(r'\bnamespace\s*{', line) and line[-1] != '\\'): - error(filename, linenum, 'build/namespaces', 4, + error(filename, linenum, 'build/namespaces_headers', 4, 'Do not use unnamed namespaces in header files. See ' 'https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces' ' for more information.') @@ -5212,7 +5753,7 @@ def CheckCasts(filename, clean_lines, linenum, error): if not expecting_function: CheckCStyleCast(filename, clean_lines, linenum, 'static_cast', - r'\((int|float|double|bool|char|u?int(16|32|64))\)', error) + r'\((int|float|double|bool|char|u?int(16|32|64)|size_t)\)', error) # This doesn't catch all cases. Consider (const char * const)"hello". # @@ -5365,11 +5906,11 @@ _HEADERS_CONTAINING_TEMPLATES = ( )), ('<limits>', ('numeric_limits',)), ('<list>', ('list',)), - ('<map>', ('map', 'multimap',)), + ('<map>', ('multimap',)), ('<memory>', ('allocator', 'make_shared', 'make_unique', 'shared_ptr', 'unique_ptr', 'weak_ptr')), ('<queue>', ('queue', 'priority_queue',)), - ('<set>', ('set', 'multiset',)), + ('<set>', ('multiset',)), ('<stack>', ('stack',)), ('<string>', ('char_traits', 'basic_string',)), ('<tuple>', ('tuple',)), @@ -5398,11 +5939,21 @@ _re_pattern_headers_maybe_templates = [] for _header, _templates in _HEADERS_MAYBE_TEMPLATES: for _template in _templates: # Match max<type>(..., ...), max(..., ...), but not foo->max, foo.max or - # type::max(). + # 'type::max()'. _re_pattern_headers_maybe_templates.append( (re.compile(r'[^>.]\b' + _template + r'(<.*?>)?\([^\)]'), _template, _header)) +# Match set<type>, but not foo->set<type>, foo.set<type> +_re_pattern_headers_maybe_templates.append( + (re.compile(r'[^>.]\bset\s*\<'), + 'set<>', + '<set>')) +# Match 'map<type> var' and 'std::map<type>(...)', but not 'map<type>(...)'' +_re_pattern_headers_maybe_templates.append( + (re.compile(r'(std\b::\bmap\s*\<)|(^(std\b::\b)map\b\(\s*\<)'), + 'map<>', + '<map>')) # Other scripts may reach in and modify this pattern. _re_pattern_templates = [] @@ -5435,7 +5986,7 @@ def FilesBelongToSameModule(filename_cc, filename_h): some false positives. This should be sufficiently rare in practice. Args: - filename_cc: is the path for the .cc file + filename_cc: is the path for the source (e.g. .cc) file filename_h: is the path for the header path Returns: @@ -5443,20 +5994,23 @@ def FilesBelongToSameModule(filename_cc, filename_h): bool: True if filename_cc and filename_h belong to the same module. string: the additional prefix needed to open the header file. """ + fileinfo_cc = FileInfo(filename_cc) + if not fileinfo_cc.Extension().lstrip('.') in GetNonHeaderExtensions(): + return (False, '') - fileinfo = FileInfo(filename_cc) - if not fileinfo.IsSource(): + fileinfo_h = FileInfo(filename_h) + if not IsHeaderExtension(fileinfo_h.Extension().lstrip('.')): return (False, '') - filename_cc = filename_cc[:-len(fileinfo.Extension())] - matched_test_suffix = Search(_TEST_FILE_SUFFIX, fileinfo.BaseName()) + + filename_cc = filename_cc[:-(len(fileinfo_cc.Extension()))] + matched_test_suffix = Search(_TEST_FILE_SUFFIX, fileinfo_cc.BaseName()) if matched_test_suffix: filename_cc = filename_cc[:-len(matched_test_suffix.group(1))] + filename_cc = filename_cc.replace('/public/', '/') filename_cc = filename_cc.replace('/internal/', '/') - if not filename_h.endswith('.h'): - return (False, '') - filename_h = filename_h[:-len('.h')] + filename_h = filename_h[:-(len(fileinfo_h.Extension()))] if filename_h.endswith('-inl'): filename_h = filename_h[:-len('-inl')] filename_h = filename_h.replace('/public/', '/') @@ -5482,18 +6036,19 @@ def UpdateIncludeState(filename, include_dict, io=codecs): """ headerfile = None try: - headerfile = io.open(filename, 'r', 'utf8', 'replace') + with io.open(filename, 'r', 'utf8', 'replace') as headerfile: + linenum = 0 + for line in headerfile: + linenum += 1 + clean_line = CleanseComments(line) + match = _RE_PATTERN_INCLUDE.search(clean_line) + if match: + include = match.group(2) + include_dict.setdefault(include, linenum) + return True except IOError: return False - linenum = 0 - for line in headerfile: - linenum += 1 - clean_line = CleanseComments(line) - match = _RE_PATTERN_INCLUDE.search(clean_line) - if match: - include = match.group(2) - include_dict.setdefault(include, linenum) - return True + def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, @@ -5571,7 +6126,7 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # include_dict is modified during iteration, so we iterate over a copy of # the keys. - header_keys = include_dict.keys() + header_keys = list(include_dict.keys()) for header in header_keys: (same_module, common_path) = FilesBelongToSameModule(abs_filename, header) fullpath = common_path + header @@ -5583,11 +6138,13 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # didn't include it in the .h file. # TODO(unknown): Do a better job of finding .h files so we are confident that # not having the .h file means there isn't one. - if filename.endswith('.cc') and not header_found: - return + if not header_found: + for extension in GetNonHeaderExtensions(): + if filename.endswith('.' + extension): + return # All the lines have been processed, report the errors found. - for required_header_unstripped in required: + for required_header_unstripped in sorted(required, key=required.__getitem__): template = required[required_header_unstripped][1] if required_header_unstripped.strip('<>"') not in include_dict: error(filename, required[required_header_unstripped][0], @@ -5726,11 +6283,9 @@ def IsBlockInNameSpace(nesting_state, is_forward_declaration): Whether or not the new block is directly in a namespace. """ if is_forward_declaration: - if len(nesting_state.stack) >= 1 and ( - isinstance(nesting_state.stack[-1], _NamespaceInfo)): - return True - else: - return False + return len(nesting_state.stack) >= 1 and ( + isinstance(nesting_state.stack[-1], _NamespaceInfo)) + return (len(nesting_state.stack) > 1 and nesting_state.stack[-1].check_namespace_indentation and @@ -5780,7 +6335,7 @@ def CheckItemIndentationInNamespace(filename, raw_lines_no_comments, linenum, def ProcessLine(filename, file_extension, clean_lines, line, include_state, function_state, nesting_state, error, - extra_check_functions=[]): + extra_check_functions=None): """Processes a single line in the file. Args: @@ -5819,8 +6374,9 @@ def ProcessLine(filename, file_extension, clean_lines, line, CheckMakePairUsesDeduction(filename, clean_lines, line, error) CheckRedundantVirtual(filename, clean_lines, line, error) CheckRedundantOverrideOrFinal(filename, clean_lines, line, error) - for check_fn in extra_check_functions: - check_fn(filename, clean_lines, line, error) + if extra_check_functions: + for check_fn in extra_check_functions: + check_fn(filename, clean_lines, line, error) def FlagCxx11Features(filename, clean_lines, linenum, error): """Flag those c++11 features that we only allow in certain places. @@ -5894,7 +6450,7 @@ def FlagCxx14Features(filename, clean_lines, linenum, error): def ProcessFileData(filename, file_extension, lines, error, - extra_check_functions=[]): + extra_check_functions=None): """Performs lint checks and reports any errors to the given error function. Args: @@ -5994,7 +6550,7 @@ def ProcessConfigOverrides(filename): if _cpplint_state.quiet: # Suppress "Ignoring file" warning when using --quiet. return False - sys.stderr.write('Ignoring "%s": file excluded by "%s". ' + _cpplint_state.PrintInfo('Ignoring "%s": file excluded by "%s". ' 'File path component "%s" matches ' 'pattern "%s"\n' % (filename, cfg_file, base_name, val)) @@ -6002,34 +6558,38 @@ def ProcessConfigOverrides(filename): elif name == 'linelength': global _line_length try: - _line_length = int(val) + _line_length = int(val) except ValueError: - sys.stderr.write('Line length must be numeric.') + _cpplint_state.PrintError('Line length must be numeric.') + elif name == 'extensions': + ProcessExtensionsOption(val) elif name == 'root': global _root # root directories are specified relative to CPPLINT.cfg dir. _root = os.path.join(os.path.dirname(cfg_file), val) elif name == 'headers': ProcessHppHeadersOption(val) + elif name == 'includeorder': + ProcessIncludeOrderOption(val) else: - sys.stderr.write( + _cpplint_state.PrintError( 'Invalid configuration option (%s) in file %s\n' % (name, cfg_file)) except IOError: - sys.stderr.write( + _cpplint_state.PrintError( "Skipping config file '%s': Can't open for reading\n" % cfg_file) keep_looking = False # Apply all the accumulated filters in reverse order (top-level directory # config options having the least priority). - for filter in reversed(cfg_filters): - _AddFilters(filter) + for cfg_filter in reversed(cfg_filters): + _AddFilters(cfg_filter) return True -def ProcessFile(filename, vlevel, extra_check_functions=[]): +def ProcessFile(filename, vlevel, extra_check_functions=None): """Does google-lint on a single file. Args: @@ -6067,7 +6627,8 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): codecs.getwriter('utf8'), 'replace').read().split('\n') else: - lines = codecs.open(filename, 'r', 'utf8', 'replace').read().split('\n') + with codecs.open(filename, 'r', 'utf8', 'replace') as target_file: + lines = target_file.read().split('\n') # Remove trailing '\r'. # The -1 accounts for the extra trailing blank line we get from split() @@ -6079,7 +6640,7 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): lf_lines.append(linenum + 1) except IOError: - sys.stderr.write( + _cpplint_state.PrintError( "Skipping input '%s': Can't open for reading\n" % filename) _RestoreFilters() return @@ -6089,9 +6650,9 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): # When reading from stdin, the extension is unknown, so no cpplint tests # should rely on the extension. - if filename != '-' and file_extension not in _valid_extensions: - sys.stderr.write('Ignoring %s; not a valid file name ' - '(%s)\n' % (filename, ', '.join(_valid_extensions))) + if filename != '-' and file_extension not in GetAllExtensions(): + _cpplint_state.PrintError('Ignoring %s; not a valid file name ' + '(%s)\n' % (filename, ', '.join(GetAllExtensions()))) else: ProcessFileData(filename, file_extension, lines, Error, extra_check_functions) @@ -6117,7 +6678,7 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): # Suppress printing anything if --quiet was passed unless the error # count has increased after processing this file. if not _cpplint_state.quiet or old_errors != _cpplint_state.error_count: - sys.stdout.write('Done processing %s\n' % filename) + _cpplint_state.PrintInfo('Done processing %s\n' % filename) _RestoreFilters() @@ -6127,12 +6688,21 @@ def PrintUsage(message): Args: message: The optional error message. """ - sys.stderr.write(_USAGE) + sys.stderr.write(_USAGE % (sorted(list(GetAllExtensions())), + ','.join(sorted(list(GetAllExtensions()))), + sorted(GetHeaderExtensions()), + ','.join(sorted(GetHeaderExtensions())))) + if message: sys.exit('\nFATAL ERROR: ' + message) else: - sys.exit(1) + sys.exit(0) +def PrintVersion(): + sys.stdout.write('Cpplint fork (https://github.com/cpplint/cpplint)\n') + sys.stdout.write('cpplint ' + __VERSION__ + '\n') + sys.stdout.write('Python ' + sys.version + '\n') + sys.exit(0) def PrintCategories(): """Prints a list of all the error-categories used by error messages. @@ -6156,12 +6726,18 @@ def ParseArguments(args): """ try: (opts, filenames) = getopt.getopt(args, '', ['help', 'output=', 'verbose=', + 'v=', + 'version', 'counting=', 'filter=', 'root=', + 'repository=', 'linelength=', 'extensions=', + 'exclude=', + 'recursive', 'headers=', + 'includeorder=', 'quiet']) except getopt.GetoptError: PrintUsage('Invalid arguments.') @@ -6171,17 +6747,21 @@ def ParseArguments(args): filters = '' quiet = _Quiet() counting_style = '' + recursive = False for (opt, val) in opts: if opt == '--help': PrintUsage(None) + if opt == '--version': + PrintVersion() elif opt == '--output': - if val not in ('emacs', 'vs7', 'eclipse'): - PrintUsage('The only allowed output formats are emacs, vs7 and eclipse.') + if val not in ('emacs', 'vs7', 'eclipse', 'junit', 'sed', 'gsed'): + PrintUsage('The only allowed output formats are emacs, vs7, eclipse ' + 'sed, gsed and junit.') output_format = val elif opt == '--quiet': quiet = True - elif opt == '--verbose': + elif opt == '--verbose' or opt == '--v': verbosity = int(val) elif opt == '--filter': filters = val @@ -6194,49 +6774,126 @@ def ParseArguments(args): elif opt == '--root': global _root _root = val + elif opt == '--repository': + global _repository + _repository = val elif opt == '--linelength': global _line_length try: - _line_length = int(val) + _line_length = int(val) except ValueError: - PrintUsage('Line length must be digits.') + PrintUsage('Line length must be digits.') + elif opt == '--exclude': + global _excludes + if not _excludes: + _excludes = set() + _excludes.update(glob.glob(val)) elif opt == '--extensions': - global _valid_extensions - try: - _valid_extensions = set(val.split(',')) - except ValueError: - PrintUsage('Extensions must be comma separated list.') + ProcessExtensionsOption(val) elif opt == '--headers': ProcessHppHeadersOption(val) + elif opt == '--recursive': + recursive = True + elif opt == '--includeorder': + ProcessIncludeOrderOption(val) if not filenames: PrintUsage('No files were specified.') + if recursive: + filenames = _ExpandDirectories(filenames) + + if _excludes: + filenames = _FilterExcludedFiles(filenames) + _SetOutputFormat(output_format) _SetQuiet(quiet) _SetVerboseLevel(verbosity) _SetFilters(filters) _SetCountingStyle(counting_style) + filenames.sort() return filenames +def _ExpandDirectories(filenames): + """Searches a list of filenames and replaces directories in the list with + all files descending from those directories. Files with extensions not in + the valid extensions list are excluded. -def main(): - filenames = ParseArguments(sys.argv[1:]) - - # Change stderr to write with replacement characters so we don't die - # if we try to print something containing non-ASCII characters. - sys.stderr = codecs.StreamReaderWriter(sys.stderr, - codecs.getreader('utf8'), - codecs.getwriter('utf8'), - 'replace') + Args: + filenames: A list of files or directories - _cpplint_state.ResetErrorCounts() + Returns: + A list of all files that are members of filenames or descended from a + directory in filenames + """ + expanded = set() for filename in filenames: - ProcessFile(filename, _cpplint_state.verbose_level) - # If --quiet is passed, suppress printing error count unless there are errors. - if not _cpplint_state.quiet or _cpplint_state.error_count > 0: - _cpplint_state.PrintErrorCounts() + if not os.path.isdir(filename): + expanded.add(filename) + continue + + for root, _, files in os.walk(filename): + for loopfile in files: + fullname = os.path.join(root, loopfile) + if fullname.startswith('.' + os.path.sep): + fullname = fullname[len('.' + os.path.sep):] + expanded.add(fullname) + + filtered = [] + for filename in expanded: + if os.path.splitext(filename)[1][1:] in GetAllExtensions(): + filtered.append(filename) + return filtered + +def _FilterExcludedFiles(fnames): + """Filters out files listed in the --exclude command line switch. File paths + in the switch are evaluated relative to the current working directory + """ + exclude_paths = [os.path.abspath(f) for f in _excludes] + # because globbing does not work recursively, exclude all subpath of all excluded entries + return [f for f in fnames + if not any(e for e in exclude_paths + if _IsParentOrSame(e, os.path.abspath(f)))] + +def _IsParentOrSame(parent, child): + """Return true if child is subdirectory of parent. + Assumes both paths are absolute and don't contain symlinks. + """ + parent = os.path.normpath(parent) + child = os.path.normpath(child) + if parent == child: + return True + + prefix = os.path.commonprefix([parent, child]) + if prefix != parent: + return False + # Note: os.path.commonprefix operates on character basis, so + # take extra care of situations like '/foo/ba' and '/foo/bar/baz' + child_suffix = child[len(prefix):] + child_suffix = child_suffix.lstrip(os.sep) + return child == os.path.join(prefix, child_suffix) + +def main(): + filenames = ParseArguments(sys.argv[1:]) + backup_err = sys.stderr + try: + # Change stderr to write with replacement characters so we don't die + # if we try to print something containing non-ASCII characters. + sys.stderr = codecs.StreamReader(sys.stderr, 'replace') + + _cpplint_state.ResetErrorCounts() + for filename in filenames: + ProcessFile(filename, _cpplint_state.verbose_level) + # If --quiet is passed, suppress printing error count unless there are errors. + if not _cpplint_state.quiet or _cpplint_state.error_count > 0: + _cpplint_state.PrintErrorCounts() + + if _cpplint_state.output_format == 'junit': + sys.stderr.write(_cpplint_state.FormatJUnitXML()) + + finally: + sys.stderr = backup_err sys.exit(_cpplint_state.error_count > 0) diff --git a/tools/cpplint.py-update b/tools/cpplint.py-update index 4af4389..3d32330 100755 --- a/tools/cpplint.py-update +++ b/tools/cpplint.py-update @@ -15,7 +15,10 @@ set -eu -GITHUB_URL="https://github.com/google/styleguide/raw/gh-pages" +# The outdated Google version that only supports Python 2. +GITHUB_URL="https://github.com/google/styleguide/raw/gh-pages/cpplint" +# The forked version with Python 3 support. +GITHUB_URL="https://github.com/cpplint/cpplint/raw/develop" SCRIPT_DIR="$(dirname "$(readlink -f -- "$0")")" usage() { @@ -46,8 +49,11 @@ main() { # Download cpplint.py from upstream. local cpplint_py="${SCRIPT_DIR}/cpplint.py" - wget "${GITHUB_URL}/cpplint/cpplint.py" -O "${cpplint_py}" - sed -i '2i# pylint: skip-file' "${cpplint_py}" + wget "${GITHUB_URL}/cpplint.py" -O "${cpplint_py}" + sed -i \ + -e '1s|python$|python3|' \ + -e '2i# pylint: skip-file' \ + "${cpplint_py}" chmod +x "${cpplint_py}" } diff --git a/tools/google-java-format.py b/tools/google-java-format.py index 5a537c0..fcb5521 100755 --- a/tools/google-java-format.py +++ b/tools/google-java-format.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +15,10 @@ """Wrapper to run google-java-format to check for any malformatted changes.""" -from __future__ import print_function - import argparse import os +import shutil import sys -from distutils.spawn import find_executable _path = os.path.realpath(__file__ + '/../..') if sys.path[0] != _path: @@ -63,40 +60,30 @@ def main(argv): parser = get_parser() opts = parser.parse_args(argv) - # google-java-format-diff.py looks for google-java-format in $PATH, so find - # the parent dir up front and inject it into $PATH when launching it. - # TODO: Pass the path in directly once this issue is resolved: - # https://github.com/google/google-java-format/issues/108 - format_path = find_executable(opts.google_java_format) + format_path = shutil.which(opts.google_java_format) if not format_path: - print('Unable to find google-java-format at %s' % - opts.google_java_format) + print( + f'Unable to find google-java-format at: {opts.google_java_format}', + file=sys.stderr + ) return 1 - extra_env = { - 'PATH': '%s%s%s' % (os.path.dirname(format_path), - os.pathsep, - os.environ['PATH']) - } - # TODO: Delegate to the tool once this issue is resolved: # https://github.com/google/google-java-format/issues/107 - diff_cmd = ['git', 'diff', '--no-ext-diff', '-U0', '%s^!' % opts.commit] + diff_cmd = ['git', 'diff', '--no-ext-diff', '-U0', f'{opts.commit}^!'] diff_cmd.extend(['--'] + opts.files) diff = rh.utils.run(diff_cmd, capture_output=True).stdout - cmd = [opts.google_java_format_diff, '-p1', '--aosp'] + cmd = [opts.google_java_format_diff, '-p1', '--aosp', '-b', format_path] if opts.fix: cmd.extend(['-i']) if not opts.sort_imports: cmd.extend(['--skip-sorting-imports']) - stdout = rh.utils.run(cmd, input=diff, capture_output=True, - extra_env=extra_env).stdout + stdout = rh.utils.run(cmd, input=diff, capture_output=True).stdout if stdout: print('One or more files in your commit have Java formatting errors.') - print('You can run `%s --fix %s` to fix this' % - (sys.argv[0], rh.shell.cmd_to_str(argv))) + print(f'You can run: {sys.argv[0]} --fix {rh.shell.cmd_to_str(argv)}') return 1 return 0 diff --git a/tools/pylint.py b/tools/pylint.py index 8af1adc..3fbb148 100755 --- a/tools/pylint.py +++ b/tools/pylint.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- # Copyright 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +15,6 @@ """Wrapper to run pylint with the right settings.""" -from __future__ import print_function - import argparse import errno import os @@ -26,8 +23,8 @@ import sys import subprocess -assert (sys.version_info.major, sys.version_info.minor) >= (3, 5), ( - 'Python 3.5 or newer is required; found %s' % (sys.version,)) +assert (sys.version_info.major, sys.version_info.minor) >= (3, 6), ( + f'Python 3.6 or newer is required; found {sys.version}') DEFAULT_PYLINTRC_PATH = os.path.join( @@ -40,8 +37,8 @@ def is_pylint3(pylint): result = subprocess.run([pylint, '--version'], stdout=subprocess.PIPE, check=True) if b'Python 3' not in result.stdout: - print('%s: unable to locate a Python 3 version of pylint; Python 3 ' - 'support cannot be guaranteed' % (__file__,), file=sys.stderr) + print(f'{__file__}: unable to locate a Python 3 version of pylint; ' + 'Python 3 support cannot be guaranteed', file=sys.stderr) return False return True @@ -58,8 +55,8 @@ def find_pylint3(): # If there's no pylint, give up. if not shutil.which('pylint'): - print('%s: unable to locate pylint; please install:\n' - 'sudo apt-get install pylint' % (__file__,), file=sys.stderr) + print(f'{__file__}: unable to locate pylint; please install:\n' + 'sudo apt-get install pylint', file=sys.stderr) sys.exit(1) return 'pylint' @@ -106,7 +103,7 @@ def main(argv): pylintrc = DEFAULT_PYLINTRC_PATH # If we pass a non-existent rcfile to pylint, it'll happily ignore # it. - assert os.path.exists(pylintrc), 'Could not find %s' % pylintrc + assert os.path.exists(pylintrc), f'Could not find {pylintrc}' cmd += ['--rcfile', pylintrc] cmd += unknown + opts.files @@ -119,10 +116,10 @@ def main(argv): return 0 except OSError as e: if e.errno == errno.ENOENT: - print('%s: unable to run `%s`: %s' % (__file__, cmd[0], e), + print(f'{__file__}: unable to run `{cmd[0]}`: {e}', file=sys.stderr) - print('%s: Try installing pylint: sudo apt-get install %s' % - (__file__, os.path.basename(cmd[0])), file=sys.stderr) + print(f'{__file__}: Try installing pylint: sudo apt-get install ' + f'{os.path.basename(cmd[0])}', file=sys.stderr) return 1 raise diff --git a/tools/pylintrc b/tools/pylintrc index 68c74ef..3abe640 100644 --- a/tools/pylintrc +++ b/tools/pylintrc @@ -73,68 +73,6 @@ confidence= # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. enable= - apply-builtin, - backtick, - bad-python3-import, - basestring-builtin, - buffer-builtin, - cmp-builtin, - cmp-method, - coerce-builtin, - coerce-method, - delslice-method, - deprecated-itertools-function, - deprecated-str-translate-call, - deprecated-string-function, - deprecated-types-field, - dict-items-not-iterating, - dict-iter-method, - dict-keys-not-iterating, - dict-values-not-iterating, - dict-view-method, - div-method, - exception-message-attribute, - execfile-builtin, - file-builtin, - filter-builtin-not-iterating, - getslice-method, - hex-method, - idiv-method, - import-star-module-level, - indexing-exception, - input-builtin, - intern-builtin, - invalid-str-codec, - long-builtin, - long-suffix, - map-builtin-not-iterating, - metaclass-assignment, - next-method-called, - next-method-defined, - nonzero-method, - oct-method, - old-division, - old-ne-operator, - old-octal-literal, - old-raise-syntax, - parameter-unpacking, - print-statement, - raising-string, - range-builtin-not-iterating, - raw_input-builtin, - rdiv-method, - reduce-builtin, - reload-builtin, - round-builtin, - setslice-method, - standarderror-builtin, - sys-max-int, - unichr-builtin, - unicode-builtin, - unpacking-in-except, - using-cmp-argument, - xrange-builtin, - zip-builtin-not-iterating, # Disable the message, report, category or checker with the given id(s). You @@ -153,10 +91,11 @@ disable= file-ignored, invalid-name, locally-disabled, - locally-enabled, missing-docstring, - no-self-use, - star-args, + no-else-break, + no-else-continue, + no-else-raise, + no-else-return, too-few-public-methods, too-many-arguments, too-many-branches, @@ -320,7 +259,7 @@ notes=FIXME,XXX,TODO [BASIC] # List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input +bad-functions=map,filter # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,x,_ diff --git a/tools/spelling.txt b/tools/spelling.txt index 9a058cf..0c8b79c 100644 --- a/tools/spelling.txt +++ b/tools/spelling.txt @@ -9,7 +9,12 @@ # abandonning||abandoning abigious||ambiguous +abitrary||arbitrary abitrate||arbitrate +abnornally||abnormally +abnrormal||abnormal +abord||abort +aboslute||absolute abov||above abreviated||abbreviated absense||absence @@ -17,6 +22,7 @@ absolut||absolute absoulte||absolute acccess||access acceess||access +accelaration||acceleration acceleratoin||acceleration accelleration||acceleration accesing||accessing @@ -25,6 +31,7 @@ accessable||accessible accesss||access accidentaly||accidentally accidentually||accidentally +acclerated||accelerated accoding||according accomodate||accommodate accomodates||accommodates @@ -34,8 +41,11 @@ accout||account accquire||acquire accquired||acquired accross||across +accumalate||accumulate +accumalator||accumulator acessable||accessible acess||access +acessing||accessing achitecture||architecture acient||ancient acitions||actions @@ -49,7 +59,9 @@ activete||activate actived||activated actualy||actually acumulating||accumulating +acumulative||accumulative acumulator||accumulator +acutally||actually adapater||adapter addional||additional additionaly||additionally @@ -58,18 +70,22 @@ addres||address adddress||address addreses||addresses addresss||address +addrress||address aditional||additional aditionally||additionally aditionaly||additionally adminstrative||administrative adress||address adresses||addresses +adrresses||addresses +advertisment||advertisement adviced||advised afecting||affecting againt||against agaist||against aggreataon||aggregation aggreation||aggregation +ajust||adjust albumns||albums alegorical||allegorical algined||aligned @@ -77,6 +93,7 @@ algorith||algorithm algorithmical||algorithmically algoritm||algorithm algoritms||algorithms +algorithmn||algorithm algorrithm||algorithm algorritm||algorithm aligment||alignment @@ -88,6 +105,7 @@ alloated||allocated allocatote||allocate allocatrd||allocated allocte||allocate +allocted||allocated allpication||application alocate||allocate alogirhtms||algorithms @@ -95,11 +113,16 @@ alogrithm||algorithm alot||a lot alow||allow alows||allows +alreay||already +alredy||already altough||although alue||value ambigious||ambiguous +ambigous||ambiguous amoung||among amout||amount +amplifer||amplifier +amplifyer||amplifier an union||a union an user||a user an userspace||a userspace @@ -130,6 +153,7 @@ arbitary||arbitrary architechture||architecture arguement||argument arguements||arguments +arithmatic||arithmetic aritmetic||arithmetic arne't||aren't arraival||arrival @@ -138,27 +162,43 @@ artillary||artillery asign||assign asser||assert assertation||assertion +assertting||asserting +assgined||assigned assiged||assigned assigment||assignment assigments||assignments assistent||assistant +assocaited||associated +assocating||associating assocation||association associcated||associated assotiated||associated +asssert||assert assum||assume assumtpion||assumption asuming||assuming asycronous||asynchronous +asychronous||asynchronous asynchnous||asynchronous +asynchromous||asynchronous +asymetric||asymmetric +asymmeric||asymmetric +atleast||at least atomatically||automatically atomicly||atomically atempt||attempt +atrributes||attributes attachement||attachment +attatch||attach attched||attached +attemp||attempt attemps||attempts attemping||attempting +attepmpt||attempt +attnetion||attention attruibutes||attributes authentification||authentication +authenicated||authenticated automaticaly||automatically automaticly||automatically automatize||automate @@ -172,6 +212,7 @@ avaible||available availabe||available availabled||available availablity||availability +availaible||available availale||available availavility||availability availble||available @@ -201,32 +242,50 @@ beter||better betweeen||between bianries||binaries bitmast||bitmask +bitwiedh||bitwidth boardcast||broadcast borad||board boundry||boundary brievely||briefly +brigde||bridge +broadcase||broadcast broadcat||broadcast +bufer||buffer +bufufer||buffer cacluated||calculated +caculate||calculate caculation||calculation +cadidate||candidate +cahces||caches calender||calendar calescing||coalescing calle||called callibration||calibration +callled||called +callser||caller calucate||calculate calulate||calculate cancelation||cancellation cancle||cancel +cant||can't +cant'||can't +canot||cannot +cann't||can't capabilites||capabilities +capabilties||capabilities capabilty||capability capabitilies||capabilities +capablity||capability capatibilities||capabilities capapbilities||capabilities +caputure||capture carefuly||carefully cariage||carriage catagory||category cehck||check challange||challenge challanges||challenges +chache||cache chanell||channel changable||changeable chanined||chained @@ -240,6 +299,7 @@ charaters||characters charcter||character chcek||check chck||check +checksumed||checksummed checksuming||checksumming childern||children childs||children @@ -255,7 +315,9 @@ claread||cleared clared||cleared closeing||closing clustred||clustered +cnfiguration||configuration coexistance||coexistence +colescing||coalescing collapsable||collapsible colorfull||colorful comand||command @@ -266,14 +328,17 @@ comminucation||communication commited||committed commiting||committing committ||commit +commnunication||communication commoditiy||commodity comsume||consume comsumer||consumer comsuming||consuming compability||compatibility compaibility||compatibility +comparsion||comparison compatability||compatibility compatable||compatible +compatibililty||compatibility compatibiliy||compatibility compatibilty||compatibility compatiblity||compatibility @@ -285,22 +350,32 @@ completly||completely complient||compliant componnents||components compoment||component +comppatible||compatible compres||compress compresion||compression comression||compression +comunicate||communicate comunication||communication conbination||combination conditionaly||conditionally +conditon||condition +condtion||condition conected||connected -connecetd||connected +conector||connector +configration||configuration +configred||configured configuartion||configuration +configuation||configuration +configued||configured configuratoin||configuration configuraton||configuration configuretion||configuration configutation||configuration conider||consider conjuction||conjunction +connecetd||connected connectinos||connections +connetor||connector connnection||connection connnections||connections consistancy||consistency @@ -310,11 +385,13 @@ containts||contains contaisn||contains contant||contact contence||contents +contiguos||contiguous continious||continuous continous||continuous continously||continuously continueing||continuing contraints||constraints +contruct||construct contol||control contoller||controller controled||controlled @@ -340,15 +417,23 @@ cunter||counter curently||currently cylic||cyclic dafault||default +deactive||deactivate deafult||default deamon||daemon +debouce||debounce +decendant||descendant +decendants||descendants decompres||decompress +decsribed||described decription||description dectected||detected defailt||default +deferal||deferral +deffered||deferred defferred||deferred definate||definite definately||definitely +definiation||definition defintion||definition defintions||definitions defualt||default @@ -362,29 +447,35 @@ delare||declare delares||declares delaring||declaring delemiter||delimiter +delievered||delivered demodualtor||demodulator demension||dimension dependancies||dependencies dependancy||dependency dependant||dependent +dependend||dependent depreacted||deprecated depreacte||deprecate desactivate||deactivate desciptor||descriptor desciptors||descriptors +descripto||descriptor descripton||description descrition||description descritptor||descriptor desctiptor||descriptor desriptor||descriptor desriptors||descriptors +desination||destination destionation||destination +destoried||destroyed destory||destroy destoryed||destroyed destorys||destroys destroied||destroyed detabase||database deteced||detected +detectt||detect develope||develop developement||development developped||developed @@ -394,44 +485,75 @@ developpment||development deveolpment||development devided||divided deviece||device +devision||division diable||disable +diabled||disabled +dicline||decline dictionnary||dictionary didnt||didn't diferent||different differrence||difference diffrent||different +differenciate||differentiate diffrentiate||differentiate difinition||definition +digial||digital +dimention||dimension dimesions||dimensions +diconnected||disconnected +disabed||disabled +disble||disable +disgest||digest +disired||desired +dispalying||displaying +dissable||disable diplay||display +directon||direction +direcly||directly direectly||directly +diregard||disregard disassocation||disassociation disapear||disappear disapeared||disappeared disappared||disappeared +disbale||disable +disbaled||disabled disble||disable disbled||disabled disconnet||disconnect discontinous||discontinuous +disharge||discharge +disnabled||disabled dispertion||dispersion dissapears||disappears +dissconect||disconnect distiction||distinction +divisable||divisible +divsiors||divisors docuentation||documentation documantation||documentation documentaion||documentation documment||document doesnt||doesn't +donwload||download +donwloading||downloading dorp||drop dosen||doesn downlad||download downlads||downloads +droped||dropped +droput||dropout druing||during +dyanmic||dynamic dynmaic||dynamic +eanable||enable +eanble||enable easilly||easily ecspecially||especially edditable||editable editting||editing efective||effective +effectivness||effectiveness efficently||efficiently ehther||ether eigth||eight @@ -439,16 +561,23 @@ elementry||elementary eletronic||electronic embeded||embedded enabledi||enabled +enbale||enable +enble||enable enchanced||enhanced encorporating||incorporating encrupted||encrypted encrypiton||encryption encryptio||encryption endianess||endianness +enpoint||endpoint enhaced||enhanced enlightnment||enlightenment +enqueing||enqueuing +entires||entries +entites||entities entrys||entries enocded||encoded +enought||enough enterily||entirely enviroiment||environment enviroment||environment @@ -460,13 +589,24 @@ equivelant||equivalent equivilant||equivalent eror||error errorr||error +errror||error estbalishment||establishment etsablishment||establishment etsbalishment||establishment +evalute||evaluate +evalutes||evaluates +evalution||evaluation excecutable||executable exceded||exceeded +exceds||exceeds +exceeed||exceed excellant||excellent +exchnage||exchange +execeeded||exceeded +execeeds||exceeds exeed||exceed +exeeds||exceeds +exeuction||execution existance||existence existant||existent exixt||exist @@ -474,6 +614,7 @@ exlcude||exclude exlcusive||exclusive exmaple||example expecially||especially +experies||expires explicite||explicit explicitely||explicitly explict||explicit @@ -482,11 +623,16 @@ explictly||explicitly expresion||expression exprimental||experimental extened||extended +exteneded||extended extensability||extensibility extention||extension +extenstion||extension extracter||extractor -falied||failed +faied||failed +faield||failed faild||failed +failded||failed +failer||failure faill||fail failied||failed faillure||failure @@ -504,8 +650,12 @@ feautures||features fetaure||feature fetaures||features fileystem||filesystem +fimrware||firmware fimware||firmware +firmare||firmware +firmaware||firmware firware||firmware +firwmare||firmware finanize||finalize findn||find finilizes||finalizes @@ -520,13 +670,18 @@ forseeable||foreseeable forse||force fortan||fortran forwardig||forwarding +frambuffer||framebuffer framming||framing framwork||framework +frequence||frequency frequncy||frequency +frequancy||frequency frome||from fucntion||function fuction||function fuctions||functions +fullill||fulfill +funcation||function funcion||function functionallity||functionality functionaly||functionally @@ -537,14 +692,19 @@ funtions||functions furthur||further futhermore||furthermore futrue||future +gatable||gateable +gateing||gating +gauage||gauge gaurenteed||guaranteed generiously||generously genereate||generate +genereted||generated genric||generic globel||global grabing||grabbing grahical||graphical grahpical||graphical +granularty||granularity grapic||graphic grranted||granted guage||gauge @@ -553,14 +713,22 @@ guarentee||guarantee halfs||halves hander||handler handfull||handful +hanlde||handle hanled||handled happend||happened +hardare||hardware harware||hardware +havind||having heirarchically||hierarchically +heirarchy||hierarchy helpfull||helpful +hearbeat||heartbeat +heterogenous||heterogeneous +hexdecimal||hexadecimal hybernate||hibernate hierachy||hierarchy hierarchie||hierarchy +homogenous||homogeneous howver||however hsould||should hypervior||hypervisor @@ -568,12 +736,16 @@ hypter||hyper identidier||identifier iligal||illegal illigal||illegal +illgal||illegal +iomaped||iomapped imblance||imbalance immeadiately||immediately immedaite||immediate +immedate||immediate immediatelly||immediately immediatly||immediately immidiate||immediate +immutible||immutable impelentation||implementation impementated||implemented implemantation||implementation @@ -591,10 +763,13 @@ incative||inactive incomming||incoming incompatabilities||incompatibilities incompatable||incompatible +incompatble||incompatible inconsistant||inconsistent increas||increase incremeted||incremented incrment||increment +incuding||including +inculde||include indendation||indentation indended||intended independant||independent @@ -603,6 +778,8 @@ independed||independent indiate||indicate indicat||indicate inexpect||inexpected +inferface||interface +infinit||infinite infomation||information informatiom||information informations||information @@ -617,14 +794,24 @@ initalize||initialize initation||initiation initators||initiators initialiazation||initialization +initializationg||initialization initializiation||initialization +initialze||initialize initialzed||initialized +initialzing||initializing initilization||initialization initilize||initialize +initliaze||initialize +initilized||initialized inofficial||unofficial +inrerface||interface insititute||institute +instace||instance instal||install +instanciate||instantiate instanciated||instantiated +instuments||instruments +insufficent||insufficient inteface||interface integreated||integrated integrety||integrity @@ -635,17 +822,20 @@ interanl||internal interchangable||interchangeable interferring||interfering interger||integer +intergrated||integrated intermittant||intermittent internel||internal interoprability||interoperability interuupt||interrupt +interupt||interrupt +interupts||interrupts interrface||interface interrrupt||interrupt interrup||interrupt interrups||interrupts interruptted||interrupted interupted||interrupted -interupt||interrupt +intiailized||initialized intial||initial intialisation||initialisation intialised||initialised @@ -654,10 +844,14 @@ intialization||initialization intialized||initialized intialize||initialize intregral||integral +intrerrupt||interrupt intrrupt||interrupt intterrupt||interrupt intuative||intuitive +inavlid||invalid invaid||invalid +invaild||invalid +invailid||invalid invald||invalid invalde||invalid invalide||invalid @@ -666,14 +860,18 @@ invalud||invalid invididual||individual invokation||invocation invokations||invocations +ireelevant||irrelevant irrelevent||irrelevant isnt||isn't isssue||issue +issus||issues +iteraions||iterations iternations||iterations itertation||iteration itslef||itself jave||java jeffies||jiffies +jumpimng||jumping juse||just jus||just kown||known @@ -683,6 +881,7 @@ langauge||language langugage||language lauch||launch layed||laid +legnth||length leightweight||lightweight lengh||length lenght||length @@ -693,29 +892,45 @@ libary||library librairies||libraries libraris||libraries licenceing||licencing +limted||limited +logaritmic||logarithmic loggging||logging loggin||login logile||logfile +loobpack||loopback loosing||losing losted||lost +maangement||management machinary||machinery +maibox||mailbox maintainance||maintenance maintainence||maintenance maintan||maintain makeing||making +mailformed||malformed malplaced||misplaced malplace||misplace managable||manageable +managament||management managment||management mangement||management +manger||manager manoeuvering||maneuvering +manufaucturing||manufacturing mappping||mapping +maping||mapping +matchs||matches mathimatical||mathematical mathimatic||mathematic mathimatics||mathematics +maximium||maximum maxium||maximum mechamism||mechanism meetign||meeting +memeory||memory +memmber||member +memoery||memory +memroy||memory ment||meant mergable||mergeable mesage||message @@ -723,11 +938,15 @@ messags||messages messgaes||messages messsage||message messsages||messages +metdata||metadata micropone||microphone microprocesspr||microprocessor +migrateable||migratable milliseonds||milliseconds minium||minimum minimam||minimum +minimun||minimum +miniumum||minimum minumum||minimum misalinged||misaligned miscelleneous||miscellaneous @@ -736,21 +955,29 @@ mispelled||misspelled mispelt||misspelt mising||missing mismactch||mismatch +missign||missing missmanaged||mismanaged missmatch||mismatch +misssing||missing miximum||maximum mmnemonic||mnemonic mnay||many +modfiy||modify +modifer||modifier +modul||module modulues||modules momery||memory memomry||memory +monitring||monitoring monochorome||monochrome monochromo||monochrome monocrome||monochrome mopdule||module mroe||more +multipler||multiplier mulitplied||multiplied multidimensionnal||multidimensional +multipe||multiple multple||multiple mumber||number muticast||multicast @@ -772,21 +999,30 @@ nerver||never nescessary||necessary nessessary||necessary noticable||noticeable +notication||notification notications||notifications +notifcations||notifications notifed||notified +notity||notify +nubmer||number numebr||number numner||number obtaion||obtain +obusing||abusing occassionally||occasionally occationally||occasionally occurance||occurrence occurances||occurrences +occurd||occurred occured||occurred occurence||occurrence occure||occurred -occured||occurred occuring||occurring +offser||offset offet||offset +offlaod||offload +offloded||offloaded +offseting||offsetting omited||omitted omiting||omitting omitt||omit @@ -794,22 +1030,29 @@ ommiting||omitting ommitted||omitted onself||oneself ony||only +openning||opening operatione||operation opertaions||operations +opportunies||opportunities optionnal||optional optmizations||optimizations orientatied||orientated orientied||oriented orignal||original +originial||original otherise||otherwise ouput||output oustanding||outstanding overaall||overall overhread||overhead overlaping||overlapping +oveflow||overflow +overflw||overflow +overlfow||overflow overide||override overrided||overridden overriden||overridden +overrrun||overrun overun||overrun overwritting||overwriting overwriten||overwritten @@ -820,6 +1063,7 @@ packege||package packge||package packtes||packets pakage||package +paket||packet pallette||palette paln||plan paramameters||parameters @@ -829,23 +1073,34 @@ parametes||parameters parametised||parametrised paramter||parameter paramters||parameters +parmaters||parameters particuarly||particularly particularily||particularly +partion||partition +partions||partitions partiton||partition pased||passed passin||passing pathes||paths +pattrns||patterns pecularities||peculiarities peformance||performance +peforming||performing peice||piece pendantic||pedantic peprocessor||preprocessor +perfomance||performance perfoming||performing +perfomring||performing +periperal||peripheral +peripherial||peripheral permissons||permissions peroid||period persistance||persistence persistant||persistent +phoneticly||phonetically plalform||platform +platfoem||platform platfrom||platform plattform||platform pleaes||please @@ -857,7 +1112,10 @@ poiter||pointer posible||possible positon||position possibilites||possibilities +potocol||protocol powerfull||powerful +pramater||parameter +preamle||preamble preample||preamble preapre||prepare preceeded||preceded @@ -868,9 +1126,16 @@ precission||precision preemptable||preemptible prefered||preferred prefferably||preferably +prefitler||prefilter +preform||perform premption||preemption prepaired||prepared +prepate||prepare +preperation||preparation +preprare||prepare pressre||pressure +presuambly||presumably +previosuly||previously primative||primitive princliple||principle priorty||priority @@ -878,6 +1143,7 @@ privilaged||privileged privilage||privilege priviledge||privilege priviledges||privileges +privleges||privileges probaly||probably procceed||proceed proccesors||processors @@ -891,12 +1157,17 @@ processsed||processed processsing||processing procteted||protected prodecure||procedure +progamming||programming progams||programs progess||progress +programable||programmable programers||programmers programm||program programms||programs +progres||progress progresss||progress +prohibitted||prohibited +prohibitting||prohibiting promiscous||promiscuous promps||prompts pronnounced||pronounced @@ -906,35 +1177,45 @@ pronunce||pronounce propery||property propigate||propagate propigation||propagation +propogation||propagation propogate||propagate prosess||process protable||portable protcol||protocol protecion||protection +protedcted||protected protocoll||protocol promixity||proximity psudo||pseudo psuedo||pseudo psychadelic||psychedelic +purgable||purgeable pwoer||power +queing||queuing quering||querying +queus||queues randomally||randomly raoming||roaming reasearcher||researcher reasearchers||researchers reasearch||research +receieve||receive recepient||recipient +recevied||received receving||receiving +recievd||received recieved||received recieve||receive reciever||receiver recieves||receives +recieving||receiving recogniced||recognised recognizeable||recognizable recommanded||recommended recyle||recycle redircet||redirect redirectrion||redirection +redundacy||redundancy reename||rename refcounf||refcount refence||reference @@ -944,7 +1225,9 @@ refering||referring refernces||references refernnce||reference refrence||reference +registed||registered registerd||registered +registeration||registration registeresd||registered registerred||registered registes||registers @@ -957,6 +1240,7 @@ regulamentations||regulations reigstration||registration releated||related relevent||relevant +reloade||reload remoote||remote remore||remote removeable||removable @@ -967,25 +1251,38 @@ replys||replies reponse||response representaion||representation reqeust||request +reqister||register +requed||requeued requestied||requested requiere||require requirment||requirement requred||required requried||required requst||request +requsted||requested +reregisteration||reregistration reseting||resetting +reseved||reserved +reseverd||reserved resizeable||resizable resouce||resource resouces||resources resoures||resources responce||response +resrouce||resource ressizes||resizes ressource||resource ressources||resources +restesting||retesting +resumbmitting||resubmitting retransmited||retransmitted retreived||retrieved retreive||retrieve +retreiving||retrieving retrive||retrieve +retrived||retrieved +retrun||return +retun||return retuned||returned reudce||reduce reuest||request @@ -1006,30 +1303,43 @@ sacrifying||sacrificing safly||safely safty||safety savable||saveable +scaleing||scaling scaned||scanned scaning||scanning scarch||search +schdule||schedule seach||search searchs||searches +secion||section secquence||sequence secund||second segement||segment +seleted||selected +semaphone||semaphore +senario||scenario senarios||scenarios sentivite||sensitive separatly||separately sepcify||specify -sepc||spec seperated||separated seperately||separately seperate||separate seperatly||separately seperator||separator sepperate||separate +seqeunce||sequence +seqeuncer||sequencer +seqeuencer||sequencer sequece||sequence +sequemce||sequence sequencial||sequential +serivce||service serveral||several +servive||service setts||sets settting||setting +shapshot||snapshot +shoft||shift shotdown||shutdown shoud||should shouldnt||shouldn't @@ -1037,6 +1347,7 @@ shoule||should shrinked||shrunk siginificantly||significantly signabl||signal +significanly||significantly similary||similarly similiar||similar simlar||similar @@ -1046,15 +1357,22 @@ singaled||signaled singal||signal singed||signed sleeped||slept +sliped||slipped +softwade||software softwares||software +soley||solely +souce||source speach||speech specfic||specific +specfield||specified speciefied||specified specifc||specific specifed||specified specificatin||specification specificaton||specification +specificed||specified specifing||specifying +specifiy||specify specifiying||specifying speficied||specified speicify||specify @@ -1071,8 +1389,12 @@ staion||station standardss||standards standartization||standardization standart||standard +standy||standby +stardard||standard staticly||statically +statuss||status stoped||stopped +stoping||stopping stoppped||stopped straming||streaming struc||struct @@ -1084,13 +1406,17 @@ sturcture||structure subdirectoires||subdirectories suble||subtle substract||subtract +submited||submitted submition||submission +succeded||succeeded +suceed||succeed succesfully||successfully succesful||successful successed||succeeded successfull||successful successfuly||successfully sucessfully||successfully +sucessful||successful sucess||success superflous||superfluous superseeded||superseded @@ -1103,11 +1429,13 @@ supportin||supporting suppoted||supported suppported||supported suppport||support +supprot||support supress||suppress surpressed||suppressed surpresses||suppresses susbsystem||subsystem suspeneded||suspended +suspsend||suspend suspicously||suspiciously swaping||swapping switchs||switches @@ -1119,9 +1447,12 @@ swithcing||switching swithed||switched swithing||switching swtich||switch +syfs||sysfs symetric||symmetric synax||syntax synchonized||synchronized +sychronization||synchronization +synchronuously||synchronously syncronize||synchronize syncronized||synchronized syncronizing||synchronizing @@ -1130,28 +1461,43 @@ syste||system sytem||system sythesis||synthesis taht||that +tansmit||transmit targetted||targeted targetting||targeting +taskelt||tasklet teh||the temorary||temporary temproarily||temporarily +temperture||temperature +thead||thread therfore||therefore thier||their threds||threads +threee||three threshhold||threshold thresold||threshold throught||through +trackling||tracking troughput||throughput +trys||tries thses||these +tiggers||triggers tiggered||triggered tipically||typically +timeing||timing timout||timeout tmis||this +toogle||toggle torerable||tolerable +traget||target +traking||tracking tramsmitted||transmitted tramsmit||transmit tranasction||transaction +tranceiver||transceiver tranfer||transfer +tranmission||transmission +transcevier||transceiver transciever||transceiver transferd||transferred transfered||transferred @@ -1162,6 +1508,8 @@ transormed||transformed trasfer||transfer trasmission||transmission treshold||threshold +triggerd||triggered +trigerred||triggered trigerring||triggering trun||turn tunning||tuning @@ -1169,8 +1517,12 @@ ture||true tyep||type udpate||update uesd||used +uknown||unknown +usccess||success uncommited||uncommitted +uncompatible||incompatible unconditionaly||unconditionally +undeflow||underflow underun||underrun unecessary||unnecessary unexecpted||unexpected @@ -1181,11 +1533,17 @@ unexpeted||unexpected unexpexted||unexpected unfortunatelly||unfortunately unifiy||unify +uniterrupted||uninterrupted +uninterruptable||uninterruptible unintialized||uninitialized +unitialized||uninitialized unkmown||unknown unknonw||unknown +unknouwn||unknown unknow||unknown unkown||unknown +unamed||unnamed +uneeded||unneeded unneded||unneeded unneccecary||unnecessary unneccesary||unnecessary @@ -1194,6 +1552,7 @@ unnecesary||unnecessary unneedingly||unnecessarily unnsupported||unsupported unmached||unmatched +unprecise||imprecise unregester||unregister unresgister||unregister unrgesiter||unregister @@ -1203,13 +1562,18 @@ unsolicitied||unsolicited unsuccessfull||unsuccessful unsuported||unsupported untill||until +ununsed||unused unuseful||useless +unvalid||invalid upate||update +upsupported||unsupported +useable||usable usefule||useful usefull||useful usege||usage usera||users usualy||usually +usupported||unsupported utilites||utilities utillities||utilities utilties||utilities @@ -1224,6 +1588,9 @@ varible||variable varient||variant vaule||value verbse||verbose +veify||verify +verfication||verification +veriosn||version verisons||versions verison||version verson||version @@ -1233,7 +1600,10 @@ virtaul||virtual virtiual||virtual visiters||visitors vitual||virtual +vunerable||vulnerable wakeus||wakeups +was't||wasn't +wathdog||watchdog wating||waiting wiat||wait wether||whether @@ -1247,8 +1617,10 @@ wiil||will wirte||write withing||within wnat||want +wont||won't workarould||workaround writeing||writing writting||writing +wtih||with zombe||zombie zomebie||zombie |