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 tabs 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# Directories to exclude
23#
24
25# Exclude all mod_test "mocks" directories
26UNIT_TEST_MOCKS = glob.glob('module/*/test/**/mocks', recursive=True)
27
28EXCLUDE_DIRECTORIES = [
29    '.git',
30    'build',
31    'contrib/cmsis/git',
32    "contrib/run-clang-format/git",
33    "contrib/cmock/git",
34    'product/rcar/src/CMSIS-FreeRTOS',
35    'unit_test/unity_mocks',
36] + UNIT_TEST_MOCKS
37
38#
39# Exclude patterns (applied to files only)
40#
41EXCLUDE = [
42    "Makefile",
43    "*.mk",
44    "*.html",
45    "*.xml",
46    "*.css",
47    "*.gif",
48    "*.dat",
49    "*.swp",
50    "*.pyc",
51    ".gitmodules",
52    "*.svg",
53    "*.a",
54    "*.pdf",
55    "Makefile.*"
56]
57
58
59def convert(path):
60    print("\tConverting all tabs in %s into spaces..." % path)
61    try:
62        file, temp_file = tempfile.mkstemp(prefix='tabs_to_spaces_')
63        print("Using %s" % temp_file)
64        subprocess.check_call('expand -t4 %s > %s' % (path, temp_file),
65                              shell=True)
66        shutil.copyfile(temp_file, path)
67    except Exception as e:
68        print("Error: Failed to convert file %s with %s" % (path, e))
69        sys.exit(1)
70    finally:
71        if os.path.exists(temp_file):
72            os.remove(temp_file)
73
74
75def main(argv=[], prog_name=''):
76    parser = argparse.ArgumentParser(prog=prog_name)
77    parser.add_argument('-c', '--convert',
78                        help='Convert tabs to 4 spaces.',
79                        action='store_true',
80                        default=False)
81    args = parser.parse_args(argv)
82
83    print('Checking the presence of tabs in the code...')
84    if args.convert:
85        print("Conversion mode is enabled.")
86
87    tabs_found_count = 0
88
89    # Get the files ignored by Git
90    # (This is better than 'git check-ignore' because it includes the files
91    #  excluded by .git/info/exclude)
92    git_clean_output = subprocess.check_output("git clean -ndX".split())
93    git_clean_output = git_clean_output.decode()
94    git_ignores = [line.split()[-1] for line in git_clean_output.splitlines()]
95
96    cwd = os.getcwd()
97    print("Executing from %s" % cwd)
98
99    for i, directory in enumerate(EXCLUDE_DIRECTORIES):
100        EXCLUDE_DIRECTORIES[i] = os.path.abspath(directory)
101        print("\tAdding to the exclude list: %s" % EXCLUDE_DIRECTORIES[i])
102
103    for root, dirs, files in os.walk(cwd, topdown=True):
104        #
105        # Exclude directories based on the EXCLUDE_DIRECTORIES pattern list
106        #
107        dirs[:] = [d for d in dirs
108                   if os.path.join(root, d) not in EXCLUDE_DIRECTORIES]
109
110        #
111        # Exclude files based on the EXCLUDE pattern list and the files
112        # Git ignores.
113        #
114        matches = list()
115
116        files = [f for f in files
117                 if os.path.join(root, f) not in git_ignores]
118
119        for filename in files:
120            for file_pattern in (EXCLUDE + git_ignores):
121                if fnmatch.fnmatch(filename, file_pattern):
122                    matches.append(filename)
123                    break
124        for match in matches:
125            files.remove(match)
126
127        #
128        # Check files
129        #
130        for filename in files:
131            path = os.path.join(root, filename)
132            print("processing %s" % filename)
133            with open(path, encoding="utf-8") as file:
134                for line, string in enumerate(file):
135                    if '\t' in string:
136                        print('%d:%s has tab' % (line, path))
137                        tabs_found_count += 1
138                        if args.convert:
139                            convert(path)
140                            break
141
142    if tabs_found_count == 0:
143        print("No tabs found")
144        return 0
145    else:
146        print('%d tab(s) found.' % tabs_found_count)
147        return 1
148
149
150if __name__ == '__main__':
151    sys.exit(main(sys.argv[1:], sys.argv[0]))
152