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