Completed
Push — develop ( d34ef5...116111 )
by Jace
24s queued 10s
created

item_validator.ItemValidator.get_issues()   F

Complexity

Conditions 15

Size

Total Lines 70
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 70
rs 2.9998
c 0
b 0
f 0
cc 15
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like item_validator.ItemValidator.get_issues() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Class ItemValidator for validation of Item objects."""
4
5
from doorstop import common, settings
6
from doorstop.common import DoorstopError, DoorstopInfo, DoorstopWarning
7
from doorstop.core.types import UID, Stamp
8
9
log = common.logger(__name__)
10
11
12
class ItemValidator:
13
    """Class for validation of Item objects."""
14
15 View Code Duplication
    def validate(self, item, skip=None, document_hook=None, item_hook=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
16
        """Check the object for validity.
17
18
        :param item: item to validate
19
        :param skip: list of document prefixes to skip
20
        :param document_hook: function to call for custom document
21
            validation
22
        :param item_hook: function to call for custom item validation
23
24
        :return: indication that the object is valid
25
26
        """
27
        valid = True
28
        # Display all issues
29
        for issue in self.get_issues(
30
            item, skip=skip, document_hook=document_hook, item_hook=item_hook
31
        ):
32
            if isinstance(issue, DoorstopInfo) and not settings.WARN_ALL:
33
                log.info(issue)
34
            elif isinstance(issue, DoorstopWarning) and not settings.ERROR_ALL:
35
                log.warning(issue)
36
            else:
37
                assert isinstance(issue, DoorstopError)
38
                log.error(issue)
39
                valid = False
40
        # Return the result
41
        return valid
42
43
    def get_issues(
44
        self, item, skip=None, document_hook=None, item_hook=None
45
    ):  # pylint: disable=unused-argument
46
        """Yield all the item's issues.
47
48
        :param skip: list of document prefixes to skip
49
50
        :return: generator of :class:`~doorstop.common.DoorstopError`,
51
                              :class:`~doorstop.common.DoorstopWarning`,
52
                              :class:`~doorstop.common.DoorstopInfo`
53
54
        """
55
        assert document_hook is None
56
        assert item_hook is None
57
        skip = [] if skip is None else skip
58
59
        log.info("checking item %s...", item)
60
61
        # Verify the file can be parsed
62
        item.load()
63
64
        # Skip inactive items
65
        if not item.active:
66
            log.info("skipped inactive item: %s", item)
67
            return
68
69
        # Delay item save if reformatting
70
        if settings.REFORMAT:
71
            item.auto = False
72
73
        # Check text
74
        if not item.text:
75
            yield DoorstopWarning("no text")
76
77
        # Check external references
78
        if settings.CHECK_REF:
79
            try:
80
                item.find_ref()
81
            except DoorstopError as exc:
82
                yield exc
83
84
        # Check links
85
        if not item.normative and item.links:
86
            yield DoorstopWarning("non-normative, but has links")
87
88
        # Check links against the document
89
        yield from self._get_issues_document(item, item.document, skip)
90
91
        if item.tree:
92
            # Check links against the tree
93
            yield from self._get_issues_tree(item, item.tree)
94
95
            # Check links against both document and tree
96
            yield from self._get_issues_both(item, item.document, item.tree, skip)
97
98
        # Check review status
99
        if not item.reviewed:
100
            if settings.CHECK_REVIEW_STATUS:
101
                if item.is_reviewed():
102
                    if settings.REVIEW_NEW_ITEMS:
103
                        item.review()
104
                    else:
105
                        yield DoorstopInfo("needs initial review")
106
                else:
107
                    yield DoorstopWarning("unreviewed changes")
108
109
        # Reformat the file
110
        if settings.REFORMAT:
111
            log.debug("reformatting item %s...", item)
112
            item.save()
113
114
    @staticmethod
115
    def _get_issues_document(item, document, skip):
116
        """Yield all the item's issues against its document."""
117
        log.debug("getting issues against document...")
118
119
        if document in skip:
120
            log.debug("skipping issues against document %s...", document)
121
            return
122
123
        # Verify an item's UID matches its document's prefix
124
        if item.prefix != document.prefix:
125
            msg = "prefix differs from document ({})".format(document.prefix)
126
            yield DoorstopInfo(msg)
127
128
        # Verify that normative, non-derived items in a child document have at
129
        # least one link.  It is recommended that these items have an upward
130
        # link to an item in the parent document, however, this is not
131
        # enforced.  An info message is generated if this is not the case.
132
        if all((document.parent, item.normative, not item.derived)) and not item.links:
133
            msg = "no links to parent document: {}".format(document.parent)
134
            yield DoorstopWarning(msg)
135
136
        # Verify an item's links are to the correct parent
137
        for uid in item.links:
138
            try:
139
                prefix = uid.prefix
140
            except DoorstopError:
141
                msg = "invalid UID in links: {}".format(uid)
142
                yield DoorstopError(msg)
143
            else:
144
                if document.parent and prefix != document.parent:
145
                    # this is only 'info' because a document is allowed
146
                    # to contain items with a different prefix, but
147
                    # Doorstop will not create items like this
148
                    msg = "parent is '{}', but linked to: {}".format(
149
                        document.parent, uid
150
                    )
151
                    yield DoorstopInfo(msg)
152
153
    def _get_issues_tree(self, item, tree):
154
        """Yield all the item's issues against its tree."""
155
        log.debug("getting issues against tree...")
156
157
        # Verify an item's links are valid
158
        identifiers = set()
159
        for uid in item.links:
160
            try:
161
                item = tree.find_item(uid)
162
            except DoorstopError:
163
                identifiers.add(uid)  # keep the invalid UID
164
                msg = "linked to unknown item: {}".format(uid)
165
                yield DoorstopError(msg)
166
            else:
167
                # check the linked item
168
                if not item.active:
169
                    msg = "linked to inactive item: {}".format(item)
170
                    yield DoorstopInfo(msg)
171
                if not item.normative:
172
                    msg = "linked to non-normative item: {}".format(item)
173
                    yield DoorstopWarning(msg)
174
                # check the link status
175
                if uid.stamp == Stamp(True):
176
                    uid.stamp = item.stamp()
177
                elif not str(uid.stamp) and settings.STAMP_NEW_LINKS:
178
                    uid.stamp = item.stamp()
179
                elif uid.stamp != item.stamp():
180
                    if settings.CHECK_SUSPECT_LINKS:
181
                        msg = "suspect link: {}".format(item)
182
                        yield DoorstopWarning(msg)
183
                # reformat the item's UID
184
                identifier2 = UID(item.uid, stamp=uid.stamp)
185
                identifiers.add(identifier2)
186
187
        # Apply the reformatted item UIDs
188
        if settings.REFORMAT:
189
            item.set_links(identifiers)
190
191
    def _get_issues_both(self, item, document, tree, skip):
192
        """Yield all the item's issues against its document and tree."""
193
        log.debug("getting issues against document and tree...")
194
195
        if document.prefix in skip:
196
            log.debug("skipping issues against document %s...", document)
197
            return
198
199
        # Verify an item is being linked to (child links)
200
        if settings.CHECK_CHILD_LINKS and item.normative:
201
            find_all = settings.CHECK_CHILD_LINKS_STRICT or False
202
            items, documents = item.find_child_items_and_documents(
203
                document=document, tree=tree, find_all=find_all
204
            )
205
206
            if not items:
207
                for child_document in documents:
208
                    if document.prefix in skip:
209
                        msg = "skipping issues against document %s..."
210
                        log.debug(msg, child_document)
211
                        continue
212
                    msg = "no links from child document: {}".format(child_document)
213
                    yield DoorstopWarning(msg)
214
            elif settings.CHECK_CHILD_LINKS_STRICT:
215
                prefix = [item.prefix for item in items]
216
                for child in document.children:
217
                    if child in skip:
218
                        continue
219
                    if child not in prefix:
220
                        msg = 'no links from document: {}'.format(child)
221
                        yield DoorstopWarning(msg)
222