| # 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: |
| IGNORE_ERROR = False |
| 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): |
| 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 |
| |
| 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 emtpy lines are 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 for tabs if they were used instead of spaces. |
| |
| TODO: check for indention |
| """ |
| 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 terminal_emulator(file): |
| """Terminal emulator has screen limited by 80 chars wide, so it ensures |
| that all the documentation content fits this limit. |
| """ |
| 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 80 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) > 80 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} > 80) 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 indention, |
| # 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]) |