Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ These papers provide a pretty good summary of cohesion metrics:
Jehad Al Dallal, _Transitive-based object-oriented lack-of-cohesion metric_,
Department of Information Science, Kuwait University, 2011.
[PDF](papers/dallal11_TLCOM.pdf).
* `yegor256`: Distance of Coupling (**DOC**).
Yegor Bugayenko, _Distance of Coupling_,
[Blog](https://www.yegor256.com/2020/10/27/distance-of-coupling.html).

## How it works?

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/jpeek/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public App(final Path source, final Path target) {
new MapEntry<>("TCC", true),
new MapEntry<>("LCC", true),
new MapEntry<>("CCM", true),
new MapEntry<>("MWE", true)
new MapEntry<>("MWE", true),
new MapEntry<>("DOC", true)
)
);
}
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/org/jpeek/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ public enum Metrics {
/**
* Maximal Weighted Entropy metric. Modeling class cohesion as mixtures of latent topics
*/
MWE(false, null, null);
MWE(false, null, null),

/**
* Distance of Coupling metric.
*/
DOC(true, null, null);

/**
* Indicates whether parameters should be included for this metric.
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/org/jpeek/skeleton/OpsOf.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/
package org.jpeek.skeleton;

import java.util.Arrays;
import java.util.List;
import org.cactoos.Text;
import org.cactoos.text.Split;
import org.cactoos.text.Sub;
Expand Down Expand Up @@ -85,4 +87,52 @@ public void visitMethodInsn(final int opcode,
}
this.target.up().up().up();
}

@Override
public void visitVarInsn(final int opcode, final int var) {
super.visitVarInsn(opcode, var);
final List<Integer> loads = Arrays.asList(
Opcodes.ILOAD,
Opcodes.LLOAD,
Opcodes.FLOAD,
Opcodes.DLOAD,
Opcodes.ALOAD
);
final List<Integer> stores = Arrays.asList(
Opcodes.ISTORE,
Opcodes.LSTORE,
Opcodes.FSTORE,
Opcodes.DSTORE,
Opcodes.ASTORE
);
if (loads.contains(opcode)) {
this.local(opcode, var, "load");
} else if (stores.contains(opcode)) {
this.local(opcode, var, "store");
}
}

/**
* Record local variable instruction.
* @param opcode Opcode
* @param var Local variable index
* @param code Op code label
*/
private void local(
final int opcode,
final int var,
final String code
) {
this.target
.strict(1)
.addIf("locals")
.add("var");
this.target
.attr("code", code)
.attr("var", var)
.attr("opcode", opcode)
.set(var)
.up()
.up();
}
}
61 changes: 61 additions & 0 deletions src/main/resources/org/jpeek/metrics/DOC.xsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2017-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="skeleton">
<metric>
<xsl:apply-templates select="@*"/>
<title>DOC</title>
<description>
<xsl:text>
DOC (Distance of Coupling) estimates how many data
transformation steps happen inside methods. It is calculated as
the average number of local-variable stores that depend on
previous local-variable values. For each method, we count how many
times a local variable is assigned after a load from any previously stored local.
The class value is the arithmetic average across its methods.
</xsl:text>
</description>
<xsl:apply-templates select="node()"/>
</metric>
</xsl:template>
<xsl:template match="class">
<xsl:variable name="methods" select="methods/method[@ctor = 'false' or count(ops/op[@code != 'call' or (@code = 'call' and not(matches(name, '\.&lt;(cl)?init&gt;$')))]) &gt; 0]"/>
<xsl:variable name="docs">
<xsl:for-each select="$methods">
<xsl:variable name="stores" select="count(locals/var[@code='store'][preceding-sibling::var[@code='load' and @var = preceding-sibling::var[@code='store']/@var]])"/>
<doc>
<xsl:value-of select="$stores"/>
</doc>
</xsl:for-each>
</xsl:variable>
<xsl:copy>
<xsl:attribute name="value">
<xsl:choose>
<xsl:when test="count($methods) &gt; 0">
<xsl:value-of select="sum($docs/doc) div count($methods)"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>0</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates select="@*"/>
<vars>
<var id="methods">
<xsl:value-of select="count($methods)"/>
</var>
<var id="stores">
<xsl:value-of select="count($methods/locals/var[@code='store'][preceding-sibling::var[@code='load' and @var = preceding-sibling::var[@code='store']/@var]])"/>
</var>
</vars>
</xsl:copy>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
40 changes: 40 additions & 0 deletions src/main/resources/org/jpeek/xsd/skeleton.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,46 @@
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="locals" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Local variable operations inside the method. These
entries are separate from &lt;ops&gt; to avoid impacting
existing metrics that rely on field access and calls.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="var" minOccurs="0" maxOccurs="unbounded">
<xs:complexType mixed="true">
<xs:attribute name="code" use="required" type="xs:string">
<xs:annotation>
<xs:documentation>
Value depends on what the instruction is doing. Currently:
- "load": if a local variable is loaded
- "store": if a local variable is stored
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="var" type="xs:int" use="required">
<xs:annotation>
<xs:documentation>
Index of the local variable used by the instruction.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="opcode" type="xs:int" use="required">
<xs:annotation>
<xs:documentation>
Raw JVM opcode integer for the instruction.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="topics" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Expand Down
30 changes: 30 additions & 0 deletions src/test/java/org/jpeek/metrics/DocTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2017-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
*/
package org.jpeek.metrics;

import org.junit.jupiter.api.Test;

/**
* Tests for DOC metric.
* @since 0.30
*/
final class DocTest {

/**
* Ensures DOC counts local variable stores.
* @throws Exception If fails
*/
@Test
void countsVariableStores() throws Exception {
final MetricBase.Report report = new MetricBase(
"org/jpeek/metrics/DOC.xsl"
).transform(
"DocDistance"
);
report.assertVariable("methods", 1);
report.assertVariable("stores", 3);
report.assertValue(3f, 0.001f);
}
}
19 changes: 19 additions & 0 deletions src/test/java/org/jpeek/skeleton/SkeletonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ void createsXml() {
).affirm();
}

@Test
void capturesLocalVariableOps() {
new Assertion<>(
"Must capture local variable ops with indexes",
XhtmlMatchers.xhtml(
new Skeleton(
new FakeBase("DocDistance")
).xml().toString()
),
XhtmlMatchers.hasXPaths(
// @checkstyle LineLength (4 lines)
"//class[@id='DocDistance']//method[@name='docThree']/locals/var[@code='store' and @var='1']",
"//class[@id='DocDistance']//method[@name='docThree']/locals/var[@code='store' and @var='2']",
"//class[@id='DocDistance']//method[@name='docThree']/locals/var[@code='store' and @var='3']",
"//class[@id='DocDistance']//method[@name='docThree']/locals/var[@code='store' and @var='4']"
)
).affirm();
}

@Test
void skeletonShouldReflectExactOverloadedCalledMethod() {
new Assertion<>(
Expand Down
22 changes: 22 additions & 0 deletions src/test/resources/org/jpeek/samples/DocDistance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2017-2026 Yegor Bugayenko
* SPDX-License-Identifier: MIT
*/
public final class DocDistance {

public int docThree() {
final Temperature temp = new Temperature();
final String txt = temp.toString();
final String[] parts = txt.split(" ");
final int t = Integer.parseInt(parts[0]);
return t;
}
}

final class Temperature {

@Override
public String toString() {
return "10 C";
}
}