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