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