Skip to content

Commit 5707af2

Browse files
committed
libs: Add envoy.code.check[version_history]
Signed-off-by: Ryan Northey <ryan@synca.io>
1 parent d64301f commit 5707af2

File tree

6 files changed

+206
-0
lines changed

6 files changed

+206
-0
lines changed

envoy.code.check/envoy/code/check/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pytooling_library(
1818
"abstract/flake8.py",
1919
"abstract/glint.py",
2020
"abstract/shellcheck.py",
21+
"abstract/version_history.py",
2122
"abstract/yapf.py",
2223
"checker.py",
2324
"cmd.py",

envoy.code.check/envoy/code/check/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
AFileCodeCheck,
99
AGlintCheck,
1010
AShellcheckCheck,
11+
AVersionHistoryCheck,
1112
AYapfCheck)
1213
from .checker import (
1314
CodeChecker,
1415
ExtensionsCheck,
1516
Flake8Check,
1617
GlintCheck,
1718
ShellcheckCheck,
19+
VersionHistoryCheck,
1820
YapfCheck)
1921
from .cmd import run, main
2022
from . import checker
@@ -42,4 +44,5 @@
4244
"run",
4345
"ShellcheckCheck",
4446
"typing",
47+
"VersionHistoryCheck",
4548
"YapfCheck")

envoy.code.check/envoy/code/check/abstract/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .flake8 import AFlake8Check
66
from .glint import AGlintCheck
77
from .shellcheck import AShellcheckCheck
8+
from .version_history import AVersionHistoryCheck
89
from .yapf import AYapfCheck
910
from . import (
1011
base,
@@ -13,6 +14,7 @@
1314
flake8,
1415
glint,
1516
shellcheck,
17+
version_history,
1618
yapf)
1719

1820

@@ -24,11 +26,13 @@
2426
"AFlake8Check",
2527
"AGlintCheck",
2628
"AShellcheckCheck",
29+
"AVersionHistoryCheck",
2730
"AYapfCheck",
2831
"base",
2932
"checker",
3033
"flake8",
3134
"glint",
3235
"extensions",
3336
"shellcheck",
37+
"version_history",
3438
"yapf")

envoy.code.check/envoy/code/check/abstract/checker.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ def summary_class(self) -> Type[CodeCheckerSummary]:
192192
"""CodeChecker's summary class."""
193193
return CodeCheckerSummary
194194

195+
@cached_property
196+
def version_history(self) -> "abstract.AVersionHistoryCheck":
197+
"""VERSION_HISTORY checker."""
198+
return self.version_history_class(self.directory, fix=self.fix)
199+
200+
@property # type:ignore
201+
@abstracts.interfacemethod
202+
def version_history_class(self) -> Type["abstract.AVersionHistoryCheck"]:
203+
raise NotImplementedError
204+
195205
@cached_property
196206
def yapf(self) -> "abstract.AYapfCheck":
197207
"""YAPF checker."""
@@ -258,6 +268,10 @@ async def check_shellcheck(self) -> None:
258268
"""Check for shellcheck issues."""
259269
await self._code_check(self.shellcheck)
260270

271+
async def check_version_history(self) -> None:
272+
"""Check for yapf issues."""
273+
await self._code_check(self.version_history)
274+
261275
@checker.preload(
262276
when=["python_flake8"],
263277
catches=[subprocess.exceptions.OSCommandError])
@@ -282,6 +296,11 @@ async def preload_shellcheck(self) -> None:
282296
async def preload_yapf(self) -> None:
283297
await self.yapf.problem_files
284298

299+
@checker.preload(
300+
when=["version_history"])
301+
async def preload_version_history(self) -> None:
302+
await self.version_history.problem_files
303+
285304
def _check_output(
286305
self,
287306
check_files: Set[str],
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import pathlib
2+
import re
3+
import sys
4+
from functools import cached_property
5+
from typing import Dict, Iterator, List, Pattern, Set, Tuple
6+
7+
import abstracts
8+
9+
from envoy.code.check import abstract
10+
11+
from aio.core.functional import async_property
12+
13+
14+
INVALID_REFLINK = r".* ref:.*"
15+
REF_WITH_PUNCTUATION_REGEX = r".*\. <[^<]*>`\s*"
16+
VERSION_HISTORY_NEW_LINE_REGEX = r"\* ([0-9a-z \-_]+): ([a-z:`]+)"
17+
VERSION_HISTORY_SECTION_NAME = r"^[A-Z][A-Za-z ]*$"
18+
# Make sure backticks come in pairs.
19+
# Exceptions: reflinks (ref:`` where the backtick won't be preceded by a space
20+
# links `title <link>`_ where the _ is checked for in the regex.
21+
BAD_TICKS_REGEX = re.compile(r".* `[^`].*`[^_]")
22+
23+
# TODO(phlax):
24+
# - generalize these checks to all rst files
25+
# - improve checks/handling of "default role"/inline literals
26+
# (perhaps using a sphinx plugin)
27+
# - add rstcheck and/or rstlint
28+
29+
30+
class CurrentVersionFile:
31+
32+
def __init__(self, path: pathlib.Path):
33+
self._path = path
34+
35+
@cached_property
36+
def backticks_re(self) -> Pattern[str]:
37+
return re.compile(BAD_TICKS_REGEX)
38+
39+
@cached_property
40+
def invalid_reflink_re(self) -> Pattern[str]:
41+
return re.compile(INVALID_REFLINK)
42+
43+
@property
44+
def lines(self) -> Iterator[str]:
45+
with open(self.path) as f:
46+
for line in f.readlines():
47+
yield line.strip()
48+
49+
@cached_property
50+
def new_line_re(self) -> Pattern[str]:
51+
return re.compile(VERSION_HISTORY_NEW_LINE_REGEX)
52+
53+
@property
54+
def path(self) -> pathlib.Path:
55+
return self._path
56+
57+
@property
58+
def prior_endswith_period(self) -> bool:
59+
return bool(
60+
self.prior_line.endswith(".")
61+
# Don't punctuation-check empty lines.
62+
or not self.prior_line
63+
# The text in the :ref ends with a .
64+
or (self.prior_line.endswith('`')
65+
and self.punctuation_re.match(self.prior_line)))
66+
67+
@cached_property
68+
def punctuation_re(self) -> Pattern[str]:
69+
return re.compile(REF_WITH_PUNCTUATION_REGEX)
70+
71+
@cached_property
72+
def section_name_re(self) -> Pattern[str]:
73+
return re.compile(VERSION_HISTORY_SECTION_NAME)
74+
75+
def check_line(self, line: str) -> List[str]:
76+
errors = self.check_reflink(line) + self.check_ticks(line)
77+
if line.startswith("* "):
78+
errors += self.check_list_item(line)
79+
elif not line:
80+
# If we hit the end of this release note block block, check the prior line.
81+
errors += self.check_previous_period()
82+
self.prior_line = ''
83+
elif self.prior_line:
84+
self.prior_line += line
85+
return errors
86+
87+
def check_list_item(self, line: str) -> List[str]:
88+
errors = []
89+
if not self.prior_endswith_period:
90+
errors.append(f"The following release note does not end with a '.'\n {self.prior_line}")
91+
92+
match = self.new_line_re.match(line)
93+
if not match:
94+
return errors + [
95+
"Version history line malformed. "
96+
f"Does not match VERSION_HISTORY_NEW_LINE_REGEX\n {line}\n"
97+
"Please use messages in the form 'category: feature explanation.', "
98+
"starting with a lower-cased letter and ending with a period."
99+
]
100+
first_word = match.groups()[0]
101+
next_word = match.groups()[1]
102+
103+
# Do basic alphabetization checks of the first word on the line and the
104+
# first word after the :
105+
if self.first_word_of_prior_line and self.first_word_of_prior_line > first_word:
106+
errors.append(
107+
f"Version history not in alphabetical order "
108+
f"({self.first_word_of_prior_line} vs {first_word}): "
109+
f"please check placement of line\n {line}. ")
110+
if self.first_word_of_prior_line == first_word and self.next_word_to_check and self.next_word_to_check > next_word:
111+
errors.append(
112+
f"Version history not in alphabetical order "
113+
f"({self.next_word_to_check} vs {next_word}): "
114+
f"please check placement of line\n {line}. ")
115+
self.set_list_tokens(line, first_word, next_word)
116+
return errors
117+
118+
def check_previous_period(self) -> List[str]:
119+
return ([f"The following release note does not end with a '.'\n {self.prior_line}"]
120+
if not self.prior_endswith_period else [])
121+
122+
def check_reflink(self, line: str) -> List[str]:
123+
return ([f"Found text \" ref:\". This should probably be \" :ref:\"\n{line}"]
124+
if self.invalid_reflink_re.match(line) else [])
125+
126+
def check_ticks(self, line: str) -> List[str]:
127+
return ([
128+
f"Backticks should come in pairs (``foo``) except for links (`title <url>`_) or refs (ref:`text <ref>`): {line}"
129+
] if (self.backticks_re.match(line)) else [])
130+
131+
def run_checks(self) -> Iterator[str]:
132+
for line_number, line in enumerate(self.lines):
133+
if self.section_name_re.match(line):
134+
if line == "Deprecated":
135+
break
136+
self.reset_list_tokens()
137+
for error in self.check_line(line):
138+
yield f"({self.path}:{line_number + 1}) {error}"
139+
140+
def reset_list_tokens(self) -> None:
141+
self.set_list_tokens("", "", "")
142+
143+
def set_list_tokens(self, line: str, first_word: str, next_word: str) -> None:
144+
self._list_tokens.update(dict(line=line, first_word=first_word, next_word=next_word))
145+
146+
@cached_property
147+
def _list_tokens(self) -> Dict[str, str]:
148+
return dict(line="", first_word="", next_word="")
149+
150+
151+
class AVersionHistoryCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
152+
153+
@async_property
154+
async def checker_files(self) -> Set[str]:
155+
return set(["docs/root/version_history/current.rst"])
156+
157+
@async_property(cache=True)
158+
async def problem_files(self) -> Dict[str, List[str]]:
159+
return (
160+
dict(await self.errors)
161+
if await self.files
162+
else {})
163+
164+
@async_property
165+
async def errors(self):
166+
return {
167+
version_history: list(
168+
CurrentVersionFile(pathlib.Path(version_history)).run_checks())
169+
for version_history
170+
in await self.absolute_paths}

envoy.code.check/envoy/code/check/checker.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class YapfCheck:
3434
pass
3535

3636

37+
@abstracts.implementer(check.AVersionHistoryCheck)
38+
class VersionHistoryCheck:
39+
pass
40+
41+
3742
@abstracts.implementer(check.ACodeChecker)
3843
class CodeChecker:
3944

@@ -65,6 +70,10 @@ def path(self) -> pathlib.Path:
6570
def shellcheck_class(self):
6671
return ShellcheckCheck
6772

73+
@property
74+
def version_history_class(self):
75+
return VersionHistoryCheck
76+
6877
@property
6978
def yapf_class(self):
7079
return YapfCheck

0 commit comments

Comments
 (0)