aboutsummaryrefslogtreecommitdiff
path: root/scripts/check_header_guards.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/check_header_guards.py')
-rw-r--r--scripts/check_header_guards.py208
1 files changed, 208 insertions, 0 deletions
diff --git a/scripts/check_header_guards.py b/scripts/check_header_guards.py
new file mode 100644
index 0000000000..5c48b7501f
--- /dev/null
+++ b/scripts/check_header_guards.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2023 Arm Limited.
+#
+# SPDX-License-Identifier: MIT
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import argparse
+from typing import List, Tuple
+import logging
+import re
+
+logger = logging.getLogger("check_header_guards")
+
+def find_code_boundaries(lines: List[str]) -> (int, int):
+ inside_comment : bool = False
+
+ start = len(lines)
+ end = -1
+ line_num = 0
+ for line in lines:
+ stripped_line : str = line.strip()
+ if stripped_line.startswith("/*"): # block comment start
+ inside_comment = True
+
+ if not inside_comment and not stripped_line.startswith("//") and stripped_line != "":
+ start = min(line_num, start)
+ end = line_num
+
+ if inside_comment and stripped_line.endswith("*/"):
+ inside_comment = False
+
+ line_num += 1
+
+ return start, end
+
+
+def is_define(line: str) -> bool:
+ return line.strip().startswith("#define")
+
+def is_endif(line: str) -> bool:
+ return line.strip().startswith("#endif")
+
+def is_ifndef(line: str) -> bool:
+ return line.strip().startswith("#ifndef")
+
+# Strips the given line from // and /* */ blocks
+def strip_comments(line: str) -> str:
+ line = re.sub(r"/\*.*\*/", "", line)
+ line = re.sub(r"//.*", "", line)
+ return line.strip()
+
+# If the line
+# 1) startswith #ifndef
+# 2) is all uppercase
+# 3) does not start with double underscore, i.e. __
+# Then
+# It "looks" like a header guard
+def looks_like_header_guard(line: str) -> bool:
+ sline = line.strip()
+ guard_candidate = strip_comments(sline[len("#ifndef"):])
+
+ return is_ifndef(sline) and not guard_candidate.startswith("__") and guard_candidate.isupper()
+
+
+def fix_header_guard(lines: List[str], expected_header_guard: str, comment_style: str) -> Tuple[List[str], bool]:
+ start_line, next_line, last_line = "", "", ""
+ start_index, last_index = find_code_boundaries(lines)
+ guards_updated: bool = True
+
+ if start_index < len(lines):
+ # if not, the file is full of comments
+ start_line = lines[start_index]
+
+ if start_index + 1 < len(lines):
+ # if not, the file has only one line of code
+ next_line = lines[start_index + 1]
+
+ if last_index < len(lines) and last_index > start_index + 1:
+ # if not, either the file is full of comments OR it has less than three code lines
+ last_line = lines[last_index]
+
+ expected_start_line = f"#ifndef {expected_header_guard}\n"
+ expected_next_line = f"#define {expected_header_guard}\n"
+
+ if comment_style == 'double_slash':
+ expected_last_line = f"#endif // {expected_header_guard}\n"
+ elif comment_style == 'slash_asterix':
+ expected_last_line = f"#endif /* {expected_header_guard} */\n"
+
+ empty_line = "\n"
+
+ if looks_like_header_guard(start_line) and is_define(next_line) and is_endif(last_line):
+ # modify the current header guard if necessary
+ lines = lines[:start_index] + [expected_start_line, expected_next_line] + \
+ lines[start_index+2:last_index] + [expected_last_line] + lines[last_index+1:]
+
+ guards_updated = (start_line != expected_start_line) or (next_line != expected_next_line) \
+ or (last_line != expected_last_line)
+ else:
+ # header guard could not be detected, add header guards
+ lines = lines[:start_index] + [empty_line, expected_start_line, expected_next_line] + \
+ [empty_line] + lines[start_index:] + [empty_line, expected_last_line]
+
+
+ return lines, guards_updated
+
+
+def find_expected_header_guard(filepath: str, prefix: str, add_extension: str, drop_outermost_subdir: str) -> str:
+ if drop_outermost_subdir:
+ arr : List[str] = filepath.split("/")
+ arr = arr[min(1, len(arr)-1):]
+ filepath = "/".join(arr)
+
+ if not add_extension:
+ filepath = ".".join(filepath.split(".")[:-1])
+
+ guard = filepath.replace("/", "_").replace(".", "_").upper() # snake case full path
+ return prefix + "_" + guard
+
+
+def skip_file(filepath: str, extensions: List[str], exclude: List[str], include: List[str]) -> bool:
+ extension = filepath.split(".")[-1]
+
+ if extension.lower() not in extensions:
+ return True
+
+ if exclude and any([filepath.startswith(exc) for exc in exclude]):
+ print(exclude)
+ return True
+
+ if include:
+ return not any([filepath.startswith(inc) for inc in include])
+
+ return False
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="Header Guard Checker. It adds full path snake case header guards with or without extension.",
+ )
+
+ parser.add_argument("files", type=str, nargs="+", help="Files to check the header guards")
+ parser.add_argument("--extensions", type=str, help="Comma separated list of extensions to run the checks. \
+ If the input file does not have any of the extensions, it'll be skipped", required=True)
+ parser.add_argument("--comment_style", choices=['double_slash', 'slash_asterix'], required=True)
+ parser.add_argument("--exclude", type=str, help="Comma separated list of paths to exclude from header guard checks", default="")
+ parser.add_argument("--include", type=str, help="Comma separated list of paths to include. Defaults to empty string, \
+ which means all the paths are included", default="")
+ parser.add_argument("--prefix", help="Prefix to apply to header guards", required=True)
+ parser.add_argument("--add_extension", action="store_true", help="If true, it adds the file extension to the end of the guard")
+ parser.add_argument("--drop_outermost_subdir", action="store_true", help="If true, it'll not use the outermost folder in the path. \
+ This is intended for using in subdirs with different rules")
+
+ args = parser.parse_args()
+
+ files = args.files
+ extensions = args.extensions.split(",")
+ exclude = args.exclude.split(",") if args.exclude != '' else []
+ include = args.include.split(",") if args.include != '' else []
+ prefix = args.prefix
+ add_extension = args.add_extension
+ drop_outermost_subdir = args.drop_outermost_subdir
+ comment_style = args.comment_style
+
+ logging_level = logging.INFO
+ logging.basicConfig(level=logging_level)
+
+ retval = 0
+ for file in files:
+ if skip_file(file, extensions, exclude, include):
+ logger.info(f"File {file} is SKIPPED")
+ continue
+
+ expected_header_guard : str = find_expected_header_guard(file, prefix, add_extension, drop_outermost_subdir)
+
+ with open(file, "r") as fd:
+ lines: List = fd.readlines()
+
+ new_lines, guards_updated = fix_header_guard(lines, expected_header_guard, comment_style)
+
+ with open(file, "w") as fd:
+ fd.writelines([f"{line}" for line in new_lines])
+
+ if guards_updated:
+ logger.info("File has been modified")
+ retval = 1
+
+ exit(retval)