You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by kx...@apache.org on 2015/03/26 01:07:39 UTC

[2/8] documentation commit: updated refs/heads/master to 1c4a6b9

Run simple linter with make check

This will make sure that our documentation and
contributors pull requests are follows the style guide.

Currently, this linter is very trivial and stupid,
but it already is able to point on some common issues.


Project: http://git-wip-us.apache.org/repos/asf/couchdb-documentation/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-documentation/commit/f3182198
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-documentation/tree/f3182198
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-documentation/diff/f3182198

Branch: refs/heads/master
Commit: f3182198b99322a86063b04f68d0230d777b1d52
Parents: 4311449
Author: Alexander Shorin <kx...@apache.org>
Authored: Fri Mar 20 01:46:00 2015 +0300
Committer: Alexander Shorin <kx...@apache.org>
Committed: Thu Mar 26 03:04:13 2015 +0300

----------------------------------------------------------------------
 .travis.yml   |   1 +
 Makefile      |   3 +
 ext/linter.py | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 286 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-documentation/blob/f3182198/.travis.yml
----------------------------------------------------------------------
diff --git a/.travis.yml b/.travis.yml
index 0bf355e..75ac54b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,5 +21,6 @@ env:
   matrix:
     - TARGET=html
     - TARGET=pdf
+    - TARGET=check
 
 cache: apt

http://git-wip-us.apache.org/repos/asf/couchdb-documentation/blob/f3182198/Makefile
----------------------------------------------------------------------
diff --git a/Makefile b/Makefile
index e0c00e8..a748810 100644
--- a/Makefile
+++ b/Makefile
@@ -50,6 +50,9 @@ info:
 man:
 	$(SPHINXBUILD) -b man $(SPHINXOPTS) $(BUILDDIR)/man
 
+check:
+	python ext/linter.py $(SOURCE)
+
 install-html:
 install-pdf:
 install-info:

http://git-wip-us.apache.org/repos/asf/couchdb-documentation/blob/f3182198/ext/linter.py
----------------------------------------------------------------------
diff --git a/ext/linter.py b/ext/linter.py
new file mode 100644
index 0000000..cbc38e0
--- /dev/null
+++ b/ext/linter.py
@@ -0,0 +1,282 @@
+# 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])