1#!/usr/bin/env python3
2# Script to check that headers are in a consistent order
3# Canonical header order is defined in scripts/HeaderOrder.txt
4# Requires Python 3.6 or later
5
6import pathlib
7
8patterns = [
9    "include/*.h",
10    "src/*.cxx",
11    "lexlib/*.cxx",
12    "lexers/*.cxx",
13    "win32/*.cxx",
14    "gtk/*.cxx",
15    "cocoa/*.mm",
16    "cocoa/*.h",
17    "test/unit/*.cxx",
18    "lexilla/src/*.cxx",
19    "lexilla/test/*.cxx",
20]
21
22def IsHeader(x):
23    return x.strip().startswith("#") and \
24        ("include" in x or "import" in x) and \
25        "dllimport" not in x
26
27def HeaderFromIncludeLine(s):
28    #\s*#\s*(include|import)\s+\S+\s*
29    return s.strip()[1:].strip()[7:].strip()
30
31def ExtractHeaders(filename):
32    with filename.open(encoding="cp437") as infile:
33        return [HeaderFromIncludeLine(l) for l in infile if IsHeader(l)]
34
35def ExcludeName(name):
36    # LexCaml adds system headers in #if to be an external lexer
37    # moc_ files are generated by Qt and follow its rules
38    return "LexCaml" in name or "moc_" in name
39
40def SortLike(incs, order):
41    return sorted(incs, key = lambda i: order.index(i))
42
43def CheckFiles(root):
44    # Find all the lexer source code files
45    filePaths = []
46    for p in patterns:
47        filePaths += root.glob(p)
48    # The Qt platform code interleaves system and Scintilla headers
49    #~ filePaths += root.glob("qt/ScintillaEditBase/*.cpp")
50    #~ filePaths += root.glob("qt/ScintillaEdit/*.cpp")
51    #~ print(filePaths)
52    scriptDirectory = root / "scripts"
53    headerOrderFile = scriptDirectory / "HeaderOrder.txt"
54    headerOrder = ExtractHeaders(headerOrderFile)
55    originalOrder = headerOrder[:]
56    orderedPaths = [p for p in sorted(filePaths) if not ExcludeName(str(p))]
57    allIncs = set()
58    for f in orderedPaths:
59        print("   File ", f.relative_to(root))
60        incs = ExtractHeaders(f)
61        #~ print("\n".join(incs))
62        news = set(incs) - set(headerOrder)
63        allIncs = allIncs.union(set(incs))
64
65        m = 0
66        i = 0
67        # Detect headers not in header order list and insert at OK position
68        needs = []
69        while i < len(incs):
70            if m == len(headerOrder):
71                #~ print("**** extend", incs[i:])
72                headerOrder.extend(incs[i:])
73                needs.extend(incs[i:])
74                break
75            if headerOrder[m] == incs[i]:
76                #~ print("equal", headerOrder[m])
77                i += 1
78                m += 1
79            else:
80                if headerOrder[m] not in incs:
81                    #~ print("skip", headerOrder[m])
82                    m += 1
83                elif incs[i] not in headerOrder:
84                    #~ print(str(f) + ":1: Add master", incs[i])
85                    headerOrder.insert(m, incs[i])
86                    needs.append(incs[i])
87                    i += 1
88                    m += 1
89                else:
90                    i += 1
91        if needs:
92            print(f"{f}:1: needs these headers:")
93            for header in needs:
94                print("#include " + header)
95
96        # Detect out of order
97        ordered = SortLike(incs, headerOrder)
98        if incs != ordered:
99            print(f"{f}:1: is out of order")
100            fOrdered = pathlib.Path(str(f) + ".ordered")
101            with fOrdered.open("w") as headerOut:
102                for header in ordered:
103                    headerOut.write("#include " + header + "\n")
104            print(f"{fOrdered}:1: is ordered")
105
106    if headerOrder != originalOrder:
107        newIncludes = set(headerOrder) - set(originalOrder)
108        headerOrderNew = scriptDirectory / "NewOrder.txt"
109        print(f"{headerOrderFile}:1: changed to {headerOrderNew}")
110        print(f"   Added {', '.join(newIncludes)}.")
111        with headerOrderNew.open("w") as headerOut:
112            for header in headerOrder:
113                headerOut.write("#include " + header + "\n")
114
115    unused = sorted(set(headerOrder) - allIncs)
116    if unused:
117        print("In HeaderOrder.txt but not used")
118        print("\n".join(unused))
119
120CheckFiles(pathlib.Path(__file__).resolve().parent.parent)
121