aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-03-25 16:09:12 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-03-25 16:09:12 +0000
commitfd09e19a769b8a5877951b4a4569cfb6b93e9acf (patch)
tree52cec87e70718b4efea41504bde845addb0022e3
parentce869c36f6a5e9cf4577bfc875c79e51609c1b76 (diff)
parente106ee3301113116bdc4e11cdb9af60ea946d12b (diff)
downloadrepohooks-androidx-transition-release.tar.gz
Snap for 11610999 from e106ee3301113116bdc4e11cdb9af60ea946d12b to androidx-transition-releaseandroidx-transition-release
Change-Id: I2aea7575f1eb89da922bef17d6c224b8339dbbf9
-rw-r--r--OWNERS1
-rw-r--r--PREUPLOAD.cfg3
-rw-r--r--README.md20
-rwxr-xr-xpre-upload.py473
-rw-r--r--rh/__init__.py13
-rw-r--r--rh/config.py89
-rwxr-xr-xrh/config_test.py17
-rwxr-xr-xrh/config_unittest.py5
-rw-r--r--rh/git.py53
-rw-r--r--rh/hooks.py274
-rwxr-xr-xrh/hooks_unittest.py153
-rw-r--r--rh/results.py59
-rwxr-xr-xrh/results_unittest.py105
-rw-r--r--rh/shell.py25
-rwxr-xr-xrh/shell_unittest.py34
-rw-r--r--rh/signals.py3
-rw-r--r--rh/terminal.py90
-rwxr-xr-xrh/terminal_unittest.py199
-rw-r--r--rh/utils.py141
-rwxr-xr-xrh/utils_unittest.py97
-rwxr-xr-xtools/android_test_mapping_format.py160
-rwxr-xr-xtools/android_test_mapping_format_unittest.py123
-rwxr-xr-xtools/checkpatch.pl1844
-rwxr-xr-xtools/checkpatch.pl-update4
-rwxr-xr-xtools/clang-format.py48
-rwxr-xr-xtools/clang-format_unittest.py108
-rw-r--r--tools/const_structs.checkpatch95
-rwxr-xr-xtools/cpplint.py1027
-rwxr-xr-xtools/cpplint.py-update12
-rwxr-xr-xtools/google-java-format.py33
-rwxr-xr-xtools/pylint.py23
-rw-r--r--tools/pylintrc71
-rw-r--r--tools/spelling.txt382
33 files changed, 4357 insertions, 1427 deletions
diff --git a/OWNERS b/OWNERS
index 918a40d..0280764 100644
--- a/OWNERS
+++ b/OWNERS
@@ -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]
diff --git a/README.md b/README.md
index dd6ffc6..5b8ffce 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/rh/git.py b/rh/git.py
index da9d55c..5496164 100644
--- a/rh/git.py
+++ b/rh/git.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 @@
"""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