item_validator.ItemValidator.get_issues()   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 71
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

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

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 refs and references
78
        if settings.CHECK_REF:
79
            try:
80
                item.find_ref()
81
                item.find_references()
82
            except DoorstopError as exc:
83
                yield exc
84
85
        # Check links
86
        if not item.normative and item.links:
87
            yield DoorstopWarning("non-normative, but has links")
88
89
        # Check links against the document
90
        yield from self._get_issues_document(item, item.document, skip)
91
92
        if item.tree:
93
            # Check links against the tree
94
            yield from self._get_issues_tree(item, item.tree)
95
96
            # Check links against both document and tree
97
            yield from self._get_issues_both(item, item.document, item.tree, skip)
98
99
        # Check review status
100
        if not item.reviewed:
101
            if settings.CHECK_REVIEW_STATUS:
102
                if not item.is_reviewed():
103
                    if settings.REVIEW_NEW_ITEMS:
104
                        item.review()
105
                    else:
106
                        yield DoorstopInfo("needs initial review")
107
                else:
108
                    yield DoorstopWarning("unreviewed changes")
109
110
        # Reformat the file
111
        if settings.REFORMAT:
112
            log.debug("reformatting item %s...", item)
113
            item.save()
114
115
    @staticmethod
116
    def _get_issues_document(item, document, skip):
117
        """Yield all the item's issues against its document."""
118
        log.debug("getting issues against document...")
119
120
        if document in skip:
121
            log.debug("skipping issues against document %s...", document)
122
            return
123
124
        # Verify an item's UID matches its document's prefix
125
        if item.uid.prefix != document.prefix:
126
            msg = "prefix differs from document ({})".format(document.prefix)
127
            yield DoorstopInfo(msg)
128
129
        # Verify that normative, non-derived items in a child document have at
130
        # least one link.  It is recommended that these items have an upward
131
        # link to an item in the parent document, however, this is not
132
        # enforced.  An info message is generated if this is not the case.
133
        if all((document.parent, item.normative, not item.derived)) and not item.links:
134
            msg = "no links to parent document: {}".format(document.parent)
135
            yield DoorstopWarning(msg)
136
137
        # Verify an item's links are to the correct parent
138
        for uid in item.links:
139
            try:
140
                prefix = uid.prefix
141
            except DoorstopError:
142
                msg = "invalid UID in links: {}".format(uid)
143
                yield DoorstopError(msg)
144
            else:
145
                if document.parent and prefix != document.parent:
146
                    # this is only 'info' because a document is allowed
147
                    # to contain items with a different prefix, but
148
                    # Doorstop will not create items like this
149
                    msg = "parent is '{}', but linked to: {}".format(
150
                        document.parent, uid
151
                    )
152
                    yield DoorstopInfo(msg)
153
154
    def _get_issues_tree(self, item, tree):
155
        """Yield all the item's issues against its tree."""
156
        log.debug("getting issues against tree...")
157
158
        # Verify an item's links are valid
159
        identifiers = set()
160
        for uid in item.links:
161
            try:
162
                parent = tree.find_item(uid)
163
            except DoorstopError:
164
                identifiers.add(uid)  # keep the invalid UID
165
                msg = "linked to unknown item: {}".format(uid)
166
                yield DoorstopError(msg)
167
            else:
168
                # check the parent item
169
                if not parent.active:
170
                    msg = "linked to inactive item: {}".format(parent)
171
                    yield DoorstopInfo(msg)
172
                if not parent.normative:
173
                    msg = "linked to non-normative item: {}".format(parent)
174
                    yield DoorstopWarning(msg)
175
                # check the link status
176
                if uid.stamp == Stamp(True):
177
                    uid.stamp = parent.stamp()
178
                elif not str(uid.stamp) and settings.STAMP_NEW_LINKS:
179
                    uid.stamp = parent.stamp()
180
                elif uid.stamp != parent.stamp():
181
                    if settings.CHECK_SUSPECT_LINKS:
182
                        msg = "suspect link: {}".format(parent)
183
                        yield DoorstopWarning(msg)
184
                # reformat the item's UID
185
                identifiers.add(UID(parent.uid, stamp=uid.stamp))
186
187
        # Apply the reformatted item UIDs
188
        if settings.REFORMAT:
189
            item.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.document.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