-
Notifications
You must be signed in to change notification settings - Fork 48
/
geopmlint.py
140 lines (112 loc) · 5.86 KB
/
geopmlint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python3
# Copyright (c) 2015 - 2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause
#
from typing import Any, Dict, Set
from docutils import nodes
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.util import logging
import collections
logger = logging.getLogger(__name__)
EXPECTED_SIGNAL_PROPERTIES = ["Aggregation", "Domain", "Format", "Unit"]
# The following section order must be followed in geopm_pio_*.7.rst files,
# for sections that are present.
EXPECTED_PIO_SECTION_ORDER = ["Description", "Requirements", "Signals",
"Controls", "Aliases", "Signal Aliases",
"Control Aliases", "See Also"]
# The maximum number of characters to include when showing examples of
# offending text in an error message
MAX_CONTEXT_STRING_LENGTH = 20
class CheckGeopmStyleBuilder(Builder):
name = "geopmlint"
epilog = "The GEOPM linter does not generate any output files."
def init(self) -> None:
pass
def get_outdated_docs(self) -> Set[str]:
return self.env.found_docs
def get_target_uri(self, docname: str, typ: str = None) -> str:
return ""
def prepare_writing(self, docnames: Set[str]) -> None:
pass
def _emit_error(self, location, message, *logger_args):
""" Log an error and set the statuscode to 1.
Args:
location: Location of the error in the source code. E.g., may be a
parser node, a docname, or a tuple of (docname, line).
message (str): Format string to send to the error logger
logger_args: Additional arguments (such as values to format) to
send to the logger.
"""
logger.error(message, *logger_args, location=location)
self.app.statuscode = 1
def write_doc(self, docname: str, doctree: nodes.Node) -> None:
""" Check for GEOPM rst documentation conventions.
- Signals should have definitions that list their properties for
aggregation, domain, format, and unit.
- Signal properties should be described in all-lowercase text.
- geopm_pio_*.7 docs have an expected section hierarchy
"""
sections_and_parents = list()
for section_node in doctree.traverse(nodes.section):
section_name = (section_node["names"][0] if
len(section_node["names"]) > 0 else None)
parent_name = (section_node.parent["names"][0] if
len(section_node.parent["names"]) > 0 else None)
if section_name is not None:
sections_and_parents.append((section_name, parent_name))
if section_name in ["signals", "controls"]:
for definition_item_node in section_node.traverse(nodes.definition_list_item):
term_node = definition_item_node.next_node(nodes.term)
definition_node = definition_item_node.next_node(nodes.definition)
bullet_prefix_counts = collections.Counter()
for bullet_item in definition_node.traverse(nodes.list_item):
bullet_text = bullet_item.astext()
for prefix in EXPECTED_SIGNAL_PROPERTIES:
if bullet_text.startswith(f"{prefix}:"):
bullet_prefix_counts[prefix] += 1
bullet_value = bullet_text.split(":", 1)[-1].strip()
if not bullet_value.islower():
if len(bullet_value) > MAX_CONTEXT_STRING_LENGTH:
bullet_value = bullet_value[:MAX_CONTEXT_STRING_LENGTH] + "..."
self._emit_error(
bullet_item,
"Value of the %s property in %s (%s) should be all lowercase",
prefix, term_node.astext(), bullet_value)
if any(bullet_prefix_counts[field] != 1
for field in EXPECTED_SIGNAL_PROPERTIES):
self._emit_error(
definition_item_node,
"Definition of %s should have exactly 1 bullet point each for "
'"Aggregation:", "Domain:", "Format:", and "Unit:".',
term_node.astext())
if docname.startswith('geopm_pio_') and docname.endswith('.7'):
section_positions = {section.lower(): position for position, (section, _)
in enumerate(sections_and_parents)}
last_position = -1
for section in EXPECTED_PIO_SECTION_ORDER:
if section.lower() in section_positions:
position = section_positions[section.lower()]
if position > last_position:
last_position = position
else:
self._emit_error(
docname,
'The "%s" section is out of place. Expected order (may omit sections): %s',
section,
", ".join(f'"{name}"' for name in EXPECTED_PIO_SECTION_ORDER))
for section, parent in sections_and_parents:
if section.lower() in ['signal aliases', 'control aliases'] and parent != 'aliases':
self._emit_error(
docname,
'The "%s" section must be a child of the "Aliases" section',
section)
def finish(self) -> None:
pass
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_builder(CheckGeopmStyleBuilder)
return {
"version": "builtin",
"parallel_read_safe": True,
"parallel_write_safe": True,
}