summary refs log tree commit diff
path: root/scripts/includemocs.py
blob: 1f48122ac035e808d872a087f9ce5ee8fdfb62b2 (plain) (blame)
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python3

#
# This file is part of KDToolBox.
#
# SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
# Author: Jesper K. Pedersen <jesper.pedersen@kdab.com>
#
# SPDX-License-Identifier: MIT
#

'''
Script to add inclusion of mocs to files recursively.
'''

# pylint: disable=redefined-outer-name

import os
import re
import argparse
import sys

dirty = False


def stripInitialSlash(path):
    if path and path.startswith("/"):
        path = path[1:]
    return path

# Returns true if the path is to be excluded from the search


def shouldExclude(root, path):
    # pylint: disable=used-before-assignment
    if not args.excludes:
        return False  # No excludes provided

    assert root.startswith(args.root)
    root = stripInitialSlash(root[len(args.root):])

    if args.headerPrefix:
        assert root.startswith(args.headerPrefix)
        root = stripInitialSlash(root[len(args.headerPrefix):])

    return (path in args.excludes) or (root + "/" + path in args.excludes)


regexp = re.compile("\\s*(Q_OBJECT|Q_GADGET|Q_NAMESPACE)\\s*")
# Returns true if the header file provides contains a Q_OBJECT, Q_GADGET or Q_NAMESPACE macro


def hasMacro(fileName):
    with open(fileName, "r", encoding="ISO-8859-1") as fileHandle:
        for line in fileHandle:
            if regexp.match(line):
                return True
        return False

# returns the matching .cpp file for the given .h file


def matchingCPPFile(root, fileName):
    assert root.startswith(args.root)
    root = stripInitialSlash(root[len(args.root):])

    if args.headerPrefix:
        assert root.startswith(args.headerPrefix)
        root = stripInitialSlash(root[len(args.headerPrefix):])

    if args.sourcePrefix:
        root = args.sourcePrefix + "/" + root

    return args.root + "/"  \
        + root + ("/" if root != "" else "") \
        + fileNameWithoutExtension(fileName) + ".cpp"


def fileNameWithoutExtension(fileName):
    return os.path.splitext(os.path.basename(fileName))[0]

# returns true if the specifies .cpp file already has the proper include


def cppHasMOCInclude(fileName):
    includeStatement = '#include "moc_%s.cpp"' % fileNameWithoutExtension(fileName)
    with open(fileName, encoding="utf8") as fileHandle:
        return includeStatement in fileHandle.read()


def getMocInsertionLocation(filename, content):
    headerIncludeRegex = re.compile(r'#include "%s\.h".*\n' % fileNameWithoutExtension(filename), re.M)
    match = headerIncludeRegex.search(content)
    if match:
        return match.end()
    return 0


def trimExistingMocInclude(content, cppFileName):
    mocStrRegex = re.compile(r'#include "moc_%s\.cpp"\n' % fileNameWithoutExtension(cppFileName))
    match = mocStrRegex.search(content)
    if match:
        return content[:match.start()] + content[match.end():]
    return content


def processFile(root, fileName):
    # pylint: disable=global-statement
    global dirty
    macroFound = hasMacro(root+"/"+fileName)
    logVerbose("Inspecting %s %s" %
               (root+"/"+fileName, "[Has Q_OBJECT / Q_GADGET / Q_NAMESPACE]" if macroFound else ""))

    if macroFound:
        cppFileName = matchingCPPFile(root, fileName)
        logVerbose("  -> %s" % cppFileName)

        if not os.path.exists(cppFileName):
            log("file %s didn't exist (which might not be an error)" % cppFileName)
            return

        if args.replaceExisting or not cppHasMOCInclude(cppFileName):
            dirty = True
            if args.dryRun:
                log("Missing moc include file: %s" % cppFileName)
            else:
                log("Updating %s" % cppFileName)

                with open(cppFileName, "r", encoding="utf8") as f:
                    content = f.read()

                if args.replaceExisting:
                    content = trimExistingMocInclude(content, cppFileName)

                loc = getMocInsertionLocation(cppFileName, content)
                if args.insertAtEnd:
                    with open(cppFileName, "a", encoding="utf8") as f:
                        f.write('\n#include "moc_%s.cpp"\n' % fileNameWithoutExtension(cppFileName))
                else:
                    with open(cppFileName, "w", encoding="utf8") as f:
                        f.write(content[:loc] + ('#include "moc_%s.cpp"\n' %
                                fileNameWithoutExtension(cppFileName)) + content[loc:])


def log(content):
    if not args.quiet:
        print(content)


def logVerbose(content):
    if args.verbose:
        print(content)


################################ MAIN #################################
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="""Script to add inclusion of mocs to files recursively.
        The source files either need to be in the same directories as the header files or in parallel directories,
        where the root of the headers are specified using --header-prefix and the root of the sources are specified using --source-prefix.
        If either header-prefix or source-prefix is the current directory, then they may be omitted.""")
    parser.add_argument("--dry-run", "-n", dest="dryRun", action='store_true', help="only report files to be updated")
    parser.add_argument("--quiet", "-q", dest="quiet", action='store_true', help="suppress output")
    parser.add_argument("--verbose", "-v", dest="verbose", action='store_true')
    parser.add_argument("--header-prefix", metavar="directory", dest="headerPrefix",
                        help="This directory will be replaced with source-prefix when "
                             "searching for matching source files")
    parser.add_argument("--source-prefix", metavar="directory", dest="sourcePrefix", help="see --header-prefix")
    parser.add_argument("--excludes", metavar="directory", dest="excludes", nargs="*",
                        help="directories to be excluded, might either be in the form of a directory name, "
                        "e.g. 3rdparty or a partial directory prefix from the root, e.g 3rdparty/parser")
    parser.add_argument("--insert-at-end", dest="insertAtEnd", action='store_true',
                        help="insert the moc include at the end of the file instead of the beginning")
    parser.add_argument("--replace-existing", dest="replaceExisting", action='store_true',
                        help="delete and readd existing MOC include statements")
    parser.add_argument(dest="root", default=".", metavar="directory",
                        nargs="?", help="root directory for the operation")

    args = parser.parse_args()

    root = args.root
    if args.headerPrefix:
        root += "/" + args.headerPrefix

    path = os.walk(root)
    for root, directories, files in path:
        # Filter out directories specified in --exclude
        directories[:] = [d for d in directories if not shouldExclude(root, d)]

        for file in files:
            if file.endswith(".h") or file.endswith(".hpp"):
                processFile(root, file)

    if not dirty:
        log("No changes needed")

    sys.exit(-1 if dirty else 0)