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