1#!/usr/bin/python3 2# 3# Copyright 2015-2022 The Khronos Group Inc. 4# 5# SPDX-License-Identifier: Apache-2.0 6 7# checkLinks.py - validate link/reference API constructs in files 8# 9# Usage: checkLinks.py [options] files > logfile 10# 11# Options: 12# -follow attempt to follow include:: directives. This script is not an 13# Asciidoctor processor, so only literal relative paths can be followed. 14# -info print some internal diagnostics. 15# -paramcheck attempt to validate param: names against the surrounding 16# context (the current structure/function being validated, for example). 17# This generates many false positives, so is not enabled by default. 18# -fatal unvalidatable links cause immediate error exit from the script. 19# Otherwise, errors are accumulated and summarized at the end. 20# 21# Depends on vkapi.py, which is a Python representation of relevant parts 22# of the Vulkan API. Only works when vkapi.py is generated for the full 23# API, e.g. 'makeAllExts checklinks'; otherwise many false-flagged errors 24# will occur. 25 26import copy, os, pdb, re, string, sys 27from vkapi import * 28 29global curFile, curLine, sectionDepth 30global errCount, warnCount, emittedPrefix, printInfo 31 32curFile = '???' 33curLine = -1 34sectionDepth = 0 35emittedPrefix = {} 36printInfo = False 37 38# Called before printing a warning or error. Only prints once prior 39# to output for a given file. 40def emitPrefix(): 41 global curFile, curLine, emittedPrefix 42 if (curFile not in emittedPrefix.keys()): 43 emittedPrefix[curFile] = None 44 print('Checking file:', curFile) 45 print('-------------------------------') 46 47def info(*args, **kwargs): 48 global curFile, curLine, printInfo 49 if (printInfo): 50 51 emitPrefix() 52 print('INFO: %s line %d:' % (curFile, curLine), 53 ' '.join([str(arg) for arg in args])) 54 55# Print a validation warning found in a file 56def warning(*args, **kwargs): 57 global curFile, curLine, warnCount 58 59 warnCount = warnCount + 1 60 emitPrefix() 61 print('WARNING: %s line %d:' % (curFile, curLine), 62 ' '.join([str(arg) for arg in args])) 63 64# Print a validation error found in a file 65def error(*args, **kwargs): 66 global curFile, curLine, errCount 67 68 errCount = errCount + 1 69 emitPrefix() 70 print('ERROR: %s line %d:' % (curFile, curLine), 71 ' '.join([str(arg) for arg in args])) 72 73# See if a tag value exists in the specified dictionary and 74# suggest it as an alternative if so. 75def checkTag(tag, value, dict, dictName, tagName): 76 if (value in dict.keys()): 77 warning(value, 'exists in the API but not as a', 78 tag + ': .', 'Try using the', tagName + ': tag.') 79 80# Report an error due to an asciidoc tag which does not match 81# a corresponding API entity. 82def foundError(errType, tag, value, fatal): 83 global curFile, curLine 84 error('no such', errType, tag + ':' + value) 85 # Try some heuristics to detect likely problems such as missing vk 86 # prefixes or the wrong tag. 87 88 # Look in all the dictionaries in vkapi.py to see if the tag 89 # is just wrong but the API entity actually exists. 90 checkTag(tag, value, flags, 'flags', 'tlink/tname') 91 checkTag(tag, value, enums, 'enums', 'elink') 92 checkTag(tag, value, structs, 'structs', 'slink/sname') 93 checkTag(tag, value, handles, 'handles', 'slink/sname') 94 checkTag(tag, value, defines, 'defines', 'slink/sname') 95 checkTag(tag, value, consts, 'consts', 'ename') 96 checkTag(tag, value, protos, 'protos', 'flink/fname') 97 checkTag(tag, value, funcpointers, 'funcpointers', 'tlink/tname') 98 99 # Look for missing vk prefixes (quirky since it is case-dependent) 100 # NOT DONE YET 101 102 if fatal: 103 print('ERROR: %s line %d:' % (curFile, curLine), 104 ' '.join(['no such', errType, tag + ':' + value]), file=sys.stderr) 105 sys.exit(1) 106 107# Look for param in the list of all parameters of the specified functions 108# Returns True if found, False otherwise 109def findParam(param, funclist): 110 for f in funclist: 111 if (param in protos[f]): 112 info('parameter:', param, 'found in function:', f) 113 return True 114 return False 115 116# Initialize tracking state for checking links/includes 117def initChecks(): 118 global curFile, curLine, curFuncs, curStruct, accumFunc, sectionDepth 119 global errCount, warnCount 120 global incPat, linkPat, pathPat, sectionPat 121 122 # Matches asciidoc single-line section tags 123 sectionPat = re.compile('^(=+) ') 124 125 # Matches any asciidoc include:: directive 126 pathPat = re.compile('^include::([\w./_]+)\[\]') 127 128 # Matches asciidoc include:: directives used in spec/ref pages (and also 129 # others such as validity). This is specific to the layout of the api/ 130 # includes and allows any path preceding 'api/' followed by the category 131 # (protos, structs, enums, etc.) followed by the name of the proto, 132 # struct, etc. file. 133 incPat = re.compile('^.*api/(\w+)/(\w+)\.adoc') 134 135 # Lists of current /protos/ (functions) and /structs/ includes. There 136 # can be several protos contiguously for different forms of a command 137 curFuncs = [] 138 curStruct = None 139 140 # Tag if we should accumulate funcs or start a new list. Any intervening 141 # pname: tags or struct includes will restart the list. 142 accumFunc = False 143 144 # Matches all link names in the current spec/man pages. Assumes these 145 # macro names are not trailing subsets of other macros. Used to 146 # precede the regexp with [^A-Za-z], but this did not catch macros 147 # at start of line. 148 linkPat = re.compile('([efpst](name|link)):(\w*)') 149 150 # Total error/warning counters 151 errCount = 0 152 warnCount = 0 153 154# Validate asciidoc internal links in specified file. 155# infile - filename to validate 156# follow - if True, recursively follow include:: directives 157# paramCheck - if True, try to verify pname: refers to valid 158# parameter/member names. This generates many false flags currently 159# included - if True, function was called recursively 160# fatalExit - if True, validation errors cause an error exit immediately 161# Links checked are: 162# fname:vkBlah - Vulkan command name (generates internal link) 163# flink:vkBlah - Vulkan command name 164# sname:VkBlah - Vulkan struct name (generates internal link) 165# slink:VkBlah - Vulkan struct name 166# elink:VkEnumName - Vulkan enumeration ('enum') type name (generates internal link) 167# ename:VK_BLAH - Vulkan enumerant token name 168# pname:name - parameter name to a command or a struct member 169# tlink:name - Other Vulkan type name (generates internal link) 170# tname:name - Other Vulkan type name 171def checkLinks(infile, follow = False, paramCheck = True, included = False, fatalExit = False): 172 global curFile, curLine, curFuncs, curStruct, accumFunc, sectionDepth 173 global errCount, warnCount 174 global incPat, linkPat, pathPat, sectionPat 175 176 # Global state which gets saved and restored by this function 177 oldCurFile = curFile 178 oldCurLine = curLine 179 curFile = infile 180 curLine = 0 181 182 # N.b. dirname() returns an empty string for a path with no directories, 183 # unlike the shell dirname(1). 184 if (not os.path.exists(curFile)): 185 error('No such file', curFile, '- skipping check') 186 # Restore global state before exiting the function 187 curFile = oldCurFile 188 curLine = oldCurLine 189 return 190 191 inPath = os.path.dirname(curFile) 192 fp = open(curFile, 'r', encoding='utf-8') 193 194 for line in fp: 195 curLine = curLine + 1 196 197 # Track changes up and down section headers, and forget 198 # the current functions/structure when popping up a level 199 match = sectionPat.search(line) 200 if (match): 201 info('Match sectionPat for line:', line) 202 depth = len(match.group(1)) 203 if (depth < sectionDepth): 204 info('Resetting current function/structure for section:', line) 205 curFuncs = [] 206 curStruct = None 207 sectionDepth = depth 208 209 match = pathPat.search(line) 210 if (match): 211 incpath = match.group(1) 212 info('Match pathPat for line:', line) 213 info(' incpath =', incpath) 214 # An include:: directive. First check if it looks like a 215 # function or struct include file, and modify the corresponding 216 # current function or struct state accordingly. 217 match = incPat.search(incpath) 218 if (match): 219 info('Match incPat for line:', line) 220 # For prototypes, if it is preceded by 221 # another include:: directive with no intervening link: tags, 222 # add to the current function list. Otherwise start a new list. 223 # There is only one current structure. 224 category = match.group(1) 225 tag = match.group(2) 226 # @ Validate tag! 227 # @ Arguably, any intervening text should shift to accumFuncs = False, 228 # e.g. only back-to-back includes separated by blank lines would be 229 # accumulated. 230 if (category == 'protos'): 231 if (tag in protos.keys()): 232 if (accumFunc): 233 curFuncs.append(tag) 234 else: 235 curFuncs = [ tag ] 236 # Restart accumulating functions 237 accumFunc = True 238 info('curFuncs =', curFuncs, 'accumFunc =', accumFunc) 239 else: 240 error('include of nonexistent function', tag) 241 elif (category == 'structs'): 242 if (tag in structs.keys()): 243 curStruct = tag 244 # Any /structs/ include means to stop accumulating /protos/ 245 accumFunc = False 246 info('curStruct =', curStruct) 247 else: 248 error('include of nonexistent struct', tag) 249 if (follow): 250 # Actually process the included file now, recursively 251 newpath = os.path.normpath(os.path.join(inPath, incpath)) 252 info(curFile, ': including file:', newpath) 253 checkLinks(newpath, follow, paramCheck, included = True, fatalExit = fatalExit) 254 255 matches = linkPat.findall(line) 256 for match in matches: 257 # Start actual validation work. Depending on what the 258 # asciidoc tag name is, look up the value in the corresponding 259 # dictionary. 260 tag = match[0] 261 value = match[2] 262 if (tag == 'fname' or tag == 'flink'): 263 if (value not in protos.keys()): 264 foundError('function', tag, value, False) 265 elif (tag == 'sname' or tag == 'slink'): 266 if (value not in structs.keys() and 267 value not in handles.keys()): 268 foundError('aggregate/scalar/handle/define type', tag, value, False) 269 elif (tag == 'ename'): 270 if (value not in consts.keys() and value not in defines.keys()): 271 foundError('enumerant/constant', tag, value, False) 272 elif (tag == 'elink'): 273 if (value not in enums.keys() and value not in flags.keys()): 274 foundError('enum/bitflag type', tag, value, fatalExit) 275 # tname and tlink are the same except if the errors are treated as fatal 276 # They can be recombined once both are error-clean 277 elif (tag == 'tname'): 278 if (value not in funcpointers.keys() and value not in flags.keys()): 279 foundError('function pointer/other type', tag, value, fatalExit) 280 elif (tag == 'tlink'): 281 if (value not in funcpointers.keys() and value not in flags.keys()): 282 foundError('function pointer/other type', tag, value, False) 283 elif (tag == 'pname'): 284 # Any pname: tag means to stop accumulating /protos/ 285 accumFunc = False 286 # See if this parameter is in the current proto(s) and struct 287 foundParam = False 288 if (curStruct and value in structs[curStruct]): 289 info('parameter', value, 'found in struct', curStruct) 290 elif (curFuncs and findParam(value, curFuncs)): 291 True 292 else: 293 if paramCheck: 294 warning('parameter', value, 'not found. curStruct =', 295 curStruct, 'curFuncs =', curFuncs) 296 else: 297 # This is a logic error 298 error('unknown tag', tag + ':' + value) 299 fp.close() 300 301 if (errCount > 0 or warnCount > 0): 302 if (not included): 303 print('Errors found:', errCount, 'Warnings found:', warnCount) 304 print('') 305 306 if (included): 307 info('----- returning from:', infile, 'to parent file', '-----') 308 309 # Do not generate any output for files without errors 310 # else: 311 # print(curFile + ': No errors found') 312 313 # Restore global state before exiting the function 314 curFile = oldCurFile 315 curLine = oldCurLine 316 317if __name__ == '__main__': 318 follow = False 319 paramCheck = False 320 included = False 321 fatalExit = False 322 323 totalErrCount = 0 324 totalWarnCount = 0 325 326 if (len(sys.argv) > 1): 327 for file in sys.argv[1:]: 328 if (file == '-follow'): 329 follow = True 330 elif (file == '-info'): 331 printInfo = True 332 elif file == '-paramcheck': 333 paramCheck = True 334 elif (file == '-fatal'): 335 fatalExit = True 336 else: 337 initChecks() 338 checkLinks(file, 339 follow, 340 paramCheck = paramCheck, 341 included = included, 342 fatalExit = fatalExit) 343 totalErrCount = totalErrCount + errCount 344 totalWarnCount = totalWarnCount + warnCount 345 else: 346 print('Need arguments: [-follow] [-info] [-paramcheck] [-fatal] infile [infile...]', file=sys.stderr) 347 348 if (totalErrCount > 0 or totalWarnCount > 0): 349 if (not included): 350 print('TOTAL Errors found:', totalErrCount, 'Warnings found:', 351 totalWarnCount) 352 if totalErrCount > 0: 353 sys.exit(1) 354