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