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.
11 from io
import TextIOWrapper
12 from typing
import Set
14 CLANG_DIR
= os
.path
.join(os
.path
.dirname(__file__
), "../..")
15 FORMAT_STYLE_FILE
= os
.path
.join(CLANG_DIR
, "include/clang/Format/Format.h")
16 INCLUDE_STYLE_FILE
= os
.path
.join(
17 CLANG_DIR
, "include/clang/Tooling/Inclusions/IncludeStyle.h"
19 DOC_FILE
= os
.path
.join(CLANG_DIR
, "docs/ClangFormatStyleOptions.rst")
21 PLURALS_FILE
= os
.path
.join(os
.path
.dirname(__file__
), "plurals.txt")
23 plurals
: Set
[str] = set()
24 with
open(PLURALS_FILE
) as f
:
26 plurals
= set(f
.read().splitlines())
29 def substitute(text
, tag
, contents
):
30 replacement
= "\n.. START_%s\n\n%s\n\n.. END_%s\n" % (tag
, contents
, tag
)
31 pattern
= r
"\n\.\. START_%s\n.*\n\.\. END_%s\n" % (tag
, tag
)
32 return re
.sub(pattern
, "%s", text
, flags
=re
.S
) % replacement
35 def register_plural(singular
: str, plural
: str):
36 if plural
not in plurals
:
37 if not hasattr(register_plural
, "generated_new_plural"):
39 "Plural generation: you can use "
40 f
"`git checkout -- {os.path.relpath(PLURALS_FILE)}` "
41 "to reemit warnings or `git add` to include new plurals\n"
43 register_plural
.generated_new_plural
= True
46 with
open(PLURALS_FILE
, "a") as f
:
47 f
.write(plural
+ "\n")
48 cf
= inspect
.currentframe()
51 lineno
= ":" + str(cf
.f_back
.f_lineno
)
53 f
"{__file__}{lineno} check if plural of {singular} is {plural}",
59 def pluralize(word
: str):
61 if len(lword
) >= 2 and lword
[-1] == "y" and lword
[-2] not in "aeiou":
62 return register_plural(word
, word
[:-1] + "ies")
63 elif lword
.endswith(("s", "sh", "ch", "x", "z")):
64 return register_plural(word
, word
[:-1] + "es")
65 elif lword
.endswith("fe"):
66 return register_plural(word
, word
[:-2] + "ves")
67 elif lword
.endswith("f") and not lword
.endswith("ff"):
68 return register_plural(word
, word
[:-1] + "ves")
70 return register_plural(word
, word
+ "s")
73 def to_yaml_type(typestr
: str):
76 elif typestr
== "int":
78 elif typestr
== "unsigned":
80 elif typestr
== "std::string":
83 match
= re
.match(r
"std::vector<(.*)>$", typestr
)
85 return "List of " + pluralize(to_yaml_type(match
.group(1)))
87 match
= re
.match(r
"std::optional<(.*)>$", typestr
)
89 return to_yaml_type(match
.group(1))
94 def doxygen2rst(text
):
95 text
= re
.sub(r
"<tt>\s*(.*?)\s*<\/tt>", r
"``\1``", text
)
96 text
= re
.sub(r
"\\c ([^ ,;\.]+)", r
"``\1``", text
)
97 text
= re
.sub(r
"\\\w+ ", "", text
)
101 def indent(text
, columns
, indent_first_line
=True):
102 indent_str
= " " * columns
103 s
= re
.sub(r
"\n([^\n])", "\n" + indent_str
+ "\\1", text
, flags
=re
.S
)
104 if not indent_first_line
or s
.startswith("\n"):
106 return indent_str
+ s
109 class Option(object):
110 def __init__(self
, name
, opt_type
, comment
, version
):
113 self
.comment
= comment
.strip()
115 self
.nested_struct
= None
116 self
.version
= version
119 s
= ".. _%s:\n\n**%s** (``%s``) " % (
122 to_yaml_type(self
.type),
125 s
+= ":versionbadge:`clang-format %s` " % self
.version
126 s
+= ":ref:`ΒΆ <%s>`\n%s" % (self
.name
, doxygen2rst(indent(self
.comment
, 2)))
127 if self
.enum
and self
.enum
.values
:
128 s
+= indent("\n\nPossible values:\n\n%s\n" % self
.enum
, 2)
129 if self
.nested_struct
:
131 "\n\nNested configuration flags:\n\n%s\n" % self
.nested_struct
, 2
133 s
= s
.replace("<option-name>", self
.name
)
137 class NestedStruct(object):
138 def __init__(self
, name
, comment
):
140 self
.comment
= comment
.strip()
144 return self
.comment
+ "\n" + "\n".join(map(str, self
.values
))
147 class NestedField(object):
148 def __init__(self
, name
, comment
, version
):
150 self
.comment
= comment
.strip()
151 self
.version
= version
155 return "\n* ``%s`` :versionbadge:`clang-format %s`\n%s" % (
158 doxygen2rst(indent(self
.comment
, 2, indent_first_line
=False)),
160 return "\n* ``%s`` %s" % (
162 doxygen2rst(indent(self
.comment
, 2, indent_first_line
=False)),
167 def __init__(self
, name
, comment
):
169 self
.comment
= comment
.strip()
173 return "\n".join(map(str, self
.values
))
176 class NestedEnum(object):
177 def __init__(self
, name
, enumtype
, comment
, version
, values
):
179 self
.comment
= comment
182 self
.version
= version
187 s
= "\n* ``%s %s`` :versionbadge:`clang-format %s`\n\n%s" % (
188 to_yaml_type(self
.type),
191 doxygen2rst(indent(self
.comment
, 2)),
194 s
= "\n* ``%s %s``\n%s" % (
195 to_yaml_type(self
.type),
197 doxygen2rst(indent(self
.comment
, 2)),
199 s
+= indent("\nPossible values:\n\n", 2)
200 s
+= indent("\n".join(map(str, self
.values
)), 2)
204 class EnumValue(object):
205 def __init__(self
, name
, comment
, config
):
207 self
.comment
= comment
211 return "* ``%s`` (in configuration: ``%s``)\n%s" % (
213 re
.sub(".*_", "", self
.config
),
214 doxygen2rst(indent(self
.comment
, 2)),
219 def __init__(self
, header
: TextIOWrapper
):
221 self
.in_code_block
= False
224 self
.last_err_lineno
= -1
226 def __file_path(self
):
227 return os
.path
.relpath(self
.header
.name
)
229 def __print_line(self
, line
: str):
230 print(f
"{self.lineno:>6} | {line}", file=sys
.stderr
)
232 def __warning(self
, msg
: str, line
: str):
233 print(f
"{self.__file_path()}:{self.lineno}: warning: {msg}:", file=sys
.stderr
)
234 self
.__print
_line
(line
)
236 def __clean_comment_line(self
, line
: str):
237 match
= re
.match(r
"^/// (?P<indent> +)?\\code(\{.(?P<lang>\w+)\})?$", line
)
239 if self
.in_code_block
:
240 self
.__warning
("`\\code` in another `\\code`", line
)
241 self
.in_code_block
= True
242 indent_str
= match
.group("indent")
245 self
.code_indent
= len(indent_str
)
246 lang
= match
.group("lang")
249 return f
"\n{indent_str}.. code-block:: {lang}\n\n"
251 endcode_match
= re
.match(r
"^/// +\\endcode$", line
)
253 if not self
.in_code_block
:
255 "no correct `\\code` found before this `\\endcode`", line
257 self
.in_code_block
= False
260 # check code block indentation
263 and not line
== "///"
264 and not line
.startswith("/// " + " " * self
.code_indent
)
266 if self
.last_err_lineno
== self
.lineno
- 1:
267 self
.__print
_line
(line
)
269 self
.__warning
("code block should be indented", line
)
270 self
.last_err_lineno
= self
.lineno
272 match
= re
.match(r
"^/// \\warning$", line
)
274 return "\n.. warning::\n\n"
276 endwarning_match
= re
.match(r
"^/// +\\endwarning$", line
)
280 match
= re
.match(r
"^/// \\note$", line
)
282 return "\n.. note::\n\n"
284 endnote_match
= re
.match(r
"^/// +\\endnote$", line
)
287 return line
[4:] + "\n"
289 def read_options(self
):
296 InNestedFieldComment
,
301 InNestedEnumMemberComment
,
304 state
= State
.BeforeStruct
315 for line
in self
.header
:
318 if state
== State
.BeforeStruct
:
319 if line
in ("struct FormatStyle {", "struct IncludeStyle {"):
320 state
= State
.InStruct
321 elif state
== State
.InStruct
:
322 if line
.startswith("///"):
323 state
= State
.InFieldComment
324 comment
= self
.__clean
_comment
_line
(line
)
326 state
= State
.Finished
328 elif state
== State
.InFieldComment
:
329 if line
.startswith(r
"/// \version"):
330 match
= re
.match(r
"/// \\version\s*(?P<version>[0-9.]+)*", line
)
332 version
= match
.group("version")
333 elif line
.startswith("/// @deprecated"):
335 elif line
.startswith("///"):
336 comment
+= self
.__clean
_comment
_line
(line
)
337 elif line
.startswith("enum"):
339 name
= re
.sub(r
"enum\s+(\w+)\s*(:((\s*\w+)+)\s*)?\{", "\\1", line
)
340 enum
= Enum(name
, comment
)
341 elif line
.startswith("struct"):
342 state
= State
.InNestedStruct
343 name
= re
.sub(r
"struct\s+(\w+)\s*\{", "\\1", line
)
344 nested_struct
= NestedStruct(name
, comment
)
345 elif line
.endswith(";"):
347 if line
.startswith(prefix
):
348 line
= line
[len(prefix
) :]
349 state
= State
.InStruct
350 field_type
, field_name
= re
.match(
351 r
"([<>:\w(,\s)]+)\s+(\w+);", line
354 field_type
= "deprecated"
358 self
.__warning
(f
"missing version for {field_name}", line
)
359 option
= Option(str(field_name
), str(field_type
), comment
, version
)
360 options
.append(option
)
364 "Invalid format, expected comment, field or enum\n" + line
366 elif state
== State
.InNestedStruct
:
367 if line
.startswith("///"):
368 state
= State
.InNestedFieldComment
369 comment
= self
.__clean
_comment
_line
(line
)
371 state
= State
.InStruct
372 nested_structs
[nested_struct
.name
] = nested_struct
373 elif state
== State
.InNestedFieldComment
:
374 if line
.startswith(r
"/// \version"):
375 match
= re
.match(r
"/// \\version\s*(?P<version>[0-9.]+)*", line
)
377 version
= match
.group("version")
378 elif line
.startswith("///"):
379 comment
+= self
.__clean
_comment
_line
(line
)
380 elif line
.startswith("enum"):
381 state
= State
.InNestedEnum
382 name
= re
.sub(r
"enum\s+(\w+)\s*(:((\s*\w+)+)\s*)?\{", "\\1", line
)
383 enum
= Enum(name
, comment
)
385 state
= State
.InNestedStruct
386 field_type
, field_name
= re
.match(
387 r
"([<>:\w(,\s)]+)\s+(\w+);", line
390 # self.__warning(f"missing version for {field_name}", line)
391 if field_type
in enums
:
392 nested_struct
.values
.append(
398 enums
[field_type
].values
,
402 nested_struct
.values
.append(
403 NestedField(field_type
+ " " + field_name
, comment
, version
)
406 elif state
== State
.InEnum
:
407 if line
.startswith("///"):
408 state
= State
.InEnumMemberComment
409 comment
= self
.__clean
_comment
_line
(line
)
411 state
= State
.InStruct
412 enums
[enum
.name
] = enum
414 # Enum member without documentation. Must be documented
415 # where the enum is used.
417 elif state
== State
.InNestedEnum
:
418 if line
.startswith("///"):
419 state
= State
.InNestedEnumMemberComment
420 comment
= self
.__clean
_comment
_line
(line
)
422 state
= State
.InNestedStruct
423 enums
[enum
.name
] = enum
425 # Enum member without documentation. Must be
426 # documented where the enum is used.
428 elif state
== State
.InEnumMemberComment
:
429 if line
.startswith("///"):
430 comment
+= self
.__clean
_comment
_line
(line
)
433 val
= line
.replace(",", "")
434 pos
= val
.find(" // ")
436 config
= val
[pos
+ 4 :]
440 enum
.values
.append(EnumValue(val
, comment
, config
))
441 elif state
== State
.InNestedEnumMemberComment
:
442 if line
.startswith("///"):
443 comment
+= self
.__clean
_comment
_line
(line
)
445 state
= State
.InNestedEnum
446 val
= line
.replace(",", "")
447 pos
= val
.find(" // ")
449 config
= val
[pos
+ 4 :]
453 enum
.values
.append(EnumValue(val
, comment
, config
))
454 if state
!= State
.Finished
:
455 raise Exception("Not finished by the end of file")
457 for option
in options
:
458 if option
.type not in [
463 "std::vector<std::string>",
464 "std::vector<IncludeCategory>",
465 "std::vector<RawStringFormat>",
466 "std::optional<unsigned>",
469 if option
.type in enums
:
470 option
.enum
= enums
[option
.type]
471 elif option
.type in nested_structs
:
472 option
.nested_struct
= nested_structs
[option
.type]
474 raise Exception("Unknown type: %s" % option
.type)
478 p
= argparse
.ArgumentParser()
479 p
.add_argument("-o", "--output", help="path of output file")
480 args
= p
.parse_args()
482 with
open(FORMAT_STYLE_FILE
) as f
:
483 opts
= OptionsReader(f
).read_options()
484 with
open(INCLUDE_STYLE_FILE
) as f
:
485 opts
+= OptionsReader(f
).read_options()
487 opts
= sorted(opts
, key
=lambda x
: x
.name
)
488 options_text
= "\n\n".join(map(str, opts
))
490 with
open(DOC_FILE
, encoding
="utf-8") as f
:
493 contents
= substitute(contents
, "FORMAT_STYLE_OPTIONS", options_text
)
496 args
.output
if args
.output
else DOC_FILE
, "w", newline
="", encoding
="utf-8"