diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1510da1d..22aadf97c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -334,6 +334,13 @@ check-commits: junit: results.xml allow_failure: true +check-whitespace: + extends: + - .fdo.ci-fairy + stage: test + script: + - .gitlab-ci/whitespace-check.py $(git ls-files hw/xwayland) + # # Workflow rules needed due to: # https://gitlab.freedesktop.org/freedesktop/freedesktop/-/issues/438 diff --git a/.gitlab-ci/whitespace-check.py b/.gitlab-ci/whitespace-check.py new file mode 100755 index 000000000..ca6745ab2 --- /dev/null +++ b/.gitlab-ci/whitespace-check.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT + +from pathlib import Path +from dataclasses import dataclass + +import argparse +import itertools +import os +import re +import sys + + +@dataclass +class WhitespaceError: + message: str + lineno: int + nlines: int = 1 + column: None | int = None + ncolumns: int = 1 + + +def test_tab_indent(lines: list[str]) -> list[WhitespaceError]: + errors = [] + for idx, l in enumerate(lines): + if re.match("^\t+.*", l): + errors.append( + WhitespaceError( + "Tab indent", idx, ncolumns=2 + ) + ) + return errors + + +def test_duplicate_empty_lines(lines: list[str]) -> list[WhitespaceError]: + errors = [] + for idx, (l1, l2) in enumerate(itertools.pairwise(lines)): + if not l1 and not l2: + errors.append(WhitespaceError("Duplicated empty lines", idx, nlines=2)) + return errors + + +def test_tab_after_space(lines: list[str]) -> list[WhitespaceError]: + errors = [] + for idx, l in enumerate(lines): + index = l.find(" \t") + if index > -1: + errors.append( + WhitespaceError( + "Tab after space", idx, nlines=index, column=index, ncolumns=2 + ) + ) + return errors + + +def test_trailing_whitespace(lines: list[str]) -> list[WhitespaceError]: + errors = [] + for idx, l in enumerate(lines): + if l.rstrip() != l: + errors.append(WhitespaceError("Trailing whitespace", idx)) + return errors + + +def test_empty_line_between_braces(lines: list[str]) -> list[WhitespaceError]: + errors = [] + for idx in range(len(lines) - 3): + l1 = lines[idx] + l2 = lines[idx + 1] + l3 = lines[idx + 2] + if l1.strip() == "}" and l3.strip() == "}" and l2.strip() == "": + errors.append(WhitespaceError("Empty line between closing braces", idx + 1)) + return errors + + +def main(): + parser = argparse.ArgumentParser(description="Whitespace checker script") + parser.add_argument( + "files", + metavar="FILES", + type=Path, + nargs="+", + help="The files to check", + ) + + args = parser.parse_args() + + have_errors: bool = False + + if os.isatty(sys.stderr.fileno()): + red = "\x1b[0;31m" + reset = "\x1b[0m" + else: + red = "" + reset = "" + + for file in args.files: + lines = [l.rstrip("\n") for l in file.open().readlines()] + + errors = [] + errors.extend(test_tab_indent(lines)) + errors.extend(test_tab_after_space(lines)) + errors.extend(test_trailing_whitespace(lines)) + if any(file.name.endswith(suffix) for suffix in [".c", ".h"]): + if not file.parts[0] == "include": + errors.extend(test_duplicate_empty_lines(lines)) + errors.extend(test_empty_line_between_braces(lines)) + + for e in errors: + print(f"{red}ERROR: {e.message} in {file}:{reset}", file=sys.stderr) + print(f"{'-' * 72}", file=sys.stderr) + lineno = max(0, e.lineno - 5) + for idx, l in enumerate(lines[lineno : lineno + 10]): + if e.lineno <= lineno + idx < e.lineno + e.nlines: + prefix = "->" + hl = red + nohl = reset + else: + prefix = " " + hl = "" + nohl = "" + print(f"{hl}{lineno + idx:3d}: {prefix} {l.rstrip()}{nohl}") + + print(f"{'-' * 72}", file=sys.stderr) + + if errors: + have_errors = True + + if have_errors: + sys.exit(1) + + +if __name__ == "__main__": + main()