Skip to content

Commit 112fa7d

Browse files
committed
Add DocGrammarBear for docstrings
This bear checks for spellings and grammatical mistakes on the descriptions of documentation comments.
1 parent 0a25352 commit 112fa7d

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import shutil
2+
3+
from coalib.bearlib.languages.documentation.DocumentationComment import (
4+
DocumentationComment)
5+
from coalib.bearlib.languages.documentation.DocstyleDefinition import (
6+
DocstyleDefinition)
7+
from coalib.bearlib.languages.documentation.DocBaseClass import (
8+
DocBaseClass)
9+
from dependency_management.requirements.PipRequirement import PipRequirement
10+
from coalib.bears.LocalBear import LocalBear
11+
from coalib.results.Result import Result
12+
from coalib.settings.Setting import typed_list
13+
14+
15+
class DocGrammarBear(DocBaseClass, LocalBear):
16+
LANGUAGES = {language for docstyle, language in
17+
DocstyleDefinition.get_available_definitions()}
18+
REQUIREMENTS = {PipRequirement('language-check', '1.0')}
19+
AUTHORS = {'The coala developers'}
20+
AUTHORS_EMAILS = {'coala-devel@googlegroups.com'}
21+
LICENSE = 'AGPL-3.0'
22+
ASCIINEMA_URL = 'https://asciinema.org/a/132564'
23+
CAN_FIX = {'Documentation', 'Spelling', 'Grammar'}
24+
25+
@classmethod
26+
def check_prerequisites(cls):
27+
if shutil.which('java') is None:
28+
return 'java is not installed.'
29+
else:
30+
try:
31+
from language_check import LanguageTool, correct
32+
LanguageTool
33+
correct
34+
return True
35+
except ImportError: # pragma: no cover
36+
return 'Please install the `language-check` pip package.'
37+
38+
def process_documentation(self,
39+
parsed,
40+
locale,
41+
languagetool_disable_rules):
42+
"""
43+
This fixes the parsed documentation comment by applying spell checking
44+
and grammatic rules via LanguageTool.
45+
46+
:param parsed:
47+
Contains parsed documentation comment.
48+
:param locale:
49+
A locale representing the language you want to have checked.
50+
Default is set to 'en-US'.
51+
:param languagetool_disable_rules:
52+
List of rules to disable checks for.
53+
:return:
54+
A tuple of fixed parsed documentation comment and warning_desc.
55+
"""
56+
# Defer import so the check_prerequisites can be run without
57+
# language_check being there.
58+
from language_check import LanguageTool, correct
59+
60+
tool = LanguageTool(locale)
61+
tool.disabled.update(languagetool_disable_rules)
62+
63+
metadata = iter(parsed)
64+
65+
new_metadata = []
66+
for comment in metadata:
67+
matches = tool.check(comment.desc)
68+
new_desc = correct(comment.desc, matches)
69+
new_metadata.append(comment._replace(desc=new_desc))
70+
71+
return (new_metadata,
72+
'Documentation has invalid Grammar/Spelling')
73+
74+
def run(self, filename, file, language: str,
75+
docstyle: str='default', locale: str='en-US',
76+
languagetool_disable_rules: typed_list(str)=()):
77+
"""
78+
Checks the main description and comments description of documentation
79+
with LanguageTool. LanguageTool finds many errors that a simple spell
80+
checker cannot detect and several grammar problems. A full list of
81+
rules for english language can be found here at:
82+
https://community.languagetool.org/rule/list?lang=en
83+
LanguageTool currently supports more than 25 languages. For further
84+
information, visit: https://www.languagetool.org/languages/
85+
86+
:param language:
87+
The programming language of the file(s).
88+
:param docstyle:
89+
The docstyle to use. For example ``default`` or ``doxygen``.
90+
Docstyles are language dependent, meaning not every language is
91+
supported by a certain docstyle.
92+
:param locale:
93+
A locale representing the language you want to have checked.
94+
Default is set to 'en-US'.
95+
:param languagetool_disable_rules:
96+
List of rules to disable checks for.
97+
"""
98+
for doc_comment in self.extract(file, language, docstyle):
99+
parsed = doc_comment.parse()
100+
101+
(new_metadata, warning_desc) = self.process_documentation(
102+
parsed, locale, languagetool_disable_rules)
103+
104+
new_comment = DocumentationComment.from_metadata(
105+
new_metadata, doc_comment.docstyle_definition,
106+
doc_comment.marker, doc_comment.indent, doc_comment.position)
107+
108+
if new_comment != doc_comment:
109+
# Something changed, let's apply a result.
110+
diff = self.generate_diff(file, doc_comment, new_comment)
111+
112+
yield Result(
113+
origin=self,
114+
message=warning_desc,
115+
affected_code=(diff.range(filename),),
116+
diffs={filename: diff})
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from queue import Queue
2+
from textwrap import dedent
3+
import unittest
4+
import shutil
5+
import platform
6+
7+
from coalib.results.Diff import Diff
8+
from coalib.settings.Section import Section
9+
from coalib.testing.LocalBearTestHelper import execute_bear
10+
from coalib.testing.BearTestHelper import generate_skip_decorator
11+
12+
from bears.documentation.DocGrammarBear import DocGrammarBear
13+
14+
15+
def make_docstring(main_desc: str='',
16+
param_desc: str='',
17+
return_desc: str=''):
18+
"""
19+
This assembles a simple docstring having a main description, a parameter
20+
description and a return description. This makes the tests readibilty
21+
clean.
22+
23+
:param main_desc:
24+
Contains the main description of the docstring.
25+
:param param_desc:
26+
Contatins the parameter description of the docstring.
27+
:param return_desc:
28+
Contains the return description of the docstring.
29+
:return:
30+
Returns an assembled docstring.
31+
"""
32+
docstring = dedent('"""\n'
33+
'{}'
34+
'\n'
35+
':param xyz:{}'
36+
':return:{}'
37+
'"""\n')
38+
return docstring.format(main_desc,
39+
param_desc,
40+
return_desc).splitlines(True)
41+
42+
43+
def test(test_data, expected_data, optional_setting=None):
44+
def test_function(self):
45+
arguments = {'language': 'python', 'docstyle': 'default'}
46+
if optional_setting:
47+
arguments.update(optional_setting)
48+
section = Section('test-section')
49+
for key, value in arguments.items():
50+
section[key] = value
51+
52+
with execute_bear(
53+
DocGrammarBear(section, Queue()),
54+
'dummy_filename',
55+
test_data,
56+
**arguments) as results:
57+
58+
diff = Diff(test_data)
59+
for result in results:
60+
# Only the given test file should contain a patch.
61+
self.assertEqual(len(result.diffs), 1)
62+
63+
diff += result.diffs['dummy_filename']
64+
65+
self.assertEqual(expected_data, diff.modified)
66+
67+
return test_function
68+
69+
70+
@generate_skip_decorator(DocGrammarBear)
71+
class DocGrammarBearTest(unittest.TestCase):
72+
73+
def test_check_prerequisites(self):
74+
_shutil_which = shutil.which
75+
try:
76+
shutil.which = lambda *args, **kwargs: None
77+
self.assertEqual(DocGrammarBear.check_prerequisites(),
78+
'java is not installed.')
79+
80+
shutil.which = lambda *args, **kwargs: 'path/to/java'
81+
self.assertTrue(DocGrammarBear.check_prerequisites())
82+
finally:
83+
shutil.which = _shutil_which
84+
85+
test_spelling = test(
86+
make_docstring(main_desc='Thiss is main descrpton.\n'),
87+
make_docstring(main_desc='This is main description.\n'))
88+
89+
test_capitalize_sentence_start = test(
90+
make_docstring(main_desc='this sentence starts with small letter\n'),
91+
make_docstring(main_desc='This sentence starts with small letter\n'))
92+
93+
test_extra_whitespace = test(
94+
make_docstring(main_desc='This sentence has extra white spaces\n'),
95+
make_docstring(main_desc='This sentence has extra white spaces\n'))
96+
97+
test_apostrophe_comma = test(
98+
make_docstring(main_desc='This sentence doesnt have an apostrophe\n'),
99+
make_docstring(main_desc='This sentence doesn\'t have an '
100+
'apostrophe\n'))
101+
102+
correct_docstring = make_docstring(
103+
main_desc='This documentation has correct grammar.\n',
104+
param_desc='Dummy description.\n',
105+
return_desc='Return Nothing.\n')
106+
107+
test_correct_grammar = test(correct_docstring, correct_docstring)
108+
109+
test_disable_setting_UPPERCASE_SENTENCE_START = test(
110+
make_docstring(main_desc='sentence starting with lowercase.\n',
111+
param_desc='dummy description.\n',
112+
return_desc='Nothing.\n'),
113+
make_docstring(main_desc='sentence starting with lowercase.\n',
114+
param_desc='dummy description.\n',
115+
return_desc='Nothing.\n'),
116+
{'languagetool_disable_rules': 'UPPERCASE_SENTENCE_START'})
117+
118+
# FRENCH_WHITESPACE adds a unicode space if it finds empty strings.
119+
# which was breaking this test case.
120+
test_language_french = unittest.skipIf(
121+
platform.system() == 'Windows',
122+
'language-check fails for different locale on windows')(
123+
test(
124+
make_docstring(main_desc='il monte en haut si il veut.\n'),
125+
make_docstring(main_desc='Il monte s’il veut.\n'),
126+
{'locale': 'fr',
127+
'languagetool_disable_rules': 'FRENCH_WHITESPACE'}))
128+
129+
# explicit language test cases to check the breakage of DocGrammarBear.
130+
test_java_explicit = test([
131+
'class Square {\n',
132+
' /**\n',
133+
' * Returnss Area of a square.\n',
134+
' *\n',
135+
' *@param side side of squaree\n',
136+
' *@return area of a square\n',
137+
' */\n',
138+
' public int Area(int side) {\n',
139+
' return side * side;\n'
140+
' }\n',
141+
'}'], [
142+
'class Square {\n',
143+
' /**\n',
144+
' * Returns Area of a square.\n',
145+
' *\n',
146+
' *@param side Side of square\n',
147+
' *@return Area of a square\n',
148+
' */\n',
149+
' public int Area(int side) {\n',
150+
' return side * side;\n'
151+
' }\n',
152+
'}'],
153+
{'language': 'java'})
154+
155+
test_python_explicit = test([
156+
'def improper_grammar(param1):\n',
157+
' """\n',
158+
' Documntation contains gramatical mistakess.DocGrammarBear\n',
159+
' doesnt check for style.\n',
160+
' :param param1: Contains parameter descrption.\n',
161+
' :return: returns nothing. first letter small.\n',
162+
' """\n',
163+
' return None'], [
164+
'def improper_grammar(param1):\n',
165+
' """\n',
166+
' Documentation contains grammatical mistakes. DocGrammarBear\n',
167+
' doesn\'t check for style.\n',
168+
' :param param1: Contains parameter description.\n',
169+
' :return: Returns nothing. First letter small.\n',
170+
' """\n',
171+
' return None'])

0 commit comments

Comments
 (0)