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): |
|
|
|
|
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
|
|
|
|