forked from cqfn/jpeek
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
411 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,276 @@ | ||
/** | ||
* The MIT License (MIT) | ||
* | ||
* Copyright (c) 2017 Yegor Bugayenko | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included | ||
* in all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
* SOFTWARE. | ||
*/ | ||
package org.jpeek.metrics.cohesion; | ||
|
||
import java.io.IOException; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
import javassist.CannotCompileException; | ||
import javassist.CtClass; | ||
import javassist.CtField; | ||
import javassist.CtMethod; | ||
import org.jpeek.Base; | ||
import org.jpeek.Metric; | ||
import org.jpeek.metrics.Colors; | ||
import org.jpeek.metrics.JavassistClasses; | ||
import org.objectweb.asm.ClassReader; | ||
import org.objectweb.asm.ClassVisitor; | ||
import org.objectweb.asm.MethodVisitor; | ||
import org.objectweb.asm.Opcodes; | ||
import org.xembly.Directive; | ||
|
||
/** | ||
* Optimistic Class Cohesion (OCC). | ||
* | ||
* <p>There is no thread-safety guarantee. | ||
* | ||
* @author Vseslav Sekorin (vssekorin@gmail.com) | ||
* @version $Id$ | ||
* @see <a href="https://www.researchgate.net/publication/268046583_A_Proposal_of_Class_Cohesion_Metrics_Using_Sizes_of_Cohesive_Parts">A Proposal of Class Cohesion Metrics Using Sizes of Cohesive Parts</a> | ||
* @since 0.2 | ||
* @checkstyle AbbreviationAsWordInNameCheck (5 lines) | ||
* @checkstyle ParameterNumberCheck (500 lines) | ||
*/ | ||
public final class OCC implements Metric { | ||
|
||
/** | ||
* The base. | ||
*/ | ||
private final Base base; | ||
|
||
/** | ||
* Ctor. | ||
* @param bse The base | ||
*/ | ||
public OCC(final Base bse) { | ||
this.base = bse; | ||
} | ||
|
||
@Override | ||
public Iterable<Directive> xembly() throws IOException { | ||
return new JavassistClasses( | ||
this.base, | ||
OCC::cohesion, | ||
// @checkstyle MagicNumberCheck (1 line) | ||
new Colors(0.35d, 0.75d, true) | ||
).xembly(); | ||
} | ||
|
||
/** | ||
* Calculate OCC metric for a single Java class. | ||
* | ||
* @param ctc The .class file | ||
* @return Metrics | ||
*/ | ||
private static double cohesion(final CtClass ctc) { | ||
final List<String> ctmethods = Arrays.stream(ctc.getDeclaredMethods()) | ||
.map(CtMethod::getName) | ||
.collect(Collectors.toList()); | ||
final int number = ctmethods.size(); | ||
final double result; | ||
if (number == 1) { | ||
result = 0; | ||
} else { | ||
result = maxRw(ctc, ctmethods) / (number - 1); | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Maximum number of methods which are reachable. | ||
* | ||
* @param ctc The .class file | ||
* @param ctmethods Methods | ||
* @return The number | ||
*/ | ||
@SuppressWarnings({"PMD.UseVarargs", "PMD.CyclomaticComplexity", | ||
"PMD.StdCyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", | ||
"PMD.UseObjectForClearerAPI"}) | ||
private static double maxRw( | ||
final CtClass ctc, final List<String> ctmethods) { | ||
final ClassReader reader; | ||
try { | ||
reader = new ClassReader(ctc.toBytecode()); | ||
} catch (final IOException | CannotCompileException ex) { | ||
throw new IllegalStateException(ex); | ||
} | ||
final Map<String, Set<String>> vars = new HashMap<>(); | ||
final Map<String, Set<String>> methods = new HashMap<>(); | ||
final List<String> ctfields = Arrays.stream(ctc.getDeclaredFields()) | ||
.map(CtField::getName) | ||
.collect(Collectors.toList()); | ||
reader.accept( | ||
//@checkstyle AnonInnerLengthCheck (50 lines) | ||
new ClassVisitor(Opcodes.ASM6) { | ||
@Override | ||
public MethodVisitor visitMethod( | ||
final int access, final String mtd, final String desc, | ||
final String signature, final String[] exceptions) { | ||
super.visitMethod(access, mtd, desc, signature, exceptions); | ||
final Set<String> attrs = new HashSet<>(); | ||
final Set<String> names = new HashSet<>(); | ||
vars.put(mtd, attrs); | ||
methods.put(mtd, names); | ||
return new MethodVisitor(Opcodes.ASM6) { | ||
@Override | ||
public void visitFieldInsn( | ||
final int opcode, final String owner, | ||
final String attr, final String details) { | ||
super.visitFieldInsn(opcode, owner, attr, details); | ||
if (ctfields.contains(attr)) { | ||
attrs.add(attr); | ||
} | ||
} | ||
@Override | ||
public void visitMethodInsn( | ||
final int opcode, final String owner, | ||
final String name, final String desc, | ||
final boolean itf) { | ||
super.visitMethodInsn( | ||
opcode, owner, name, desc, itf | ||
); | ||
if (!name.equals(mtd) && ctmethods.contains(name)) { | ||
names.add(name); | ||
} | ||
} | ||
}; | ||
} | ||
}, | ||
0 | ||
); | ||
methods.forEach( | ||
(key, value) -> value.forEach( | ||
item -> vars.get(key).addAll(vars.get(item)) | ||
) | ||
); | ||
final int number = ctmethods.size(); | ||
final boolean[][] reachability = new boolean[number][number]; | ||
boolean[][] result = new boolean[number][number]; | ||
boolean[][] current = new boolean[number][number]; | ||
for (int row = 0; row < number; ++row) { | ||
for (int col = 0; col < number; ++col) { | ||
reachability[row][col] = hasWeakConnection( | ||
vars.get(ctmethods.get(row)), | ||
vars.get(ctmethods.get(col)) | ||
); | ||
current[row][col] = reachability[row][col]; | ||
result[row][col] = reachability[row][col]; | ||
} | ||
} | ||
for (int ind = 2; ind <= number; ++ind) { | ||
current = multiply(current, reachability, number); | ||
result = disjunction(current, result, number); | ||
} | ||
return maxWays(result, number); | ||
} | ||
|
||
/** | ||
* Check weak-connection of two method. | ||
* | ||
* @param first First method's vars | ||
* @param second Second method's vars | ||
* @return Result | ||
*/ | ||
private static boolean hasWeakConnection( | ||
final Set<?> first, final Set<?> second) { | ||
boolean result = false; | ||
for (final Object item : first) { | ||
if (second.contains(item)) { | ||
result = true; | ||
break; | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Multiply of two boolean matrix. | ||
* | ||
* @param first The first matrix | ||
* @param second The second matrix | ||
* @param size Matrix size | ||
* @return Result matrix | ||
*/ | ||
private static boolean[][] multiply( | ||
final boolean[][] first, final boolean[][] second, final int size) { | ||
final boolean[][] result = new boolean[size][size]; | ||
//@checkstyle NestedForDepthCheck (10 lines) | ||
for (int row = 0; row < size; ++row) { | ||
for (int col = 0; col < size; ++col) { | ||
for (int inner = 0; inner < size; ++inner) { | ||
result[row][col] = result[row][col] | ||
|| first[row][inner] && second[inner][col]; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* Disjunction of two boolean matrix. | ||
* | ||
* @param first The first matrix | ||
* @param second The second matrix | ||
* @param size Matrix size | ||
* @return Result matrix | ||
*/ | ||
private static boolean[][] disjunction( | ||
final boolean[][] first, final boolean[][] second, final int size) { | ||
final boolean[][] result = new boolean[size][size]; | ||
for (int row = 0; row < size; ++row) { | ||
for (int col = 0; col < size; ++col) { | ||
result[row][col] = first[row][col] || second[row][col]; | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* The maximum number of paths in the reachability matrix. | ||
* | ||
* @param array Matrix | ||
* @param size Size of matrix | ||
* @return The number | ||
*/ | ||
private static int maxWays(final boolean[][] array, final int size) { | ||
int result = 0; | ||
for (int row = 0; row < size; ++row) { | ||
int current = 0; | ||
for (int col = 0; col < size; ++col) { | ||
if (array[row][col] && row != col) { | ||
++current; | ||
} | ||
} | ||
if (result < current) { | ||
result = current; | ||
} | ||
} | ||
return result; | ||
} | ||
} |
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,98 @@ | ||
/** | ||
* The MIT License (MIT) | ||
* | ||
* Copyright (c) 2017 Yegor Bugayenko | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included | ||
* in all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
* SOFTWARE. | ||
*/ | ||
package org.jpeek.metrics.cohesion; | ||
|
||
import com.jcabi.matchers.XhtmlMatchers; | ||
import java.io.IOException; | ||
import java.nio.file.Paths; | ||
import org.hamcrest.MatcherAssert; | ||
import org.jpeek.DefaultBase; | ||
import org.jpeek.metrics.FakeBase; | ||
import org.junit.Test; | ||
import org.xembly.Xembler; | ||
|
||
/** | ||
* Test case for {@link OCC}. | ||
* | ||
* @author Vseslav Sekorin (vssekorin@gmail.com) | ||
* @version $Id$ | ||
* @since 0.2 | ||
* @checkstyle AbbreviationAsWordInNameCheck (5 lines) | ||
* @checkstyle JavadocMethodCheck (500 lines) | ||
*/ | ||
public final class OCCTest { | ||
|
||
@Test | ||
public void createsBigXmlReport() throws IOException { | ||
MatcherAssert.assertThat( | ||
XhtmlMatchers.xhtml( | ||
new Xembler( | ||
new OCC( | ||
new DefaultBase(Paths.get(".")) | ||
).xembly() | ||
).xmlQuietly() | ||
), | ||
XhtmlMatchers.hasXPaths( | ||
"/app/package/class[@id='OCCTest']", | ||
"//class[@id='Base' and @value='0.0000']" | ||
) | ||
); | ||
} | ||
|
||
@Test | ||
public void createsXmlReportForYellowClass() throws IOException { | ||
MatcherAssert.assertThat( | ||
XhtmlMatchers.xhtml( | ||
new Xembler( | ||
new OCC( | ||
new FakeBase("Test") | ||
).xembly() | ||
).xmlQuietly() | ||
), | ||
XhtmlMatchers.hasXPaths( | ||
"/app/package/class[@id='Test']", | ||
"//class[@id='Test' and @value='0.5000']", | ||
"//class[@id='Test' and @color='yellow']" | ||
) | ||
); | ||
} | ||
|
||
@Test | ||
public void createsXmlReportForRedClass() throws IOException { | ||
MatcherAssert.assertThat( | ||
XhtmlMatchers.xhtml( | ||
new Xembler( | ||
new OCC( | ||
new FakeBase("C") | ||
).xembly() | ||
).xmlQuietly() | ||
), | ||
XhtmlMatchers.hasXPaths( | ||
"/app/package/class[@id='C']", | ||
"//class[@id='C' and @value='1.0000']", | ||
"//class[@id='C' and @color='red']" | ||
) | ||
); | ||
} | ||
} |
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,19 @@ | ||
public class C { | ||
private int a1, a2; | ||
|
||
public void m1(int x) { | ||
this.a1 = x; | ||
} | ||
|
||
public void m2(int y) { | ||
this.a2 = y; | ||
} | ||
|
||
public int m3() { | ||
return this.a1 + this.a2; | ||
} | ||
|
||
public void m4() { | ||
this.m1(0); | ||
} | ||
} |
Oops, something went wrong.