| #!/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) |