summaryrefslogtreecommitdiff
path: root/maint/check_doc_features
blob: 6b593455a167828345415fc8ce3503778b3cfe39 (plain)
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

import os
from list_crates import list_crates
from collections import Counter

# This contains annotations the script think are missing,
# but actually they don't need to be there
additional_provided = {}

# This contains annotations the script detected and think shouldn't be there,
# but actually they should
additional_required = {}


# Not interested in the low-level interfaces we provide only for fuzzing
additional_provided["equix"] = [
    (
        "{BucketArray, BucketArrayMemory, BucketArrayPair, Count, Uninit}",
        'feature = "bucket-array"',
    ),
]

# PreferredRuntime has a somewhat more complex rule for existing
additional_provided["tor-rtcompat"] = [
    ("PreferredRuntime", 'feature = "native-tls"'),
    ("PreferredRuntime", 'feature = "native-tls"'),
    ("PreferredRuntime", 'feature = "native-tls"'),
    ("PreferredRuntime", 'all(feature = "rustls", not(feature = "native-tls"))'),
    ("PreferredRuntime", 'all(feature = "rustls", not(feature = "native-tls"))'),
    ("PreferredRuntime", 'all(feature = "rustls", not(feature = "native-tls"))'),
]
# "unix::SocketAddr" is present unconditionally,
# though it has different definitions.
additional_provided["tor-general-addr"] = [
    ("SocketAddr", "unix"),
]


# We're not very interested in the testing feature
additional_provided["tor-guardmgr"] = [
    ("TestConfig", 'any(test, feature = "testing")'),
]

# Sha1 is present both ways
additional_provided["tor-llcrypto"] = [
    ("Sha1", 'feature = "with-openssl"'),
    ("Sha1", 'not(feature = "with-openssl")'),
]

additional_required["tor-llcrypto"] = [
    ("aes", "all()"),
    ("aes", "all()"),
]

additional_required["tor-hsservice"] = [
    ("restricted_discovery", "all()"),
]

# This is an * include; expended wildcard must be in additional_required
additional_provided["tor-proto"] = [
    ("*", 'feature = "testing"'),
]

additional_required["tor-proto"] = [
    ("CtrlMsg", 'feature = "testing"'),
    ("CreateResponse", 'feature = "testing"'),
    # I have no idea, but empirically this stops the CI complaining -Diziet
    ("Conversation", 'feature = "send-control-msg"'),
]

additional_required["tor-netdoc"] = [
    ("ConsensusBuilder", 'feature = "build_docs"'),
    ("RouterStatusBuilder", 'feature = "build_docs"'),
]
additional_provided["tor-netdoc"] = [
    ("NsConsensus", 'feature = "ns_consensus"'),
    ("NsRouterStatus", 'feature = "ns_consensus"'),
    ("MdConsensusBuilder", 'feature = "build_docs"'),
    (
        "PlainConsensusBuilder",
        'all(feature = "build_docs", feature = "plain-consensus")',
    ),
    ("UncheckedNsConsensus", 'feature = "ns_consensus"'),
    ("UnvalidatedNsConsensus", 'feature = "ns_consensus"'),
]


def extract_feature_pub_use(path):
    START_CFG = "#[cfg("
    END_CFG = ")]"
    PUB_USE = "pub use "
    res = []
    cfg = None
    with open(path, "r") as file:
        for line in file.readlines():
            if line.find(PUB_USE) != -1 and cfg:
                # last line was a #[cfg(..)] line and this is a pub use
                pubuse_pos = line.find(PUB_USE)
                # ignore comments
                if "//" in line[:pubuse_pos]:
                    continue
                # extract ident
                #
                # (BUG: this still doesn't handle `pub use {A,B,C} very well.)
                start = line.rfind(":")
                if start == -1:
                    start = line.rfind(" ")
                ident = line[start + 1 :]
                if (pos := ident.find(";")) != -1:
                    ident = ident[:pos]
                res.append((ident, cfg))
                cfg = None
                continue

            # check if we are on a #[cfg(..)] line, if so, remember it
            start_cfg = line.find(START_CFG)
            end_cfg = line.find(END_CFG)
            if start_cfg == -1 or end_cfg == -1:
                cfg = None
            else:
                start_cfg += len(START_CFG)
                cfg = line[start_cfg:end_cfg]
    return res


def extract_cfg_attr(path):
    START_CFG = "#[cfg_attr(docsrs, doc(cfg("
    END_CFG = ")))]"
    res = []
    cfg = None
    with open(path, "r") as file:
        for line in file.readlines():
            pos = max(
                [line.find(kw + " ") for kw in ["struct", "enum", "mod", "trait"]]
            )
            if pos != -1 and cfg:
                # last line was a cfg and this is a declaration
                subline = line[pos:]
                subline = subline[subline.find(" ") + 1 :]
                end = min(
                    subline.find(pat) for pat in " (<;" if subline.find(pat) != -1
                )
                ident = subline[:end]
                res.append((ident, cfg))
                cfg = None
                continue

            # check if we are on a #[cfg_attr(docsrs, doc(cfg(..)))] line, if so, remember it
            start_cfg = line.find(START_CFG)
            end_cfg = line.find(END_CFG)
            if start_cfg != -1 and end_cfg != -1:
                start_cfg += len(START_CFG)
                cfg = line[start_cfg:end_cfg]
                # don't reset when it's a cfg_attr followed by some other #[something]
            elif "#[" not in line:
                cfg = None
    return res


def for_each_rs(path, fn):
    res = []
    for dir_, _, files in os.walk(path):
        for file in files:
            if not file.endswith(".rs"):
                continue
            res += fn(os.path.join(dir_, file))
    return res


def main():
    for crate_info in list_crates():
        crate = crate_info.name
        print(f"processing {crate}")
        crate_path = f"crates/{crate}/src"
        required = for_each_rs(
            crate_path, extract_feature_pub_use
        ) + additional_required.get(crate, [])
        provided = for_each_rs(crate_path, extract_cfg_attr) + additional_provided.get(
            crate, []
        )
        req = Counter(required)
        prov = Counter(provided)
        ok = True
        for elem in (req - prov).elements():
            ok = False
            print(f"feature but no cfg_attr(docsrs): {elem}")
        for elem in (prov - req).elements():
            ok = False
            print(f"cfg_attr(docsrs) but no feature: {elem}")
        if not ok:
            print("Found error, exiting")
            exit(1)


if __name__ == "__main__":
    main()