1#!/usr/bin/env python3 2# A tool to parse the FormatStyle struct from Format.h and update the 3# documentation in ../ClangFormatStyleOptions.rst automatically. 4# Run from the directory in which this file is located to update the docs. 5 6import inspect 7import os 8import re 9from typing import Set 10 11CLANG_DIR = os.path.join(os.path.dirname(__file__), '../..') 12FORMAT_STYLE_FILE = os.path.join(CLANG_DIR, 'include/clang/Format/Format.h') 13INCLUDE_STYLE_FILE = os.path.join(CLANG_DIR, 'include/clang/Tooling/Inclusions/IncludeStyle.h') 14DOC_FILE = os.path.join(CLANG_DIR, 'docs/ClangFormatStyleOptions.rst') 15 16PLURALS_FILE = os.path.join(os.path.dirname(__file__), 'plurals.txt') 17 18plurals: Set[str] = set() 19with open(PLURALS_FILE, 'a+') as f: 20 f.seek(0) 21 plurals = set(f.read().splitlines()) 22 23def substitute(text, tag, contents): 24 replacement = '\n.. START_%s\n\n%s\n\n.. END_%s\n' % (tag, contents, tag) 25 pattern = r'\n\.\. START_%s\n.*\n\.\. END_%s\n' % (tag, tag) 26 return re.sub(pattern, '%s', text, flags=re.S) % replacement 27 28def register_plural(singular: str, plural: str): 29 if plural not in plurals: 30 if not hasattr(register_plural, "generated_new_plural"): 31 print('Plural generation: you can use ' 32 f'`git checkout -- {os.path.relpath(PLURALS_FILE)}` ' 33 'to reemit warnings or `git add` to include new plurals\n') 34 register_plural.generated_new_plural = True 35 36 plurals.add(plural) 37 with open(PLURALS_FILE, 'a') as f: 38 f.write(plural + '\n') 39 cf = inspect.currentframe() 40 lineno = '' 41 if cf and cf.f_back: 42 lineno = ':' + str(cf.f_back.f_lineno) 43 print(f'{__file__}{lineno} check if plural of {singular} is {plural}', file=os.sys.stderr) 44 return plural 45 46def pluralize(word: str): 47 lword = word.lower() 48 if len(lword) >= 2 and lword[-1] == 'y' and lword[-2] not in 'aeiou': 49 return register_plural(word, word[:-1] + 'ies') 50 elif lword.endswith(('s', 'sh', 'ch', 'x', 'z')): 51 return register_plural(word, word[:-1] + 'es') 52 elif lword.endswith('fe'): 53 return register_plural(word, word[:-2] + 'ves') 54 elif lword.endswith('f') and not lword.endswith('ff'): 55 return register_plural(word, word[:-1] + 'ves') 56 else: 57 return register_plural(word, word + 's') 58 59 60def to_yaml_type(typestr: str): 61 if typestr == 'bool': 62 return 'Boolean' 63 elif typestr == 'int': 64 return 'Integer' 65 elif typestr == 'unsigned': 66 return 'Unsigned' 67 elif typestr == 'std::string': 68 return 'String' 69 70 subtype, napplied = re.subn(r'^std::vector<(.*)>$', r'\1', typestr) 71 if napplied == 1: 72 return 'List of ' + pluralize(to_yaml_type(subtype)) 73 74 return typestr 75 76def doxygen2rst(text): 77 text = re.sub(r'<tt>\s*(.*?)\s*<\/tt>', r'``\1``', text) 78 text = re.sub(r'\\c ([^ ,;\.]+)', r'``\1``', text) 79 text = re.sub(r'\\\w+ ', '', text) 80 return text 81 82def indent(text, columns, indent_first_line=True): 83 indent = ' ' * columns 84 s = re.sub(r'\n([^\n])', '\n' + indent + '\\1', text, flags=re.S) 85 if not indent_first_line or s.startswith('\n'): 86 return s 87 return indent + s 88 89class Option(object): 90 def __init__(self, name, type, comment): 91 self.name = name 92 self.type = type 93 self.comment = comment.strip() 94 self.enum = None 95 self.nested_struct = None 96 97 def __str__(self): 98 s = '**%s** (``%s``)\n%s' % (self.name, to_yaml_type(self.type), 99 doxygen2rst(indent(self.comment, 2))) 100 if self.enum and self.enum.values: 101 s += indent('\n\nPossible values:\n\n%s\n' % self.enum, 2) 102 if self.nested_struct: 103 s += indent('\n\nNested configuration flags:\n\n%s\n' %self.nested_struct, 104 2) 105 return s 106 107class NestedStruct(object): 108 def __init__(self, name, comment): 109 self.name = name 110 self.comment = comment.strip() 111 self.values = [] 112 113 def __str__(self): 114 return '\n'.join(map(str, self.values)) 115 116class NestedField(object): 117 def __init__(self, name, comment): 118 self.name = name 119 self.comment = comment.strip() 120 121 def __str__(self): 122 return '\n* ``%s`` %s' % ( 123 self.name, 124 doxygen2rst(indent(self.comment, 2, indent_first_line=False))) 125 126class Enum(object): 127 def __init__(self, name, comment): 128 self.name = name 129 self.comment = comment.strip() 130 self.values = [] 131 132 def __str__(self): 133 return '\n'.join(map(str, self.values)) 134 135class NestedEnum(object): 136 def __init__(self, name, enumtype, comment, values): 137 self.name = name 138 self.comment = comment 139 self.values = values 140 self.type = enumtype 141 142 def __str__(self): 143 s = '\n* ``%s %s``\n%s' % (to_yaml_type(self.type), self.name, 144 doxygen2rst(indent(self.comment, 2))) 145 s += indent('\nPossible values:\n\n', 2) 146 s += indent('\n'.join(map(str, self.values)),2) 147 return s; 148 149class EnumValue(object): 150 def __init__(self, name, comment, config): 151 self.name = name 152 self.comment = comment 153 self.config = config 154 155 def __str__(self): 156 return '* ``%s`` (in configuration: ``%s``)\n%s' % ( 157 self.name, 158 re.sub('.*_', '', self.config), 159 doxygen2rst(indent(self.comment, 2))) 160 161def clean_comment_line(line): 162 match = re.match(r'^/// (?P<indent> +)?\\code(\{.(?P<lang>\w+)\})?$', line) 163 if match: 164 indent = match.group('indent') 165 if not indent: 166 indent = '' 167 lang = match.group('lang') 168 if not lang: 169 lang = 'c++' 170 return '\n%s.. code-block:: %s\n\n' % (indent, lang) 171 172 endcode_match = re.match(r'^/// +\\endcode$', line) 173 if endcode_match: 174 return '' 175 176 match = re.match(r'^/// \\warning$', line) 177 if match: 178 return '\n.. warning:: \n\n' 179 180 endwarning_match = re.match(r'^/// +\\endwarning$', line) 181 if endwarning_match: 182 return '' 183 return line[4:] + '\n' 184 185def read_options(header): 186 class State(object): 187 BeforeStruct, Finished, InStruct, InNestedStruct, InNestedFieldComment, \ 188 InFieldComment, InEnum, InEnumMemberComment = range(8) 189 state = State.BeforeStruct 190 191 options = [] 192 enums = {} 193 nested_structs = {} 194 comment = '' 195 enum = None 196 nested_struct = None 197 198 for line in header: 199 line = line.strip() 200 if state == State.BeforeStruct: 201 if line == 'struct FormatStyle {' or line == 'struct IncludeStyle {': 202 state = State.InStruct 203 elif state == State.InStruct: 204 if line.startswith('///'): 205 state = State.InFieldComment 206 comment = clean_comment_line(line) 207 elif line == '};': 208 state = State.Finished 209 break 210 elif state == State.InFieldComment: 211 if line.startswith('///'): 212 comment += clean_comment_line(line) 213 elif line.startswith('enum'): 214 state = State.InEnum 215 name = re.sub(r'enum\s+(\w+)\s*(:((\s*\w+)+)\s*)?\{', '\\1', line) 216 enum = Enum(name, comment) 217 elif line.startswith('struct'): 218 state = State.InNestedStruct 219 name = re.sub(r'struct\s+(\w+)\s*\{', '\\1', line) 220 nested_struct = NestedStruct(name, comment) 221 elif line.endswith(';'): 222 state = State.InStruct 223 field_type, field_name = re.match(r'([<>:\w(,\s)]+)\s+(\w+);', 224 line).groups() 225 option = Option(str(field_name), str(field_type), comment) 226 options.append(option) 227 else: 228 raise Exception('Invalid format, expected comment, field or enum') 229 elif state == State.InNestedStruct: 230 if line.startswith('///'): 231 state = State.InNestedFieldComment 232 comment = clean_comment_line(line) 233 elif line == '};': 234 state = State.InStruct 235 nested_structs[nested_struct.name] = nested_struct 236 elif state == State.InNestedFieldComment: 237 if line.startswith('///'): 238 comment += clean_comment_line(line) 239 else: 240 state = State.InNestedStruct 241 field_type, field_name = re.match(r'([<>:\w(,\s)]+)\s+(\w+);',line).groups() 242 if field_type in enums: 243 nested_struct.values.append(NestedEnum(field_name,field_type,comment,enums[field_type].values)) 244 else: 245 nested_struct.values.append(NestedField(field_type + " " + field_name, comment)) 246 247 elif state == State.InEnum: 248 if line.startswith('///'): 249 state = State.InEnumMemberComment 250 comment = clean_comment_line(line) 251 elif line == '};': 252 state = State.InStruct 253 enums[enum.name] = enum 254 else: 255 # Enum member without documentation. Must be documented where the enum 256 # is used. 257 pass 258 elif state == State.InEnumMemberComment: 259 if line.startswith('///'): 260 comment += clean_comment_line(line) 261 else: 262 state = State.InEnum 263 val = line.replace(',', '') 264 pos = val.find(" // ") 265 if (pos != -1): 266 config = val[pos+4:] 267 val = val[:pos] 268 else: 269 config = val; 270 enum.values.append(EnumValue(val, comment,config)) 271 if state != State.Finished: 272 raise Exception('Not finished by the end of file') 273 274 for option in options: 275 if not option.type in ['bool', 'unsigned', 'int', 'std::string', 276 'std::vector<std::string>', 277 'std::vector<IncludeCategory>', 278 'std::vector<RawStringFormat>']: 279 if option.type in enums: 280 option.enum = enums[option.type] 281 elif option.type in nested_structs: 282 option.nested_struct = nested_structs[option.type] 283 else: 284 raise Exception('Unknown type: %s' % option.type) 285 return options 286 287options = read_options(open(FORMAT_STYLE_FILE)) 288options += read_options(open(INCLUDE_STYLE_FILE)) 289 290options = sorted(options, key=lambda x: x.name) 291options_text = '\n\n'.join(map(str, options)) 292 293contents = open(DOC_FILE).read() 294 295contents = substitute(contents, 'FORMAT_STYLE_OPTIONS', options_text) 296 297with open(DOC_FILE, 'wb') as output: 298 output.write(contents.encode()) 299