From 22f5b6111fd0dde3a53adfcb8dfd1eaa5e4070b4 Mon Sep 17 00:00:00 2001 From: Rito12 Date: Sun, 14 Dec 2025 21:02:16 +0100 Subject: [PATCH] Codechange: Add GitHub workflow that checks descriptions of source files. --- .github/file-descriptions.py | 77 +++++++++++++++++++++++++ .github/workflows/file-descriptions.yml | 27 +++++++++ 2 files changed, 104 insertions(+) create mode 100644 .github/file-descriptions.py create mode 100644 .github/workflows/file-descriptions.yml diff --git a/.github/file-descriptions.py b/.github/file-descriptions.py new file mode 100644 index 0000000000..a18f6f2082 --- /dev/null +++ b/.github/file-descriptions.py @@ -0,0 +1,77 @@ +""" +Script to scan the OpenTTD source-tree for doxygen @file annotations. +Checks whether they exist, are recognised by doxygen and match coding style. +""" + +import os +import sys + +END_OF_SENTENCE = [".", "!", "?"] +SOURCE_FILE_EXTENSION = ["cpp", "c", "hpp", "h", "mm", "m", "cc"] +TEMPLATE_FILE_EXTENSION = ["preamble", "in"] +EXCLUDED_FILES = ["./src/script/api/squirrel_export.sq.hpp.in", "./src/script/api/script_includes.hpp.in"] + + +def read_files_list_from_file(file_path): + with open(file_path, "r") as f: + while line := f.readline(): + yield line[:-1] + + +def list_files_walk(start_path="."): + for root, dirs, files in os.walk(start_path): + for file in files: + yield os.path.join(root, file) + + +def check_descriptions(files): + errors = [] + for path in files: + if path in EXCLUDED_FILES: + continue + if path.find("3rdparty") != -1 or path.find("lang") != -1: + continue + name = path[path.rfind("/") + 1 :] + while True: + extension = name[name.rfind('.') + 1 :] + if extension not in TEMPLATE_FILE_EXTENSION: + break + name = name[0 : -len(extension) - 1] + if extension not in SOURCE_FILE_EXTENSION: + continue + with open(path, "r") as f: + content = f.read() + ann = content.find(f"@file {name} ") + if ann == -1: + if content.find("@file") == -1: + errors.append(f'File "{path}" does not provide description.') + else: + errors.append(f'Description of file "{path}" does not match coding style.') + continue + end = content.find("\n", ann) + start = content.rfind("\n", 0, ann) + 1 + if content[start : ann] == "/** " and content[end - 3 : end] == " */" and content[end - 4] in END_OF_SENTENCE: + continue + elif content[start : ann] == " * " and content[start - 4 : start - 1] == "/**" and content[end - 1] in END_OF_SENTENCE and content[end + 1 : end + 4] != " */": + continue + errors.append(f'Description of file "{path}" does not match coding style.') + return errors + + +def main(): + if len(sys.argv) == 1: + files = list_files_walk("./src") + else: + files = read_files_list_from_file(sys.argv[1]) + + errors = check_descriptions(files) + + if errors: + print("\n".join(errors)) + sys.exit(1) + + print("OK") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/file-descriptions.yml b/.github/workflows/file-descriptions.yml new file mode 100644 index 0000000000..8c02b90bb2 --- /dev/null +++ b/.github/workflows/file-descriptions.yml @@ -0,0 +1,27 @@ +name: File descriptions + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + file-descriptions: + name: File descriptions + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Get pull-request commits + uses: OpenTTD/actions/checkout-pull-request@v6 + + - name: Check descriptions of source files + run: | + git diff-tree --no-commit-id --name-only -r HEAD HEAD^ > modified_files.txt + cat modified_files.txt + set -ex + python3 .github/file-descriptions.py modified_files.txt