1#!/usr/bin/python3 2# 3# Copyright (c) 2015-2018 The Khronos Group Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# checkLinks.py - validate link/reference API constructs in files 18# 19# Usage: checkLinks.py files > logfile 20# 21# Uses vkapi.py, which is a Python representation of relevant parts 22# of the Vulkan API. 23 24import copy, os, pdb, re, string, sys 25from vkapi import * 26 27global curFile, curLine, sectionDepth 28global errCount, warnCount, emittedPrefix, printInfo 29 30curFile = '???' 31curLine = -1 32sectionDepth = 0 33errCount = 0 34warnCount = 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 doesn't match 81# a corresponding API entity. 82def foundError(errType, tag, value): 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', 'elink') 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's case-dependent) 100 # NOT DONE YET 101 102# Look for param in the list of all parameters of the specified functions 103# Returns True if found, False otherwise 104def findParam(param, funclist): 105 for f in funclist: 106 if (param in protos[f]): 107 info('parameter:', param, 'found in function:', f) 108 return True 109 return False 110 111# Initialize tracking state for checking links/includes 112def initChecks(): 113 global curFile, curLine, curFuncs, curStruct, accumFunc, sectionDepth 114 global errCount, warnCount 115 global incPat, linkPat, pathPat, sectionPat 116 117 # Matches asciidoc single-line section tags 118 sectionPat = re.compile('^(=+) ') 119 120 # Matches any asciidoc include:: directive 121 pathPat = re.compile('^include::([\w./_]+)\[\]') 122 123 # Matches asciidoc include:: directives used in spec/ref pages (and also 124 # others such as validity). This is specific to the layout of the api/ 125 # includes and allows any path precding 'api/' followed by the category 126 # (protos, structs, enums, etc.) followed by the name of the proto, 127 # struct, etc. file. 128 incPat = re.compile('^.*api/(\w+)/(\w+)\.txt') 129 130 # Lists of current /protos/ (functions) and /structs/ includes. There 131 # can be several protos contiguously for different forms of a command 132 curFuncs = [] 133 curStruct = None 134 135 # Tag if we should accumulate funcs or start a new list. Any intervening 136 # pname: tags or struct includes will restart the list. 137 accumFunc = False 138 139 # Matches all link names in the current spec/man pages. Assumes these 140 # macro names are not trailing subsets of other macros. Used to 141 # precede the regexp with [^A-Za-z], but this didn't catch macros 142 # at start of line. 143 linkPat = re.compile('([efpst](name|link)):(\w*)') 144 145 # Total error/warning counters 146 errCount = 0 147 warnCount = 0 148 149# Validate asciidoc internal links in specified file. 150# infile - filename to validate 151# follow - if True, recursively follow include:: directives 152# included - if True, function was called recursively 153# Links checked are: 154# fname:vkBlah - Vulkan command name (generates internal link) 155# flink:vkBlah - Vulkan command name 156# sname:VkBlah - Vulkan struct name (generates internal link) 157# slink:VkBlah - Vulkan struct name 158# elink:VkEnumName - Vulkan enumeration ('enum') type name 159# ename:VK_BLAH - Vulkan enumerant token name 160# pname:name - parameter name to a command or a struct member 161# tlink:name - Other Vulkan type name (generates internal link) 162# tname:name - Other Vulkan type name 163def checkLinks(infile, follow = False, included = False): 164 global curFile, curLine, curFuncs, curStruct, accumFunc, sectionDepth 165 global errCount, warnCount 166 global incPat, linkPat, pathPat, sectionPat 167 168 # Global state which gets saved and restored by this function 169 oldCurFile = curFile 170 oldCurLine = curLine 171 curFile = infile 172 curLine = 0 173 174 # N.b. dirname() returns an empty string for a path with no directories, 175 # unlike the shell dirname(1). 176 if (not os.path.exists(curFile)): 177 error('No such file', curFile, '- skipping check') 178 # Restore global state before exiting the function 179 curFile = oldCurFile 180 curLine = oldCurLine 181 return 182 183 inPath = os.path.dirname(curFile) 184 fp = open(curFile, 'r', encoding='utf-8') 185 186 for line in fp: 187 curLine = curLine + 1 188 189 # Track changes up and down section headers, and forget 190 # the current functions/structure when popping up a level 191 match = sectionPat.search(line) 192 if (match): 193 info('Match sectionPat for line:', line) 194 depth = len(match.group(1)) 195 if (depth < sectionDepth): 196 info('Resetting current function/structure for section:', line) 197 curFuncs = [] 198 curStruct = None 199 sectionDepth = depth 200 201 match = pathPat.search(line) 202 if (match): 203 incpath = match.group(1) 204 info('Match pathPat for line:', line) 205 info(' incpath =', incpath) 206 # An include:: directive. First check if it looks like a 207 # function or struct include file, and modify the corresponding 208 # current function or struct state accordingly. 209 match = incPat.search(incpath) 210 if (match): 211 info('Match incPat for line:', line) 212 # For prototypes, if it is preceded by 213 # another include:: directive with no intervening link: tags, 214 # add to the current function list. Otherwise start a new list. 215 # There is only one current structure. 216 category = match.group(1) 217 tag = match.group(2) 218 # @ Validate tag! 219 # @ Arguably, any intervening text should shift to accumFuncs = False, 220 # e.g. only back-to-back includes separated by blank lines would be 221 # accumulated. 222 if (category == 'protos'): 223 if (tag in protos.keys()): 224 if (accumFunc): 225 curFuncs.append(tag) 226 else: 227 curFuncs = [ tag ] 228 # Restart accumulating functions 229 accumFunc = True 230 info('curFuncs =', curFuncs, 'accumFunc =', accumFunc) 231 else: 232 error('include of nonexistent function', tag) 233 elif (category == 'structs'): 234 if (tag in structs.keys()): 235 curStruct = tag 236 # Any /structs/ include means to stop accumulating /protos/ 237 accumFunc = False 238 info('curStruct =', curStruct) 239 else: 240 error('include of nonexistent struct', tag) 241 if (follow): 242 # Actually process the included file now, recursively 243 newpath = os.path.normpath(os.path.join(inPath, incpath)) 244 info(curFile, ': including file:', newpath) 245 checkLinks(newpath, follow, included=True) 246 247 matches = linkPat.findall(line) 248 for match in matches: 249 # Start actual validation work. Depending on what the 250 # asciidoc tag name is, look up the value in the corresponding 251 # dictionary. 252 tag = match[0] 253 value = match[2] 254 if (tag == 'fname' or tag == 'flink'): 255 if (value not in protos.keys()): 256 foundError('function', tag, value) 257 elif (tag == 'sname' or tag == 'slink'): 258 if (value not in structs.keys() and 259 value not in handles.keys()): 260 foundError('aggregate/scalar/handle/define type', tag, value) 261 elif (tag == 'ename'): 262 if (value not in consts.keys() and value not in defines.keys()): 263 foundError('enumerant/constant', tag, value) 264 elif (tag == 'elink'): 265 if (value not in enums.keys() and value not in flags.keys()): 266 foundError('enum/bitflag type', tag, value) 267 elif (tag == 'tlink' or tag == 'tname'): 268 if (value not in funcpointers.keys()): 269 foundError('function pointer/other type', tag, value) 270 elif (tag == 'pname'): 271 # Any pname: tag means to stop accumulating /protos/ 272 accumFunc = False 273 # See if this parameter is in the current proto(s) and struct 274 foundParam = False 275 if (curStruct and value in structs[curStruct]): 276 info('parameter', value, 'found in struct', curStruct) 277 elif (curFuncs and findParam(value, curFuncs)): 278 True 279 else: 280 warning('parameter', value, 'not found. curStruct =', 281 curStruct, 'curFuncs =', curFuncs) 282 else: 283 # This is a logic error 284 error('unknown tag', tag + ':' + value) 285 fp.close() 286 287 if (errCount > 0 or warnCount > 0): 288 if (not included): 289 print('Errors found:', errCount, 'Warnings found:', warnCount) 290 print('') 291 292 if (included): 293 info('----- returning from:', infile, 'to parent file', '-----') 294 295 # Don't generate any output for files without errors 296 # else: 297 # print(curFile + ': No errors found') 298 299 # Restore global state before exiting the function 300 curFile = oldCurFile 301 curLine = oldCurLine 302 303if __name__ == '__main__': 304 follow = False 305 if (len(sys.argv) > 1): 306 for file in sys.argv[1:]: 307 if (file == '-follow'): 308 follow = True 309 elif (file == '-info'): 310 printInfo = True 311 else: 312 initChecks() 313 checkLinks(file, follow) 314 else: 315 print('Need arguments: [-follow] [-info] infile [infile...]', file=sys.stderr) 316