File size: 8,453 Bytes
055eba4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | #!/usr/bin/env python
import glob
import os.path
import re
import sys
warnings = 0
def report(rule, location, description):
global warnings
warnings += 1
print(f'{warnings:3}. {location}: {description} [{rule}]')
def check_structure():
expected_sections = [
'Template parameters',
'Specializations',
'Iterator invalidation',
'Requirements',
'Member types',
'Member functions',
'Member variables',
'Static functions',
'Non-member functions',
'Literals',
'Helper classes',
'Parameters',
'Return value',
'Exception safety',
'Exceptions',
'Complexity',
'Possible implementation',
'Default definition',
'Notes',
'Examples',
'See also',
'Version history'
]
required_sections = [
'Examples',
'Version history'
]
files = sorted(glob.glob('api/**/*.md', recursive=True))
for file in files:
with open(file) as file_content:
section_idx = -1 # the index of the current h2 section
existing_sections = [] # the list of h2 sections in the file
in_initial_code_example = False # whether we are inside the first code example block
previous_line = None # the previous read line
h1sections = 0 # the number of h1 sections in the file
last_overload = 0 # the last seen overload number in the code example
documented_overloads = {} # the overloads that have been documented in the current block
current_section = None # the name of the current section
for lineno, original_line in enumerate(file_content.readlines()):
line = original_line.strip()
if line.startswith('# '):
h1sections += 1
# there should only be one top-level title
if h1sections > 1:
report('structure/unexpected_section', f'{file}:{lineno+1}', f'unexpected top-level title "{line}"')
h1sections = 1
# Overview pages should have a better title
if line == '# Overview':
report('style/title', f'{file}:{lineno+1}', 'overview pages should have a better title than "Overview"')
# lines longer than 160 characters are bad (unless they are tables)
if len(line) > 160 and '|' not in line:
report('whitespace/line_length', f'{file}:{lineno+1} ({current_section})', f'line is too long ({len(line)} vs. 160 chars)')
# sections in `<!-- NOLINT -->` comments are treated as present
if line.startswith('<!-- NOLINT'):
current_section = line.strip('<!-- NOLINT')
current_section = current_section.strip(' -->')
existing_sections.append(current_section)
# check if sections are correct
if line.startswith('## '):
# before starting a new section, check if the previous one documented all overloads
if current_section in documented_overloads and last_overload != 0:
if len(documented_overloads[current_section]) > 0 and len(documented_overloads[current_section]) != last_overload:
expected = list(range(1, last_overload+1))
undocumented = [x for x in expected if x not in documented_overloads[current_section]]
unexpected = [x for x in documented_overloads[current_section] if x not in expected]
if len(undocumented):
report('style/numbering', f'{file}:{lineno} ({current_section})', f'undocumented overloads: {", ".join([f"({x})" for x in undocumented])}')
if len(unexpected):
report('style/numbering', f'{file}:{lineno} ({current_section})', f'unexpected overloads: {", ".join([f"({x})" for x in unexpected])}')
current_section = line.strip('## ')
existing_sections.append(current_section)
if current_section in expected_sections:
idx = expected_sections.index(current_section)
if idx <= section_idx:
report('structure/section_order', f'{file}:{lineno+1}', f'section "{current_section}" is in an unexpected order (should be before "{expected_sections[section_idx]}")')
section_idx = idx
else:
if 'index.md' not in file: # index.md files may have a different structure
report('structure/unknown_section', f'{file}:{lineno+1}', f'section "{current_section}" is not part of the expected sections')
# collect the numbered items of the current section to later check if they match the number of overloads
if last_overload != 0 and not in_initial_code_example:
if len(original_line) and original_line[0].isdigit():
number = int(re.findall(r"^(\d+).", original_line)[0])
if current_section not in documented_overloads:
documented_overloads[current_section] = []
documented_overloads[current_section].append(number)
# code example
if line == '```cpp' and section_idx == -1:
in_initial_code_example = True
if in_initial_code_example and line.startswith('//') and line not in ['// since C++20', '// until C++20']:
# check numbering of overloads
if any(map(str.isdigit, line)):
number = int(re.findall(r'\d+', line)[0])
if number != last_overload + 1:
report('style/numbering', f'{file}:{lineno+1}', f'expected number ({number}) to be ({last_overload +1 })')
last_overload = number
if any(map(str.isdigit, line)) and '(' not in line:
report('style/numbering', f'{file}:{lineno+1}', f'number should be in parentheses: {line}')
if line == '```' and in_initial_code_example:
in_initial_code_example = False
# consecutive blank lines are bad
if line == '' and previous_line == '':
report('whitespace/blank_lines', f'{file}:{lineno}-{lineno+1} ({current_section})', 'consecutive blank lines')
# check that non-example admonitions have titles
untitled_admonition = re.match(r'^(\?\?\?|!!!) ([^ ]+)$', line)
if untitled_admonition and untitled_admonition.group(2) != 'example':
report('style/admonition_title', f'{file}:{lineno} ({current_section})', f'"{untitled_admonition.group(2)}" admonitions should have a title')
previous_line = line
if 'index.md' not in file: # index.md files may have a different structure
for required_section in required_sections:
if required_section not in existing_sections:
report('structure/missing_section', f'{file}:{lineno+1}', f'required section "{required_section}" was not found')
def check_examples():
example_files = sorted(glob.glob('../../examples/*.cpp'))
markdown_files = sorted(glob.glob('**/*.md', recursive=True))
# check if every example file is used in at least one markdown file
for example_file in example_files:
example_file = os.path.join('examples', os.path.basename(example_file))
found = False
for markdown_file in markdown_files:
content = ' '.join(open(markdown_file).readlines())
if example_file in content:
found = True
break
if not found:
report('examples/missing', f'{example_file}', 'example file is not used in any documentation file')
if __name__ == '__main__':
print(120 * '-')
check_structure()
check_examples()
print(120 * '-')
if warnings > 0:
sys.exit(1)
|