-
Notifications
You must be signed in to change notification settings - Fork 581
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This bear checks for spellings and grammatical mistakes on the descriptions of documentation comments.
- Loading branch information
1 parent
0a25352
commit 112fa7d
Showing
2 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']) |