File: check_feature_macros.py

package info (click to toggle)
simdutf 8.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,524 kB
  • sloc: cpp: 64,498; ansic: 15,347; python: 3,592; sh: 366; makefile: 12
file content (156 lines) | stat: -rw-r--r-- 7,159 bytes parent folder | download
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()