StrictDoc Documentation
strictdoc/core/query_engine/query_object.py
Source file coverage
Path:
strictdoc/core/query_engine/query_object.py
Lines:
416
Non-empty lines:
344
Non-empty lines covered with requirements:
344 / 344 (100.0%)
Functions:
69
Functions covered by requirements:
69 / 69 (100.0%)
1
"""
2
@relation(SDOC-SRS-155, scope=file)
3
"""
4
 
5
from typing import Any, List, Optional
6
 
7
from strictdoc.backend.sdoc.models.document import SDocDocument
8
from strictdoc.backend.sdoc.models.document_grammar import (
9
    DocumentGrammar,
10
)
11
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
12
from strictdoc.backend.sdoc.models.model import (
13
    SDocExtendedElementIF,
14
)
15
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
16
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
17
    SourceFileTraceabilityInfo,
18
)
19
from strictdoc.core.traceability_index import TraceabilityIndex
20
from strictdoc.helpers.cast import assert_cast
21
 
22
 
23
class Expression:
24
    pass
25
 
26
 
27
class StringExpression:
28
    def __init__(self, parent: Any, string: str):
29
        self.parent: Any = parent
30
        self.string: str = string
31
 
32
 
33
class NoneExpression:
34
    def __init__(self, parent: Any, _: str):
35
        self.parent: Any = parent
36
 
37
 
38
class StringListExpression:
39
    def __init__(self, parent: Any, strings: List[StringExpression]):
40
        self.parent: Any = parent
41
        self.strings: List[StringExpression] = strings
42
 
43
 
44
class NodeFieldExpression:
45
    def __init__(self, parent: Any, field_name: str):
46
        self.parent: Any = parent
47
        self.field_name = field_name
48
 
49
 
50
class NodeContainsExpression:
51
    def __init__(self, parent: Any, string: str):
52
        self.parent: Any = parent
53
        self.string: str = string
54
 
55
 
56
class NodeHasParentRequirementsExpression:
57
    def __init__(self, parent: Any, _: Any) -> None:
58
        self.parent: Any = parent
59
 
60
 
61
class NodeContainsAnyFreeTextExpression:
62
    def __init__(self, parent: Any, _: Any):
63
        self.parent: Any = parent
64
 
65
 
66
class NodeHasChildRequirementsExpression:
67
    def __init__(self, parent: Any, _: Any):
68
        self.parent: Any = parent
69
 
70
 
71
class NodeIsRequirementExpression:
72
    def __init__(self, parent: Any, _: Any):
73
        self.parent: Any = parent
74
 
75
 
76
class NodeIsSourceFileExpression:
77
    def __init__(self, parent: Any, _: Any):
78
        self.parent: Any = parent
79
 
80
 
81
class NodeIsSourceFileWithCompleteCoverageExpression:
82
    def __init__(self, parent: Any, _: Any):
83
        self.parent: Any = parent
84
 
85
 
86
class NodeIsSourceFileWithPartialCoverageExpression:
87
    def __init__(self, parent: Any, _: Any):
88
        self.parent: Any = parent
89
 
90
 
91
class NodeIsSourceFileWithNoCoverageExpression:
92
    def __init__(self, parent: Any, _: Any):
93
        self.parent: Any = parent
94
 
95
 
96
class NodeIsSectionExpression:
97
    def __init__(self, parent: Any, _: Any):
98
        self.parent: Any = parent
99
 
100
 
101
class NodeIsRootExpression:
102
    def __init__(self, parent: Any, _: Any):
103
        self.parent: Any = parent
104
 
105
 
106
class EqualExpression:
107
    def __init__(self, parent: Any, lhs_expr: Expression, rhs_expr: Expression):
108
        self.parent: Any = parent
109
        self.lhs_expr: Expression = lhs_expr
110
        self.rhs_expr: Expression = rhs_expr
111
 
112
 
113
class AndExpression:
114
    def __init__(self, parent: Any, expressions: List[Expression]):
115
        self.parent: Any = parent
116
        self.expressions: List[Expression] = expressions
117
 
118
 
119
class OrExpression:
120
    def __init__(self, parent: Any, expressions: List[Expression]):
121
        self.parent: Any = parent
122
        self.expressions: List[Expression] = expressions
123
 
124
 
125
class NotExpression:
126
    def __init__(self, parent: Any, expression: Expression):
127
        self.parent: Any = parent
128
        self.expression: Expression = expression
129
 
130
 
131
class NotEqualExpression:
132
    def __init__(self, parent: Any, lhs_expr: Expression, rhs_expr: Expression):
133
        self.parent: Any = parent
134
        self.lhs_expr: Expression = lhs_expr
135
        self.rhs_expr: Expression = rhs_expr
136
 
137
 
138
class InExpression:
139
    def __init__(self, parent: Any, lhs_expr: Expression, rhs_expr: Expression):
140
        self.parent: Any = parent
141
        self.lhs_expr: Expression = lhs_expr
142
        self.rhs_expr: Expression = rhs_expr
143
 
144
 
145
class NotInExpression:
146
    def __init__(self, parent: Any, lhs_expr: Expression, rhs_expr: Expression):
147
        self.parent: Any = parent
148
        self.lhs_expr: Expression = lhs_expr
149
        self.rhs_expr: Expression = rhs_expr
150
 
151
 
152
class AnyInExpression:
153
    def __init__(
154
        self,
155
        parent: Any,
156
        lhs_expr: StringListExpression,
157
        rhs_expr: Expression,
158
    ):
159
        self.parent: Any = parent
160
        self.lhs_expr: StringListExpression = lhs_expr
161
        self.rhs_expr: Expression = rhs_expr
162
 
163
 
164
class AllInExpression:
165
    def __init__(
166
        self,
167
        parent: Any,
168
        lhs_expr: StringListExpression,
169
        rhs_expr: Expression,
170
    ):
171
        self.parent: Any = parent
172
        self.lhs_expr: StringListExpression = lhs_expr
173
        self.rhs_expr: Expression = rhs_expr
174
 
175
 
176
class NoneInExpression:
177
    def __init__(
178
        self,
179
        parent: Any,
180
        lhs_expr: StringListExpression,
181
        rhs_expr: Expression,
182
    ):
183
        self.parent: Any = parent
184
        self.lhs_expr: StringListExpression = lhs_expr
185
        self.rhs_expr: Expression = rhs_expr
186
 
187
 
188
class Query:
189
    def __init__(self, root_expression: Any):
190
        self.root_expression: Any = root_expression
191
 
192
 
193
class QueryNullObject:
194
    def evaluate(self, _: Any) -> bool:
195
        return True
196
 
197
 
198
class QueryObject:
199
    def __init__(
200
        self, query: Query, traceability_index: TraceabilityIndex
201
    ) -> None:
202
        self.query: Query = query
203
        self.traceability_index: TraceabilityIndex = traceability_index
204
 
205
    def evaluate(self, node: SDocExtendedElementIF) -> bool:
206
        return self._evaluate(node, self.query.root_expression)
207
 
208
    def _evaluate(self, node: SDocExtendedElementIF, expression: Any) -> bool:
209
        if isinstance(expression, EqualExpression):
210
            return self._evaluate_equal(node, expression)
211
        if isinstance(expression, NotEqualExpression):
212
            return self._evaluate_not_equal(node, expression)
213
        if isinstance(expression, NodeContainsExpression):
214
            return self._evaluate_node_contains(node, expression)
215
        if isinstance(expression, NodeContainsAnyFreeTextExpression):
216
            return self._evaluate_node_contains_any_text(node)
217
        if isinstance(expression, NodeHasParentRequirementsExpression):
218
            return self._evaluate_node_has_parent_requirements(node)
219
        if isinstance(expression, NodeHasChildRequirementsExpression):
220
            return self._evaluate_node_has_child_requirements(node)
221
        if isinstance(expression, NodeIsRequirementExpression):
222
            return (
223
                isinstance(node, SDocNode) and node.node_type == "REQUIREMENT"
224
            )
225
        if isinstance(expression, NodeIsSectionExpression):
226
            return isinstance(node, SDocNode) and node.node_type == "SECTION"
227
        if isinstance(expression, NodeIsSourceFileExpression):
228
            return isinstance(node, SourceFileTraceabilityInfo)
229
        if isinstance(
230
            expression, NodeIsSourceFileWithCompleteCoverageExpression
231
        ):
232
            return (
233
                isinstance(node, SourceFileTraceabilityInfo)
234
                and node.get_coverage() == 100
235
            )
236
        if isinstance(
237
            expression, NodeIsSourceFileWithPartialCoverageExpression
238
        ):
239
            return isinstance(node, SourceFileTraceabilityInfo) and (
240
                0 < node.get_coverage() < 100
241
            )
242
        if isinstance(expression, NodeIsSourceFileWithNoCoverageExpression):
243
            return (
244
                isinstance(node, SourceFileTraceabilityInfo)
245
                and node.get_coverage() == 0
246
            )
247
        if isinstance(expression, NodeIsRootExpression):
248
            if isinstance(node, SDocNode):
249
                return node.is_root
250
            raise RuntimeError(
251
                "The node.is_root expression can be only called on nodes."
252
            )
253
        if isinstance(expression, NotExpression):
254
            return not self._evaluate(node, expression.expression)
255
        if isinstance(expression, AndExpression):
256
            for sub_expression_ in expression.expressions:
257
                if not self._evaluate(node, sub_expression_):
258
                    return False
259
            return True
260
        if isinstance(expression, OrExpression):
261
            for sub_expression_ in expression.expressions:
262
                if self._evaluate(node, sub_expression_):
263
                    return True
264
            return False
265
        if isinstance(expression, InExpression):
266
            if (
267
                rhs_value := self._evaluate_value(node, expression.rhs_expr)
268
            ) is not None and (
269
                lhs_value := self._evaluate_value(node, expression.lhs_expr)
270
            ) is not None:
271
                return lhs_value in rhs_value
272
            return False
273
        if isinstance(expression, NotInExpression):
274
            if (
275
                rhs_value := self._evaluate_value(node, expression.rhs_expr)
276
            ) is not None and (
277
                lhs_value := self._evaluate_value(node, expression.lhs_expr)
278
            ) is not None:
279
                return lhs_value not in rhs_value
280
            return False
281
        if isinstance(expression, AnyInExpression):
282
            return any(
283
                self._evaluate_list_in(node, lhs_expr_, expression.rhs_expr)
284
                for lhs_expr_ in expression.lhs_expr.strings
285
            )
286
        if isinstance(expression, AllInExpression):
287
            return all(
288
                self._evaluate_list_in(node, lhs_expr_, expression.rhs_expr)
289
                for lhs_expr_ in expression.lhs_expr.strings
290
            )
291
        if isinstance(expression, NoneInExpression):
292
            return not any(
293
                self._evaluate_list_in(node, lhs_expr_, expression.rhs_expr)
294
                for lhs_expr_ in expression.lhs_expr.strings
295
            )
296
        assert 0, expression
297
 
298
    def _evaluate_list_in(
299
        self,
300
        node: SDocExtendedElementIF,
301
        lhs_expr: StringExpression,
302
        rhs_expr: Expression,
303
    ) -> bool:
304
        if (rhs_value := self._evaluate_value(node, rhs_expr)) is not None and (
305
            lhs_value := self._evaluate_value(node, lhs_expr)
306
        ) is not None:
307
            if (
308
                isinstance(rhs_expr, NodeFieldExpression)
309
                and rhs_expr.field_name == "TAGS"
310
            ):
311
                return lhs_value in self._split_list_field_value(rhs_value)
312
            return lhs_value in rhs_value
313
        return False
314
 
315
    @staticmethod
316
    def _split_list_field_value(field_value: str) -> List[str]:
317
        return field_value.split(", ")
318
 
319
    def _evaluate_equal(
320
        self, node: SDocExtendedElementIF, expression: EqualExpression
321
    ) -> bool:
322
        return self._evaluate_value(
323
            node, expression.lhs_expr
324
        ) == self._evaluate_value(node, expression.rhs_expr)
325
 
326
    def _evaluate_not_equal(
327
        self, node: SDocExtendedElementIF, expression: NotEqualExpression
328
    ) -> bool:
329
        return self._evaluate_value(
330
            node, expression.lhs_expr
331
        ) != self._evaluate_value(node, expression.rhs_expr)
332
 
333
    def _evaluate_value(
334
        self, node: SDocExtendedElementIF, expression: Any
335
    ) -> Optional[str]:
336
        if isinstance(expression, NodeFieldExpression):
337
            return self._evaluate_node_field_expression(node, expression)
338
        if isinstance(expression, StringExpression):
339
            return expression.string
340
        if isinstance(expression, NoneExpression):
341
            return None
342
        assert 0, expression
343
 
344
    def _evaluate_node_field_expression(
345
        self,
346
        node: SDocExtendedElementIF,
347
        expression: NodeFieldExpression,
348
    ) -> Optional[str]:
349
        field_name = expression.field_name
350
        if isinstance(node, SDocNode):
351
            requirement: SDocNode = assert_cast(node, SDocNode)
352
            requirement_document: SDocDocument = assert_cast(
353
                requirement.get_document(), SDocDocument
354
            )
355
            document_grammar: DocumentGrammar = assert_cast(
356
                requirement_document.grammar, DocumentGrammar
357
            )
358
            element: GrammarElement = document_grammar.elements_by_type[
359
                requirement.node_type
360
            ]
361
            grammar_field_titles = list(map(lambda f: f.title, element.fields))
362
            if field_name not in grammar_field_titles:
363
                return None
364
            field_value = requirement._get_cached_field(field_name, False)
365
            if field_value is not None:
366
                return field_value
367
            return None
368
        else:
369
            raise NotImplementedError
370
 
371
    def _evaluate_node_has_parent_requirements(
372
        self, node: SDocExtendedElementIF
373
    ) -> bool:
374
        if not isinstance(node, SDocNode):
375
            raise TypeError(
376
                f"node.has_parent_requirements can be only called on "
377
                f"Requirement objects, got: {node.__class__.__name__}. To fix "
378
                f"the error, prepend your query with node.is_requirement."
379
            )
380
        return self.traceability_index.has_parent_requirements(node)
381
 
382
    def _evaluate_node_has_child_requirements(
383
        self, node: SDocExtendedElementIF
384
    ) -> bool:
385
        if not isinstance(node, SDocNode):
386
            raise TypeError(
387
                f"node.has_child_requirements can be only called on "
388
                f"Requirement objects, got: {node.__class__.__name__}. To fix "
389
                f"the error, prepend your query with node.is_requirement."
390
            )
391
        return self.traceability_index.has_children_requirements(node)
392
 
393
    def _evaluate_node_contains(
394
        self,
395
        node: SDocExtendedElementIF,
396
        expression: NodeContainsExpression,
397
    ) -> bool:
398
        if isinstance(node, SDocNode):
399
            requirement = assert_cast(node, SDocNode)
400
            requirement_field_: SDocNodeField
401
            for requirement_field_ in requirement.enumerate_fields():
402
                if expression.string in requirement_field_.get_text_value():
403
                    return True
404
            return False
405
        raise NotImplementedError
406
 
407
    def _evaluate_node_contains_any_text(
408
        self, node: SDocExtendedElementIF
409
    ) -> bool:
410
        if not (isinstance(node, SDocNode) and node.node_type == "SECTION"):
411
            raise TypeError(
412
                f"node.contains_any_text can be only called on "
413
                f"SECTION objects, got: {node.__class__.__name__}. To fix "
414
                f"the error, prepend your query with node.is_section."
415
            )
416
        return node.has_any_text_nodes()