Skip to content
This repository was archived by the owner on Nov 4, 2022. It is now read-only.

Commit 4d31c75

Browse files
authored
Merge pull request #2 from goonnors/master
Calculate CC and compound names
2 parents 2c56c58 + 6978563 commit 4d31c75

21 files changed

+426
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ article.pdf
1313
*.bbl
1414
*.blg
1515
*.m
16+
__pycache__/

Makefile

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
# SOFTWARE.
2222

23+
SHELL := /bin/bash
2324
all: install search clone calc summary draw article
2425

2526
install:
2627
bundle update
28+
python3 -m pip install -r requirements.txt
2729

2830
clean:
2931
rm -rf *.tex
@@ -49,9 +51,13 @@ clone:
4951
uncalc:
5052
rm -rf metrics
5153

54+
calc_test:
55+
python3 computation/calc_unit_test.py
56+
python3 computation/calc_integration_test.py
57+
5258
calc:
5359
mkdir -p metrics
54-
for r in $$(find clones/ -type directory -depth 2); do \
60+
for r in $$(find clones/ -type d -maxdepth 2 ); do \
5561
d="metrics/$${r/clones\/\//}"; \
5662
if [ -e "$${d}" ]; then \
5763
echo "Dir with metrics already here: $${d}"; \
@@ -60,9 +66,11 @@ calc:
6066
for f in $$(find $${r} -name '*.java'); do \
6167
m="metrics/$${f/clones\/\//}.m"; \
6268
mkdir -p $$(dirname "$${m}"); \
63-
echo '1,1' > "$${m}"; \
69+
if [ ! -e "$${m}" ]; then \
70+
python3 computation/calc.py "$${f}" > "$${m}"; \
71+
fi \
6472
done; \
65-
echo "$$(find $${d} -type file | wc -l) Java classes analyzed into $${d}"; \
73+
echo "$$(find $${d} -type f | wc -l) Java classes analyzed into $${d}"; \
6674
fi; \
6775
done
6876

computation/calc.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# The MIT License (MIT)
2+
#
3+
# Copyright (c) 2020 Yegor Bugayenko
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included
13+
# in all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
import sys
24+
import re
25+
from javalang import tree, parse
26+
27+
"""Determines the number of branches for a node
28+
according to the Extended Cyclomatic Complexity metric.
29+
Binary operations (&&, ||) and each case statement are taken into account.
30+
31+
:param node: class provided by the parser and targeted to Java 8 spec
32+
:returns: number
33+
"""
34+
def branches(node):
35+
count = 0
36+
if (isinstance(node, tree.BinaryOperation)):
37+
if(node.operator == '&&' or node.operator == '||'):
38+
count = 1
39+
elif(isinstance(node, (
40+
tree.ForStatement,
41+
tree.IfStatement,
42+
tree.WhileStatement,
43+
tree.DoStatement,
44+
tree.TernaryExpression
45+
))):
46+
count = 1
47+
elif(isinstance(node, tree.SwitchStatementCase)):
48+
count = len(node.case)
49+
elif(isinstance(node, tree.TryStatement)):
50+
count = len(node.catches)
51+
return count
52+
53+
"""Check the name for compound inside the methods (i.e. for local variables)
54+
55+
:param node: class provided by the parser and targeted to Java 8 spec
56+
:returns: boolean
57+
"""
58+
def compound(node):
59+
flag = False
60+
if (isinstance(node, tree.LocalVariableDeclaration)):
61+
name = node.declarators[0].name
62+
flag = len(re.findall(r'(?:[a-z][A-Z]+)|(?:[_])', name)) != 0
63+
return flag
64+
65+
66+
java = sys.argv[1]
67+
with open(java, encoding='utf-8') as f:
68+
try:
69+
cc = 1
70+
names = 0
71+
ast = parse.parse(f.read())
72+
for path, node in ast:
73+
cc += branches(node)
74+
names += int(compound(node))
75+
print(str(cc) + ',' + str(names))
76+
except Exception as e:
77+
sys.exit(str(e) + ': ' + java)

computation/calc_integration_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
import sys
3+
import unittest
4+
import javalang
5+
from glob import glob
6+
import re
7+
8+
try:
9+
from unittest.mock import patch
10+
except ImportError:
11+
from mock import patch
12+
13+
fileDir = os.path.dirname(os.path.realpath(__file__))
14+
testargs = ["", os.path.join(fileDir, 'java/cc/SwitchCaseStatement.java')]
15+
with patch.object(sys, 'argv', testargs):
16+
from calc import branches, compound
17+
18+
def cc(tree):
19+
var = next(tree.filter(javalang.tree.VariableDeclarator))[1]
20+
if (var.name != 'cc'):
21+
raise ValueError('file not tested')
22+
return int(var.initializer.value)
23+
24+
def names(ast):
25+
comment = next(ast.filter(javalang.tree.Documented))[1]
26+
return int(re.search(r'[\d]+', comment.documentation).group(0))
27+
28+
for java in glob(os.path.join(fileDir, 'java/names/*.java')):
29+
with open(java, encoding='utf-8') as f:
30+
try:
31+
ast = javalang.parse.parse(f.read())
32+
receivedNames = 0
33+
expectedNames = names(ast)
34+
35+
for path, node in ast:
36+
receivedNames += compound(node)
37+
38+
if (receivedNames != expectedNames):
39+
raise Exception('\nTest failed. Expected ' + str(expectedNames) + ', received ' + str(receivedNames))
40+
except Exception as e:
41+
sys.exit(str(e) + ': ' + java)
42+
43+
for java in glob(os.path.join(fileDir, 'java/cc/*.java')):
44+
with open(java, encoding='utf-8') as f:
45+
try:
46+
ast = javalang.parse.parse(f.read())
47+
receivedCC = 1
48+
expectedCC = cc(ast)
49+
50+
for path, node in ast:
51+
receivedCC += branches(node)
52+
53+
if (receivedCC != expectedCC):
54+
raise Exception('\nTest failed. Expected ' + str(expectedCC) + ', received ' + str(receivedCC))
55+
56+
print('.', end='', flush=True),
57+
except Exception as e:
58+
sys.exit(str(e) + ': ' + java)
59+
60+
print('\nOK')
61+

computation/calc_unit_test.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import os
2+
import sys
3+
import unittest
4+
import javalang
5+
6+
try:
7+
from unittest.mock import patch
8+
except ImportError:
9+
from mock import patch
10+
11+
fileDir = os.path.dirname(os.path.realpath(__file__))
12+
testargs = ["", os.path.join(fileDir, 'java/cc/SwitchCaseStatement.java')]
13+
with patch.object(sys, 'argv', testargs):
14+
from calc import branches, compound
15+
16+
class TestCalc(unittest.TestCase):
17+
def test_for_statement_count(self):
18+
code = """
19+
for (int i = 0; i < amounts.length; i++) {
20+
result += amounts[i];
21+
}
22+
"""
23+
node = self.statement(code)
24+
self.assertEqual(branches(node), 1)
25+
26+
def test_branch_not_count(self):
27+
code = "nextKey = new BlockKey(serialNo, System.currentTimeMillis() + 3.0);"
28+
node = self.expression(code)
29+
self.assertEqual(branches(node), 0)
30+
31+
def test_if_statement_count(self):
32+
code = """
33+
if (itr == '\r') {
34+
int status = 1;
35+
}
36+
"""
37+
node = self.statement(code)
38+
self.assertEqual(branches(node), 1)
39+
40+
def test_while_statement_count(self):
41+
code = """
42+
while (i < 5) {
43+
System.out.println(i);
44+
i++;
45+
}
46+
"""
47+
node = self.statement(code)
48+
self.assertEqual(branches(node), 1)
49+
50+
def test_do_statement_count(self):
51+
code = """
52+
do
53+
a-- ;
54+
while ( a );
55+
"""
56+
node = self.statement(code)
57+
self.assertEqual(branches(node), 1)
58+
59+
def test_switch_statement_count(self):
60+
code = """
61+
switch ( a )
62+
{
63+
case 1:
64+
return ;
65+
}
66+
"""
67+
node = self.statement(code)
68+
self.assertEqual(branches(node.cases[0]), 1)
69+
70+
def test_logic_and_operator_count(self):
71+
code = 'if ( a && b ) {}'
72+
ifNode = self.statement(code)
73+
self.assertEqual(branches(ifNode.children[1]), 1)
74+
75+
def test_logic_or_operator_count(self):
76+
code = 'if ( a || b ) {}'
77+
ifNode = self.statement(code)
78+
self.assertEqual(branches(ifNode.children[1]), 1)
79+
80+
def test_logic_operator_not_count(self):
81+
code = 'if ( a > b ) {}'
82+
ifNode = self.statement(code)
83+
self.assertEqual(branches(ifNode.children[1]), 0)
84+
85+
def test_ternary_operator(self):
86+
code = 'value == "uppercase" ? "JOHN" : "john";'
87+
node = self.expression(code);
88+
self.assertEqual(branches(node), 1)
89+
90+
def test_catch_statements(self):
91+
code = """
92+
try {
93+
Throwable t = new Exception();
94+
throw t;
95+
} catch (RuntimeException e) {
96+
System.err.println("catch RuntimeException");
97+
} catch (Exception e) {
98+
System.err.println("catch Exception");
99+
} catch (Throwable e) {
100+
System.err.println("catch Throwable");
101+
}
102+
System.err.println("next statement");
103+
"""
104+
node = self.statement(code)
105+
self.assertEqual(branches(node), 3)
106+
107+
def test_compound_with_camel_and_snake_cases(self):
108+
for s in ['camelCase', 'CamelCase', 'camelCASE', 'snake_case', 'snake_CASE', 'SNAKE_CASE', 'SNAKE_case']:
109+
code = "int %s = 0;" % (s)
110+
node = self.parser(code).parse_local_variable_declaration_statement()
111+
self.assertEqual(compound(node), 1)
112+
113+
def test_compound_without_camel_and_snake_cases(self):
114+
for s in ['camelcase, CAMELCASE, Camelcase']:
115+
code = "int %s = 0;" % (s)
116+
node = self.parser(code).parse_local_variable_declaration_statement()
117+
self.assertEqual(compound(node), 0)
118+
119+
def expression(self, code):
120+
return self.parser(code).parse_expression()
121+
122+
def statement(self, code):
123+
return self.parser(code).parse_statement()
124+
125+
def parser(self, code):
126+
return javalang.parser.Parser(
127+
javalang.tokenizer.tokenize(code)
128+
)
129+
130+
if __name__ == '__main__':
131+
unittest.main()

computation/java/cc/Cheating.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class Cheating {
2+
public int cc = 1;
3+
4+
public boolean foo(Account account, int amount) {
5+
boolean result = true;
6+
result &= account.getBalance() >= amount;
7+
result &= !account.isLocked();
8+
return result;
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class DoWhileStatement {
2+
public int cc = 2;
3+
4+
void foo( int a ) {
5+
do
6+
a-- ;
7+
while ( a );
8+
}
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class ElseIfStatement {
2+
public int cc = 3;
3+
4+
public boolean checkWithdrawal() {
5+
boolean result = false;
6+
if (account.getBalance() >= amount) {
7+
result = true;
8+
} else if (account.isLocked()) {
9+
result = false;
10+
}
11+
return result;
12+
}
13+
}

computation/java/cc/ForStatement.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public class ForStatement {
2+
public int cc = 2;
3+
4+
public int sum(int[] amounts) {
5+
int result = 0;
6+
for (int i = 0; i < amounts.length; i++) {
7+
result += amounts[i];
8+
}
9+
return result;
10+
}
11+
}

computation/java/cc/IfStatement.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class IfStatement {
2+
public int cc = 3;
3+
4+
public boolean checkWithdrawal() {
5+
boolean result = false;
6+
if (account.getBalance() >= amount) {
7+
result = true;
8+
}
9+
if (account.isLocked()) {
10+
result = false;
11+
}
12+
return result;
13+
}
14+
}

0 commit comments

Comments
 (0)