Repository: ishepard/pydriller
Pull request #124 (2020-05-19 15:06:48)

Properly handle zero-change modifications in DMM
Let the DMM proportion of good over (good+bad) change yield "None"
if the (good+bad) change equals zero (until now, 1.0 was returned instead).

Also undo the special treatment of the case in which delta_high < 0 and delta_high = 0, to make the pydriller implementation fully consisten with the published paper.

Update documentation and tests accordingly.


Patch:
diff --git a/docs/deltamaintainability.rst b/docs/deltamaintainability.rst
index d664952..33a8d11 100644
--- a/docs/deltamaintainability.rst
+++ b/docs/deltamaintainability.rst
@@ -29,6 +29,7 @@ The delta risk profile can then be used to determine good and bad change:
 - Decreases in high risk code are good, and decreases in low risk code are good only if no high risk code is added instead, and bad otherwise.
 
 The dmm value is then computed as: *good change / (good change + bad change)*.
+It is undefined (``None``) if the denominator is zero.
 
 .. _Properties:
 
@@ -82,7 +83,7 @@ PyDriller's OS-DMM and SIG's DMM differ in the following ways:
 
 - OS-DMM offers only support for the approximately 15 languages supported by `Lizard <https://github.com/terryyin/lizard>`_.
 - OS-DMM relies on Lizard for the identification of *methods* (units) in source files. While for simple cases SIG and Lizard tooling will agree, this may not be the case for more intricate cases involving e.g., lambdas, inner classes, nested functions, etc.
-- OS-DMM relies on Lizard for simple line counting, which also counts white space. SIG's DMM on the other hand ignores blank lines.
+- OS-DMM relies on Lizard for simple line counting, which also counts white space. SIG's DMM on the other hand ignores lines that contain no statements (such as blank lines or lines with just ``{...}`` brackets)
 - OS-DMM uses the thresholds as empirically determined by SIG, based on SIG's measurement methodology [Alves2010]_. OS-DMM's Lizard-based metric values may be different, and hence may classify methods in different risk bins for methods close to the thresholds.
 
 Consequently, individual DMM values are likely to differ a few percentage points between the SIG DMM and OS-DMM implementations. However, in terms of trends and statistical analysis, the outcomes will likely be very similar.
diff --git a/pydriller/domain/commit.py b/pydriller/domain/commit.py
index 9f52d63..e8bcd87 100644
--- a/pydriller/domain/commit.py
+++ b/pydriller/domain/commit.py
@@ -719,7 +719,6 @@ class Commit:
         The DMM value is computed as the proportion of good change in the commit:
         Good changes: Adding low risk code or removing high risk codee.
         Bad changes: Adding high risk code or removing low risk code.
-        Furthermore, the special case where only low risk code is removed is also given the highest score.
 
         :param dmm_prop: Property indicating the type of risk
         :return: dmm value (between 0.0 and 1.0) for the property represented in the property.
@@ -727,14 +726,7 @@ class Commit:
         delta_profile = self._delta_risk_profile(dmm_prop)
         if delta_profile:
             (delta_low, delta_high) = delta_profile
-            dmm = self._good_change_proportion(delta_low, delta_high)
-            if delta_low < 0 and delta_high == 0:
-                assert dmm == 0.0
-                # special case where removal of good (low risk) code is OK.
-                # re-adjust dmm accordingly:
-                dmm = 1.0
-            assert 0.0 <= dmm <= 1.0
-            return dmm
+            return self._good_change_proportion(delta_low, delta_high)
         return None
 
     def _delta_risk_profile(self, dmm_prop: DMMProperty) -> Optional[Tuple[int, int]]:
@@ -755,13 +747,13 @@ class Commit:
         return None
 
     @staticmethod
-    def _good_change_proportion(low_risk_delta: int, high_risk_delta: int) -> float:
+    def _good_change_proportion(low_risk_delta: int, high_risk_delta: int) -> Optional[float]:
         """
         Given a delta risk profile, compute the proportion of "good" change in the total change.
         Increasing low risk code, or decreasing high risk code, is considered good.
         Other types of changes are considered not good.
 
-        :return: proportion of good change in total change.
+        :return: proportion of good change in total change, or None if the total change is zero.
         """
         bad_change, good_change = (0, 0)
 
@@ -777,10 +769,13 @@ class Commit:
         assert good_change >= 0 and bad_change >= 0
 
         total_change = good_change + bad_change
-        result = good_change / total_change if total_change > 0 else 1.0
-        assert 0.0 <= result <= 1.0
+        if total_change == 0:
+            proportion = None
+        else:
+            proportion = good_change / total_change
+            assert 0.0 <= proportion <= 1.0
 
-        return result
+        return proportion
 
     # pylint disable=R0902
     @staticmethod


Test patch:
diff --git a/tests/test_dmm.py b/tests/test_dmm.py
index 4bb0767..b8cbf81 100644
--- a/tests/test_dmm.py
+++ b/tests/test_dmm.py
@@ -44,14 +44,14 @@ UNIT_SIZE_TEST_DATA = [
     # delta high > 0, delta low < 0 --- always DMM 0.0
     ('Make large larger, make small smaller', 0.0),
 
-    # delta high = 0, delta low = 0 --- always DMM 1.0
-    ('Modify every line in large method', 1.0),
+    # delta high = 0, delta low = 0 --- no delta-changes, dmm None
+    ('Modify every line in large method', None),
 
     # delta high = 0, delta low > 0 --- always DMM 1.0
     ('Make small method a bit larger', 1.0),
 
-    # delta high = 0, delta low < 0 --- alwyas DMM 1.0 (corner case)
-    ('Make small smaller', 1.0),
+    # delta high = 0, delta low < 0 --- alwyas DMM 0.0
+    ('Make small smaller', 0.0),
 
     # delta high < 0, delta low < 0 --- DMM = ratio
     ('Make large smaller, make small smaller', 2/3),
@@ -87,8 +87,8 @@ UNIT_INTERFACING_TEST_DATA = [
     # Large method, but no parameters
     ('Commit with one large method', 1.0),
 
-    # Method with nr of paramters exactly on the border
-    ('Add method with interfacing on-point', 1.0),
+    # Adjust method with nr of paramters exactly on the border, same size
+    ('Add method with interfacing on-point', None),
 
     # Method with nr of parameters at off point
     ('Increase interfacing to risky', 0.0)
@@ -122,9 +122,9 @@ def test_unsupported_language(repo: GitRepository):
     assert  commit.dmm_unit_size is None
 
 def test_mixin_unsupported_language(repo: GitRepository):
-    # Add .txt file and update .java files
+    # Add .txt file and update (comments in) .java files
     commit = commit_by_msg(repo, 'Release under Apache 2 license')
-    assert  commit.dmm_unit_size == 1.0
+    assert  commit.dmm_unit_size == None
 
 def test_delta_profile_modification(repo: GitRepository):
     commit = commit_by_msg(repo, 'Increase unit size to risky')
@@ -149,7 +149,7 @@ def test_supported_languages(repo: GitRepository):
 
 @pytest.mark.parametrize(
     'dlo,dhi,prop', [
-    ( 0,  0, 1.0),
+    ( 0,  0, None),
     ( 1,  0, 1.0),
     (-1,  0, 0.0),
     ( 0,  1, 0.0),