• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Provides the MacroChecker class."""
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
9from io import StringIO
10import re
11
12
13class MacroChecker(object):
14    """Perform and track checking of one or more files in an API spec.
15
16    This does not necessarily need to be subclassed per-API: it is sufficiently
17    parameterized in the constructor for expected usage.
18    """
19
20    def __init__(self, enabled_messages, entity_db,
21                 macro_checker_file_type, root_path):
22        """Construct an object that tracks checking one or more files in an API spec.
23
24        enabled_messages -- a set of MessageId that should be enabled.
25        entity_db -- an object of a EntityDatabase subclass for this API.
26        macro_checker_file_type -- Type to instantiate to create the right
27                                   MacroCheckerFile subclass for this API.
28        root_path -- A Path object for the root of this repository.
29        """
30        self.enabled_messages = enabled_messages
31        self.entity_db = entity_db
32        self.macro_checker_file_type = macro_checker_file_type
33        self.root_path = root_path
34
35        self.files = []
36
37        self.refpages = set()
38
39        # keys: entity names. values: MessageContext
40        self.links = {}
41        self.apiIncludes = {}
42        self.validityIncludes = {}
43        self.headings = {}
44
45        # Regexes that are members because they depend on the name prefix.
46
47        # apiPrefix, followed by some word characters or * as many times as desired,
48        # NOT followed by >> and NOT preceded by one of the characters in that first character class.
49        # (which distinguish "names being used somewhere other than prose").
50        self.suspected_missing_macro_re = re.compile(
51            r'\b(?<![-=:/[\.`+,])(?P<entity_name>{}[\w*]+)\b(?!>>)'.format(
52                self.entity_db.case_insensitive_name_prefix_pattern)
53        )
54        self.heading_command_re = re.compile(
55            r'=+ (?P<command>{}[\w]+)'.format(self.entity_db.name_prefix)
56        )
57
58        macros_pattern = '|'.join((re.escape(macro)
59                                   for macro in self.entity_db.macros))
60        # the "formatting" group is to strip matching */**/_/__
61        # surrounding an entire macro.
62        self.macro_re = re.compile(
63            r'(?P<formatting>\**|_*)(?P<macro>{}):(?P<entity_name>[\w*]+((?P<subscript>[\[][^\]]*[\]]))?)(?P=formatting)'.format(macros_pattern))
64
65    def haveLinkTarget(self, entity):
66        """Report if we have parsed an API include (or heading) for an entity.
67
68        None if there is no entity with that name.
69        """
70        if not self.findEntity(entity):
71            return None
72        if entity in self.apiIncludes:
73            return True
74        return entity in self.headings
75
76    def hasFixes(self):
77        """Report if any files have auto-fixes."""
78        for f in self.files:
79            if f.hasFixes():
80                return True
81        return False
82
83    def addLinkToEntity(self, entity, context):
84        """Record seeing a link to an entity's docs from a context."""
85        if entity not in self.links:
86            self.links[entity] = []
87        self.links[entity].append(context)
88
89    def seenRefPage(self, entity):
90        """Check if a ref-page markup block has been seen for an entity."""
91        return entity in self.refpages
92
93    def addRefPage(self, entity):
94        """Record seeing a ref-page markup block for an entity."""
95        self.refpages.add(entity)
96
97    def findMacroAndEntity(self, macro, entity):
98        """Look up EntityData by macro and entity pair.
99
100        Forwards to the EntityDatabase.
101        """
102        return self.entity_db.findMacroAndEntity(macro, entity)
103
104    def findEntity(self, entity):
105        """Look up EntityData by entity name (case-sensitive).
106
107        Forwards to the EntityDatabase.
108        """
109        return self.entity_db.findEntity(entity)
110
111    def findEntityCaseInsensitive(self, entity):
112        """Look up EntityData by entity name (case-insensitive).
113
114        Forwards to the EntityDatabase.
115        """
116        return self.entity_db.findEntityCaseInsensitive(entity)
117
118    def getMemberNames(self, commandOrStruct):
119        """Given a command or struct name, retrieve the names of each member/param.
120
121        Returns an empty list if the entity is not found or doesn't have members/params.
122
123        Forwards to the EntityDatabase.
124        """
125        return self.entity_db.getMemberNames(commandOrStruct)
126
127    def likelyRecognizedEntity(self, entity_name):
128        """Guess (based on name prefix alone) if an entity is likely to be recognized.
129
130        Forwards to the EntityDatabase.
131        """
132        return self.entity_db.likelyRecognizedEntity(entity_name)
133
134    def isLinkedMacro(self, macro):
135        """Identify if a macro is considered a "linked" macro.
136
137        Forwards to the EntityDatabase.
138        """
139        return self.entity_db.isLinkedMacro(macro)
140
141    def processFile(self, filename):
142        """Parse an .adoc file belonging to the spec and check it for errors."""
143        class FileStreamMaker(object):
144            def __init__(self, filename):
145                self.filename = filename
146
147            def make_stream(self):
148                return open(self.filename, 'r', encoding='utf-8')
149
150        f = self.macro_checker_file_type(self, filename, self.enabled_messages,
151                                         FileStreamMaker(filename))
152        f.process()
153        self.files.append(f)
154
155    def processString(self, s):
156        """Process a string as if it were a spec file.
157
158        Used for testing purposes.
159        """
160        if "\n" in s.rstrip():
161            # remove leading spaces from each line to allow easier
162            # block-quoting in tests
163            s = "\n".join((line.lstrip() for line in s.split("\n")))
164            # fabricate a "filename" that will display better.
165            filename = "string{}\n****START OF STRING****\n{}\n****END OF STRING****\n".format(
166                len(self.files), s.rstrip())
167
168        else:
169            filename = "string{}: {}".format(
170                len(self.files), s.rstrip())
171
172        class StringStreamMaker(object):
173            def __init__(self, string):
174                self.string = string
175
176            def make_stream(self):
177                return StringIO(self.string)
178
179        f = self.macro_checker_file_type(self, filename, self.enabled_messages,
180                                         StringStreamMaker(s))
181        f.process()
182        self.files.append(f)
183        return f
184
185    def numDiagnostics(self):
186        """Return the total number of diagnostics (warnings and errors) over all the files processed."""
187        return sum((f.numDiagnostics() for f in self.files))
188
189    def numErrors(self):
190        """Return the total number of errors over all the files processed."""
191        return sum((f.numErrors() for f in self.files))
192
193    def getMissingUnreferencedApiIncludes(self):
194        """Return the unreferenced entity names that we expected to see an API include or link target for, but did not.
195
196        Counterpart to getBrokenLinks(): This method returns the entity names
197        that were not used in a linking macro (and thus wouldn't create a broken link),
198        but were nevertheless expected and not seen.
199        """
200        return (entity for entity in self.entity_db.generating_entities
201                if (not self.haveLinkTarget(entity)) and entity not in self.links)
202
203    def getBrokenLinks(self):
204        """Return the entity names and usage contexts that we expected to see an API include or link target for, but did not.
205
206        Counterpart to getMissingUnreferencedApiIncludes(): This method returns only the
207        entity names that were used in a linking macro (and thus create a broken link),
208        but were not seen. The values of the dictionary are a list of MessageContext objects
209        for each linking macro usage for this entity name.
210        """
211        return {entity: contexts for entity, contexts in self.links.items()
212                if self.entity_db.entityGenerates(entity) and not self.haveLinkTarget(entity)}
213
214    def getMissingRefPages(self):
215        """Return a list of entities that we expected, but did not see, a ref page block for.
216
217        The heuristics here are rather crude: we expect a ref page for every generating entry.
218        """
219        return (entity for entity in sorted(self.entity_db.generating_entities)
220                if entity not in self.refpages)
221