#!/usr/bin/env python3 # A tool to parse the FormatStyle struct from Format.h and update the # documentation in ../ClangFormatStyleOptions.rst automatically. # Run from the directory in which this file is located to update the docs. import inspect import os import re import sys from io import TextIOWrapper from typing import Set CLANG_DIR = os.path.join(os.path.dirname(__file__), '../..') FORMAT_STYLE_FILE = os.path.join(CLANG_DIR, 'include/clang/Format/Format.h') INCLUDE_STYLE_FILE = os.path.join(CLANG_DIR, 'include/clang/Tooling/Inclusions/IncludeStyle.h') DOC_FILE = os.path.join(CLANG_DIR, 'docs/ClangFormatStyleOptions.rst') PLURALS_FILE = os.path.join(os.path.dirname(__file__), 'plurals.txt') plurals: Set[str] = set() with open(PLURALS_FILE, 'a+') as f: f.seek(0) plurals = set(f.read().splitlines()) def substitute(text, tag, contents): replacement = '\n.. START_%s\n\n%s\n\n.. END_%s\n' % (tag, contents, tag) pattern = r'\n\.\. START_%s\n.*\n\.\. END_%s\n' % (tag, tag) return re.sub(pattern, '%s', text, flags=re.S) % replacement def register_plural(singular: str, plural: str): if plural not in plurals: if not hasattr(register_plural, "generated_new_plural"): print('Plural generation: you can use ' f'`git checkout -- {os.path.relpath(PLURALS_FILE)}` ' 'to reemit warnings or `git add` to include new plurals\n') register_plural.generated_new_plural = True plurals.add(plural) with open(PLURALS_FILE, 'a') as f: f.write(plural + '\n') cf = inspect.currentframe() lineno = '' if cf and cf.f_back: lineno = ':' + str(cf.f_back.f_lineno) print(f'{__file__}{lineno} check if plural of {singular} is {plural}', file=sys.stderr) return plural def pluralize(word: str): lword = word.lower() if len(lword) >= 2 and lword[-1] == 'y' and lword[-2] not in 'aeiou': return register_plural(word, word[:-1] + 'ies') elif lword.endswith(('s', 'sh', 'ch', 'x', 'z')): return register_plural(word, word[:-1] + 'es') elif lword.endswith('fe'): return register_plural(word, word[:-2] + 'ves') elif lword.endswith('f') and not lword.endswith('ff'): return register_plural(word, word[:-1] + 'ves') else: return register_plural(word, word + 's') def to_yaml_type(typestr: str): if typestr == 'bool': return 'Boolean' elif typestr == 'int': return 'Integer' elif typestr == 'unsigned': return 'Unsigned' elif typestr == 'std::string': return 'String' subtype, napplied = re.subn(r'^std::vector<(.*)>$', r'\1', typestr) if napplied == 1: return 'List of ' + pluralize(to_yaml_type(subtype)) return typestr def doxygen2rst(text): text = re.sub(r'\s*(.*?)\s*<\/tt>', r'``\1``', text) text = re.sub(r'\\c ([^ ,;\.]+)', r'``\1``', text) text = re.sub(r'\\\w+ ', '', text) return text def indent(text, columns, indent_first_line=True): indent_str = ' ' * columns s = re.sub(r'\n([^\n])', '\n' + indent_str + '\\1', text, flags=re.S) if not indent_first_line or s.startswith('\n'): return s return indent_str + s class Option(object): def __init__(self, name, opt_type, comment, version): self.name = name self.type = opt_type self.comment = comment.strip() self.enum = None self.nested_struct = None self.version = version def __str__(self): if self.version: s = '**%s** (``%s``) :versionbadge:`clang-format %s`\n%s' % (self.name, to_yaml_type(self.type), self.version, doxygen2rst(indent(self.comment, 2))) else: s = '**%s** (``%s``)\n%s' % (self.name, to_yaml_type(self.type), doxygen2rst(indent(self.comment, 2))) if self.enum and self.enum.values: s += indent('\n\nPossible values:\n\n%s\n' % self.enum, 2) if self.nested_struct: s += indent('\n\nNested configuration flags:\n\n%s\n' %self.nested_struct, 2) return s class NestedStruct(object): def __init__(self, name, comment): self.name = name self.comment = comment.strip() self.values = [] def __str__(self): return '\n'.join(map(str, self.values)) class NestedField(object): def __init__(self, name, comment): self.name = name self.comment = comment.strip() def __str__(self): return '\n* ``%s`` %s' % ( self.name, doxygen2rst(indent(self.comment, 2, indent_first_line=False))) class Enum(object): def __init__(self, name, comment): self.name = name self.comment = comment.strip() self.values = [] def __str__(self): return '\n'.join(map(str, self.values)) class NestedEnum(object): def __init__(self, name, enumtype, comment, values): self.name = name self.comment = comment self.values = values self.type = enumtype def __str__(self): s = '\n* ``%s %s``\n%s' % (to_yaml_type(self.type), self.name, doxygen2rst(indent(self.comment, 2))) s += indent('\nPossible values:\n\n', 2) s += indent('\n'.join(map(str, self.values)), 2) return s class EnumValue(object): def __init__(self, name, comment, config): self.name = name self.comment = comment self.config = config def __str__(self): return '* ``%s`` (in configuration: ``%s``)\n%s' % ( self.name, re.sub('.*_', '', self.config), doxygen2rst(indent(self.comment, 2))) class OptionsReader: def __init__(self, header: TextIOWrapper): self.header = header self.in_code_block = False self.code_indent = 0 self.lineno = 0 self.last_err_lineno = -1 def __file_path(self): return os.path.relpath(self.header.name) def __print_line(self, line: str): print(f'{self.lineno:>6} | {line}', file=sys.stderr) def __warning(self, msg: str, line: str): print(f'{self.__file_path()}:{self.lineno}: warning: {msg}:', file=sys.stderr) self.__print_line(line) def __clean_comment_line(self, line: str): match = re.match(r'^/// (?P +)?\\code(\{.(?P\w+)\})?$', line) if match: if self.in_code_block: self.__warning('`\\code` in another `\\code`', line) self.in_code_block = True indent_str = match.group('indent') if not indent_str: indent_str = '' self.code_indent = len(indent_str) lang = match.group('lang') if not lang: lang = 'c++' return f'\n{indent_str}.. code-block:: {lang}\n\n' endcode_match = re.match(r'^/// +\\endcode$', line) if endcode_match: if not self.in_code_block: self.__warning('no correct `\\code` found before this `\\endcode`', line) self.in_code_block = False return '' # check code block indentation if (self.in_code_block and not line == '///' and not line.startswith('/// ' + ' ' * self.code_indent)): if self.last_err_lineno == self.lineno - 1: self.__print_line(line) else: self.__warning('code block should be indented', line) self.last_err_lineno = self.lineno match = re.match(r'^/// \\warning$', line) if match: return '\n.. warning:: \n\n' endwarning_match = re.match(r'^/// +\\endwarning$', line) if endwarning_match: return '' return line[4:] + '\n' def read_options(self): class State: BeforeStruct, Finished, InStruct, InNestedStruct, InNestedFieldComment, \ InFieldComment, InEnum, InEnumMemberComment = range(8) state = State.BeforeStruct options = [] enums = {} nested_structs = {} comment = '' enum = None nested_struct = None version = None for line in self.header: self.lineno += 1 line = line.strip() if state == State.BeforeStruct: if line in ('struct FormatStyle {', 'struct IncludeStyle {'): state = State.InStruct elif state == State.InStruct: if line.startswith('///'): state = State.InFieldComment comment = self.__clean_comment_line(line) elif line == '};': state = State.Finished break elif state == State.InFieldComment: if line.startswith(r'/// \version'): match = re.match(r'/// \\version\s*(?P[0-9.]+)*', line) if match: version = match.group('version') elif line.startswith('///'): comment += self.__clean_comment_line(line) elif line.startswith('enum'): state = State.InEnum name = re.sub(r'enum\s+(\w+)\s*(:((\s*\w+)+)\s*)?\{', '\\1', line) enum = Enum(name, comment) elif line.startswith('struct'): state = State.InNestedStruct name = re.sub(r'struct\s+(\w+)\s*\{', '\\1', line) nested_struct = NestedStruct(name, comment) elif line.endswith(';'): state = State.InStruct field_type, field_name = re.match(r'([<>:\w(,\s)]+)\s+(\w+);', line).groups() if not version: self.__warning(f'missing version for {field_name}', line) option = Option(str(field_name), str(field_type), comment, version) options.append(option) version = None else: raise Exception('Invalid format, expected comment, field or enum\n' + line) elif state == State.InNestedStruct: if line.startswith('///'): state = State.InNestedFieldComment comment = self.__clean_comment_line(line) elif line == '};': state = State.InStruct nested_structs[nested_struct.name] = nested_struct elif state == State.InNestedFieldComment: if line.startswith('///'): comment += self.__clean_comment_line(line) else: state = State.InNestedStruct field_type, field_name = re.match(r'([<>:\w(,\s)]+)\s+(\w+);', line).groups() if field_type in enums: nested_struct.values.append(NestedEnum(field_name, field_type, comment, enums[field_type].values)) else: nested_struct.values.append(NestedField(field_type + " " + field_name, comment)) elif state == State.InEnum: if line.startswith('///'): state = State.InEnumMemberComment comment = self.__clean_comment_line(line) elif line == '};': state = State.InStruct enums[enum.name] = enum else: # Enum member without documentation. Must be documented where the enum # is used. pass elif state == State.InEnumMemberComment: if line.startswith('///'): comment += self.__clean_comment_line(line) else: state = State.InEnum val = line.replace(',', '') pos = val.find(" // ") if pos != -1: config = val[pos + 4:] val = val[:pos] else: config = val enum.values.append(EnumValue(val, comment, config)) if state != State.Finished: raise Exception('Not finished by the end of file') for option in options: if option.type not in ['bool', 'unsigned', 'int', 'std::string', 'std::vector', 'std::vector', 'std::vector']: if option.type in enums: option.enum = enums[option.type] elif option.type in nested_structs: option.nested_struct = nested_structs[option.type] else: raise Exception('Unknown type: %s' % option.type) return options with open(FORMAT_STYLE_FILE) as f: opts = OptionsReader(f).read_options() with open(INCLUDE_STYLE_FILE) as f: opts += OptionsReader(f).read_options() opts = sorted(opts, key=lambda x: x.name) options_text = '\n\n'.join(map(str, opts)) with open(DOC_FILE) as f: contents = f.read() contents = substitute(contents, 'FORMAT_STYLE_OPTIONS', options_text) with open(DOC_FILE, 'wb') as output: output.write(contents.encode())