1#!/usr/bin/env python3
2#
3# check-config - a config flag documentation checker for Mercurial
4#
5# Copyright 2015 Olivia Mackall <olivia@selenic.com>
6#
7# This software may be used and distributed according to the terms of the
8# GNU General Public License version 2 or any later version.
9
10from __future__ import absolute_import, print_function
11import re
12import sys
13
14foundopts = {}
15documented = {}
16allowinconsistent = set()
17
18configre = re.compile(
19    br'''
20    # Function call
21    ui\.config(?P<ctype>|int|bool|list)\(
22        # First argument.
23        ['"](?P<section>\S+)['"],\s*
24        # Second argument
25        ['"](?P<option>\S+)['"](,\s+
26        (?:default=)?(?P<default>\S+?))?
27    \)''',
28    re.VERBOSE | re.MULTILINE,
29)
30
31configwithre = re.compile(
32    br'''
33    ui\.config(?P<ctype>with)\(
34        # First argument is callback function. This doesn't parse robustly
35        # if it is e.g. a function call.
36        [^,]+,\s*
37        ['"](?P<section>\S+)['"],\s*
38        ['"](?P<option>\S+)['"](,\s+
39        (?:default=)?(?P<default>\S+?))?
40    \)''',
41    re.VERBOSE | re.MULTILINE,
42)
43
44configpartialre = br"""ui\.config"""
45
46ignorere = re.compile(
47    br'''
48    \#\s(?P<reason>internal|experimental|deprecated|developer|inconsistent)\s
49    config:\s(?P<config>\S+\.\S+)$
50    ''',
51    re.VERBOSE | re.MULTILINE,
52)
53
54if sys.version_info[0] > 2:
55
56    def mkstr(b):
57        if isinstance(b, str):
58            return b
59        return b.decode('utf8')
60
61
62else:
63    mkstr = lambda x: x
64
65
66def main(args):
67    for f in args:
68        sect = b''
69        prevname = b''
70        confsect = b''
71        carryover = b''
72        linenum = 0
73        for l in open(f, 'rb'):
74            linenum += 1
75
76            # check topic-like bits
77            m = re.match(br'\s*``(\S+)``', l)
78            if m:
79                prevname = m.group(1)
80            if re.match(br'^\s*-+$', l):
81                sect = prevname
82                prevname = b''
83
84            if sect and prevname:
85                name = sect + b'.' + prevname
86                documented[name] = 1
87
88            # check docstring bits
89            m = re.match(br'^\s+\[(\S+)\]', l)
90            if m:
91                confsect = m.group(1)
92                continue
93            m = re.match(br'^\s+(?:#\s*)?(\S+) = ', l)
94            if m:
95                name = confsect + b'.' + m.group(1)
96                documented[name] = 1
97
98            # like the bugzilla extension
99            m = re.match(br'^\s*(\S+\.\S+)$', l)
100            if m:
101                documented[m.group(1)] = 1
102
103            # like convert
104            m = re.match(br'^\s*:(\S+\.\S+):\s+', l)
105            if m:
106                documented[m.group(1)] = 1
107
108            # quoted in help or docstrings
109            m = re.match(br'.*?``(\S+\.\S+)``', l)
110            if m:
111                documented[m.group(1)] = 1
112
113            # look for ignore markers
114            m = ignorere.search(l)
115            if m:
116                if m.group('reason') == b'inconsistent':
117                    allowinconsistent.add(m.group('config'))
118                else:
119                    documented[m.group('config')] = 1
120
121            # look for code-like bits
122            line = carryover + l
123            m = configre.search(line) or configwithre.search(line)
124            if m:
125                ctype = m.group('ctype')
126                if not ctype:
127                    ctype = 'str'
128                name = m.group('section') + b"." + m.group('option')
129                default = m.group('default')
130                if default in (
131                    None,
132                    b'False',
133                    b'None',
134                    b'0',
135                    b'[]',
136                    b'""',
137                    b"''",
138                ):
139                    default = b''
140                if re.match(b'[a-z.]+$', default):
141                    default = b'<variable>'
142                if (
143                    name in foundopts
144                    and (ctype, default) != foundopts[name]
145                    and name not in allowinconsistent
146                ):
147                    print(mkstr(l.rstrip()))
148                    fctype, fdefault = foundopts[name]
149                    print(
150                        "conflict on %s: %r != %r"
151                        % (
152                            mkstr(name),
153                            (mkstr(ctype), mkstr(default)),
154                            (mkstr(fctype), mkstr(fdefault)),
155                        )
156                    )
157                    print("at %s:%d:" % (mkstr(f), linenum))
158                foundopts[name] = (ctype, default)
159                carryover = b''
160            else:
161                m = re.search(configpartialre, line)
162                if m:
163                    carryover = line
164                else:
165                    carryover = b''
166
167    for name in sorted(foundopts):
168        if name not in documented:
169            if not (
170                name.startswith(b"devel.")
171                or name.startswith(b"experimental.")
172                or name.startswith(b"debug.")
173            ):
174                ctype, default = foundopts[name]
175                if default:
176                    if isinstance(default, bytes):
177                        default = mkstr(default)
178                    default = ' [%s]' % default
179                elif isinstance(default, bytes):
180                    default = mkstr(default)
181                print(
182                    "undocumented: %s (%s)%s"
183                    % (mkstr(name), mkstr(ctype), default)
184                )
185
186
187if __name__ == "__main__":
188    if len(sys.argv) > 1:
189        sys.exit(main(sys.argv[1:]))
190    else:
191        sys.exit(main([l.rstrip() for l in sys.stdin]))
192