1#!/usr/bin/env python3
2#
3# Arm SCP/MCP Software
4# Copyright (c) 2015-2023, Arm Limited and Contributors. All rights reserved.
5#
6# SPDX-License-Identifier: BSD-3-Clause
7#
8"""
9    Check for trailing spaces and non-UNIX line endings in the source code.
10"""
11import argparse
12import os
13import re
14import shutil
15import subprocess
16import sys
17import tempfile
18import fnmatch
19import glob
20
21
22#
23# Directories to exclude
24#
25
26# Exclude all mod_test "mocks" directories
27UNIT_TEST_MOCKS = glob.glob('module/*/test/**/mocks', recursive=True)
28
29EXCLUDE_DIRECTORIES = [
30    '.git',
31    'build',
32    'tools',
33    'contrib/cmsis/git',
34    "contrib/run-clang-format/git",
35    "contrib/cmock/git",
36    'product/rcar/src/CMSIS-FreeRTOS',
37    'unit_test/unity_mocks',
38] + UNIT_TEST_MOCKS
39
40#
41# Exclude patterns (applied to files only)
42#
43EXCLUDE = [
44    "*.html",
45    "*.xml",
46    "*.css",
47    "*.gif",
48    "*.dat",
49    "*.pyc",
50    "*.jar",
51    "*.md",
52    "*.swp",
53    "*.a",
54    "*.pdf",
55    ".*"
56]
57
58KEYWORDS = [
59    'for',
60    'if',
61    'switch',
62    'while',
63]
64
65#
66# File types for which spaces after keywords will be corrected
67#
68FILE_TYPES = [
69    '*.c',
70    '*.h',
71]
72
73
74def is_valid_type(filename):
75    for file_type in FILE_TYPES:
76        if fnmatch.fnmatch(filename, file_type):
77            return True
78    return False
79
80
81def main(argv=[], prog_name=''):
82    parser = argparse.ArgumentParser(prog=prog_name)
83    parser.add_argument('-t', '--trim',
84                        help='Remove trailing spaces.',
85                        action='store_true',
86                        default=False)
87    parser.add_argument('-c', '--correct',
88                        help='Correct spaces after keywords.',
89                        action='store_true',
90                        default=False)
91    args = parser.parse_args(argv)
92
93    print('Checking for incorrect spacing in the code...')
94    if args.trim:
95        print("Trim mode is enabled.")
96    if args.correct:
97        print("Correct mode is enabled.")
98
99    regex_patterns = dict.fromkeys(KEYWORDS, 0)
100
101    trailing_spaces_count = 0
102    trailing_lines_count = 0
103    correct_spaces_count = 0
104    modified_files = 0
105    non_unix_eol_files = 0
106    missing_new_lines_files = 0
107
108    cwd = os.getcwd()
109    print("Executing from {}".format(cwd))
110
111    for i, directory in enumerate(EXCLUDE_DIRECTORIES):
112        EXCLUDE_DIRECTORIES[i] = os.path.abspath(directory)
113        print("\tAdding to the exclude list: {}"
114              .format(EXCLUDE_DIRECTORIES[i]))
115
116    for root, dirs, files in os.walk(cwd, topdown=True):
117        #
118        # Exclude directories based on the EXCLUDE_DIRECTORIES pattern list
119        #
120        dirs[:] = [d for d in dirs
121                   if os.path.join(root, d) not in EXCLUDE_DIRECTORIES]
122
123        #
124        # Exclude files based on the EXCLUDE pattern list
125        #
126        matches = list()
127        for filename in files:
128            for file_pattern in EXCLUDE:
129                if fnmatch.fnmatch(filename, file_pattern):
130                    matches.append(filename)
131                    break
132        for match in matches:
133            files.remove(match)
134
135        for keyword in KEYWORDS:
136            regex_patterns[keyword] = re.compile(
137                '(.*\\W)(%s)(\\s*)(\\(.*)' % keyword)
138        #
139        # Check files
140        #
141        for filename in files:
142            path = os.path.join(root, filename)
143            content = ''
144            trailing_spaces = 0
145            trailing_lines = 0
146            incorrect_spaces = 0
147
148            with open(path, encoding="utf-8") as file:
149                lines = file.readlines()
150                last_line_number = len(lines)-1
151                for line, string in enumerate(lines):
152                    if line == last_line_number and string[-1] != '\n':
153                        print('{} is missing a new line at the end of file'.
154                              format(path))
155                        missing_new_lines_files += 1
156
157                    # Note that all newlines are converted to '\n',
158                    # so the following will work regardless of
159                    # what the underlying file format is using to
160                    # represent a line break.
161                    if string.endswith(' \n'):
162                        print('{}:{} has trailing space'.format(line, path))
163                        trailing_spaces += 1
164                        if args.trim:
165                            string = string.rstrip()+'\n'
166                    if not is_valid_type(os.path.basename(path)):
167                        content += string
168                        continue
169                    for keyword in KEYWORDS:
170                        key_index = string.find(keyword)
171                        if key_index == -1:
172                            continue
173                        m = regex_patterns[keyword].search(string)
174                        if m and m.group(3) != ' ':
175                            incorrect_spaces += 1
176                            print('Abnormal spacing. "{}", {}:{} --> {}'
177                                  .format(keyword, path, line,
178                                          string.rstrip()))
179                            if args.correct:
180                                string = m.group(1) + m.group(2) + ' ' + \
181                                         m.group(4) + '\n'
182                    content += string
183
184                if content.endswith('\n\n'):
185                    print('Blank line at the end of file --> {}'.format(path))
186                    c_len = len(content)
187                    if args.trim:
188                        content = content.rstrip()+'\n'
189                    trailing_lines += c_len - len(content.rstrip() + '\n')
190                #
191                # If file.newlines has been set it is either a string with
192                # the determined line ending or a tuple with all the line
193                # endings we have encountered
194                #
195                if file.newlines:
196                    if isinstance(file.newlines, tuple):
197                        print('{} has mixed line endings'.format(path))
198                        non_unix_eol_files += 1
199                    elif file.newlines != '\n':
200                        print('{} has non-UNIX line endings'.format(path))
201                        non_unix_eol_files += 1
202
203            #
204            # Trim and/or correct file, depending on the provided arguments
205            #
206            write_file = False
207            if args.trim and (trailing_spaces or trailing_lines) != 0:
208                print("Trimming {}...".format(path))
209                write_file = True
210            if args.correct and incorrect_spaces != 0:
211                print("Correcting {}...".format(path))
212                write_file = True
213            if write_file:
214                modified_files += 1
215                with open(path, 'w') as file:
216                    file.write(content)
217
218            trailing_spaces_count += trailing_spaces
219            trailing_lines_count += trailing_lines
220            correct_spaces_count += incorrect_spaces
221
222    if trailing_spaces_count == 0:
223        print("No trailing spaces found")
224    else:
225        print('{} trailing spaces found.'.format(trailing_spaces_count))
226
227    if trailing_lines_count == 0:
228        print("No trailing lines found")
229    else:
230        print('{} trailing lines found.'.format(trailing_lines_count))
231
232    if correct_spaces_count == 0:
233        print("No abnormal spaces found")
234    else:
235        print('Abnormal spaces found on {} lines.'
236              .format(correct_spaces_count))
237
238    if (args.trim or args.correct) and modified_files:
239        print("{} files modified".format(modified_files))
240
241    if non_unix_eol_files == 0:
242        print("No files with non-UNIX or mixed line endings found")
243    else:
244        print("{} files have non-UNIX or mixed line endings"
245              .format(non_unix_eol_files))
246
247    if missing_new_lines_files == 0:
248        print("No files with missing newlines at EOF found")
249    else:
250        print("{} text files are missing newlines at EOF"
251              .format(missing_new_lines_files))
252
253    if (trailing_spaces_count or
254            trailing_lines_count or
255            correct_spaces_count or
256            non_unix_eol_files or
257            missing_new_lines_files):
258        return 1
259    return 0
260
261
262if __name__ == '__main__':
263    sys.exit(main(sys.argv[1:], sys.argv[0]))
264