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