summaryrefslogtreecommitdiff
path: root/utils/checkstyle.py
diff options
context:
space:
mode:
Diffstat (limited to 'utils/checkstyle.py')
-rwxr-xr-xutils/checkstyle.py421
1 files changed, 212 insertions, 209 deletions
diff --git a/utils/checkstyle.py b/utils/checkstyle.py
index 4185c39a..f6229bbd 100755
--- a/utils/checkstyle.py
+++ b/utils/checkstyle.py
@@ -23,7 +23,6 @@ import subprocess
import sys
dependencies = {
- 'clang-format': True,
'git': True,
}
@@ -211,36 +210,66 @@ class CommitFile:
class Commit:
def __init__(self, commit):
- self.commit = commit
+ self._commit = commit
+ self._author = None
self._trailers = []
self._parse()
- def _parse_trailers(self, lines):
- for index in range(1, len(lines)):
- line = lines[index]
- if not line:
- break
+ def _parse_commit(self):
+ # Get and parse the commit message.
+ ret = subprocess.run(['git', 'show', '--format=%H%n%an <%ae>%n%s%n%b',
+ '--no-patch', self.commit],
+ stdout=subprocess.PIPE).stdout.decode('utf-8')
+ lines = ret.splitlines()
- self._trailers.append(line)
+ self._commit = lines[0]
+ self._author = lines[1]
+ self._title = lines[2]
+ self._body = lines[3:]
- return index
+ # Parse the trailers. Feed git-interpret-trailers with a full commit
+ # message that includes both the title and the body, as it otherwise
+ # fails to find trailers when the body contains trailers only.
+ message = self._title + '\n\n' + '\n'.join(self._body)
+ trailers = subprocess.run(['git', 'interpret-trailers', '--parse'],
+ input=message.encode('utf-8'),
+ stdout=subprocess.PIPE).stdout.decode('utf-8')
+
+ self._trailers = trailers.splitlines()
def _parse(self):
- # Get the commit title and list of files.
- ret = subprocess.run(['git', 'show', '--format=%s%n%(trailers:only,unfold)', '--name-status',
+ self._parse_commit()
+
+ # Get the list of files. Use an empty format specifier to suppress the
+ # commit message completely.
+ ret = subprocess.run(['git', 'show', '--format=', '--name-status',
self.commit],
stdout=subprocess.PIPE).stdout.decode('utf-8')
- lines = ret.splitlines()
-
- self._title = lines[0]
+ self._files = [CommitFile(f) for f in ret.splitlines()]
- index = self._parse_trailers(lines)
- self._files = [CommitFile(f) for f in lines[index:] if f]
+ def __repr__(self):
+ return '\n'.join([
+ f'commit {self.commit}',
+ f'Author: {self.author}',
+ f'',
+ f' {self.title}',
+ '',
+ '\n'.join([line and f' {line}' or '' for line in self._body]),
+ 'Trailers:',
+ ] + self.trailers)
def files(self, filter='AMR'):
return [f.filename for f in self._files if f.status in filter]
@property
+ def author(self):
+ return self._author
+
+ @property
+ def commit(self):
+ return self._commit
+
+ @property
def title(self):
return self._title
@@ -278,20 +307,14 @@ class StagedChanges(Commit):
class Amendment(Commit):
def __init__(self):
- Commit.__init__(self, '')
+ Commit.__init__(self, 'HEAD')
def _parse(self):
- # Create a title using HEAD commit and parse the trailers.
- ret = subprocess.run(['git', 'show', '--format=%H %s%n%(trailers:only,unfold)',
- '--no-patch'],
- stdout=subprocess.PIPE).stdout.decode('utf-8')
- lines = ret.splitlines()
-
- self._title = 'Amendment of ' + lines[0].strip()
+ self._parse_commit()
- self._parse_trailers(lines)
+ self._title = f'Amendment of "{self.title}"'
- # Extract the list of modified files
+ # Extract the list of modified files.
ret = subprocess.run(['git', 'diff', '--staged', '--name-status', 'HEAD~'],
stdout=subprocess.PIPE).stdout.decode('utf-8')
self._files = [CommitFile(f) for f in ret.splitlines()]
@@ -310,40 +333,83 @@ class Amendment(Commit):
class ClassRegistry(type):
def __new__(cls, clsname, bases, attrs):
newclass = super().__new__(cls, clsname, bases, attrs)
- if bases:
- bases[0].subclasses.append(newclass)
- bases[0].subclasses.sort(key=lambda x: getattr(x, 'priority', 0),
- reverse=True)
+ if bases and bases[0] != CheckerBase:
+ base = bases[0]
+
+ if not hasattr(base, 'subclasses'):
+ base.subclasses = []
+ base.subclasses.append(newclass)
+ base.subclasses.sort(key=lambda x: getattr(x, 'priority', 0),
+ reverse=True)
return newclass
-# ------------------------------------------------------------------------------
-# Commit Checkers
-#
+class CheckerBase(metaclass=ClassRegistry):
+ @classmethod
+ def instances(cls, obj, names):
+ for instance in cls.subclasses:
+ if names and instance.__name__ not in names:
+ continue
+ if instance.supports(obj):
+ yield instance
-class CommitChecker(metaclass=ClassRegistry):
- subclasses = []
+ @classmethod
+ def supports(cls, obj):
+ if hasattr(cls, 'commit_types'):
+ return type(obj) in cls.commit_types
- def __init__(self):
- pass
+ if hasattr(cls, 'patterns'):
+ for pattern in cls.patterns:
+ if fnmatch.fnmatch(os.path.basename(obj), pattern):
+ return True
+
+ return False
- #
- # Class methods
- #
@classmethod
- def checkers(cls, names):
- for checker in cls.subclasses:
- if names and checker.__name__ not in names:
- continue
- yield checker
+ def all_patterns(cls):
+ patterns = set()
+ for instance in cls.subclasses:
+ if hasattr(instance, 'patterns'):
+ patterns.update(instance.patterns)
+
+ return patterns
+
+ @classmethod
+ def check_dependencies(cls):
+ if not hasattr(cls, 'dependencies'):
+ return []
+
+ issues = []
+
+ for command in cls.dependencies:
+ if command not in dependencies:
+ dependencies[command] = shutil.which(command)
+
+ if not dependencies[command]:
+ issues.append(CommitIssue(f'Missing {command} to run {cls.__name__}'))
+
+ return issues
+
+
+# ------------------------------------------------------------------------------
+# Commit Checkers
+#
+
+class CommitChecker(CheckerBase):
+ pass
class CommitIssue(object):
def __init__(self, msg):
self.msg = msg
+ def __str__(self):
+ return f'{Colours.fg(Colours.Yellow)}{self.msg}{Colours.reset()}'
+
class HeaderAddChecker(CommitChecker):
+ commit_types = (Commit, StagedChanges, Amendment)
+
@classmethod
def check(cls, commit, top_level):
issues = []
@@ -388,6 +454,8 @@ class HeaderAddChecker(CommitChecker):
class TitleChecker(CommitChecker):
+ commit_types = (Commit,)
+
prefix_regex = re.compile(r'^([a-zA-Z0-9_.-]+: )+')
release_regex = re.compile(r'libcamera v[0-9]+\.[0-9]+\.[0-9]+')
@@ -395,11 +463,6 @@ class TitleChecker(CommitChecker):
def check(cls, commit, top_level):
title = commit.title
- # Skip the check when validating staged changes (as done through a
- # pre-commit hook) as there is no title to check in that case.
- if isinstance(commit, StagedChanges):
- return []
-
# Ignore release commits, they don't need a prefix.
if TitleChecker.release_regex.fullmatch(title):
return []
@@ -455,6 +518,8 @@ class TitleChecker(CommitChecker):
class TrailersChecker(CommitChecker):
+ commit_types = (Commit,)
+
commit_regex = re.compile(r'[0-9a-f]{12}[0-9a-f]* \(".*"\)')
coverity_regex = re.compile(r'Coverity CID=.*')
@@ -493,6 +558,8 @@ class TrailersChecker(CommitChecker):
def check(cls, commit, top_level):
issues = []
+ sob_found = False
+
for trailer in commit.trailers:
match = TrailersChecker.trailer_regex.fullmatch(trailer)
if not match:
@@ -515,6 +582,13 @@ class TrailersChecker(CommitChecker):
issues.append(CommitIssue(f"Malformed value '{value}' for commit trailer '{key}'"))
continue
+ if key == 'Signed-off-by':
+ if value == commit.author:
+ sob_found = True
+
+ if not sob_found:
+ issues.append(CommitIssue(f"No 'Signed-off-by' trailer matching author '{commit.author}', see Documentation/contributing.rst"))
+
return issues
@@ -522,37 +596,8 @@ class TrailersChecker(CommitChecker):
# Style Checkers
#
-class StyleChecker(metaclass=ClassRegistry):
- subclasses = []
-
- def __init__(self):
- pass
-
- #
- # Class methods
- #
- @classmethod
- def checkers(cls, filename, names):
- for checker in cls.subclasses:
- if names and checker.__name__ not in names:
- continue
- if checker.supports(filename):
- yield checker
-
- @classmethod
- def supports(cls, filename):
- for pattern in cls.patterns:
- if fnmatch.fnmatch(os.path.basename(filename), pattern):
- return True
- return False
-
- @classmethod
- def all_patterns(cls):
- patterns = set()
- for checker in cls.subclasses:
- patterns.update(checker.patterns)
-
- return patterns
+class StyleChecker(CheckerBase):
+ pass
class StyleIssue(object):
@@ -562,21 +607,36 @@ class StyleIssue(object):
self.line = line
self.msg = msg
+ def __str__(self):
+ s = []
+ s.append(f'{Colours.fg(Colours.Yellow)}#{self.line_number}: {self.msg}{Colours.reset()}')
+ if self.line is not None:
+ s.append(f'{Colours.fg(Colours.Yellow)}+{self.line.rstrip()}{Colours.reset()}')
+
+ if self.position is not None:
+ # Align the position marker by using the original line with
+ # all characters except for tabs replaced with spaces. This
+ # ensures proper alignment regardless of how the code is
+ # indented.
+ start = self.position[0]
+ prefix = ''.join([c if c == '\t' else ' ' for c in self.line[:start]])
+ length = self.position[1] - start - 1
+ s.append(f' {prefix}^{"~" * length}')
+
+ return '\n'.join(s)
+
class HexValueChecker(StyleChecker):
patterns = ('*.c', '*.cpp', '*.h')
regex = re.compile(r'\b0[xX][0-9a-fA-F]+\b')
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
+ @classmethod
+ def check(cls, content, line_numbers):
issues = []
for line_number in line_numbers:
- line = self.__content[line_number - 1]
+ line = content[line_number - 1]
match = HexValueChecker.regex.search(line)
if not match:
continue
@@ -600,15 +660,12 @@ class IncludeChecker(StyleChecker):
'cwchar', 'cwctype', 'math.h')
include_regex = re.compile(r'^#include <([a-z.]*)>')
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
+ @classmethod
+ def check(self, content, line_numbers):
issues = []
for line_number in line_numbers:
- line = self.__content[line_number - 1]
+ line = content[line_number - 1]
match = IncludeChecker.include_regex.match(line)
if not match:
continue
@@ -634,14 +691,11 @@ class LogCategoryChecker(StyleChecker):
log_regex = re.compile(r'\bLOG\((Debug|Info|Warning|Error|Fatal)\)')
patterns = ('*.cpp',)
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
+ @classmethod
+ def check(cls, content, line_numbers):
issues = []
for line_number in line_numbers:
- line = self.__content[line_number-1]
+ line = content[line_number - 1]
match = LogCategoryChecker.log_regex.search(line)
if not match:
continue
@@ -655,14 +709,11 @@ class LogCategoryChecker(StyleChecker):
class MesonChecker(StyleChecker):
patterns = ('meson.build',)
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
+ @classmethod
+ def check(cls, content, line_numbers):
issues = []
for line_number in line_numbers:
- line = self.__content[line_number-1]
+ line = content[line_number - 1]
pos = line.find('\t')
if pos != -1:
issues.append(StyleIssue(line_number, [pos, pos], line,
@@ -670,57 +721,18 @@ class MesonChecker(StyleChecker):
return issues
-class Pep8Checker(StyleChecker):
- patterns = ('*.py',)
- results_regex = re.compile(r'stdin:([0-9]+):([0-9]+)(.*)')
-
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
- issues = []
- data = ''.join(self.__content).encode('utf-8')
-
- try:
- ret = subprocess.run(['pycodestyle', '--ignore=E501', '-'],
- input=data, stdout=subprocess.PIPE)
- except FileNotFoundError:
- issues.append(StyleIssue(0, None, None, 'Please install pycodestyle to validate python additions'))
- return issues
-
- results = ret.stdout.decode('utf-8').splitlines()
- for item in results:
- search = re.search(Pep8Checker.results_regex, item)
- line_number = int(search.group(1))
- position = int(search.group(2))
- msg = search.group(3)
-
- if line_number in line_numbers:
- line = self.__content[line_number - 1]
- issues.append(StyleIssue(line_number, None, line, msg))
-
- return issues
-
-
class ShellChecker(StyleChecker):
+ dependencies = ('shellcheck',)
patterns = ('*.sh',)
results_line_regex = re.compile(r'In - line ([0-9]+):')
- def __init__(self, content):
- super().__init__()
- self.__content = content
-
- def check(self, line_numbers):
+ @classmethod
+ def check(cls, content, line_numbers):
issues = []
- data = ''.join(self.__content).encode('utf-8')
+ data = ''.join(content).encode('utf-8')
- try:
- ret = subprocess.run(['shellcheck', '-Cnever', '-'],
- input=data, stdout=subprocess.PIPE)
- except FileNotFoundError:
- issues.append(StyleIssue(0, None, None, 'Please install shellcheck to validate shell script additions'))
- return issues
+ ret = subprocess.run(['shellcheck', '-Cnever', '-'],
+ input=data, stdout=subprocess.PIPE)
results = ret.stdout.decode('utf-8').splitlines()
for nr, item in enumerate(results):
@@ -742,40 +754,12 @@ class ShellChecker(StyleChecker):
# Formatters
#
-class Formatter(metaclass=ClassRegistry):
- subclasses = []
-
- def __init__(self):
- pass
-
- #
- # Class methods
- #
- @classmethod
- def formatters(cls, filename, names):
- for formatter in cls.subclasses:
- if names and formatter.__name__ not in names:
- continue
- if formatter.supports(filename):
- yield formatter
-
- @classmethod
- def supports(cls, filename):
- for pattern in cls.patterns:
- if fnmatch.fnmatch(os.path.basename(filename), pattern):
- return True
- return False
-
- @classmethod
- def all_patterns(cls):
- patterns = set()
- for formatter in cls.subclasses:
- patterns.update(formatter.patterns)
-
- return patterns
+class Formatter(CheckerBase):
+ pass
class CLangFormatter(Formatter):
+ dependencies = ('clang-format',)
patterns = ('*.c', '*.cpp', '*.h')
priority = -1
@@ -904,6 +888,17 @@ class IncludeOrderFormatter(Formatter):
return '\n'.join(lines)
+class Pep8Formatter(Formatter):
+ dependencies = ('autopep8',)
+ patterns = ('*.py',)
+
+ @classmethod
+ def format(cls, filename, data):
+ ret = subprocess.run(['autopep8', '--ignore=E501', '-'],
+ input=data.encode('utf-8'), stdout=subprocess.PIPE)
+ return ret.stdout.decode('utf-8')
+
+
class StripTrailingSpaceFormatter(Formatter):
patterns = ('*.c', '*.cpp', '*.h', '*.py', 'meson.build')
@@ -919,6 +914,24 @@ class StripTrailingSpaceFormatter(Formatter):
# Style checking
#
+def check_commit(top_level, commit, checkers):
+ issues = []
+
+ # Apply the commit checkers first.
+ for checker in CommitChecker.instances(commit, checkers):
+ issues_ = checker.check_dependencies()
+ if issues_:
+ issues += issues_
+ continue
+
+ issues += checker.check(commit, top_level)
+
+ for issue in issues:
+ print(issue)
+
+ return len(issues)
+
+
def check_file(top_level, commit, filename, checkers):
# Extract the line numbers touched by the commit.
commit_diff = commit.get_diff(top_level, filename)
@@ -934,9 +947,15 @@ def check_file(top_level, commit, filename, checkers):
# Format the file after the commit with all formatters and compute the diff
# between the unformatted and formatted contents.
after = commit.get_file(filename)
+ issues = []
formatted = after
- for formatter in Formatter.formatters(filename, checkers):
+ for formatter in Formatter.instances(filename, checkers):
+ issues_ = formatter.check_dependencies()
+ if issues_:
+ issues += issues_
+ continue
+
formatted = formatter.format(filename, formatted)
after = after.splitlines(True)
@@ -949,11 +968,14 @@ def check_file(top_level, commit, filename, checkers):
formatted_diff = [hunk for hunk in formatted_diff if hunk.intersects(lines)]
# Check for code issues not related to formatting.
- issues = []
- for checker in StyleChecker.checkers(filename, checkers):
- checker = checker(after)
+ for checker in StyleChecker.instances(filename, checkers):
+ issues_ = checker.check_dependencies()
+ if issues_:
+ issues += issues_
+ continue
+
for hunk in commit_diff:
- issues += checker.check(hunk.side('to').touched)
+ issues += checker.check(after, hunk.side('to').touched)
# Print the detected issues.
if len(issues) == 0 and len(formatted_diff) == 0:
@@ -967,23 +989,9 @@ def check_file(top_level, commit, filename, checkers):
print(hunk)
if len(issues):
- issues = sorted(issues, key=lambda i: i.line_number)
+ issues = sorted(issues, key=lambda i: getattr(i, 'line_number', -1))
for issue in issues:
- print('%s#%u: %s%s' % (Colours.fg(Colours.Yellow), issue.line_number,
- issue.msg, Colours.reset()))
- if issue.line is not None:
- print('%s+%s%s' % (Colours.fg(Colours.Yellow), issue.line.rstrip(),
- Colours.reset()))
-
- if issue.position is not None:
- # Align the position marker by using the original line with
- # all characters except for tabs replaced with spaces. This
- # ensures proper alignment regardless of how the code is
- # indented.
- start = issue.position[0]
- prefix = ''.join([c if c == '\t' else ' ' for c in issue.line[:start]])
- length = issue.position[1] - start - 1
- print(' ' + prefix + '^' + '~' * length)
+ print(issue)
return len(formatted_diff) + len(issues)
@@ -995,13 +1003,8 @@ def check_style(top_level, commit, checkers):
print(title)
print(separator)
- issues = 0
-
# Apply the commit checkers first.
- for checker in CommitChecker.checkers(checkers):
- for issue in checker.check(commit, top_level):
- print('%s%s%s' % (Colours.fg(Colours.Yellow), issue.msg, Colours.reset()))
- issues += 1
+ issues = check_commit(top_level, commit, checkers)
# Filter out files we have no checker for.
patterns = set()
@@ -1073,7 +1076,7 @@ def main(argv):
if args.checkers:
args.checkers = args.checkers.split(',')
- # Check for required dependencies.
+ # Check for required common dependencies.
for command, mandatory in dependencies.items():
found = shutil.which(command)
if mandatory and not found: