blob: 0efa2b06795a0be423aa15740c2a86cdb12b9540 [file] [log] [blame]
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# This is very-very-very simple linter made in one evening without thoughts of
# making something great, but just a thing that works.
import os
import re
RULES = []
HAS_ERRORS = False
IGNORE_ERROR = False
def error_report(file, line, msg, _state=[]):
global HAS_ERRORS, IGNORE_ERROR
if IGNORE_ERROR:
return
if _state and _state[0] == file.name:
pass
else:
if _state:
_state[0] = file.name
else:
_state.append(file.name)
sys.stderr.write(file.name + "\n")
sys.stderr.write(" ".join([" line", str(line), ":", msg]) + "\n")
HAS_ERRORS = True
def register_rule(func):
RULES.append(func)
return func
def main(path):
for file in iter_rst_files(os.path.abspath(path)):
validate(file)
sys.exit(HAS_ERRORS)
def iter_rst_files(path):
if os.path.isfile(path):
with open(path) as f:
yield f
return
for root, dirs, files in os.walk(path):
for file in files:
if file.endswith(".rst"):
with open(os.path.join(root, file), "rb") as f:
yield f
def validate(file):
global IGNORE_ERROR
IGNORE_ERROR = False
rules = [rule(file) for rule in RULES]
for rule in rules:
for _ in rule:
# initialize coroutine
break
while True:
line = file.readline().decode("utf-8")
exhausted = []
for idx, rule in enumerate(rules):
try:
error = rule.send(line)
except StopIteration:
exhausted.append(rule)
else:
if error:
error_report(*error)
# not very optimal, but I'm lazy to figure anything better
for rule in exhausted:
rules.pop(rules.index(rule))
if not line:
break
@register_rule
def silent_scream(file):
"""Sometimes we must accept presence of some errors by some relevant
reasons. Here we're doing that."""
global IGNORE_ERROR
counter = 0
while True:
line = yield None
if not line:
break
if counter:
IGNORE_ERROR = True
counter -= 1
else:
IGNORE_ERROR = False
match = re.match("\s*.. lint: ignore errors for the next (\d+) line?", line)
if match:
# +1 for empty line right after comment
counter = int(match.group(1)) + 1
@register_rule
def license_adviser(file):
"""Each source file must include ASF license header."""
header = iter(
"""
.. Licensed under the Apache License, Version 2.0 (the "License"); you may not
.. use this file except in compliance with the License. You may obtain a copy of
.. the License at
..
.. http://www.apache.org/licenses/LICENSE-2.0
..
.. Unless required by applicable law or agreed to in writing, software
.. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
.. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
.. License for the specific language governing permissions and limitations under
.. the License.
""".lstrip().splitlines(
False
)
)
error = None
for n, hline in enumerate(header):
fline = yield error
error = None
if hline != fline.strip("\r\n"):
error = (
file,
n + 1,
"bad ASF license header\n"
" expected: {0}\n"
" found: {1}".format(hline, fline.strip()),
)
@register_rule
def whitespace_committee(file):
"""Whitespace committee takes care about whitespace (surprise!) characters
in files. The documentation style guide says:
- There should be no trailing white space;
- More than one empty line is not allowed and there shouldn't be such
at the end of file;
- The last line should ends with newline character
Additionally it alerts about tabs if they were used instead of spaces.
TODO: check for indentation
"""
error = prev = None
n = 0
while True:
line = yield error
error = None
if not line:
break
n += 1
# Check for trailing whitespace
if line.strip("\r\n").endswith(" "):
error = (file, n + 1, "trailing whitespace detected!\n" "{0}".format(line))
# Check for continuous empty lines
if prev is not None:
if prev.strip() == line.strip() == "":
error = (file, n + 1, "too many empty lines")
# Nobody loves tabs-spaces cocktail, we prefer spaces
if "\t" in line:
error = (file, n + 1, "no tabs please")
prev = line
# Accidentally empty file committed?
if prev is None:
error = (file, 0, "oh no! file seems empty!")
# Empty last lines not welcome
elif prev.strip() == "":
error = (file, n + 1, "no empty last lines please")
# Last line should ends with newline character
elif not prev.endswith("\n"):
error = (file, n + 1, "last line should ends with newline character")
yield error
return
@register_rule
def line_length_checker(file):
"""Use a modern max line length of 90 chars, as recommended by things like
https://github.com/ambv/black and https://youtu.be/wf-BqAjZb8M?t=260 .
"""
in_code_block = False
seen_emptyline = False
n = 0
error = None
while True:
line = yield error
error = None
if not line:
break
n += 1
line = line.rstrip()
# We have to ignore stuff in code blocks since it's hard to keep it
# within 90 chars wide box.
if line.strip().startswith(".. code") or line.endswith("::"):
in_code_block = True
continue
# Check for line length unless we're not in code block
if len(line) > 90 and not in_code_block:
if line.startswith(".."):
# Ignore long lines with external links
continue
if line.endswith(">`_"):
# Ignore long lines because of URLs
# TODO: need to be more smart here
continue
error = (
file,
n + 1,
"too long ({0} > 90) line\n{1}\n" "".format(len(line), line),
)
# Empty lines are acts as separators for code block content
elif not line:
seen_emptyline = True
# So if we saw an empty line and here goes content without indentation,
# so it mostly have to sign about the end of our code block
# (if it ever occurs)
elif seen_emptyline and line and not line.startswith(" "):
seen_emptyline = False
in_code_block = False
else:
seen_emptyline = False
@register_rule
def my_lovely_hat(file):
"""Everyone loves to wear a nice hat on they head, so articles does too."""
error = None
n = 0
while True:
line = yield error
error = None
if not line:
break
line = line.strip()
if not line:
continue
if line.startswith(".."):
continue
if set(line) < set(["#", "-", "=", "*"]):
break
else:
lines = [line, "\n", (yield None), (yield None)]
yield (file, n + 1, "bad title header:\n" "{}".format("".join(lines)))
return
if __name__ == "__main__":
import sys
if len(sys.argv) == 1:
sys.stderr.write("Argument with the target path is missed")
sys.exit(2)
main(sys.argv[1])