1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
|
#!/usr/bin/env python3
import subprocess
import re
import os
import argparse
import sys
from typing import List, Tuple
def get_git_files(limit_to_src_include: bool) -> List[str]:
"""Retrieve list of .cpp and .h files checked into the Git repository."""
try:
patterns = ['src/*.cpp', 'src/*.h', 'include/*.cpp', 'include/*.h'] if limit_to_src_include else ['*.cpp', '*.h']
result = subprocess.run(
['git', 'ls-files'] + patterns,
capture_output=True,
text=True,
check=True
)
files = result.stdout.splitlines()
if limit_to_src_include:
files = [f for f in files if f.startswith(('src/', 'include/'))]
return files
except subprocess.CalledProcessError as e:
print(f"Error running git ls-files: {e}")
return []
def is_preprocessor_directive(line: str) -> bool:
"""Check if the line contains a preprocessor directive (not in a comment)."""
line = re.sub(r'//.*$', '', line)
return bool(re.match(r'^\s*#', line))
def extract_directive(lines: List[str], line_number: int) -> Tuple[str, str]:
"""Extract directive type and condition (if any) from a preprocessor line."""
line = lines[line_number].strip()
# Sometimes, the condition may span multiple lines due to backslashes
# Handle multi-line conditions for #if directives
# We recursively gather lines if they end with a backslash
def build_full_condition(comment: str, line_number: int) -> str:
if comment.endswith('\\') and line_number + 1 < len(lines):
line_number += 1
return comment[:-1].strip() + ' ' + build_full_condition(lines[line_number].strip(), line_number)
else:
return comment.strip()
if line.startswith('#if '):
return 'if', build_full_condition(line[4:].strip(), line_number)
elif line.startswith('#ifdef '):
return 'ifdef', build_full_condition(line[7:].strip(), line_number)
elif line.startswith('#ifndef '):
return 'ifndef', build_full_condition(line[8:].strip(), line_number)
elif line.startswith('#endif'):
match = re.match(r'#endif\s*(?://\s*(.*))?$', line)
## In some instances, the comment may be on the next line(s)
## We use the following loop to gather such comments
## The heuristic is that the next line starts with '//' and contains 'SIMDUTF_FEATURE'
## Note that if we don't check for SIMDUTF_FEATURE, we may pick up unrelated comments.
comment = match.group(1).strip() if match and match.group(1) else ''
while line_number + 1 < len(lines):
next_line = lines[line_number + 1].strip()
if next_line.startswith('//') and 'SIMDUTF_FEATURE' in next_line:
comment_part = next_line[2:].strip()
comment = (comment + ' ' + comment_part).strip() if comment else comment_part
line_number += 1
else:
break
return 'endif', comment
return '', ''
def is_feature_if_directive(condition: str) -> bool:
"""Check if the #if condition contains a macro starting with SIMDUTF_FEATURE."""
return bool(re.search(r'\bSIMDUTF_FEATURE\w*\b', condition))
def check_file(filename: str) -> List[str]:
"""Check #if directives with SIMDUTF_FEATURE macros for matching #endif comments."""
errors = []
stack = [] # Stack of (directive, condition, is_feature, line_number)
statistics = {'if': 0, 'ifdef': 0, 'ifndef': 0, 'endif': 0}
try:
with open(filename, 'r', encoding='utf-8') as f:
lines = f.readlines()
for line_number, line in enumerate(lines):
if not is_preprocessor_directive(line):
continue
directive, condition = extract_directive(lines, line_number)
if not directive:
continue
statistics[directive] += 1
if directive in ('if', 'ifdef', 'ifndef'):
is_feature = directive == 'if' and is_feature_if_directive(condition)
stack.append((directive, condition, is_feature, line_number))
elif directive == 'endif' and stack:
orig_directive, orig_condition, is_feature, orig_line = stack.pop()
if is_feature:
if not condition:
errors.append(
f"\033[31mERROR: {filename}:{line_number}: #endif missing comment (expected '{orig_condition}')\033[0m"
)
elif not re.match(r'SIMDUTF_FEATURE\w*', condition):
errors.append(
f"\033[31mERROR: {filename}:{line_number}: #endif comment '{condition}' does not start with SIMDUTF_FEATURE "
f"(expected '{orig_condition}')\033[0m"
)
elif condition != orig_condition:
errors.append(
f"\033[31mERROR: {filename}:{line_number}: #endif comment '{condition}' does not match "
f"#if condition '{orig_condition}' at line {orig_line}\033[0m"
)
# Check for unclosed feature-related #if directives
for directive, condition, is_feature, line in stack:
if is_feature:
red_warning = f"\033[31mWARNING: {filename}:{line}: Unclosed #if '{condition}' at end of file\033[0m"
errors.append(red_warning)
return errors, statistics
except FileNotFoundError:
return [f"{filename}: File not found"], statistics
except UnicodeDecodeError:
return [f"{filename}: Unable to decode file (invalid encoding)"], statistics
def main():
"""Main function to check .cpp and .h files for SIMDUTF feature #if...#endif pairs."""
parser = argparse.ArgumentParser(description="Check #if...#endif pairs for SIMDUTF feature macros in .cpp and .h files.")
parser.add_argument('--limit', action='store_true', help="Limit checks to files in src/ and include/ directories")
args = parser.parse_args()
files = get_git_files(args.limit)
if not files:
print("No .cpp or .h files found in the Git repository" + (" in src/ or include/" if args.limit else "") + ".")
sys.exit(1) # Exit with error code if no files are found
all_errors = []
statistics = {'if': 0, 'ifdef': 0, 'ifndef': 0, 'endif': 0}
for filename in files:
errors, file_stats = check_file(filename)
all_errors.extend(errors)
for key in statistics:
statistics[key] += file_stats.get(key, 0)
if all_errors:
print("Errors found:")
for error in all_errors:
print(error)
sys.exit(1) # Exit with error code if errors are found
else:
for key, count in statistics.items():
print(f"Total #{key} directives: {count}")
print("All #if...#endif pairs for SIMDUTF feature macros are correctly matched with valid comments.")
sys.exit(0) # Exit with success code
if __name__ == "__main__":
main()
|