summary refs log tree commit diff
path: root/src/etc/featureck.py
blob: d6cc25177e4ae63797a58ca7c8564a95076a7ce0 (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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# Copyright 2015 The Rust Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution and at
# http://rust-lang.org/COPYRIGHT.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.

# This script does a tree-wide sanity checks against stability
# attributes, currently:
#   * For all feature_name/level pairs the 'since' field is the same
#   * That no features are both stable and unstable.
#   * That lib features don't have the same name as lang features
#     unless they are on the 'joint_features' whitelist
#   * That features that exist in both lang and lib and are stable
#     since the same version
#   * Prints information about features

import sys
import os
import re
import codecs

if len(sys.argv) < 2:
    print("usage: featureck.py <src-dir>")
    sys.exit(1)

src_dir = sys.argv[1]

# Features that are allowed to exist in both the language and the library
joint_features = [ ]

# Grab the list of language features from the compiler
language_gate_statuses = [ "Active", "Deprecated", "Removed", "Accepted" ]
feature_gate_source = os.path.join(src_dir, "libsyntax", "feature_gate.rs")
language_features = []
language_feature_names = []
with open(feature_gate_source, 'r') as f:
    for line in f:
        original_line = line
        line = line.strip()
        is_feature_line = False
        for status in language_gate_statuses:
            if status in line and line.startswith("("):
                is_feature_line = True

        if is_feature_line:
            # turn `    ("foo", "1.0.0", Some(10), Active)` into
            # `"foo", "1.0.0", Some(10), Active`
            line = line.strip(' ,()')
            parts = line.split(",")
            if len(parts) != 4:
                print("error: unexpected number of components in line: " + original_line)
                sys.exit(1)
            feature_name = parts[0].strip().replace('"', "")
            since = parts[1].strip().replace('"', "")
            issue = parts[2].strip()
            status = parts[3].strip()
            assert len(feature_name) > 0
            assert len(since) > 0
            assert len(issue) > 0
            assert len(status) > 0

            language_feature_names += [feature_name]
            language_features += [(feature_name, since, issue, status)]

assert len(language_features) > 0

errors = False

lib_features = { }
lib_features_and_level = { }
for (dirpath, dirnames, filenames) in os.walk(src_dir):
    # Don't look for feature names in tests
    if "src/test" in dirpath:
        continue

    # Takes a long time to traverse LLVM
    if "src/llvm" in dirpath:
        continue

    for filename in filenames:
        if not filename.endswith(".rs"):
            continue

        path = os.path.join(dirpath, filename)
        with codecs.open(filename=path, mode='r', encoding="utf-8") as f:
            line_num = 0
            for line in f:
                line_num += 1
                level = None
                if "[unstable(" in line:
                    level = "unstable"
                elif "[stable(" in line:
                    level = "stable"
                else:
                    continue

                # This is a stability attribute. For the purposes of this
                # script we expect both the 'feature' and 'since' attributes on
                # the same line, e.g.
                # `#[unstable(feature = "foo", since = "1.0.0")]`

                p = re.compile('(unstable|stable).*feature *= *"(\w*)"')
                m = p.search(line)
                if not m is None:
                    feature_name = m.group(2)
                    since = None
                    if re.compile("\[ *stable").search(line) is not None:
                        pp = re.compile('since *= *"([\w\.]*)"')
                        mm = pp.search(line)
                        if not mm is None:
                            since = mm.group(1)
                        else:
                            print("error: misformed stability attribute")
                            print("line %d of %:" % (line_num, path))
                            print(line)
                            errors = True

                    lib_features[feature_name] = feature_name
                    if lib_features_and_level.get((feature_name, level)) is None:
                        # Add it to the observed features
                        lib_features_and_level[(feature_name, level)] = \
                            (since, path, line_num, line)
                    else:
                        # Verify that for this combination of feature_name and level the 'since'
                        # attribute matches.
                        (expected_since, source_path, source_line_num, source_line) = \
                            lib_features_and_level.get((feature_name, level))
                        if since != expected_since:
                            print("error: mismatch in %s feature '%s'" % (level, feature_name))
                            print("line %d of %s:" % (source_line_num, source_path))
                            print(source_line)
                            print("line %d of %s:" % (line_num, path))
                            print(line)
                            errors = True

                    # Verify that this lib feature doesn't duplicate a lang feature
                    if feature_name in language_feature_names:
                        print("error: lib feature '%s' duplicates a lang feature" % (feature_name))
                        print("line %d of %s:" % (line_num, path))
                        print(line)
                        errors = True

                else:
                    print("error: misformed stability attribute")
                    print("line %d of %s:" % (line_num, path))
                    print(line)
                    errors = True

# Merge data about both lists
# name, lang, lib, status, stable since

language_feature_stats = {}

for f in language_features:
    name = f[0]
    lang = True
    lib = False
    status = "unstable"
    stable_since = None

    if f[3] == "Accepted":
        status = "stable"
    if status == "stable":
        stable_since = f[1]

    language_feature_stats[name] = (name, lang, lib, status, stable_since)

lib_feature_stats = {}

for f in lib_features:
    name = f
    lang = False
    lib = True
    status = "unstable"
    stable_since = None

    is_stable = lib_features_and_level.get((name, "stable")) is not None
    is_unstable = lib_features_and_level.get((name, "unstable")) is not None

    if is_stable and is_unstable:
        print("error: feature '%s' is both stable and unstable" % (name))
        errors = True

    if is_stable:
        status = "stable"
        stable_since = lib_features_and_level[(name, "stable")][0]
    elif is_unstable:
        status = "unstable"

    lib_feature_stats[name] = (name, lang, lib, status, stable_since)

# Check for overlap in two sets
merged_stats = { }

for name in lib_feature_stats:
    if language_feature_stats.get(name) is not None:
        if not name in joint_features:
            print("error: feature '%s' is both a lang and lib feature but not whitelisted" % (name))
            errors = True
        lang_status = language_feature_stats[name][3]
        lib_status = lib_feature_stats[name][3]
        lang_stable_since = language_feature_stats[name][4]
        lib_stable_since = lib_feature_stats[name][4]

        if lang_status != lib_status and lib_status != "rustc_deprecated":
            print("error: feature '%s' has lang status %s " +
                  "but lib status %s" % (name, lang_status, lib_status))
            errors = True

        if lang_stable_since != lib_stable_since:
            print("error: feature '%s' has lang stable since %s " +
                  "but lib stable since %s" % (name, lang_stable_since, lib_stable_since))
            errors = True

        merged_stats[name] = (name, True, True, lang_status, lang_stable_since)

        del language_feature_stats[name]
        del lib_feature_stats[name]

if errors:
    sys.exit(1)

# Finally, display the stats
stats = {}
stats.update(language_feature_stats)
stats.update(lib_feature_stats)
stats.update(merged_stats)
lines = []
for s in stats:
    s = stats[s]
    type_ = "lang"
    if s[1] and s[2]:
        type_ = "lang/lib"
    elif s[2]:
        type_ = "lib"
    line = "{: <32}".format(s[0]) + \
           "{: <8}".format(type_) + \
           "{: <12}".format(s[3]) + \
           "{: <8}".format(str(s[4]))
    lines += [line]

lines.sort()

print
for line in lines:
    print("* " + line)
print