• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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