• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Provides MacroCheckerFile, a subclassable type that validates a single file in the spec."""
2
3# Copyright (c) 2018-2019 Collabora, Ltd.
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7# Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
8
9import logging
10import re
11from collections import OrderedDict, namedtuple
12from enum import Enum
13from inspect import currentframe
14
15from .shared import (AUTO_FIX_STRING, CATEGORIES_WITH_VALIDITY,
16                     EXTENSION_CATEGORY, NON_EXISTENT_MACROS, EntityData,
17                     Message, MessageContext, MessageId, MessageType,
18                     generateInclude, toNameAndLine)
19
20# Code blocks may start and end with any number of ----
21CODE_BLOCK_DELIM = '----'
22
23# Mostly for ref page blocks, but also used elsewhere?
24REF_PAGE_LIKE_BLOCK_DELIM = '--'
25
26# For insets/blocks like the implicit valid usage
27# TODO think it must start with this - does it have to be exactly this?
28BOX_BLOCK_DELIM = '****'
29
30
31INTERNAL_PLACEHOLDER = re.compile(
32    r'(?P<delim>__+)([a-zA-Z]+)(?P=delim)'
33)
34
35# Matches any include line.
36# Used to check for a leading path attribute.
37INCLUDE_PATH_ATTRIBUTE = re.compile(
38    r'include::(\{(?P<path_attribute>[a-z]+)\})?.*\[\]')
39
40allowed_path_attributes = {
41    '{appendices}',
42    '{chapters}',
43    '{config}',
44    '{generated}',
45    '{promoted}',
46    '{style}',
47}
48
49# Matches a generated (api or validity) include line.
50INCLUDE = re.compile(
51    r'include::(?P<directory_traverse>((../){1,4}|\{generated\}/)(generated/)?)(?P<generated_type>(api|validity))/(?P<category>\w+)/(?P<entity_name>[^./]+).adoc[\[][\]]')
52
53# Matches an [[AnchorLikeThis]]
54ANCHOR = re.compile(r'\[\[(?P<entity_name>[^\]]+)\]\]')
55
56# Looks for flink:foo:: or slink::foo:: at the end of string:
57# used to detect explicit pname context.
58PRECEDING_MEMBER_REFERENCE = re.compile(
59    r'\b(?P<macro>[fs](text|link)):(?P<entity_name>[\w*]+)::$')
60
61# Matches something like slink:foo::pname:bar as well as
62# the under-marked-up slink:foo::bar.
63MEMBER_REFERENCE = re.compile(
64    r'\b(?P<first_part>(?P<scope_macro>[fs](text|link)):(?P<scope>[\w*]+))(?P<double_colons>::)(?P<second_part>(?P<member_macro>pname:?)(?P<entity_name>[\w]+))\b'
65)
66
67# Matches if a string ends while a link is still "open".
68# (first half of a link being broken across two lines,
69# or containing our interested area when matched against the text preceding).
70# Used to skip checking in some places.
71OPEN_LINK = re.compile(
72    r'.*(?<!`)<<[^>]*$'
73)
74
75# Matches if a string begins and is followed by a link "close" without a matching open.
76# (second half of a link being broken across two lines)
77# Used to skip checking in some places.
78CLOSE_LINK = re.compile(
79    r'[^<]*>>.*$'
80)
81
82# Matches if a line should be skipped without further considering.
83# Matches lines starting with:
84# - `ifdef:`
85# - `endif:`
86# - `todo` (followed by something matching \b, like : or (. capitalization ignored)
87SKIP_LINE = re.compile(
88    r'^(ifdef:)|(endif:)|([tT][oO][dD][oO]\b).*'
89)
90
91# Matches the whole inside of a refpage tag.
92BRACKETS = re.compile(r'\[(?P<tags>.*)\]')
93
94# Matches a key='value' pair from a ref page tag.
95REF_PAGE_ATTRIB = re.compile(
96    r"(?P<key>[a-z]+)='(?P<value>[^'\\]*(?:\\.[^'\\]*)*)'")
97
98# Exceptions to:
99# error: Definition of link target {} with macro etext (used for category enums) does not exist. (-Wwrong_macro)
100# typically caused by using Vulkan-only enums in Vulkan SC blocks with "etext", or because they
101# are suffixed differently.
102CHECK_UNRECOGNIZED_ETEXT_EXCEPTIONS = (
103    'VK_COLORSPACE_SRGB_NONLINEAR_KHR',
104    'VK_COLOR_SPACE_DCI_P3_LINEAR_EXT',
105    'VK_PIPELINE_CACHE_CREATE_READ_ONLY_BIT',
106    'VK_STENCIL_FRONT_AND_BACK',
107    'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETER_FEATURES',
108    'VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTER_FEATURES',
109    'VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES2_EXT',
110)
111
112# Exceptions to:
113# warning: Definition of link target {} with macro ename (used for category enums) does not exist. (-Wbad_enumerant)
114# typically caused by Vulkan SC enums not being recognized in Vulkan build
115CHECK_UNRECOGNIZED_ENAME_EXCEPTIONS = (
116    'VK_ERROR_INVALID_PIPELINE_CACHE_DATA',
117    'VK_ERROR_NO_PIPELINE_MATCH',
118    'VK_ERROR_VALIDATION_FAILED',
119    'VK_MEMORY_HEAP_SEU_SAFE_BIT',
120    'VK_PIPELINE_CACHE_CREATE_READ_ONLY_BIT',
121    'VK_PIPELINE_CACHE_CREATE_USE_APPLICATION_STORAGE_BIT',
122    'VK_PIPELINE_CACHE_HEADER_VERSION_SAFETY_CRITICAL_ONE',
123)
124
125class Attrib(Enum):
126    """Attributes of a ref page."""
127
128    REFPAGE = 'refpage'
129    DESC = 'desc'
130    TYPE = 'type'
131    ALIAS = 'alias'
132    XREFS = 'xrefs'
133    ANCHOR = 'anchor'
134
135
136VALID_REF_PAGE_ATTRIBS = set(
137    (e.value for e in Attrib))
138
139AttribData = namedtuple('AttribData', ['match', 'key', 'value'])
140
141
142def makeAttribFromMatch(match):
143    """Turn a match of REF_PAGE_ATTRIB into an AttribData value."""
144    return AttribData(match=match, key=match.group(
145        'key'), value=match.group('value'))
146
147
148def parseRefPageAttribs(line):
149    """Parse a ref page tag into a dictionary of attribute_name: AttribData."""
150    return {m.group('key'): makeAttribFromMatch(m)
151            for m in REF_PAGE_ATTRIB.finditer(line)}
152
153
154def regenerateIncludeFromMatch(match, generated_type):
155    """Create an include directive from an INCLUDE match and a (new or replacement) generated_type."""
156    return generateInclude(
157        match.group('directory_traverse'),
158        generated_type,
159        match.group('category'),
160        match.group('entity_name'))
161
162
163BlockEntry = namedtuple(
164    'BlockEntry', ['delimiter', 'context', 'block_type', 'refpage'])
165
166
167class BlockType(Enum):
168    """Enumeration of the various distinct block types known."""
169    CODE = 'code'
170    REF_PAGE_LIKE = 'ref-page-like'  # with or without a ref page tag before
171    BOX = 'box'
172
173    @classmethod
174    def lineToBlockType(self, line):
175        """Return a BlockType if the given line is a block delimiter.
176
177        Returns None otherwise.
178        """
179        if line == REF_PAGE_LIKE_BLOCK_DELIM:
180            return BlockType.REF_PAGE_LIKE
181        if line.startswith(CODE_BLOCK_DELIM):
182            return BlockType.CODE
183        if line.startswith(BOX_BLOCK_DELIM):
184            return BlockType.BOX
185
186        return None
187
188
189def _pluralize(word, num):
190    if num == 1:
191        return word
192    if word.endswith('y'):
193        return word[:-1] + 'ies'
194    return word + 's'
195
196
197def _s_suffix(num):
198    """Simplify pluralization."""
199    if num > 1:
200        return 's'
201    return ''
202
203
204def shouldEntityBeText(entity, subscript):
205    """Determine if an entity name appears to use placeholders, wildcards, etc. and thus merits use of a *text macro.
206
207    Call with the entity and subscript groups from a match of MacroChecker.macro_re.
208    """
209    entity_only = entity
210    if subscript:
211        if subscript == '[]' or subscript == '[i]' or subscript.startswith(
212                '[_') or subscript.endswith('_]'):
213            return True
214        entity_only = entity[:-len(subscript)]
215
216    if ('*' in entity) or entity.startswith('_') or entity_only.endswith('_'):
217        return True
218
219    if INTERNAL_PLACEHOLDER.search(entity):
220        return True
221    return False
222
223
224class MacroCheckerFile(object):
225    """Object performing processing of a single AsciiDoctor file from a specification.
226
227    For testing purposes, may also process a string as if it were a file.
228    """
229
230    def __init__(self, checker, filename, enabled_messages, stream_maker):
231        """Construct a MacroCheckerFile object.
232
233        Typically called by MacroChecker.processFile or MacroChecker.processString().
234
235        Arguments:
236        checker -- A MacroChecker object.
237        filename -- A string to use in messages to refer to this checker, typically the file name.
238        enabled_messages -- A set() of MessageId values that should be considered "enabled" and thus stored.
239        stream_maker -- An object with a makeStream() method that returns a stream.
240        """
241        self.checker = checker
242        self.filename = filename
243        self.stream_maker = stream_maker
244        self.enabled_messages = enabled_messages
245        self.missing_validity_suppressions = set(
246            self.getMissingValiditySuppressions())
247
248        self.logger = logging.getLogger(__name__)
249        self.logger.addHandler(logging.NullHandler())
250
251        self.fixes = set()
252        self.messages = []
253
254        self.pname_data = None
255        self.pname_mentions = {}
256
257        self.refpage_includes = {}
258
259        self.lines = []
260
261        # For both of these:
262        # keys: entity name
263        # values: MessageContext
264        self.fs_api_includes = {}
265        self.validity_includes = {}
266
267        self.in_code_block = False
268        self.in_ref_page = False
269        self.prev_line_ref_page_tag = None
270        self.current_ref_page = None
271
272        # Stack of block-starting delimiters.
273        self.block_stack = []
274
275        # Regexes that are members because they depend on the name prefix.
276        self.suspected_missing_macro_re = self.checker.suspected_missing_macro_re
277        self.heading_command_re = self.checker.heading_command_re
278
279    ###
280    # Main process/checking methods, arranged roughly from largest scope to smallest scope.
281    ###
282
283    def process(self):
284        """Check the stream (file, string) created by the streammaker supplied to the constructor.
285
286        This is the top-level method for checking a spec file.
287        """
288        self.logger.info("processing file %s", self.filename)
289
290        # File content checks - performed line-by-line
291        with self.stream_maker.make_stream() as f:
292            # Iterate through lines, calling processLine on each.
293            for lineIndex, line in enumerate(f):
294                trimmedLine = line.rstrip()
295                self.lines.append(trimmedLine)
296                self.processLine(lineIndex + 1, trimmedLine)
297
298        # End of file checks follow:
299
300        # Check "state" at end of file: should have blocks closed.
301        if self.prev_line_ref_page_tag:
302            self.error(MessageId.REFPAGE_BLOCK,
303                       "Reference page tag seen, but block not opened before end of file.",
304                       context=self.storeMessageContext(match=None))
305
306        if self.block_stack:
307            locations = (x.context for x in self.block_stack)
308            formatted_locations = ['{} opened at {}'.format(x.delimiter, self.getBriefLocation(x.context))
309                                   for x in self.block_stack]
310            self.logger.warning("Unclosed blocks: %s",
311                                ', '.join(formatted_locations))
312
313            self.error(MessageId.UNCLOSED_BLOCK,
314                       ["Reached end of page, with these unclosed blocks remaining:"] +
315                       formatted_locations,
316                       context=self.storeMessageContext(match=None),
317                       see_also=locations)
318
319        # Check that every include of an /api/ file in the protos or structs category
320        # had a matching /validity/ include
321        for entity, includeContext in self.fs_api_includes.items():
322            if not self.checker.entity_db.entityHasValidity(entity):
323                continue
324
325            if entity in self.missing_validity_suppressions:
326                continue
327
328            if entity not in self.validity_includes:
329                self.warning(MessageId.MISSING_VALIDITY_INCLUDE,
330                             ['Saw /api/ include for {}, but no matching /validity/ include'.format(entity),
331                              'Expected a line with ' + regenerateIncludeFromMatch(includeContext.match, 'validity')],
332                             context=includeContext)
333
334        # Check that we never include a /validity/ file
335        # without a matching /api/ include
336        for entity, includeContext in self.validity_includes.items():
337            if entity not in self.fs_api_includes:
338                self.error(MessageId.MISSING_API_INCLUDE,
339                           ['Saw /validity/ include for {}, but no matching /api/ include'.format(entity),
340                            'Expected a line with ' + regenerateIncludeFromMatch(includeContext.match, 'api')],
341                           context=includeContext)
342
343        if not self.numDiagnostics():
344            # no problems, exit quietly
345            return
346
347        print('\nFor file {}:'.format(self.filename))
348
349        self.printMessageCounts()
350        numFixes = len(self.fixes)
351        if numFixes > 0:
352            fixes = ', '.join(('{} -> {}'.format(search, replace)
353                               for search, replace in self.fixes))
354
355            print('{} unique auto-fix {} recorded: {}'.format(numFixes,
356                                                              _pluralize('pattern', numFixes), fixes))
357
358    def processLine(self, lineNum, line):
359        """Check the contents of a single line from a file.
360
361        Eventually populates self.match, self.entity, self.macro,
362        before calling processMatch.
363        """
364        self.lineNum = lineNum
365        self.line = line
366        self.match = None
367        self.entity = None
368        self.macro = None
369
370        self.logger.debug("processing line %d", lineNum)
371
372        if self.processPossibleBlockDelimiter():
373            # This is a block delimiter - proceed to next line.
374            # Block-type-specific stuff goes in processBlockOpen and processBlockClosed.
375            return
376
377        if self.in_code_block:
378            # We do no processing in a code block.
379            return
380
381        ###
382        # Detect if the previous line was [open,...] starting a refpage
383        # but this line isn't --
384        # If the line is some other block delimiter,
385        # the related code in self.processPossibleBlockDelimiter()
386        # would have handled it.
387        # (because execution would never get to here for that line)
388        if self.prev_line_ref_page_tag:
389            self.handleExpectedRefpageBlock()
390
391        ###
392        # Detect headings
393        if line.startswith('=='):
394            # Headings cause us to clear our pname_context
395            self.pname_data = None
396
397            command = self.heading_command_re.match(line)
398            if command:
399                data = self.checker.findEntity(command)
400                if data:
401                    self.pname_data = data
402            return
403
404        ###
405        # Detect [open, lines for manpages
406        if line.startswith('[open,'):
407            self.checkRefPage()
408            return
409
410        ###
411        # Skip comments
412        if line.lstrip().startswith('//'):
413            return
414
415        ###
416        # Skip ifdef/endif
417        if SKIP_LINE.match(line):
418            return
419
420        ###
421        # Detect any include:: lines
422        match = INCLUDE_PATH_ATTRIBUTE.match(line)
423        if match:
424            path_attribute = match.group(1)
425            if path_attribute is None:
426                self.error(MessageId.MISSING_INCLUDE_PATH_ATTRIBUTE,
427                           '(no path attribute is present)')
428                return
429            if path_attribute not in allowed_path_attributes:
430                self.error(MessageId.MISSING_INCLUDE_PATH_ATTRIBUTE,
431                           f'(path attribute {{path_attribute}} is not one of {sorted(allowed_path_attributes)})')
432                return
433
434        ###
435        # Detect include:::....[] lines for generated API fragments
436        match = INCLUDE.match(line)
437        if match:
438            self.match = match
439            entity = match.group('entity_name')
440
441            data = self.checker.findEntity(entity)
442            if not data:
443                self.error(MessageId.UNKNOWN_INCLUDE,
444                           'Saw include for {}, but that entity is unknown.'.format(entity))
445                self.pname_data = None
446                return
447
448            self.pname_data = data
449
450            if match.group('generated_type') == 'api':
451                self.recordInclude(self.checker.apiIncludes)
452
453                # Set mentions to None. The first time we see something like `* pname:paramHere`,
454                # we will set it to an empty set
455                self.pname_mentions[entity] = None
456
457                if match.group('category') in CATEGORIES_WITH_VALIDITY:
458                    self.fs_api_includes[entity] = self.storeMessageContext()
459
460                if entity in self.validity_includes:
461                    name_and_line = toNameAndLine(
462                        self.validity_includes[entity], root_path=self.checker.root_path)
463                    self.error(MessageId.API_VALIDITY_ORDER,
464                               ['/api/ include found for {} after a corresponding /validity/ include'.format(entity),
465                                'Validity include located at {}'.format(name_and_line)])
466
467            elif match.group('generated_type') == 'validity':
468                self.recordInclude(self.checker.validityIncludes)
469                self.validity_includes[entity] = self.storeMessageContext()
470
471                if entity not in self.pname_mentions:
472                    self.error(MessageId.API_VALIDITY_ORDER,
473                               '/validity/ include found for {} without a preceding /api/ include'.format(entity))
474                    return
475
476                if self.pname_mentions[entity]:
477                    # Got a validity include and we have seen at least one * pname: line
478                    # since we got the API include
479                    # so we can warn if we haven't seen a reference to every
480                    # parameter/member.
481                    members = self.checker.getMemberNames(entity)
482                    missing = [member for member in members
483                               if member not in self.pname_mentions[entity]]
484                    if missing:
485                        self.error(MessageId.UNDOCUMENTED_MEMBER,
486                                   ['Validity include found for {}, but not all members/params apparently documented'.format(entity),
487                                    'Members/params not mentioned with pname: {}'.format(', '.join(missing))])
488
489            # If we found an include line, we're done with this line.
490            return
491
492        if self.pname_data is not None and '* pname:' in line:
493            context_entity = self.pname_data.entity
494            if self.pname_mentions[context_entity] is None:
495                # First time seeing * pname: after an api include, prepare the set that
496                # tracks
497                self.pname_mentions[context_entity] = set()
498
499        ###
500        # Detect [[Entity]] anchors
501        for match in ANCHOR.finditer(line):
502            entity = match.group('entity_name')
503            if self.checker.findEntity(entity):
504                # We found an anchor with the same name as an entity:
505                # treat it (mostly) like an API include
506                self.match = match
507                self.recordInclude(self.checker.apiIncludes,
508                                   generated_type='api (manual anchor)')
509
510        ###
511        # Detect :: without pname
512        for match in MEMBER_REFERENCE.finditer(line):
513            if not match.group('member_macro'):
514                self.match = match
515                # Got :: but not followed by pname
516
517                search = match.group()
518                replacement = match.group(
519                    'first_part') + '::pname:' + match.group('second_part')
520                self.error(MessageId.MEMBER_PNAME_MISSING,
521                           'Found a function parameter or struct member reference with :: but missing pname:',
522                           group='double_colons',
523                           replacement='::pname:',
524                           fix=(search, replacement))
525
526                # check pname here because it won't come up in normal iteration below
527                # because of the missing macro
528                self.entity = match.group('entity_name')
529                self.checkPname(match.group('scope'))
530
531        ###
532        # Look for things that seem like a missing macro.
533        for match in self.suspected_missing_macro_re.finditer(line):
534            if OPEN_LINK.match(line, endpos=match.start()):
535                # this is in a link, skip it.
536                continue
537            if CLOSE_LINK.match(line[match.end():]):
538                # this is in a link, skip it.
539                continue
540
541            entity = match.group('entity_name')
542            self.match = match
543            self.entity = entity
544            data = self.checker.findEntity(entity)
545            if data:
546
547                if data.category == EXTENSION_CATEGORY:
548                    # Ah, this is an extension
549                    self.warning(MessageId.EXTENSION, "Seems like this is an extension name that was not linked.",
550                                 group='entity_name', replacement=self.makeExtensionLink())
551                else:
552                    self.warning(MessageId.MISSING_MACRO,
553                                 ['Seems like a "{}" macro was omitted for this reference to a known entity in category "{}".'.format(data.macro, data.category),
554                                  'Wrap in ` ` to silence this if you do not want a verified macro here.'],
555                                 group='entity_name',
556                                 replacement=self.makeMacroMarkup(data.macro))
557            else:
558
559                dataArray = self.checker.findEntityCaseInsensitive(entity)
560                # We might have found the goof...
561
562                if dataArray:
563                    if len(dataArray) == 1:
564                        # Yep, found the goof:
565                        # incorrect macro and entity capitalization
566                        data = dataArray[0]
567                        if data.category == EXTENSION_CATEGORY:
568                            # Ah, this is an extension
569                            self.warning(MessageId.EXTENSION,
570                                         "Seems like this is an extension name that was not linked.",
571                                         group='entity_name', replacement=self.makeExtensionLink(data.entity))
572                        else:
573                            self.warning(MessageId.MISSING_MACRO,
574                                         'Seems like a macro was omitted for this reference to a known entity in category "{}", found by searching case-insensitively.'.format(
575                                             data.category),
576                                         replacement=self.makeMacroMarkup(data=data))
577
578                    else:
579                        # Ugh, more than one resolution
580
581                        self.warning(MessageId.MISSING_MACRO,
582                                     ['Seems like a macro was omitted for this reference to a known entity, found by searching case-insensitively.',
583                                      'More than one apparent match.'],
584                                     group='entity_name', see_also=dataArray[:])
585
586        ###
587        # Main operations: detect markup macros
588        for match in self.checker.macro_re.finditer(line):
589            self.match = match
590            self.macro = match.group('macro')
591            self.entity = match.group('entity_name')
592            self.subscript = match.group('subscript')
593            self.processMatch()
594
595    def processPossibleBlockDelimiter(self):
596        """Look at the current line, and if it's a delimiter, update the block stack.
597
598        Calls self.processBlockDelimiter() as required.
599
600        Returns True if a delimiter was processed, False otherwise.
601        """
602        line = self.line
603        new_block_type = BlockType.lineToBlockType(line)
604        if not new_block_type:
605            return False
606
607        ###
608        # Detect if the previous line was [open,...] starting a refpage
609        # but this line is some block delimiter other than --
610        # Must do this here because if we get a different block open instead of the one we want,
611        # the order of block opening will be wrong.
612        if new_block_type != BlockType.REF_PAGE_LIKE and self.prev_line_ref_page_tag:
613            self.handleExpectedRefpageBlock()
614
615        # Delegate to the main process for delimiters.
616        self.processBlockDelimiter(line, new_block_type)
617
618        return True
619
620    def processBlockDelimiter(self, line, new_block_type, context=None):
621        """Update the block stack based on the current or supplied line.
622
623        Calls self.processBlockOpen() or self.processBlockClosed() as required.
624
625        Called by self.processPossibleBlockDelimiter() both in normal operation, as well as
626        when "faking" a ref page block open.
627
628        Returns BlockProcessResult.
629        """
630        if not context:
631            context = self.storeMessageContext()
632
633        location = self.getBriefLocation(context)
634
635        top = self.getInnermostBlockEntry()
636        top_delim = self.getInnermostBlockDelimiter()
637        if top_delim == line:
638            self.processBlockClosed()
639            return
640
641        if top and top.block_type == new_block_type:
642            # Same block type, but not matching - might be an error?
643            # TODO maybe create a diagnostic here?
644            self.logger.warning(
645                "processPossibleBlockDelimiter: %s: Matched delimiter type %s, but did not exactly match current delim %s to top of stack %s, may be a typo?",
646                location, new_block_type, line, top_delim)
647
648        # Empty stack, or top doesn't match us.
649        self.processBlockOpen(new_block_type, delimiter=line)
650
651    def processBlockOpen(self, block_type, context=None, delimiter=None):
652        """Do any block-type-specific processing and push the new block.
653
654        Must call self.pushBlock().
655        May be overridden (carefully) or extended.
656
657        Called by self.processBlockDelimiter().
658        """
659        if block_type == BlockType.REF_PAGE_LIKE:
660            if self.prev_line_ref_page_tag:
661                if self.current_ref_page:
662                    refpage = self.current_ref_page
663                else:
664                    refpage = '?refpage-with-invalid-tag?'
665
666                self.logger.info(
667                    'processBlockOpen: Opening refpage for %s', refpage)
668                # Opening of refpage block "consumes" the preceding ref
669                # page context
670                self.prev_line_ref_page_tag = None
671                self.pushBlock(block_type, refpage=refpage,
672                               context=context, delimiter=delimiter)
673                self.in_ref_page = True
674                return
675
676        if block_type == BlockType.CODE:
677            self.in_code_block = True
678
679        self.pushBlock(block_type, context=context, delimiter=delimiter)
680
681    def processBlockClosed(self):
682        """Do any block-type-specific processing and pop the top block.
683
684        Must call self.popBlock().
685        May be overridden (carefully) or extended.
686
687        Called by self.processPossibleBlockDelimiter().
688        """
689        old_top = self.popBlock()
690
691        if old_top.block_type == BlockType.CODE:
692            self.in_code_block = False
693
694        elif old_top.block_type == BlockType.REF_PAGE_LIKE and old_top.refpage:
695            self.logger.info(
696                'processBlockClosed: Closing refpage for %s', old_top.refpage)
697            # leaving a ref page so reset associated state.
698            self.current_ref_page = None
699            self.prev_line_ref_page_tag = None
700            self.in_ref_page = False
701
702    def processMatch(self):
703        """Process a match of the macro:entity regex for correctness."""
704        match = self.match
705        entity = self.entity
706        macro = self.macro
707
708        ###
709        # Track entities that we're actually linking to.
710        ###
711        if self.checker.entity_db.isLinkedMacro(macro):
712            self.checker.addLinkToEntity(entity, self.storeMessageContext())
713
714        ###
715        # Link everything that should be, and nothing that shouldn't be
716        ###
717        if self.checkRecognizedEntity():
718            # if this returns true,
719            # then there is no need to do the remaining checks on this match
720            return
721
722        ###
723        # Non-existent macros
724        if macro in NON_EXISTENT_MACROS:
725            self.error(MessageId.BAD_MACRO, '{} is not a macro provided in the specification, despite resembling other macros.'.format(
726                macro), group='macro')
727
728        ###
729        # Wildcards (or leading underscore, or square brackets)
730        # if and only if a 'text' macro
731        self.checkText()
732
733        # Do some validation of pname references.
734        if macro == 'pname':
735            # See if there's an immediately-preceding entity
736            preceding = self.line[:match.start()]
737            scope = PRECEDING_MEMBER_REFERENCE.search(preceding)
738            if scope:
739                # Yes there is, check it out.
740                self.checkPname(scope.group('entity_name'))
741            elif self.current_ref_page is not None:
742                # No, but there is a current ref page: very reliable
743                self.checkPnameImpliedContext(self.current_ref_page)
744            elif self.pname_data is not None:
745                # No, but there is a pname_context - better than nothing.
746                self.checkPnameImpliedContext(self.pname_data)
747            else:
748                # no, and no existing context we can imply:
749                # can't check this.
750                pass
751
752    def checkRecognizedEntity(self):
753        """Check the current macro:entity match to see if it is recognized.
754
755        Returns True if there is no need to perform further checks on this match.
756
757        Helps avoid duplicate warnings/errors: typically each macro should have at most
758        one of this class of errors.
759        """
760        entity = self.entity
761        macro = self.macro
762        if self.checker.findMacroAndEntity(macro, entity) is not None:
763            # We know this macro-entity combo
764            return True
765
766        # We don't know this macro-entity combo.
767        possibleCats = self.checker.entity_db.getCategoriesForMacro(macro)
768        if possibleCats is None:
769            possibleCats = ['???']
770        msg = ['Definition of link target {} with macro {} (used for {} {}) does not exist.'.format(
771            entity,
772            macro,
773            _pluralize('category', len(possibleCats)),
774            ', '.join(possibleCats))]
775
776        data = self.checker.findEntity(entity)
777        if data:
778            if entity in CHECK_UNRECOGNIZED_ETEXT_EXCEPTIONS:
779                return False
780
781            # We found the goof: incorrect macro
782            msg.append('Apparently matching entity in category {} found.'.format(
783                data.category))
784            self.handleWrongMacro(msg, data)
785            return True
786
787        see_also = []
788        dataArray = self.checker.findEntityCaseInsensitive(entity)
789        if dataArray:
790            # We might have found the goof...
791
792            if len(dataArray) == 1:
793                # Yep, found the goof:
794                # incorrect macro and entity capitalization
795                data = dataArray[0]
796                msg.append('Apparently matching entity in category {} found by searching case-insensitively.'.format(
797                    data.category))
798                self.handleWrongMacro(msg, data)
799                return True
800            else:
801                # Ugh, more than one resolution
802                msg.append(
803                    'More than one apparent match found by searching case-insensitively, cannot auto-fix.')
804                see_also = dataArray[:]
805
806        # OK, so we don't recognize this entity (and couldn't auto-fix it).
807
808        if self.checker.entity_db.shouldBeRecognized(macro, entity):
809            # We should know the target - it's a link macro,
810            # or there's some reason the entity DB thinks we should know it.
811            if self.checker.likelyRecognizedEntity(entity):
812                # Should be linked and it matches our pattern,
813                # so probably not wrong macro.
814                # Human brains required.
815                if not self.checkText():
816                    self.error(MessageId.BAD_ENTITY, msg + ['Might be a misspelling, or, less likely, the wrong macro.'],
817                               see_also=see_also)
818            else:
819                # Doesn't match our pattern,
820                # so probably should be name instead of link.
821                newMacro = macro[0] + 'name'
822                if self.checker.entity_db.isValidMacro(newMacro):
823                    self.error(MessageId.BAD_ENTITY, msg +
824                               ['Entity name does not fit the pattern for this API, which would mean it should be a "name" macro instead of a "link" macro'],
825                               group='macro', replacement=newMacro, fix=self.makeFix(newMacro=newMacro), see_also=see_also)
826                else:
827                    self.error(MessageId.BAD_ENTITY, msg +
828                               ['Entity name does not fit the pattern for this API, which would mean it should be a "name" macro instead of a "link" macro',
829                                'However, {} is not a known macro so cannot auto-fix.'.format(newMacro)], see_also=see_also)
830
831        elif macro == 'ename':
832            # TODO This might be an ambiguity in the style guide - ename might be a known enumerant value,
833            # or it might be an enumerant value in an external library, etc. that we don't know about - so
834            # hard to check this.
835            if self.checker.likelyRecognizedEntity(entity):
836                if not self.checkText():
837                    if entity in CHECK_UNRECOGNIZED_ENAME_EXCEPTIONS:
838                        return False
839                    else:
840                        self.warning(MessageId.BAD_ENUMERANT, msg +
841                                 ['Unrecognized ename:{} that we would expect to recognize since it fits the pattern for this API.'.format(entity)], see_also=see_also)
842        else:
843            # This is fine:
844            # it doesn't need to be recognized since it's not linked.
845            pass
846        # Don't skip other tests.
847        return False
848
849    def checkText(self):
850        """Evaluate the usage (or non-usage) of a *text macro.
851
852        Wildcards (or leading or trailing underscore, or square brackets with
853        nothing or a placeholder) if and only if a 'text' macro.
854
855        Called by checkRecognizedEntity() when appropriate.
856        """
857        macro = self.macro
858        entity = self.entity
859        shouldBeText = shouldEntityBeText(entity, self.subscript)
860        if shouldBeText and not self.macro.endswith(
861                'text') and not self.macro == 'code':
862            newMacro = macro[0] + 'text'
863            if self.checker.entity_db.getCategoriesForMacro(newMacro):
864                self.error(MessageId.MISSING_TEXT,
865                           ['Asterisk/leading or trailing underscore/bracket found - macro should end with "text:", probably {}:'.format(newMacro),
866                            AUTO_FIX_STRING],
867                           group='macro', replacement=newMacro, fix=self.makeFix(newMacro=newMacro))
868            else:
869                self.error(MessageId.MISSING_TEXT,
870                           ['Asterisk/leading or trailing underscore/bracket found, so macro should end with "text:".',
871                            'However {}: is not a known macro so cannot auto-fix.'.format(newMacro)],
872                           group='macro')
873            return True
874        elif macro.endswith('text') and not shouldBeText:
875            msg = [
876                "No asterisk/leading or trailing underscore/bracket in the entity, so this might be a mistaken use of the 'text' macro {}:".format(macro)]
877            data = self.checker.findEntity(entity)
878            if data:
879                if entity in CHECK_UNRECOGNIZED_ETEXT_EXCEPTIONS:
880                    return False
881
882                # We found the goof: incorrect macro
883                msg.append('Apparently matching entity in category {} found.'.format(
884                    data.category))
885                msg.append(AUTO_FIX_STRING)
886                replacement = self.makeFix(data=data)
887                if data.category == EXTENSION_CATEGORY:
888                    self.error(MessageId.EXTENSION, msg,
889                               replacement=replacement, fix=replacement)
890                else:
891                    self.error(MessageId.WRONG_MACRO, msg,
892                               group='macro', replacement=data.macro, fix=replacement)
893            else:
894                if self.checker.likelyRecognizedEntity(entity):
895                    # This is a use of *text: for something that fits the pattern but isn't in the spec.
896                    # This is OK.
897                    return False
898                msg.append('Entity not found in spec, either.')
899                if macro[0] != 'e':
900                    # Only suggest a macro if we aren't in elink/ename/etext,
901                    # since ename and elink are not related in an equivalent way
902                    # to the relationship between flink and fname.
903                    newMacro = macro[0] + 'name'
904                    if self.checker.entity_db.getCategoriesForMacro(newMacro):
905                        msg.append(
906                            'Consider if {}: might be the correct macro to use here.'.format(newMacro))
907                    else:
908                        msg.append(
909                            'Cannot suggest a new macro because {}: is not a known macro.'.format(newMacro))
910                self.warning(MessageId.MISUSED_TEXT, msg)
911            return True
912        return False
913
914    def checkPnameImpliedContext(self, pname_context):
915        """Handle pname: macros not immediately preceded by something like flink:entity or slink:entity.
916
917        Also records pname: mentions of members/parameters for completeness checking in doc blocks.
918
919        Contains call to self.checkPname().
920        Called by self.processMatch()
921        """
922        self.checkPname(pname_context.entity)
923        if pname_context.entity in self.pname_mentions and \
924                self.pname_mentions[pname_context.entity] is not None:
925            # Record this mention,
926            # in case we're in the documentation block.
927            self.pname_mentions[pname_context.entity].add(self.entity)
928
929    def checkPname(self, pname_context):
930        """Check the current match (as a pname: usage) with the given entity as its 'pname context', if possible.
931
932        e.g. slink:foo::pname:bar, pname_context would be 'foo', while self.entity would be 'bar', etc.
933
934        Called by self.processLine(), self.processMatch(), as well as from self.checkPnameImpliedContext().
935        """
936        if '*' in pname_context:
937            # This context has a placeholder, can't verify it.
938            return
939
940        entity = self.entity
941
942        context_data = self.checker.findEntity(pname_context)
943        members = self.checker.getMemberNames(pname_context)
944
945        if context_data and not members:
946            # This is a recognized parent entity that doesn't have detectable member names,
947            # skip validation
948            # TODO: Annotate parameters of function pointer types with <name>
949            # and <param>?
950            return
951        if not members:
952            self.warning(MessageId.UNRECOGNIZED_CONTEXT,
953                         'pname context entity was un-recognized {}'.format(pname_context))
954            return
955
956        if entity not in members:
957            self.warning(MessageId.UNKNOWN_MEMBER, ["Could not find member/param named '{}' in {}".format(entity, pname_context),
958                                                    'Known {} member/param names are: {}'.format(
959                pname_context, ', '.join(members))], group='entity_name')
960
961    def checkIncludeRefPageRelation(self, entity, generated_type):
962        """Identify if our current ref page (or lack thereof) is appropriate for an include just recorded.
963
964        Called by self.recordInclude().
965        """
966        if not self.in_ref_page:
967            # Not in a ref page block: This probably means this entity needs a
968            # ref-page block added.
969            self.handleIncludeMissingRefPage(entity, generated_type)
970            return
971
972        if not isinstance(self.current_ref_page, EntityData):
973            # This isn't a fully-valid ref page, so can't check the includes any better.
974            return
975
976        ref_page_entity = self.current_ref_page.entity
977        if ref_page_entity not in self.refpage_includes:
978            self.refpage_includes[ref_page_entity] = set()
979        expected_ref_page_entity = self.computeExpectedRefPageFromInclude(
980            entity)
981        self.refpage_includes[ref_page_entity].add((generated_type, entity))
982
983        if ref_page_entity == expected_ref_page_entity:
984            # OK, this is a total match.
985            pass
986        elif self.checker.entity_db.areAliases(expected_ref_page_entity, ref_page_entity):
987            # This appears to be a promoted synonym which is OK.
988            pass
989        else:
990            # OK, we are in a ref page block that doesn't match
991            self.handleIncludeMismatchRefPage(entity, generated_type)
992
993    def perform_entity_check(self, type):
994        """Returns True if an entity check should be performed on this
995           refpage type.
996
997           May override."""
998
999        return True
1000
1001    def checkRefPage(self):
1002        """Check if the current line (a refpage tag) meets requirements.
1003
1004        Called by self.processLine().
1005        """
1006        line = self.line
1007
1008        # Should always be found
1009        self.match = BRACKETS.match(line)
1010
1011        data = None
1012        directory = None
1013        if self.in_ref_page:
1014            msg = ["Found reference page markup, but we are already in a refpage block.",
1015                   "The block before the first message of this type is most likely not closed.", ]
1016            # Fake-close the previous ref page, if it's trivial to do so.
1017            if self.getInnermostBlockEntry().block_type == BlockType.REF_PAGE_LIKE:
1018                msg.append(
1019                    "Pretending that there was a line with `--` immediately above to close that ref page, for more readable messages.")
1020                self.processBlockDelimiter(
1021                    REF_PAGE_LIKE_BLOCK_DELIM, BlockType.REF_PAGE_LIKE)
1022            else:
1023                msg.append(
1024                    "Ref page wasn't the last block opened, so not pretending to auto-close it for more readable messages.")
1025
1026            self.error(MessageId.REFPAGE_BLOCK, msg)
1027
1028        attribs = parseRefPageAttribs(line)
1029
1030        unknown_attribs = set(attribs.keys()).difference(
1031            VALID_REF_PAGE_ATTRIBS)
1032        if unknown_attribs:
1033            self.error(MessageId.REFPAGE_UNKNOWN_ATTRIB,
1034                       "Found unknown attrib(s) in reference page markup: " + ','.join(unknown_attribs))
1035
1036        # Required field: refpage='xrValidEntityHere'
1037        if Attrib.REFPAGE.value in attribs:
1038            attrib = attribs[Attrib.REFPAGE.value]
1039            text = attrib.value
1040            self.entity = text
1041
1042            context = self.storeMessageContext(
1043                group='value', match=attrib.match)
1044            if self.checker.seenRefPage(text):
1045                self.error(MessageId.REFPAGE_DUPLICATE,
1046                           ["Found reference page markup when we already saw refpage='{}' elsewhere.".format(
1047                               text),
1048                            "This (or the other mention) may be a copy-paste error."],
1049                           context=context)
1050            self.checker.addRefPage(text)
1051
1052            # Entity check can be skipped depending on the refpage type
1053            # Determine page type for use in several places
1054            type_text = ''
1055            if Attrib.TYPE.value in attribs:
1056                type_text = attribs[Attrib.TYPE.value].value
1057
1058            if self.perform_entity_check(type_text):
1059                data = self.checker.findEntity(text)
1060                if data:
1061                    # OK, this is a known entity that we're seeing a refpage for.
1062                    directory = data.directory
1063                    self.current_ref_page = data
1064                else:
1065                    # TODO suggest fixes here if applicable
1066                    self.error(MessageId.REFPAGE_NAME,
1067                               [ "Found reference page markup, but refpage='{}' type='{}' does not refer to a recognized entity".format(
1068                                   text, type_text),
1069                                 'If this is intentional, add the entity to EXTRA_DEFINES or EXTRA_REFPAGES in check_spec_links.py.' ],
1070                               context=context)
1071        else:
1072            self.error(MessageId.REFPAGE_TAG,
1073                       "Found apparent reference page markup, but missing refpage='...'",
1074                       group=None)
1075
1076        # Required field: desc='preferably non-empty'
1077        if Attrib.DESC.value in attribs:
1078            attrib = attribs[Attrib.DESC.value]
1079            text = attrib.value
1080            if not text:
1081                context = self.storeMessageContext(
1082                    group=None, match=attrib.match)
1083                self.warning(MessageId.REFPAGE_MISSING_DESC,
1084                             "Found reference page markup, but desc='' is empty",
1085                             context=context)
1086        else:
1087            self.error(MessageId.REFPAGE_TAG,
1088                       "Found apparent reference page markup, but missing desc='...'",
1089                       group=None)
1090
1091        # Required field: type='protos' for example
1092        # (used by genRef.py to compute the macro to use)
1093        if Attrib.TYPE.value in attribs:
1094            attrib = attribs[Attrib.TYPE.value]
1095            text = attrib.value
1096            if directory and not text == directory:
1097                context = self.storeMessageContext(
1098                    group='value', match=attrib.match)
1099                self.error(MessageId.REFPAGE_TYPE,
1100                           "Found reference page markup, but type='{}' is not the expected value '{}'".format(
1101                               text, directory),
1102                           context=context)
1103        else:
1104            self.error(MessageId.REFPAGE_TAG,
1105                       "Found apparent reference page markup, but missing type='...'",
1106                       group=None)
1107
1108        # Optional field: alias='spaceDelimited validEntities'
1109        # Currently does nothing. Could modify checkRefPageXrefs to also
1110        # check alias= attribute value
1111        # if Attrib.ALIAS.value in attribs:
1112        #    # This field is optional
1113        #    self.checkRefPageXrefs(attribs[Attrib.XREFS.value])
1114
1115        # Optional field: xrefs='spaceDelimited validEntities'
1116        if Attrib.XREFS.value in attribs:
1117            # This field is optional
1118            self.checkRefPageXrefs(attribs[Attrib.XREFS.value])
1119        self.prev_line_ref_page_tag = self.storeMessageContext()
1120
1121    def checkRefPageXrefs(self, xrefs_attrib):
1122        """Check all cross-refs indicated in an xrefs attribute for a ref page.
1123
1124        Called by self.checkRefPage().
1125
1126        Argument:
1127        xrefs_attrib -- A match of REF_PAGE_ATTRIB where the group 'key' is 'xrefs'.
1128        """
1129        text = xrefs_attrib.value
1130        context = self.storeMessageContext(
1131            group='value', match=xrefs_attrib.match)
1132
1133        def splitRefs(s):
1134            """Split the string on whitespace, into individual references."""
1135            return s.split()  # [x for x in s.split() if x]
1136
1137        def remakeRefs(refs):
1138            """Re-create a xrefs string from something list-shaped."""
1139            return ' '.join(refs)
1140
1141        refs = splitRefs(text)
1142
1143        # Pre-checking if messages are enabled, so that we can correctly determine
1144        # the current string following any auto-fixes:
1145        # the fixes for messages directly in this method would interact,
1146        # and thus must be in the order specified here.
1147
1148        if self.messageEnabled(MessageId.REFPAGE_XREFS_COMMA) and ',' in text:
1149            old_text = text
1150            # Re-split after replacing commas.
1151            refs = splitRefs(text.replace(',', ' '))
1152            # Re-create the space-delimited text.
1153            text = remakeRefs(refs)
1154            self.error(MessageId.REFPAGE_XREFS_COMMA,
1155                       "Found reference page markup, with an unexpected comma in the (space-delimited) xrefs attribute",
1156                       context=context,
1157                       replacement=text,
1158                       fix=(old_text, text))
1159
1160        # We could conditionally perform this creation, but the code complexity would increase substantially,
1161        # for presumably minimal runtime improvement.
1162        unique_refs = OrderedDict.fromkeys(refs)
1163        if self.messageEnabled(MessageId.REFPAGE_XREF_DUPE) and len(unique_refs) != len(refs):
1164            # TODO is it safe to auto-fix here?
1165            old_text = text
1166            text = remakeRefs(unique_refs.keys())
1167            self.warning(MessageId.REFPAGE_XREF_DUPE,
1168                         ["Reference page for {} contains at least one duplicate in its cross-references.".format(
1169                             self.entity),
1170                             "Look carefully to see if this is a copy and paste error and should be changed to a different but related entity:",
1171                             "auto-fix simply removes the duplicate."],
1172                         context=context,
1173                         replacement=text,
1174                         fix=(old_text, text))
1175
1176        if self.messageEnabled(MessageId.REFPAGE_SELF_XREF) and self.entity and self.entity in unique_refs:
1177            # Not modifying unique_refs here because that would accidentally affect the whitespace auto-fix.
1178            new_text = remakeRefs(
1179                [x for x in unique_refs.keys() if x != self.entity])
1180
1181            # DON'T AUTOFIX HERE because these are likely copy-paste between related entities:
1182            # e.g. a Create function and the associated CreateInfo struct.
1183            self.warning(MessageId.REFPAGE_SELF_XREF,
1184                         ["Reference page for {} included itself in its cross-references.".format(self.entity),
1185                          "This is typically a copy and paste error, and the dupe should likely be changed to a different but related entity.",
1186                          "Not auto-fixing for this reason."],
1187                         context=context,
1188                         replacement=new_text,)
1189
1190        # We didn't have another reason to replace the whole attribute value,
1191        # so let's make sure it doesn't have any extra spaces
1192        if self.messageEnabled(MessageId.REFPAGE_WHITESPACE) and xrefs_attrib.value == text:
1193            old_text = text
1194            text = remakeRefs(unique_refs.keys())
1195            if old_text != text:
1196                self.warning(MessageId.REFPAGE_WHITESPACE,
1197                             ["Cross-references for reference page for {} had non-minimal whitespace,".format(self.entity),
1198                              "and no other enabled message has re-constructed this value already."],
1199                             context=context,
1200                             replacement=text,
1201                             fix=(old_text, text))
1202
1203        for entity in unique_refs.keys():
1204            self.checkRefPageXref(entity, context)
1205
1206    @property
1207    def allowEnumXrefs(self):
1208        """Returns True if enums can be specified in the 'xrefs' attribute
1209        of a refpage.
1210
1211        May override.
1212        """
1213        return False
1214
1215    def checkRefPageXref(self, referenced_entity, line_context):
1216        """Check a single cross-reference entry for a refpage.
1217
1218        Called by self.checkRefPageXrefs().
1219
1220        Arguments:
1221        referenced_entity -- The individual entity under consideration from the xrefs='...' string.
1222        line_context -- A MessageContext referring to the entire line.
1223        """
1224        data = self.checker.findEntity(referenced_entity)
1225        context = line_context
1226        match = re.search(r'\b{}\b'.format(referenced_entity), self.line)
1227        if match:
1228            context = self.storeMessageContext(
1229                group=None, match=match)
1230
1231        if data and data.category == "enumvalues" and not self.allowEnumXrefs:
1232            msg = ["Found reference page markup, with an enum value listed: {}".format(
1233                referenced_entity)]
1234            self.error(MessageId.REFPAGE_XREFS,
1235                    msg,
1236                    context=context)
1237            return
1238
1239        if data:
1240            # This is OK: we found it, and it's not an enum value
1241            return
1242
1243        msg = ["Found reference page markup, with an unrecognized entity listed: {}".format(
1244            referenced_entity)]
1245
1246        see_also = None
1247        dataArray = self.checker.findEntityCaseInsensitive(
1248            referenced_entity)
1249
1250        if dataArray:
1251            # We might have found the goof...
1252
1253            if len(dataArray) == 1:
1254                # Yep, found the goof - incorrect entity capitalization
1255                data = dataArray[0]
1256                new_entity = data.entity
1257                self.error(MessageId.REFPAGE_XREFS, msg + [
1258                    'Apparently matching entity in category {} found by searching case-insensitively.'.format(
1259                        data.category),
1260                    AUTO_FIX_STRING],
1261                    replacement=new_entity,
1262                    fix=(referenced_entity, new_entity),
1263                    context=context)
1264                return
1265
1266            # Ugh, more than one resolution
1267            msg.append(
1268                'More than one apparent match found by searching case-insensitively, cannot auto-fix.')
1269            see_also = dataArray[:]
1270        else:
1271            # Probably not just a typo
1272            msg.append(
1273                'If this is intentional, add the entity to EXTRA_DEFINES or EXTRA_REFPAGES in check_spec_links.py.')
1274
1275        # Multiple or no resolutions found
1276        self.error(MessageId.REFPAGE_XREFS,
1277                   msg,
1278                   see_also=see_also,
1279                   context=context)
1280
1281    ###
1282    # Message-related methods.
1283    ###
1284
1285    def warning(self, message_id, messageLines, context=None, group=None,
1286                replacement=None, fix=None, see_also=None, frame=None):
1287        """Log a warning for the file, if the message ID is enabled.
1288
1289        Wrapper around self.diag() that automatically sets severity as well as frame.
1290
1291        Arguments:
1292        message_id -- A MessageId value.
1293        messageLines -- A string or list of strings containing a human-readable error description.
1294
1295        Optional, named arguments:
1296        context -- A MessageContext. If None, will be constructed from self.match and group.
1297        group -- The name of the regex group in self.match that contains the problem. Only used if context is None.
1298          If needed and is None, self.group is used instead.
1299        replacement -- The string, if any, that should be suggested as a replacement for the group in question.
1300          Does not create an auto-fix: sometimes we want to show a possible fix but aren't confident enough
1301          (or can't easily phrase a regex) to do it automatically.
1302        fix -- A (old text, new text) pair if this error is auto-fixable safely.
1303        see_also -- An optional array of other MessageContext locations relevant to this message.
1304        frame -- The 'inspect' stack frame corresponding to the location that raised this message.
1305          If None, will assume it is the direct caller of self.warning().
1306        """
1307        if not frame:
1308            frame = currentframe().f_back
1309        self.diag(MessageType.WARNING, message_id, messageLines, group=group,
1310                  replacement=replacement, context=context, fix=fix, see_also=see_also, frame=frame)
1311
1312    def error(self, message_id, messageLines, group=None, replacement=None,
1313              context=None, fix=None, see_also=None, frame=None):
1314        """Log an error for the file, if the message ID is enabled.
1315
1316        Wrapper around self.diag() that automatically sets severity as well as frame.
1317
1318        Arguments:
1319        message_id -- A MessageId value.
1320        messageLines -- A string or list of strings containing a human-readable error description.
1321
1322        Optional, named arguments:
1323        context -- A MessageContext. If None, will be constructed from self.match and group.
1324        group -- The name of the regex group in self.match that contains the problem. Only used if context is None.
1325          If needed and is None, self.group is used instead.
1326        replacement -- The string, if any, that should be suggested as a replacement for the group in question.
1327          Does not create an auto-fix: sometimes we want to show a possible fix but aren't confident enough
1328          (or can't easily phrase a regex) to do it automatically.
1329        fix -- A (old text, new text) pair if this error is auto-fixable safely.
1330        see_also -- An optional array of other MessageContext locations relevant to this message.
1331        frame -- The 'inspect' stack frame corresponding to the location that raised this message.
1332          If None, will assume it is the direct caller of self.error().
1333        """
1334        if not frame:
1335            frame = currentframe().f_back
1336        self.diag(MessageType.ERROR, message_id, messageLines, group=group,
1337                  replacement=replacement, context=context, fix=fix, see_also=see_also, frame=frame)
1338
1339    def diag(self, severity, message_id, messageLines, context=None, group=None,
1340             replacement=None, fix=None, see_also=None, frame=None):
1341        """Log a diagnostic for the file, if the message ID is enabled.
1342
1343        Also records the auto-fix, if applicable.
1344
1345        Arguments:
1346        severity -- A MessageType value.
1347        message_id -- A MessageId value.
1348        messageLines -- A string or list of strings containing a human-readable error description.
1349
1350        Optional, named arguments:
1351        context -- A MessageContext. If None, will be constructed from self.match and group.
1352        group -- The name of the regex group in self.match that contains the problem. Only used if context is None.
1353          If needed and is None, self.group is used instead.
1354        replacement -- The string, if any, that should be suggested as a replacement for the group in question.
1355          Does not create an auto-fix: sometimes we want to show a possible fix but aren't confident enough
1356          (or can't easily phrase a regex) to do it automatically.
1357        fix -- A (old text, new text) pair if this error is auto-fixable safely.
1358        see_also -- An optional array of other MessageContext locations relevant to this message.
1359        frame -- The 'inspect' stack frame corresponding to the location that raised this message.
1360          If None, will assume it is the direct caller of self.diag().
1361        """
1362        if not self.messageEnabled(message_id):
1363            self.logger.debug(
1364                'Discarding a %s message because it is disabled.', message_id)
1365            return
1366
1367        if isinstance(messageLines, str):
1368            messageLines = [messageLines]
1369
1370        self.logger.info('Recording a %s message: %s',
1371                         message_id, ' '.join(messageLines))
1372
1373        # Ensure all auto-fixes are marked as such.
1374        if fix is not None and AUTO_FIX_STRING not in messageLines:
1375            messageLines.append(AUTO_FIX_STRING)
1376
1377        if not frame:
1378            frame = currentframe().f_back
1379        if context is None:
1380            message = Message(message_id=message_id,
1381                              message_type=severity,
1382                              message=messageLines,
1383                              context=self.storeMessageContext(group=group),
1384                              replacement=replacement,
1385                              see_also=see_also,
1386                              fix=fix,
1387                              frame=frame)
1388        else:
1389            message = Message(message_id=message_id,
1390                              message_type=severity,
1391                              message=messageLines,
1392                              context=context,
1393                              replacement=replacement,
1394                              see_also=see_also,
1395                              fix=fix,
1396                              frame=frame)
1397        if fix is not None:
1398            self.fixes.add(fix)
1399        self.messages.append(message)
1400
1401    def messageEnabled(self, message_id):
1402        """Return true if the given message ID is enabled."""
1403        return message_id in self.enabled_messages
1404
1405    ###
1406    # Accessors for externally-interesting information
1407
1408    def numDiagnostics(self):
1409        """Count the total number of diagnostics (errors or warnings) for this file."""
1410        return len(self.messages)
1411
1412    def numErrors(self):
1413        """Count the total number of errors for this file."""
1414        return self.numMessagesOfType(MessageType.ERROR)
1415
1416    def numMessagesOfType(self, message_type):
1417        """Count the number of messages of a particular type (severity)."""
1418        return len(
1419            [msg for msg in self.messages if msg.message_type == message_type])
1420
1421    def hasFixes(self):
1422        """Return True if any messages included auto-fix patterns."""
1423        return len(self.fixes) > 0
1424
1425    ###
1426    # Assorted internal methods.
1427    def printMessageCounts(self):
1428        """Print a simple count of each MessageType of diagnostics."""
1429        for message_type in [MessageType.ERROR, MessageType.WARNING]:
1430            count = self.numMessagesOfType(message_type)
1431            if count > 0:
1432                print('{num} {mtype}{s} generated.'.format(
1433                    num=count, mtype=message_type, s=_s_suffix(count)))
1434
1435    def dumpInternals(self):
1436        """Dump internal variables to screen, for debugging."""
1437        print('self.lineNum: ', self.lineNum)
1438        print('self.line:', self.line)
1439        print('self.prev_line_ref_page_tag: ', self.prev_line_ref_page_tag)
1440        print('self.current_ref_page:', self.current_ref_page)
1441
1442    def getMissingValiditySuppressions(self):
1443        """Return an enumerable of entity names that we shouldn't warn about missing validity.
1444
1445        May override.
1446        """
1447        return []
1448
1449    def recordInclude(self, include_dict, generated_type=None):
1450        """Store the current line as being the location of an include directive or equivalent.
1451
1452        Reports duplicate include errors, as well as include/ref-page mismatch or missing ref-page,
1453        by calling self.checkIncludeRefPageRelation() for "actual" includes (where generated_type is None).
1454
1455        Arguments:
1456        include_dict -- The include dictionary to update: one of self.apiIncludes or self.validityIncludes.
1457        generated_type -- The type of include (e.g. 'api', 'valid', etc). By default, extracted from self.match.
1458        """
1459        entity = self.match.group('entity_name')
1460        if generated_type is None:
1461            generated_type = self.match.group('generated_type')
1462
1463            # Only checking the ref page relation if it's retrieved from regex.
1464            # Otherwise it might be a manual anchor recorded as an include,
1465            # etc.
1466            self.checkIncludeRefPageRelation(entity, generated_type)
1467
1468        if entity in include_dict:
1469            self.error(MessageId.DUPLICATE_INCLUDE,
1470                       "Included {} docs for {} when they were already included.".format(generated_type,
1471                                                                                         entity), see_also=include_dict[entity])
1472            include_dict[entity].append(self.storeMessageContext())
1473        else:
1474            include_dict[entity] = [self.storeMessageContext()]
1475
1476    def getInnermostBlockEntry(self):
1477        """Get the BlockEntry for the top block delim on our stack."""
1478        if not self.block_stack:
1479            return None
1480        return self.block_stack[-1]
1481
1482    def getInnermostBlockDelimiter(self):
1483        """Get the delimiter for the top block on our stack."""
1484        top = self.getInnermostBlockEntry()
1485        if not top:
1486            return None
1487        return top.delimiter
1488
1489    def pushBlock(self, block_type, refpage=None, context=None, delimiter=None):
1490        """Push a new entry on the block stack."""
1491        if not delimiter:
1492            self.logger.info("pushBlock: not given delimiter")
1493            delimiter = self.line
1494        if not context:
1495            context = self.storeMessageContext()
1496
1497        old_top_delim = self.getInnermostBlockDelimiter()
1498
1499        self.block_stack.append(BlockEntry(
1500            delimiter=delimiter,
1501            context=context,
1502            refpage=refpage,
1503            block_type=block_type))
1504
1505        location = self.getBriefLocation(context)
1506        self.logger.info(
1507            "pushBlock: %s: Pushed %s delimiter %s, previous top was %s, now %d elements on the stack",
1508            location, block_type.value, delimiter, old_top_delim, len(self.block_stack))
1509
1510        self.dumpBlockStack()
1511
1512    def popBlock(self):
1513        """Pop and return the top entry from the block stack."""
1514        old_top = self.block_stack.pop()
1515        location = self.getBriefLocation(old_top.context)
1516        self.logger.info(
1517            "popBlock: %s: popping %s delimiter %s, now %d elements on the stack",
1518            location, old_top.block_type.value, old_top.delimiter, len(self.block_stack))
1519
1520        self.dumpBlockStack()
1521
1522        return old_top
1523
1524    def dumpBlockStack(self):
1525        self.logger.debug('Block stack, top first:')
1526        for distFromTop, x in enumerate(reversed(self.block_stack)):
1527            self.logger.debug(' - block_stack[%d]: Line %d: "%s" refpage=%s',
1528                              -1 - distFromTop,
1529                              x.context.lineNum, x.delimiter, x.refpage)
1530
1531    def getBriefLocation(self, context):
1532        """Format a context briefly - omitting the filename if it has newlines in it."""
1533        if '\n' in context.filename:
1534            return 'input string line {}'.format(context.lineNum)
1535        return '{}:{}'.format(
1536            context.filename, context.lineNum)
1537
1538    ###
1539    # Handlers for a variety of diagnostic-meriting conditions
1540    #
1541    # Split out for clarity and for allowing fine-grained override on a per-project basis.
1542    ###
1543
1544    def handleIncludeMissingRefPage(self, entity, generated_type):
1545        """Report a message about an include outside of a ref-page block."""
1546        msg = ["Found {} include for {} outside of a reference page block.".format(generated_type, entity),
1547               "This is probably a missing reference page block."]
1548        refpage = self.computeExpectedRefPageFromInclude(entity)
1549        data = self.checker.findEntity(refpage)
1550        if data:
1551            msg.append('Expected ref page block might start like:')
1552            msg.append(self.makeRefPageTag(refpage, data=data))
1553        else:
1554            msg.append(
1555                "But, expected ref page entity name {} isn't recognized...".format(refpage))
1556        self.warning(MessageId.REFPAGE_MISSING, msg)
1557
1558    def handleIncludeMismatchRefPage(self, entity, generated_type):
1559        """Report a message about an include not matching its containing ref-page block."""
1560        self.warning(MessageId.REFPAGE_MISMATCH, "Found {} include for {}, inside the reference page block of {}".format(
1561            generated_type, entity, self.current_ref_page.entity))
1562
1563    def handleWrongMacro(self, msg, data):
1564        """Report an appropriate message when we found that the macro used is incorrect.
1565
1566        May be overridden depending on each API's behavior regarding macro misuse:
1567        e.g. in some cases, it may be considered a MessageId.LEGACY warning rather than
1568        a MessageId.WRONG_MACRO or MessageId.EXTENSION.
1569        """
1570        message_type = MessageType.WARNING
1571        message_id = MessageId.WRONG_MACRO
1572        group = 'macro'
1573
1574        if data.category == EXTENSION_CATEGORY:
1575            # Ah, this is an extension
1576            msg.append(
1577                'This is apparently an extension name, which should be marked up as a link.')
1578            message_id = MessageId.EXTENSION
1579            group = None  # replace the whole thing
1580        else:
1581            # Non-extension, we found the macro though.
1582            message_type = MessageType.ERROR
1583        msg.append(AUTO_FIX_STRING)
1584        self.diag(message_type, message_id, msg,
1585                  group=group, replacement=self.makeMacroMarkup(data=data), fix=self.makeFix(data=data))
1586
1587    def handleExpectedRefpageBlock(self):
1588        """Handle expecting to see -- to start a refpage block, but not seeing that at all."""
1589        self.error(MessageId.REFPAGE_BLOCK,
1590                   ["Expected, but did not find, a line containing only -- following a reference page tag,",
1591                    "Pretending to insert one, for more readable messages."],
1592                   see_also=[self.prev_line_ref_page_tag])
1593        # Fake "in ref page" regardless, to avoid spurious extra errors.
1594        self.processBlockDelimiter('--', BlockType.REF_PAGE_LIKE,
1595                                   context=self.prev_line_ref_page_tag)
1596
1597    ###
1598    # Construct related values (typically named tuples) based on object state and supplied arguments.
1599    #
1600    # Results are typically supplied to another method call.
1601    ###
1602
1603    def storeMessageContext(self, group=None, match=None):
1604        """Create message context from corresponding instance variables.
1605
1606        Arguments:
1607        group -- The regex group name, if any, identifying the part of the match to highlight.
1608        match -- The regex match. If None, will use self.match.
1609        """
1610        if match is None:
1611            match = self.match
1612        return MessageContext(filename=self.filename,
1613                              lineNum=self.lineNum,
1614                              line=self.line,
1615                              match=match,
1616                              group=group)
1617
1618    def makeFix(self, newMacro=None, newEntity=None, data=None):
1619        """Construct a fix pair for replacing the old macro:entity with new.
1620
1621        Wrapper around self.makeSearch() and self.makeMacroMarkup().
1622        """
1623        return (self.makeSearch(), self.makeMacroMarkup(
1624            newMacro, newEntity, data))
1625
1626    def makeSearch(self):
1627        """Construct the string self.macro:self.entity, for use in the old text part of a fix pair."""
1628        return '{}:{}'.format(self.macro, self.entity)
1629
1630    def makeMacroMarkup(self, newMacro=None, newEntity=None, data=None):
1631        """Construct appropriate markup for referring to an entity.
1632
1633        Typically constructs macro:entity, but can construct `<<EXTENSION_NAME>>` if the supplied
1634        entity is identified as an extension.
1635
1636        Arguments:
1637        newMacro -- The macro to use. Defaults to data.macro (if available), otherwise self.macro.
1638        newEntity -- The entity to use. Defaults to data.entity (if available), otherwise self.entity.
1639        data -- An EntityData value corresponding to this entity. If not provided, will be looked up by newEntity.
1640        """
1641        if not newEntity:
1642            if data:
1643                newEntity = data.entity
1644            else:
1645                newEntity = self.entity
1646        if not newMacro:
1647            if data:
1648                newMacro = data.macro
1649            else:
1650                newMacro = self.macro
1651        if not data:
1652            data = self.checker.findEntity(newEntity)
1653        if data and data.category == EXTENSION_CATEGORY:
1654            return self.makeExtensionLink(newEntity)
1655        return '{}:{}'.format(newMacro, newEntity)
1656
1657    def makeExtensionLink(self, newEntity=None):
1658        """Create a correctly-formatted link to an extension.
1659
1660        Result takes the form `<<EXTENSION_NAME>>`.
1661
1662        Argument:
1663        newEntity -- The extension name to link to. Defaults to self.entity.
1664        """
1665        if not newEntity:
1666            newEntity = self.entity
1667        return '`<<{}>>`'.format(newEntity)
1668
1669    def computeExpectedRefPageFromInclude(self, entity):
1670        """Compute the expected ref page entity based on an include entity name."""
1671        # No-op in general.
1672        return entity
1673
1674    def makeRefPageTag(self, entity, data=None,
1675                       ref_type=None, desc='', xrefs=None):
1676        """Construct a ref page tag string from attribute values."""
1677        if ref_type is None and data is not None:
1678            ref_type = data.directory
1679        if ref_type is None:
1680            ref_type = "????"
1681        return "[open,refpage='{}',type='{}',desc='{}',xrefs='{}']".format(
1682            entity, ref_type, desc, ' '.join(xrefs or []))
1683