summaryrefslogtreecommitdiff
path: root/src
AgeCommit message (Expand)Author
2022-10-24apps: cam: Fix compilation error with clang when libtiff-4 is not foundLaurent Pinchart
2022-10-20apps: Move libtiff dependency to src/apps/meson.buildLaurent Pinchart
2022-10-20apps: Move libevent dependency to src/apps/meson.buildLaurent Pinchart
2022-10-20Move test applications to src/apps/Laurent Pinchart
2022-10-20qcam: Simplify dependency handling for libtiffLaurent Pinchart
2022-10-20cam: Don't print DNG option text if libtiff isn't foundLaurent Pinchart
2022-10-20ipa: raspberrypi: Rename ov9281.json to ov9281_mono.jsonNaushir Patuck
2022-10-20libcamera: base: log: Fix LogCategory creation issuesTomi Valkeinen
2022-10-20libcamera: base: log: Fix use of freed nameTomi Valkeinen
2022-10-19ipa: rkisp1: Downgrade sensor controls range message to DebugLaurent Pinchart
2022-10-19ipa: Drop period at end of \brief or \paramLaurent Pinchart
2022-10-19cam: dng_writer: Add support for 8-bit raw formatsPaul Elder
2022-10-19cam: file_sink: Add support for DNG outputPaul Elder
2022-10-19qcam, cam: Move DNGWriter from qcam to camPaul Elder
2022-10-18ipa: vimc: Add Flags to parametersPaul Elder
2022-10-18ipa: vimc: Add IPAOperationCode to init() parameter listPaul Elder
2022-10-18ipa: raspberrypi: Extract line length from the embedded data parserNaushir Patuck
2022-10-18ipa: raspberrypi: Allow full line length controlNaushir Patuck
2022-10-18ipa: raspberrypi: Add line length to DeviceStatusNaushir Patuck
2022-10-18ipa: raspberrypi: Add line length calculations helper functionsNaushir Patuck
2022-10-18ipa: raspberrypi: Add pixel clock rate to the CameraMode structureNaushir Patuck
2022-10-18pipeline: ipa: raspberrypi: Add HBLANK control to DelayedControlsNaushir Patuck
2022-10-18ipa: raspberrypi: Remove initialized_ field from CamHelperNaushir Patuck
2022-10-18ipa: raspberrypi: Pass lineLength into the CamHelper APINaushir Patuck
2022-10-18ipa: raspberrypi: Add minimum and maximum line length fields to CameraModeNaushir Patuck
2022-10-18camera_sensor: Add minimum and maximum line length to IPACameraSensorInfoNaushir Patuck
2022-10-17qcam: dng: Make TIFFTAG_CFAPATTERN variable countJean-Michel Hautbois via libcamera-devel
2022-10-15libcamera: v4l2_subdevice: Add JPEG_1X8 and BGR888_1X24 mbus formats to forma...Xavier Roumegue
2022-10-13meson: Shared Object version handlingKieran Bingham
2022-10-10libcamera: v4l2_videodevice: Warn if bytesused == 0 when queuing output bufferLaurent Pinchart
2022-10-10pipeline: rkisp1: Set bytesused before queuing parameters bufferLaurent Pinchart
2022-10-10pipeline: ipu3: Set bytesused before queuing parameters bufferLaurent Pinchart
2022-10-10libcamera: framebuffer: Move remaining private data to Private classLaurent Pinchart
2022-10-10libcamera: base: utils: Drop defoptLaurent Pinchart
2022-10-10ipa: rkisp1: Drop use of utils::defoptLaurent Pinchart
2022-10-07ipa: raspberrypi: Remove unneeded Span castsLaurent Pinchart
2022-10-07libcamera: controls: Construct Span with size for array controlsLaurent Pinchart
2022-10-07utils: gen-controls: Improve YAML notation for variable-size array controlsLaurent Pinchart
2022-10-07libcamera: pipeline_handler: Implement factories through class templatesLaurent Pinchart
2022-10-07libcamera: pipeline_handler: Return unique_ptr from createInstanceLaurent Pinchart
2022-10-07libcamera: pipeline_handler: Make factory create() function constLaurent Pinchart
2022-10-07ipa: camera_sensor_helper: Implement factories through class templatesLaurent Pinchart
2022-10-07ipa: camera_sensor_helper: Return unique_ptr from createInstanceLaurent Pinchart
2022-10-07ipa: camera_sensor_helper: Make registerType() and createInstance() privateLaurent Pinchart
2022-10-07ipa: camera_sensor_helper: Make factory createInstance() function constLaurent Pinchart
2022-10-07ipa: ipu3: Fix minor Doxygen issues in IPAFrameContextLaurent Pinchart
2022-10-05test: threads: Fix link failure due to missing dependencyLaurent Pinchart
2022-10-05ipa: raspberrypi: Fix the imx296 mono tuning black levelNaushir Patuck
2022-10-05ipa: raspberrypi: Add a tuning file for the colour variant of IMX296Naushir Patuck
2022-10-05pipeline: raspberrypi: Update naming convention for tuning filesNaushir Patuck
'>417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2018, Google Inc.
#
# Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
#
# checkstyle.py - A patch style checker script based on astyle or clang-format
#
# TODO:
#
# - Support other formatting tools and checkers (cppcheck, cpplint, kwstyle, ...)
# - Split large hunks to minimize context noise
# - Improve style issues counting
#

import argparse
import difflib
import fnmatch
import os.path
import re
import shutil
import subprocess
import sys

astyle_options = (
    '-n',
    '--style=linux',
    '--indent=force-tab=8',
    '--attach-namespaces',
    '--attach-extern-c',
    '--pad-oper',
    '--align-pointer=name',
    '--align-reference=name',
    '--keep-one-line-blocks',
    '--max-code-length=120'
)

dependencies = {
    'astyle': False,
    'clang-format': False,
    'git': True,
}

# ------------------------------------------------------------------------------
# Colour terminal handling
#

class Colours:
    Default = 0
    Black = 0
    Red = 31
    Green = 32
    Yellow = 33
    Blue = 34
    Magenta = 35
    Cyan = 36
    LightGrey = 37
    DarkGrey = 90
    LightRed = 91
    LightGreen = 92
    Lightyellow = 93
    LightBlue = 94
    LightMagenta = 95
    LightCyan = 96
    White = 97

    @staticmethod
    def fg(colour):
        if sys.stdout.isatty():
            return '\033[%um' % colour
        else:
            return ''

    @staticmethod
    def bg(colour):
        if sys.stdout.isatty():
            return '\033[%um' % (colour + 10)
        else:
            return ''

    @staticmethod
    def reset():
        if sys.stdout.isatty():
            return '\033[0m'
        else:
            return ''


# ------------------------------------------------------------------------------
# Diff parsing, handling and printing
#

class DiffHunkSide(object):
    """A side of a diff hunk, recording line numbers"""
    def __init__(self, start):
        self.start = start
        self.touched = []
        self.untouched = []

    def __len__(self):
        return len(self.touched) + len(self.untouched)


class DiffHunk(object):
    diff_header_regex = re.compile(r'@@ -([0-9]+),?([0-9]+)? \+([0-9]+),?([0-9]+)? @@')

    def __init__(self, line):
        match = DiffHunk.diff_header_regex.match(line)
        if not match:
            raise RuntimeError("Malformed diff hunk header '%s'" % line)

        self.__from_line = int(match.group(1))
        self.__to_line = int(match.group(3))
        self.__from = DiffHunkSide(self.__from_line)
        self.__to = DiffHunkSide(self.__to_line)

        self.lines = []

    def __repr__(self):
        s = '%s@@ -%u,%u +%u,%u @@\n' % \
                (Colours.fg(Colours.Cyan),
                 self.__from.start, len(self.__from),
                 self.__to.start, len(self.__to))

        for line in self.lines:
            if line[0] == '-':
                s += Colours.fg(Colours.Red)
            elif line[0] == '+':
                s += Colours.fg(Colours.Green)

            if line[0] == '-':
                spaces = 0
                for i in range(len(line)):
                    if line[-i-1].isspace():
                        spaces += 1
                    else:
                        break
                spaces = len(line) - spaces
                line = line[0:spaces] + Colours.bg(Colours.Red) + line[spaces:]

            s += line
            s += Colours.reset()
            s += '\n'

        return s[:-1]

    def append(self, line):
        if line[0] == ' ':
            self.__from.untouched.append(self.__from_line)
            self.__from_line += 1
            self.__to.untouched.append(self.__to_line)
            self.__to_line += 1
        elif line[0] == '-':
            self.__from.touched.append(self.__from_line)
            self.__from_line += 1
        elif line[0] == '+':
            self.__to.touched.append(self.__to_line)
            self.__to_line += 1

        self.lines.append(line.rstrip('\n'))

    def intersects(self, lines):
        for line in lines:
            if line in self.__from.touched:
                return True
        return False

    def side(self, side):
        if side == 'from':
            return self.__from
        else:
            return self.__to


def parse_diff(diff):
    hunks = []
    hunk = None
    for line in diff:
        if line.startswith('@@'):
            if hunk:
                hunks.append(hunk)
            hunk = DiffHunk(line)

        elif hunk is not None:
            hunk.append(line)

    if hunk:
        hunks.append(hunk)

    return hunks


# ------------------------------------------------------------------------------
# Style Checkers
#

_style_checkers = []

class StyleCheckerRegistry(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(StyleCheckerRegistry, cls).__new__(cls, clsname, bases, attrs)
        if clsname != 'StyleChecker':
            _style_checkers.append(newclass)
        return newclass


class StyleChecker(metaclass=StyleCheckerRegistry):
    def __init__(self):
        pass

    #
    # Class methods
    #
    @classmethod
    def checkers(cls, filename):
        for checker in _style_checkers:
            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 _style_checkers:
            patterns.update(checker.patterns)

        return patterns


class StyleIssue(object):
    def __init__(self, line_number, line, msg):
        self.line_number = line_number
        self.line = line
        self.msg = msg


class IncludeChecker(StyleChecker):
    patterns = ('*.cpp', '*.h')

    headers = ('assert', 'ctype', 'errno', 'fenv', 'float', 'inttypes',
               'limits', 'locale', 'setjmp', 'signal', 'stdarg', 'stddef',
               'stdint', 'stdio', 'stdlib', 'string', 'time', 'uchar', 'wchar',
               'wctype')
    include_regex = re.compile('^#include <c([a-z]*)>')

    def __init__(self, content):
        super().__init__()
        self.__content = content

    def check(self, line_numbers):
        issues = []

        for line_number in line_numbers:
            line = self.__content[line_number - 1]
            match = IncludeChecker.include_regex.match(line)
            if not match:
                continue

            header = match.group(1)
            if header not in IncludeChecker.headers:
                continue

            issues.append(StyleIssue(line_number, line,
                                     'C compatibility header <%s.h> is preferred' % header))

        return issues


class LogCategoryChecker(StyleChecker):
    log_regex = re.compile('\\bLOG\((Debug|Info|Warning|Error|Fatal)\)')
    patterns = ('*.cpp',)

    def __init__(self, content):
        super().__init__()
        self.__content = content

    def check(self, line_numbers):
        issues = []
        for line_number in line_numbers:
            line = self.__content[line_number-1]
            if not LogCategoryChecker.log_regex.search(line):
                continue

            issues.append(StyleIssue(line_number, line, 'LOG() should use categories'))

        return issues


class MesonChecker(StyleChecker):
    patterns = ('meson.build',)

    def __init__(self, content):
        super().__init__()
        self.__content = content

    def check(self, line_numbers):
        issues = []
        for line_number in line_numbers:
            line = self.__content[line_number-1]
            if line.find('\t') != -1:
                issues.append(StyleIssue(line_number, line, 'meson.build should use spaces for indentation'))
        return issues


class Pep8Checker(StyleChecker):
    patterns = ('*.py',)
    results_regex = re.compile('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, "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, line, msg))

        return issues


class ShellChecker(StyleChecker):
    patterns = ('*.sh',)
    results_line_regex = re.compile('In - line ([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(['shellcheck', '-Cnever', '-'],
                                 input=data, stdout=subprocess.PIPE)
        except FileNotFoundError:
            issues.append(StyleIssue(0, None, "Please install shellcheck to validate shell script additions"))
            return issues

        results = ret.stdout.decode('utf-8').splitlines()
        for nr, item in enumerate(results):
            search = re.search(ShellChecker.results_line_regex, item)
            if search is None:
                continue

            line_number = int(search.group(1))
            line = results[nr + 1]
            msg = results[nr + 2]

            # Determined, but not yet used
            position = msg.find('^') + 1

            if line_number in line_numbers:
                issues.append(StyleIssue(line_number, line, msg))

        return issues


# ------------------------------------------------------------------------------
# Formatters
#

_formatters = []

class FormatterRegistry(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(FormatterRegistry, cls).__new__(cls, clsname, bases, attrs)
        if clsname != 'Formatter':
            _formatters.append(newclass)
        return newclass


class Formatter(metaclass=FormatterRegistry):
    enabled = True

    def __init__(self):
        pass

    #
    # Class methods
    #
    @classmethod
    def formatters(cls, filename):
        for formatter in _formatters:
            if not cls.enabled:
                continue
            if formatter.supports(filename):
                yield formatter

    @classmethod
    def supports(cls, filename):
        if not cls.enabled:
            return False
        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 _formatters:
            if not cls.enabled:
                continue
            patterns.update(formatter.patterns)

        return patterns


class AStyleFormatter(Formatter):
    enabled = False
    patterns = ('*.c', '*.cpp', '*.h')

    @classmethod
    def format(cls, filename, data):
        ret = subprocess.run(['astyle', *astyle_options],
                             input=data.encode('utf-8'), stdout=subprocess.PIPE)
        return ret.stdout.decode('utf-8')


class CLangFormatter(Formatter):
    enabled = False
    patterns = ('*.c', '*.cpp', '*.h')

    @classmethod
    def format(cls, filename, data):
        ret = subprocess.run(['clang-format', '-style=file',
                              '-assume-filename=' + filename],
                             input=data.encode('utf-8'), stdout=subprocess.PIPE)
        return ret.stdout.decode('utf-8')


class DoxygenFormatter(Formatter):
    patterns = ('*.c', '*.cpp')

    return_regex = re.compile(' +\\* +\\\\return +[a-z]')

    @classmethod
    def format(cls, filename, data):
        lines = []
        in_doxygen = False

        for line in data.split('\n'):
            if line.find('/**') != -1:
                in_doxygen = True

            if not in_doxygen:
                lines.append(line)
                continue

            line = cls.return_regex.sub(lambda m: m.group(0)[:-1] + m.group(0)[-1].upper(), line)

            if line.find('*/') != -1:
                in_doxygen = False

            lines.append(line)

        return '\n'.join(lines)


class DPointerFormatter(Formatter):
    # Ensure consistent naming of variables related to the d-pointer design
    # pattern.
    patterns = ('*.cpp', '*.h')

    # The clang formatter runs first, we can thus rely on appropriate coding
    # style.
    declare_regex = re.compile(r'^(\t*)(const )?([a-zA-Z0-9_]+) \*( ?const )?([a-zA-Z0-9_]+) = (LIBCAMERA_[DO]_PTR)\(([a-zA-Z0-9_]+)\);$')

    @classmethod
    def format(cls, filename, data):
        lines = []

        for line in data.split('\n'):
            match = cls.declare_regex.match(line)
            if match:
                indent = match.group(1) or ''
                const = match.group(2) or ''
                macro = match.group(6)
                klass = match.group(7)
                if macro == 'LIBCAMERA_D_PTR':
                    var = 'Private *const d'
                else:
                    var = f'{klass} *const o'

                line = f'{indent}{const}{var} = {macro}({klass});'

            lines.append(line)

        return '\n'.join(lines)


class IncludeOrderFormatter(Formatter):
    patterns = ('*.cpp', '*.h')

    include_regex = re.compile('^#include ["<]([^">]*)[">]')

    @classmethod
    def format(cls, filename, data):
        lines = []
        includes = []

        # Parse blocks of #include statements, and output them as a sorted list
        # when we reach a non #include statement.
        for line in data.split('\n'):
            match = IncludeOrderFormatter.include_regex.match(line)
            if match:
                # If the current line is an #include statement, add it to the
                # includes group and continue to the next line.
                includes.append((line, match.group(1)))
                continue

            # The current line is not an #include statement, output the sorted
            # stashed includes first, and then the current line.
            if len(includes):
                includes.sort(key=lambda i: i[1])
                for include in includes:
                    lines.append(include[0])
                includes = []

            lines.append(line)

        # In the unlikely case the file ends with an #include statement, make
        # sure we output the stashed includes.
        if len(includes):
            includes.sort(key=lambda i: i[1])
            for include in includes:
                lines.append(include[0])
            includes = []

        return '\n'.join(lines)


class StripTrailingSpaceFormatter(Formatter):
    patterns = ('*.c', '*.cpp', '*.h', '*.py', 'meson.build')

    @classmethod
    def format(cls, filename, data):
        lines = data.split('\n')
        for i in range(len(lines)):
            lines[i] = lines[i].rstrip() + '\n'
        return ''.join(lines)


# ------------------------------------------------------------------------------
# Style checking
#

class Commit:
    def __init__(self, commit):
        self.commit = commit

    def get_info(self):
        # Get the commit title and list of files.
        ret = subprocess.run(['git', 'show', '--pretty=oneline', '--name-only',
                              self.commit],
                             stdout=subprocess.PIPE).stdout.decode('utf-8')
        files = ret.splitlines()
        # Returning title and files list as a tuple
        return files[0], files[1:]

    def get_diff(self, top_level, filename):
        return subprocess.run(['git', 'diff', '%s~..%s' % (self.commit, self.commit),
                               '--', '%s/%s' % (top_level, filename)],
                              stdout=subprocess.PIPE).stdout.decode('utf-8')

    def get_file(self, filename):
        return subprocess.run(['git', 'show', '%s:%s' % (self.commit, filename)],
                              stdout=subprocess.PIPE).stdout.decode('utf-8')


class StagedChanges(Commit):
    def __init__(self):
        Commit.__init__(self, '')

    def get_info(self):
        ret = subprocess.run(['git', 'diff', '--staged', '--name-only'],
                             stdout=subprocess.PIPE).stdout.decode('utf-8')
        return "Staged changes", ret.splitlines()

    def get_diff(self, top_level, filename):