Skip to content

Commit

Permalink
Add DocGrammarBear for docstrings
Browse files Browse the repository at this point in the history
This bear checks for spellings and grammatical mistakes
on the descriptions of documentation comments.
  • Loading branch information
damngamerz committed Aug 15, 2017
1 parent 0a25352 commit 112fa7d
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
116 changes: 116 additions & 0 deletions bears/documentation/DocGrammarBear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import shutil

from coalib.bearlib.languages.documentation.DocumentationComment import (
DocumentationComment)
from coalib.bearlib.languages.documentation.DocstyleDefinition import (
DocstyleDefinition)
from coalib.bearlib.languages.documentation.DocBaseClass import (
DocBaseClass)
from dependency_management.requirements.PipRequirement import PipRequirement
from coalib.bears.LocalBear import LocalBear
from coalib.results.Result import Result
from coalib.settings.Setting import typed_list


class DocGrammarBear(DocBaseClass, LocalBear):
LANGUAGES = {language for docstyle, language in
DocstyleDefinition.get_available_definitions()}
REQUIREMENTS = {PipRequirement('language-check', '1.0')}
AUTHORS = {'The coala developers'}
AUTHORS_EMAILS = {'coala-devel@googlegroups.com'}
LICENSE = 'AGPL-3.0'
ASCIINEMA_URL = 'https://asciinema.org/a/132564'
CAN_FIX = {'Documentation', 'Spelling', 'Grammar'}

@classmethod
def check_prerequisites(cls):
if shutil.which('java') is None:
return 'java is not installed.'
else:
try:
from language_check import LanguageTool, correct
LanguageTool
correct
return True
except ImportError: # pragma: no cover
return 'Please install the `language-check` pip package.'

def process_documentation(self,
parsed,
locale,
languagetool_disable_rules):
"""
This fixes the parsed documentation comment by applying spell checking
and grammatic rules via LanguageTool.
:param parsed:
Contains parsed documentation comment.
:param locale:
A locale representing the language you want to have checked.
Default is set to 'en-US'.
:param languagetool_disable_rules:
List of rules to disable checks for.
:return:
A tuple of fixed parsed documentation comment and warning_desc.
"""
# Defer import so the check_prerequisites can be run without
# language_check being there.
from language_check import LanguageTool, correct

tool = LanguageTool(locale)
tool.disabled.update(languagetool_disable_rules)

metadata = iter(parsed)

new_metadata = []
for comment in metadata:
matches = tool.check(comment.desc)
new_desc = correct(comment.desc, matches)
new_metadata.append(comment._replace(desc=new_desc))

return (new_metadata,
'Documentation has invalid Grammar/Spelling')

def run(self, filename, file, language: str,
docstyle: str='default', locale: str='en-US',
languagetool_disable_rules: typed_list(str)=()):
"""
Checks the main description and comments description of documentation
with LanguageTool. LanguageTool finds many errors that a simple spell
checker cannot detect and several grammar problems. A full list of
rules for english language can be found here at:
https://community.languagetool.org/rule/list?lang=en
LanguageTool currently supports more than 25 languages. For further
information, visit: https://www.languagetool.org/languages/
:param language:
The programming language of the file(s).
:param docstyle:
The docstyle to use. For example ``default`` or ``doxygen``.
Docstyles are language dependent, meaning not every language is
supported by a certain docstyle.
:param locale:
A locale representing the language you want to have checked.
Default is set to 'en-US'.
:param languagetool_disable_rules:
List of rules to disable checks for.
"""
for doc_comment in self.extract(file, language, docstyle):
parsed = doc_comment.parse()

(new_metadata, warning_desc) = self.process_documentation(
parsed, locale, languagetool_disable_rules)

new_comment = DocumentationComment.from_metadata(
new_metadata, doc_comment.docstyle_definition,
doc_comment.marker, doc_comment.indent, doc_comment.position)

if new_comment != doc_comment:
# Something changed, let's apply a result.
diff = self.generate_diff(file, doc_comment, new_comment)

yield Result(
origin=self,
message=warning_desc,
affected_code=(diff.range(filename),),
diffs={filename: diff})
171 changes: 171 additions & 0 deletions tests/documentation/DocGrammarBearTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from queue import Queue
from textwrap import dedent
import unittest
import shutil
import platform

from coalib.results.Diff import Diff
from coalib.settings.Section import Section
from coalib.testing.LocalBearTestHelper import execute_bear
from coalib.testing.BearTestHelper import generate_skip_decorator

from bears.documentation.DocGrammarBear import DocGrammarBear


def make_docstring(main_desc: str='',
param_desc: str='',
return_desc: str=''):
"""
This assembles a simple docstring having a main description, a parameter
description and a return description. This makes the tests readibilty
clean.
:param main_desc:
Contains the main description of the docstring.
:param param_desc:
Contatins the parameter description of the docstring.
:param return_desc:
Contains the return description of the docstring.
:return:
Returns an assembled docstring.
"""
docstring = dedent('"""\n'
'{}'
'\n'
':param xyz:{}'
':return:{}'
'"""\n')
return docstring.format(main_desc,
param_desc,
return_desc).splitlines(True)


def test(test_data, expected_data, optional_setting=None):
def test_function(self):
arguments = {'language': 'python', 'docstyle': 'default'}
if optional_setting:
arguments.update(optional_setting)
section = Section('test-section')
for key, value in arguments.items():
section[key] = value

with execute_bear(
DocGrammarBear(section, Queue()),
'dummy_filename',
test_data,
**arguments) as results:

diff = Diff(test_data)
for result in results:
# Only the given test file should contain a patch.
self.assertEqual(len(result.diffs), 1)

diff += result.diffs['dummy_filename']

self.assertEqual(expected_data, diff.modified)

return test_function


@generate_skip_decorator(DocGrammarBear)
class DocGrammarBearTest(unittest.TestCase):

def test_check_prerequisites(self):
_shutil_which = shutil.which
try:
shutil.which = lambda *args, **kwargs: None
self.assertEqual(DocGrammarBear.check_prerequisites(),
'java is not installed.')

shutil.which = lambda *args, **kwargs: 'path/to/java'
self.assertTrue(DocGrammarBear.check_prerequisites())
finally:
shutil.which = _shutil_which

test_spelling = test(
make_docstring(main_desc='Thiss is main descrpton.\n'),
make_docstring(main_desc='This is main description.\n'))

test_capitalize_sentence_start = test(
make_docstring(main_desc='this sentence starts with small letter\n'),
make_docstring(main_desc='This sentence starts with small letter\n'))

test_extra_whitespace = test(
make_docstring(main_desc='This sentence has extra white spaces\n'),
make_docstring(main_desc='This sentence has extra white spaces\n'))

test_apostrophe_comma = test(
make_docstring(main_desc='This sentence doesnt have an apostrophe\n'),
make_docstring(main_desc='This sentence doesn\'t have an '
'apostrophe\n'))

correct_docstring = make_docstring(
main_desc='This documentation has correct grammar.\n',
param_desc='Dummy description.\n',
return_desc='Return Nothing.\n')

test_correct_grammar = test(correct_docstring, correct_docstring)

test_disable_setting_UPPERCASE_SENTENCE_START = test(
make_docstring(main_desc='sentence starting with lowercase.\n',
param_desc='dummy description.\n',
return_desc='Nothing.\n'),
make_docstring(main_desc='sentence starting with lowercase.\n',
param_desc='dummy description.\n',
return_desc='Nothing.\n'),
{'languagetool_disable_rules': 'UPPERCASE_SENTENCE_START'})

# FRENCH_WHITESPACE adds a unicode space if it finds empty strings.
# which was breaking this test case.
test_language_french = unittest.skipIf(
platform.system() == 'Windows',
'language-check fails for different locale on windows')(
test(
make_docstring(main_desc='il monte en haut si il veut.\n'),
make_docstring(main_desc='Il monte s’il veut.\n'),
{'locale': 'fr',
'languagetool_disable_rules': 'FRENCH_WHITESPACE'}))

# explicit language test cases to check the breakage of DocGrammarBear.
test_java_explicit = test([
'class Square {\n',
' /**\n',
' * Returnss Area of a square.\n',
' *\n',
' *@param side side of squaree\n',
' *@return area of a square\n',
' */\n',
' public int Area(int side) {\n',
' return side * side;\n'
' }\n',
'}'], [
'class Square {\n',
' /**\n',
' * Returns Area of a square.\n',
' *\n',
' *@param side Side of square\n',
' *@return Area of a square\n',
' */\n',
' public int Area(int side) {\n',
' return side * side;\n'
' }\n',
'}'],
{'language': 'java'})

test_python_explicit = test([
'def improper_grammar(param1):\n',
' """\n',
' Documntation contains gramatical mistakess.DocGrammarBear\n',
' doesnt check for style.\n',
' :param param1: Contains parameter descrption.\n',
' :return: returns nothing. first letter small.\n',
' """\n',
' return None'], [
'def improper_grammar(param1):\n',
' """\n',
' Documentation contains grammatical mistakes. DocGrammarBear\n',
' doesn\'t check for style.\n',
' :param param1: Contains parameter description.\n',
' :return: Returns nothing. First letter small.\n',
' """\n',
' return None'])

0 comments on commit 112fa7d

Please sign in to comment.