2021-12-29 13:41:01 +01:00
#!/usr/bin/env python
import glob
import os . path
2022-04-25 22:40:45 +02:00
import re
2022-08-05 19:51:39 +02:00
import sys
2022-04-25 22:40:45 +02:00
2025-01-17 06:53:35 +01:00
import yaml
2022-04-25 22:40:45 +02:00
warnings = 0
2025-01-17 06:53:35 +01:00
def report ( rule , location , description ) - > None :
2022-04-25 22:40:45 +02:00
global warnings
warnings + = 1
print ( f ' { warnings : 3 } . { location } : { description } [ { rule } ] ' )
2021-12-29 13:41:01 +01:00
2025-01-17 06:53:35 +01:00
def check_structure ( ) - > None :
2022-04-25 22:40:45 +02:00
expected_sections = [
2025-01-17 06:53:35 +01:00
" 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 " ,
2021-12-29 13:41:01 +01:00
]
2022-04-25 22:40:45 +02:00
required_sections = [
2025-01-17 06:53:35 +01:00
" Examples " ,
" Version history " ,
2021-12-29 13:41:01 +01:00
]
2025-01-17 06:53:35 +01:00
files = sorted ( glob . glob ( " api/**/*.md " , recursive = True ) )
2021-12-29 13:41:01 +01:00
for file in files :
with open ( file ) as file_content :
2022-05-17 13:08:56 +02:00
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 ( )
2021-12-29 13:41:01 +01:00
2025-01-17 06:53:35 +01:00
if line . startswith ( " # " ) :
2021-12-29 13:41:01 +01:00
h1sections + = 1
# there should only be one top-level title
if h1sections > 1 :
2025-01-17 06:53:35 +01:00
report ( " structure/unexpected_section " , f " { file } : { lineno + 1 } " , f ' unexpected top-level title " { line } " ' )
2021-12-29 13:41:01 +01:00
h1sections = 1
# Overview pages should have a better title
2025-01-17 06:53:35 +01:00
if line == " # Overview " :
report ( " style/title " , f " { file } : { lineno + 1 } " , ' overview pages should have a better title than " Overview " ' )
2021-12-29 13:41:01 +01:00
# lines longer than 160 characters are bad (unless they are tables)
2025-01-17 06:53:35 +01:00
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) " )
2021-12-29 13:41:01 +01:00
2022-08-05 19:51:39 +02:00
# sections in `<!-- NOLINT -->` comments are treated as present
2025-01-17 06:53:35 +01:00
if line . startswith ( " <!-- NOLINT " ) :
current_section = line . strip ( " <!-- NOLINT " )
current_section = current_section . strip ( " --> " )
2022-08-05 19:51:39 +02:00
existing_sections . append ( current_section )
2022-04-25 22:40:45 +02:00
# check if sections are correct
2025-01-17 06:53:35 +01:00
if line . startswith ( " ## " ) :
2022-05-17 13:08:56 +02:00
# 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 ) :
2025-01-17 06:53:35 +01:00
report ( " style/numbering " , f " { file } : { lineno } ( { current_section } ) " , f ' undocumented overloads: { " , " . join ( [ f " ( { x } ) " for x in undocumented ] ) } ' )
2022-05-17 13:08:56 +02:00
if len ( unexpected ) :
2025-01-17 06:53:35 +01:00
report ( " style/numbering " , f " { file } : { lineno } ( { current_section } ) " , f ' unexpected overloads: { " , " . join ( [ f " ( { x } ) " for x in unexpected ] ) } ' )
2022-05-17 13:08:56 +02:00
2025-01-17 06:53:35 +01:00
current_section = line . strip ( " ## " )
2022-04-25 22:40:45 +02:00
existing_sections . append ( current_section )
if current_section in expected_sections :
idx = expected_sections . index ( current_section )
if idx < = section_idx :
2025-01-17 06:53:35 +01:00
report ( " structure/section_order " , f " { file } : { lineno + 1 } " , f ' section " { current_section } " is in an unexpected order (should be before " { expected_sections [ section_idx ] } " ) ' )
2022-04-25 22:40:45 +02:00
section_idx = idx
2025-01-17 06:53:35 +01:00
elif " 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 ' )
2022-05-17 13:08:56 +02:00
# 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 )
2021-12-29 13:41:01 +01:00
# code example
2025-01-17 06:53:35 +01:00
if line == " ```cpp " and section_idx == - 1 :
2021-12-29 13:41:01 +01:00
in_initial_code_example = True
2025-01-17 06:53:35 +01:00
if in_initial_code_example and line . startswith ( " // " ) and line not in [ " // since C++20 " , " // until C++20 " ] :
2022-05-17 13:08:56 +02:00
# check numbering of overloads
if any ( map ( str . isdigit , line ) ) :
2025-01-17 06:53:35 +01:00
number = int ( re . findall ( r " \ d+ " , line ) [ 0 ] )
2022-05-17 13:08:56 +02:00
if number != last_overload + 1 :
2025-01-17 06:53:35 +01:00
report ( " style/numbering " , f " { file } : { lineno + 1 } " , f " expected number ( { number } ) to be ( { last_overload + 1 } ) " )
2022-05-17 13:08:56 +02:00
last_overload = number
2025-01-17 06:53:35 +01:00
if any ( map ( str . isdigit , line ) ) and " ( " not in line :
report ( " style/numbering " , f " { file } : { lineno + 1 } " , f " number should be in parentheses: { line } " )
2021-12-29 13:41:01 +01:00
2025-01-17 06:53:35 +01:00
if line == " ``` " and in_initial_code_example :
2021-12-29 13:41:01 +01:00
in_initial_code_example = False
# consecutive blank lines are bad
2025-01-17 06:53:35 +01:00
if line == " " and previous_line == " " :
report ( " whitespace/blank_lines " , f " { file } : { lineno } - { lineno + 1 } ( { current_section } ) " , " consecutive blank lines " )
2022-04-25 22:40:45 +02:00
# check that non-example admonitions have titles
2025-01-17 06:53:35 +01:00
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 ' )
2021-12-29 13:41:01 +01:00
previous_line = line
2025-01-17 06:53:35 +01:00
if " index.md " not in file : # index.md files may have a different structure
2022-05-17 13:08:56 +02:00
for required_section in required_sections :
if required_section not in existing_sections :
2025-01-17 06:53:35 +01:00
report ( " structure/missing_section " , f " { file } : { lineno + 1 } " , f ' required section " { required_section } " was not found ' )
2021-12-29 13:41:01 +01:00
2025-01-17 06:53:35 +01:00
def check_examples ( ) - > None :
example_files = sorted ( glob . glob ( " ../../examples/*.cpp " ) )
markdown_files = sorted ( glob . glob ( " **/*.md " , recursive = True ) )
2021-12-29 13:41:01 +01:00
# check if every example file is used in at least one markdown file
for example_file in example_files :
2025-01-17 06:53:35 +01:00
example_file = os . path . join ( " examples " , os . path . basename ( example_file ) )
2021-12-29 13:41:01 +01:00
found = False
for markdown_file in markdown_files :
2025-01-17 06:53:35 +01:00
content = " " . join ( open ( markdown_file ) . readlines ( ) )
2021-12-29 13:41:01 +01:00
if example_file in content :
found = True
break
if not found :
2025-01-17 06:53:35 +01:00
report ( " examples/missing " , f " { example_file } " , " example file is not used in any documentation file " )
def check_links ( ) - > None :
""" Check that every entry in the navigation (nav in mkdocs.yml) links to at most one file. If a file is linked more
than once , then the first entry is repeated . See https : / / github . com / nlohmann / json / issues / 4564 for the issue in
this project and https : / / github . com / mkdocs / mkdocs / issues / 3428 for the root cause .
The issue can be fixed by merging the keys , so
- ' NLOHMANN_JSON_VERSION_MAJOR ' : api / macros / nlohmann_json_version_major . md
- ' NLOHMANN_JSON_VERSION_MINOR ' : api / macros / nlohmann_json_version_major . md
would be replaced with
- ' NLOHMANN_JSON_VERSION_MAJOR, NLOHMANN_JSON_VERSION_MINOR ' : api / macros / nlohmann_json_version_major . md
"""
file_with_path = { }
def collect_links ( node , path = " " ) - > None :
if isinstance ( node , list ) :
for x in node :
collect_links ( x , path )
elif isinstance ( node , dict ) :
for p , x in node . items ( ) :
collect_links ( x , path + " / " + p )
else :
if node not in file_with_path :
file_with_path [ node ] = [ ]
file_with_path [ node ] . append ( path )
with open ( " ../mkdocs.yml " ) as mkdocs_file :
# see https://github.com/yaml/pyyaml/issues/86#issuecomment-1042485535
yaml . add_multi_constructor ( " tag:yaml.org,2002:python/name " , lambda loader , suffix , node : None , Loader = yaml . SafeLoader )
yaml . add_multi_constructor ( " !ENV " , lambda loader , suffix , node : None , Loader = yaml . SafeLoader )
y = yaml . safe_load ( mkdocs_file )
collect_links ( y [ " nav " ] )
for duplicate_file in [ x for x in file_with_path if len ( file_with_path [ x ] ) > 1 ] :
file_list = [ f ' " { x } " ' for x in file_with_path [ duplicate_file ] ]
file_list_str = " , " . join ( file_list )
report ( " nav/duplicate_files " , " mkdocs.yml " , f ' file " { duplicate_file } " is linked with multiple keys in " nav " : { file_list_str } ; only one is rendered properly, see #4564 ' )
2021-12-29 13:41:01 +01:00
2025-01-17 06:53:35 +01:00
if __name__ == " __main__ " :
print ( 120 * " - " )
2021-12-29 13:41:01 +01:00
check_structure ( )
check_examples ( )
2025-01-17 06:53:35 +01:00
check_links ( )
print ( 120 * " - " )
2022-08-05 19:51:39 +02:00
if warnings > 0 :
sys . exit ( 1 )