Skip to content

Commit f114587

Browse files
author
Patrick Brosset
committed
Various fixes and improvements
- fixed missing keyboard shortcut for sublime on windows - added grade to sublime plugin report - added reviewer for comments with if/or/and/but - moved test scripts in tests directory
1 parent 7df6923 commit f114587

30 files changed

+730
-536
lines changed

Default (Windows).sublime-keymap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"keys": ["ctrl+shift+c"],
4+
"command": "cleanjs"
5+
}
6+
]

TODO.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ Missing reviewers:
88
- Check jsdoc format and accepted @ statements
99
- Check that jsdoc block have description
1010
- Check that file has first a license, then a jsdoc block that is at least some nb of lines with example. Display info if there is no `pre` tag cause it means there's no example
11-
- Verify if a jsdoc block was just copied from another function/class ??
1211
- unused
1312
- Unused fields this. In class
1413
- code duplication
1514
- comparing branches of the AST ?
16-
- jslint
15+
- syntax
16+
- jslint
1717
- new Array() and new Object()
1818
- check for global vars assignments (missing var)
1919
- check ; is present after assignements
20+
- Check that when a local method is called, it is called with the right number of arguments
2021
- complexity
2122
- inline function is often sign of difficult to read code
2223
- comments:
@@ -46,3 +47,4 @@ Other misc stuff to do:
4647
- https://gist.github.com/1607354 is a good example
4748
- http://ominian.com/2012/01/06/working-with-using-pynarcissus-to-parse-javascript-in-python/
4849
- parser originally is pynarcissus
50+
- Find a way to have errors/warnings/infos into the editor directly, rather than in the console

package-metadata.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"url": "https://github.com/captainbrosset/cleanjs", "description": "Javascript clean code checker tool that will hurt your feelings!"}

reporters/console.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,40 @@ def output_messages(result, file_data):
2525
report += _get_report_post_border(max_content_length)
2626
report += "\n"
2727

28+
# Output the rating
29+
report += _output_one_message("Rating | ", max_header_length, result["rating"], max_content_length)
30+
2831
# Now output messages
2932
for index, message in enumerate(messages):
30-
pre_padding_length = 0
31-
if len(headers[index]) < max_header_length:
32-
pre_padding_length = max_header_length - len(headers[index])
33-
34-
post_padding_length = 0
35-
if len(contents[index]) < max_content_length:
36-
post_padding_length = max_content_length - len(contents[index])
37-
38-
report += "|"
39-
report += "".join([" " for i in range(pre_padding_length)])
40-
report += headers[index]
41-
report += contents[index]
42-
report += "".join([" " for i in range(post_padding_length)])
43-
report += "|\n"
44-
report += _get_report_pre_border(max_header_length)
45-
report += _get_report_post_border(max_content_length)
46-
report += "\n"
33+
report += _output_one_message(headers[index], max_header_length, contents[index], max_content_length)
4734

4835
report += "\n"
4936

5037
return report
5138

39+
def _output_one_message(header, max_header_length, content, max_content_length):
40+
report = ""
41+
42+
pre_padding_length = 0
43+
if len(header) < max_header_length:
44+
pre_padding_length = max_header_length - len(header)
45+
46+
post_padding_length = 0
47+
if len(content) < max_content_length:
48+
post_padding_length = max_content_length - len(content)
49+
50+
report += "|"
51+
report += "".join([" " for i in range(pre_padding_length)])
52+
report += header
53+
report += content
54+
report += "".join([" " for i in range(post_padding_length)])
55+
report += "|\n"
56+
report += _get_report_pre_border(max_header_length)
57+
report += _get_report_post_border(max_content_length)
58+
report += "\n"
59+
60+
return report
61+
5262
def _get_message_report_header(message):
5363
header = ""
5464
header += message.reviewer + " " + message.type

reviewers/comments.py

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22

33
from helpers import similartocode
4+
from helpers import general
45

56
class Reviewer():
67
MAX_CODE_COMMENT_RATIO_IN_FUNCTION = 0.3
@@ -9,6 +10,9 @@ class Reviewer():
910
SEPARATOR_CHARACTERS = ["-", "\|", "!", "#", "\.", "\*", "=", "/", "~", "+", "\\"]
1011
MAX_NUMBER_OF_SEPARATOR_CHARACTERS_IN_COMMENTS = 3
1112

13+
WARN_MAX_NB_OF_BUTIFORAND_CONDITION = 0
14+
ERROR_MAX_NB_OF_BUTIFORAND_CONDITION = 2
15+
1216
def get_name(self):
1317
return "comments"
1418

@@ -79,12 +83,38 @@ def review_comment_code_similarity(self, lines, message_bag):
7983
if similartocode.is_code_and_comment_similar(code, comment):
8084
message_bag.add_warning(self, "It seems this comment is very similar to the code directly beneath it. Don't you think you can get rid of it?", line.line_number)
8185

86+
def get_all_comment_blocks(self, lines):
87+
blocks = []
88+
for line in lines:
89+
if not line.is_only_comments():
90+
# No comments, start a new block
91+
blocks.append({"line_nb": None, "comment": ""})
92+
else:
93+
# Comments, put that into the last comment block opened
94+
if len(blocks) == 0:
95+
blocks.append({"line_nb": None, "comment": ""})
96+
blocks[len(blocks) - 1]["comment"] += line.comments
97+
if not blocks[len(blocks) - 1]["line_nb"]:
98+
blocks[len(blocks) - 1]["line_nb"] = line.line_number
99+
return general.filter_empty_items_from_dict_list(blocks, "comment")
100+
101+
def review_multiple_responsibilities(self, lines, message_bag):
102+
# 'if', 'but', 'and','or', then it's likely that the commented code has more than one responsibility
103+
comment_blocks = self.get_all_comment_blocks(lines)
104+
for comment in comment_blocks:
105+
nb_of_conditions = len(re.findall("and|or|but|if", comment["comment"]))
106+
if nb_of_conditions > Reviewer.WARN_MAX_NB_OF_BUTIFORAND_CONDITION:
107+
message_bag.add_warning(self, "It seems this comment tries to explain a piece of code that has several responsibilities", comment["line_nb"])
108+
elif nb_of_conditions > Reviewer.ERROR_MAX_NB_OF_BUTIFORAND_CONDITION:
109+
message_bag.add_error(self, "For sure, this comment corresponds to code that has more than 1 responsibility", comment["line_nb"])
110+
82111
def review(self, file_data, message_bag):
83112
self.review_multiple_comment_lines(file_data.lines.all_lines, message_bag)
84113
self.review_comments_ratio_in_functions(file_data.functions, message_bag)
85114
self.review_comments_after_statements(file_data.lines.all_lines, message_bag)
86115
self.review_separator_comments(file_data.lines.all_lines, message_bag)
87116
self.review_comment_code_similarity(file_data.lines.all_lines, message_bag)
117+
self.review_multiple_responsibilities(file_data.lines.all_lines, message_bag)
88118

89119

90120
if __name__ == "__main__":
@@ -97,24 +127,54 @@ def add_warning(self, reviewer, message, line_number):
97127
self.warnings.append(line_number)
98128

99129
class MockLine:
100-
def __init__(self, comments):
130+
def __init__(self, comments, is_only_comments=True):
101131
self.comments = comments
102132
self.line_number = 0
103-
104-
reviewer = Reviewer()
105-
bag = MockBag()
106-
line1 = MockLine("this is something ..")
107-
line2 = MockLine("this is something ----")
108-
line3 = MockLine("this is something --#-~-")
109-
line4 = MockLine("// \\\\ ...")
110-
lines = []
111-
lines.append(line1)
112-
lines.append(line2)
113-
lines.append(line3)
114-
lines.append(line4)
115-
116-
reviewer.review_separator_comments(lines, bag)
117-
118-
assert len(bag.warnings) == 2, "Wrong number of separator comments found"
133+
self._is_only_comments = is_only_comments
134+
def is_only_comments(self):
135+
return self._is_only_comments
119136

137+
def test_separator_comments():
138+
reviewer = Reviewer()
139+
bag = MockBag()
140+
141+
line1 = MockLine("this is something ..")
142+
line2 = MockLine("this is something ----")
143+
line3 = MockLine("this is something --#-~-")
144+
line4 = MockLine("// \\\\ ...")
145+
lines = []
146+
lines.append(line1)
147+
lines.append(line2)
148+
lines.append(line3)
149+
lines.append(line4)
150+
151+
reviewer.review_separator_comments(lines, bag)
152+
153+
assert len(bag.warnings) == 2, "Wrong number of separator comments found"
154+
155+
def test_get_all_comment_blocks():
156+
line1 = MockLine("/**", True)
157+
line2 = MockLine(" * Class super.great.Framework", True)
158+
line3 = MockLine(" * This class is the main entry point of the framework", True)
159+
line4 = MockLine(" * It provides the main utilities of the framework and also a way to load other classes", True)
160+
line5 = MockLine(" */", True)
161+
line6 = MockLine("super.great.Framework = function() {", False)
162+
line7 = MockLine(" this.test = 2; // wow, we initialize stuff", False)
163+
line8 = MockLine(" /**", True)
164+
line9 = MockLine(" * Name of the instance", True)
165+
line10 = MockLine(" * @type {String}", True)
166+
line11 = MockLine(" */", True)
167+
line12 = MockLine(" this.name = \"\"", False)
168+
169+
lines = [line1,line2,line3,line4,line5,line6,line7,line8,line9,line10,line11,line12]
170+
171+
blocks = Reviewer().get_all_comment_blocks(lines)
172+
assert len(blocks) == 2, "Wrong number of comment blocks found in lines"
173+
174+
blocks = Reviewer().get_all_comment_blocks([])
175+
assert len(blocks) == 0
176+
177+
test_separator_comments()
178+
test_get_all_comment_blocks()
179+
120180
print "ALL TESTS OK " + __file__

reviewers/complexity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def review_ifs_complexity(self, lines, message_bag):
4040
message_bag.add_error(self, "Found an IF statement with more than " + str(Reviewer.ERROR_MAX_NB_OF_CONDITIONS_IN_IF) + " AND or OR! Wrap them in a function like isABC()", line.line_number)
4141
elif len(conditions) > Reviewer.WARN_MAX_NB_OF_CONDITIONS_IN_IF:
4242
message_bag.add_warning(self, "Found an IF statement with more than " + str(Reviewer.WARN_MAX_NB_OF_CONDITIONS_IN_IF) + " AND or OR! Could you extract this in a function like isABC()?", line.line_number)
43-
43+
4444
def review(self, file_data, message_bag):
4545
self.review_functions_complexity(file_data.functions, message_bag)
4646
self.review_ifs_complexity(file_data.lines.all_lines, message_bag)

reviewers/helpers/general.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
def filter_empty_items_from_dict_list(list, object_property_to_check):
2+
"""Given a list of dictionaries, filter out the ones for which the
3+
object_property_to_check key/value evaluates to False"""
4+
5+
return filter(lambda item: not not item[object_property_to_check], list)
6+
7+
def filter_dups_from_list(the_list):
8+
"""Given a list of hashable items, remove the duplicates from that list"""
9+
10+
return list(set(the_list))
11+
12+
if __name__ == "__main__":
13+
assert filter_empty_items_from_dict_list([], "test") == []
14+
assert filter_empty_items_from_dict_list([{"test": ""}, {"test": []}, {"test": "a"}], "test") == [{"test": "a"}]
15+
16+
assert filter_dups_from_list([]) == []
17+
assert filter_dups_from_list([1,1,1,2,3,1,5]) == [1,2,3,5]
18+
assert sorted(filter_dups_from_list(["a", 4, False, False, 4, "a", "b"])) == sorted(["a", 4, False, "b"])
19+
20+
print "ALL TESTS OK " + __file__

reviewers/helpers/similartocode.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import re
22

33
import extractwords
4-
5-
def get_unique_words(words):
6-
return list(set(words))
4+
import general
75

86
def is_code_and_comment_similar(code, comment):
9-
comment_words = get_unique_words(extractwords.get_all_words_from_line(comment))
7+
comment_words = general.filter_dups_from_list(extractwords.get_all_words_from_line(comment))
108
comment_words_similar_to_code = []
11-
code_words = get_unique_words(extractwords.get_all_words_from_line(code))
9+
code_words = general.filter_dups_from_list(extractwords.get_all_words_from_line(code))
1210
code_words_similar_to_comment = []
1311

1412
if len(comment_words) == 0 or len(code_words) == 0:

tests/runtests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ def run_unit_tests(dir):
1111
pass
1212

1313
def run_full_tests():
14-
for item in os.listdir("../testscripts"):
14+
for item in os.listdir("scripts"):
1515
if item[-3:] == ".js":
1616
print "-- RUNNING CLEANJS ON " + item
17-
os.system("python ../cleanjs_cmdline.py ../testscripts/" + item + " ../testscripts/reports/" + item + "-report.html")
17+
os.system("python ../cleanjs_cmdline.py scripts/" + item + " scripts/reports/" + item + "-report.html")
1818

1919
print ""
2020
print "-- RUNNING ALL UNIT TETS"

tests/scripts/CommentedClass.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
var my = {package: {}}
2+
3+
/**
4+
* @class my.package.MyClass
5+
* This class is used to store the data model on the client, using local storage or session storage depending on the case.
6+
* If the setting is set to LOCAL, then local storage is used, otherwise session storage is used
7+
*/
8+
my.package.MyClass = function(setting) {
9+
this.setting = setting;
10+
};
11+
12+
// Set the setting
13+
my.package.MyClass.prototype.setSetting = function(setting) {
14+
this._setting = setting;
15+
};

0 commit comments

Comments
 (0)