blob: 5c48b7501fb1cfd5fb79a0f06869d100663d1466 [file] [log] [blame]
Gunes Bayiread57c22023-08-11 21:46:23 +01001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2023 Arm Limited.
5#
6# SPDX-License-Identifier: MIT
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to
10# deal in the Software without restriction, including without limitation the
11# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12# sell copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in all
16# copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24# SOFTWARE.
25
26import argparse
27from typing import List, Tuple
28import logging
29import re
30
31logger = logging.getLogger("check_header_guards")
32
33def find_code_boundaries(lines: List[str]) -> (int, int):
34 inside_comment : bool = False
35
36 start = len(lines)
37 end = -1
38 line_num = 0
39 for line in lines:
40 stripped_line : str = line.strip()
41 if stripped_line.startswith("/*"): # block comment start
42 inside_comment = True
43
44 if not inside_comment and not stripped_line.startswith("//") and stripped_line != "":
45 start = min(line_num, start)
46 end = line_num
47
48 if inside_comment and stripped_line.endswith("*/"):
49 inside_comment = False
50
51 line_num += 1
52
53 return start, end
54
55
56def is_define(line: str) -> bool:
57 return line.strip().startswith("#define")
58
59def is_endif(line: str) -> bool:
60 return line.strip().startswith("#endif")
61
62def is_ifndef(line: str) -> bool:
63 return line.strip().startswith("#ifndef")
64
65# Strips the given line from // and /* */ blocks
66def strip_comments(line: str) -> str:
67 line = re.sub(r"/\*.*\*/", "", line)
68 line = re.sub(r"//.*", "", line)
69 return line.strip()
70
71# If the line
72# 1) startswith #ifndef
73# 2) is all uppercase
74# 3) does not start with double underscore, i.e. __
75# Then
76# It "looks" like a header guard
77def looks_like_header_guard(line: str) -> bool:
78 sline = line.strip()
79 guard_candidate = strip_comments(sline[len("#ifndef"):])
80
81 return is_ifndef(sline) and not guard_candidate.startswith("__") and guard_candidate.isupper()
82
83
84def fix_header_guard(lines: List[str], expected_header_guard: str, comment_style: str) -> Tuple[List[str], bool]:
85 start_line, next_line, last_line = "", "", ""
86 start_index, last_index = find_code_boundaries(lines)
87 guards_updated: bool = True
88
89 if start_index < len(lines):
90 # if not, the file is full of comments
91 start_line = lines[start_index]
92
93 if start_index + 1 < len(lines):
94 # if not, the file has only one line of code
95 next_line = lines[start_index + 1]
96
97 if last_index < len(lines) and last_index > start_index + 1:
98 # if not, either the file is full of comments OR it has less than three code lines
99 last_line = lines[last_index]
100
101 expected_start_line = f"#ifndef {expected_header_guard}\n"
102 expected_next_line = f"#define {expected_header_guard}\n"
103
104 if comment_style == 'double_slash':
105 expected_last_line = f"#endif // {expected_header_guard}\n"
106 elif comment_style == 'slash_asterix':
107 expected_last_line = f"#endif /* {expected_header_guard} */\n"
108
109 empty_line = "\n"
110
111 if looks_like_header_guard(start_line) and is_define(next_line) and is_endif(last_line):
112 # modify the current header guard if necessary
113 lines = lines[:start_index] + [expected_start_line, expected_next_line] + \
114 lines[start_index+2:last_index] + [expected_last_line] + lines[last_index+1:]
115
116 guards_updated = (start_line != expected_start_line) or (next_line != expected_next_line) \
117 or (last_line != expected_last_line)
118 else:
119 # header guard could not be detected, add header guards
120 lines = lines[:start_index] + [empty_line, expected_start_line, expected_next_line] + \
121 [empty_line] + lines[start_index:] + [empty_line, expected_last_line]
122
123
124 return lines, guards_updated
125
126
127def find_expected_header_guard(filepath: str, prefix: str, add_extension: str, drop_outermost_subdir: str) -> str:
128 if drop_outermost_subdir:
129 arr : List[str] = filepath.split("/")
130 arr = arr[min(1, len(arr)-1):]
131 filepath = "/".join(arr)
132
133 if not add_extension:
134 filepath = ".".join(filepath.split(".")[:-1])
135
136 guard = filepath.replace("/", "_").replace(".", "_").upper() # snake case full path
137 return prefix + "_" + guard
138
139
140def skip_file(filepath: str, extensions: List[str], exclude: List[str], include: List[str]) -> bool:
141 extension = filepath.split(".")[-1]
142
143 if extension.lower() not in extensions:
144 return True
145
146 if exclude and any([filepath.startswith(exc) for exc in exclude]):
147 print(exclude)
148 return True
149
150 if include:
151 return not any([filepath.startswith(inc) for inc in include])
152
153 return False
154
155
156if __name__ == "__main__":
157 parser = argparse.ArgumentParser(
158 formatter_class=argparse.RawDescriptionHelpFormatter,
159 description="Header Guard Checker. It adds full path snake case header guards with or without extension.",
160 )
161
162 parser.add_argument("files", type=str, nargs="+", help="Files to check the header guards")
163 parser.add_argument("--extensions", type=str, help="Comma separated list of extensions to run the checks. \
164 If the input file does not have any of the extensions, it'll be skipped", required=True)
165 parser.add_argument("--comment_style", choices=['double_slash', 'slash_asterix'], required=True)
166 parser.add_argument("--exclude", type=str, help="Comma separated list of paths to exclude from header guard checks", default="")
167 parser.add_argument("--include", type=str, help="Comma separated list of paths to include. Defaults to empty string, \
168 which means all the paths are included", default="")
169 parser.add_argument("--prefix", help="Prefix to apply to header guards", required=True)
170 parser.add_argument("--add_extension", action="store_true", help="If true, it adds the file extension to the end of the guard")
171 parser.add_argument("--drop_outermost_subdir", action="store_true", help="If true, it'll not use the outermost folder in the path. \
172 This is intended for using in subdirs with different rules")
173
174 args = parser.parse_args()
175
176 files = args.files
177 extensions = args.extensions.split(",")
178 exclude = args.exclude.split(",") if args.exclude != '' else []
179 include = args.include.split(",") if args.include != '' else []
180 prefix = args.prefix
181 add_extension = args.add_extension
182 drop_outermost_subdir = args.drop_outermost_subdir
183 comment_style = args.comment_style
184
185 logging_level = logging.INFO
186 logging.basicConfig(level=logging_level)
187
188 retval = 0
189 for file in files:
190 if skip_file(file, extensions, exclude, include):
191 logger.info(f"File {file} is SKIPPED")
192 continue
193
194 expected_header_guard : str = find_expected_header_guard(file, prefix, add_extension, drop_outermost_subdir)
195
196 with open(file, "r") as fd:
197 lines: List = fd.readlines()
198
199 new_lines, guards_updated = fix_header_guard(lines, expected_header_guard, comment_style)
200
201 with open(file, "w") as fd:
202 fd.writelines([f"{line}" for line in new_lines])
203
204 if guards_updated:
205 logger.info("File has been modified")
206 retval = 1
207
208 exit(retval)