#!/usr/bin/env python3

# Copyright (C) 2024-2026 Free Software Foundation, Inc.
#
# This file is part of GDB.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# This is intended to be run from pre-commit.  You can also run it by
# hand by passing repository-relative filenames to it, like:
#   ./gdb/check-include-guards.py [--update] gdb/*.h
# When --update is used, rewrite the files in place as needed.


import fnmatch
import re
import sys
from typing import List

DEF = re.compile("^#ifndef ([A-Za-z0-9_]+)\n")
OLDDEF = re.compile("^#if !defined *\\(([A-Za-z0-9_]+)\\)\n")

# Some headers -- in particular, ones that aren't maintained by gdb --
# should be excluded from the checks.
#
# This is interpreted as a list of patterns as interpreted by fnmatch.
EXCLUDED = ("gdbsupport/unordered_dense/*",)


# See if
write_files = False
args = sys.argv[1:]
if len(args) > 0 and args[0] == "--update":
    write_files = True
    args = args[1:]


def failure(filename: str, ndx: int, text: str):
    print(filename + ":" + str(ndx + 1) + ": " + text, file=sys.stderr)
    sys.exit(1)


def skip_comments_and_blanks(ndx: int, contents: List[str]):
    while ndx < len(contents) and contents[ndx].startswith("/*"):
        while ndx < len(contents):
            ndx += 1
            if contents[ndx - 1].endswith("*/\n"):
                break
        # Skip blank lines.
        while ndx < len(contents):
            if contents[ndx].strip() != "":
                break
            ndx += 1
    return ndx


def write_header(filename: str, contents: List[str]):
    with open(filename, "w") as f:
        f.writelines(contents)


def check_header(filename: str):
    for pat in EXCLUDED:
        if fnmatch.fnmatch(filename, pat):
            return

    # Turn x/y-z.h into X_Y_Z_H.
    assert filename.endswith(".h")
    expected = filename.replace("-", "_")
    expected = expected.replace(".", "_")
    expected = expected.replace("/", "_")
    expected = expected.upper()
    with open(filename) as f:
        contents = list(f)
    if len(contents) == 0:
        # Empty file -- pathological but we can just ignore rather
        # than crashing.
        return
    if "THIS FILE IS GENERATED" in contents[0]:
        # Ignore.
        return
    if not contents[0].startswith("/*"):
        failure(filename, 0, "header should start with comment")
    i = skip_comments_and_blanks(0, contents)
    if i == len(contents):
        failure(filename, i, "unterminated intro comment or missing body")
    m = DEF.match(contents[i])
    force_rewrite = False
    if not m:
        m = OLDDEF.match(contents[i])
        if not m:
            failure(filename, i, "no header guard")
        force_rewrite = True
    symbol = m.group(1)
    # Either None or a tuple like (LINE, TEXT) that describes a needed
    # update.
    updated = None
    if symbol != expected:
        force_rewrite = True
    if force_rewrite:
        contents[i] = "#ifndef " + expected + "\n"
        updated = (i, "wrong symbol in ifndef")
    i += 1
    if i == len(contents):
        failure(filename, i, "premature EOF")
    if not contents[i].startswith("#define "):
        failure(filename, i, "no define of header guard")
    if contents[i] != "#define " + expected + "\n":
        contents[i] = "#define " + expected + "\n"
        if updated is None:
            updated = (i, "wrong symbol in define")
    i = len(contents) - 1
    if not contents[i].startswith("#endif"):
        failure(filename, i, "no trailing endif")
    if contents[i] != "#endif /* " + expected + " */\n":
        contents[i] = "#endif /* " + expected + " */\n"
        if updated is None:
            updated = (i, "wrong endif line")
    if updated is not None:
        if write_files:
            write_header(filename, contents)
        else:
            failure(filename, *updated)


for filename in args:
    check_header(filename)
