From ca596b50aa1d374cd1cf70d616c619cdc6b9ae65 Mon Sep 17 00:00:00 2001
From: parinaz-st
Date: Wed, 27 May 2026 21:56:48 +0330
Subject: [PATCH] Added support for uncommented Block detection and comment
statement transition detection for matched blocks
---
.../BlockTrackerChangeHistory.java | 82 ++++---
.../java/org/codetracker/change/Change.java | 6 +-
.../org/codetracker/change/ChangeFactory.java | 13 ++
.../change/block/CommentedOutCodeInBlock.java | 13 ++
.../change/block/UncommentedBlock.java | 13 ++
.../change/block/UncommentedCodeInBlock.java | 15 ++
.../util/CommentTransitionAnalyzer.java | 213 ++++++++++++++++++
7 files changed, 319 insertions(+), 36 deletions(-)
create mode 100644 src/main/java/org/codetracker/change/block/CommentedOutCodeInBlock.java
create mode 100644 src/main/java/org/codetracker/change/block/UncommentedBlock.java
create mode 100644 src/main/java/org/codetracker/change/block/UncommentedCodeInBlock.java
create mode 100644 src/main/java/org/codetracker/util/CommentTransitionAnalyzer.java
diff --git a/src/main/java/org/codetracker/BlockTrackerChangeHistory.java b/src/main/java/org/codetracker/BlockTrackerChangeHistory.java
index bf01484a079..e7ad033fda5 100644
--- a/src/main/java/org/codetracker/BlockTrackerChangeHistory.java
+++ b/src/main/java/org/codetracker/BlockTrackerChangeHistory.java
@@ -2,17 +2,10 @@
import java.io.IOException;
import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
import java.util.function.Predicate;
-
+import gr.uom.java.xmi.*;
+import gr.uom.java.xmi.diff.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.codetracker.api.History;
@@ -37,6 +30,7 @@
import org.codetracker.change.method.BodyChange;
import org.codetracker.element.Block;
import org.codetracker.element.Method;
+import org.codetracker.util.Util;
import org.refactoringminer.api.Refactoring;
import com.github.difflib.DiffUtils;
@@ -46,11 +40,7 @@
import com.github.difflib.patch.InsertDelta;
import com.github.difflib.patch.Patch;
-import gr.uom.java.xmi.UMLOperation;
-import gr.uom.java.xmi.UMLType;
-import gr.uom.java.xmi.VariableDeclarationContainer;
import gr.uom.java.xmi.LocationInfo.CodeElementType;
-import gr.uom.java.xmi.UMLAnonymousClass;
import gr.uom.java.xmi.decomposition.AbstractCodeFragment;
import gr.uom.java.xmi.decomposition.AbstractCodeMapping;
import gr.uom.java.xmi.decomposition.AbstractExpression;
@@ -63,23 +53,9 @@
import gr.uom.java.xmi.decomposition.TryStatementObject;
import gr.uom.java.xmi.decomposition.UMLOperationBodyMapper;
import gr.uom.java.xmi.decomposition.VariableDeclaration;
-import gr.uom.java.xmi.diff.ExtractOperationRefactoring;
-import gr.uom.java.xmi.diff.InlineOperationRefactoring;
-import gr.uom.java.xmi.diff.MergeOperationRefactoring;
-import gr.uom.java.xmi.diff.MoveCodeRefactoring;
-import gr.uom.java.xmi.diff.MoveOperationRefactoring;
-import gr.uom.java.xmi.diff.PullUpOperationRefactoring;
-import gr.uom.java.xmi.diff.PushDownOperationRefactoring;
-import gr.uom.java.xmi.diff.RenameOperationRefactoring;
-import gr.uom.java.xmi.diff.ReplaceAnonymousWithClassRefactoring;
-import gr.uom.java.xmi.diff.ReplaceAnonymousWithLambdaRefactoring;
-import gr.uom.java.xmi.diff.ReplaceConditionalWithTernaryRefactoring;
-import gr.uom.java.xmi.diff.ReplaceLoopWithPipelineRefactoring;
-import gr.uom.java.xmi.diff.ReplacePipelineWithLoopRefactoring;
-import gr.uom.java.xmi.diff.SplitConditionalRefactoring;
-import gr.uom.java.xmi.diff.SplitOperationRefactoring;
-import gr.uom.java.xmi.diff.UMLAbstractClassDiff;
-import gr.uom.java.xmi.diff.UMLAnonymousClassDiff;
+
+import static org.codetracker.util.CommentTransitionAnalyzer.*;
+
public class BlockTrackerChangeHistory extends AbstractChangeHistory {
private final ChangeHistory blockChangeHistory = new ChangeHistory<>();
@@ -441,7 +417,7 @@ else if (mapping instanceof LeafMapping && mapping.getFragment1() instanceof Sta
break;
}
*/
- case EXTRACT_FIXTURE:
+// case EXTRACT_FIXTURE: Update RM Version
case MOVE_CODE: {
MoveCodeRefactoring moveCodeRefactoring = (MoveCodeRefactoring) refactoring;
Method extractedMethod = Method.of(moveCodeRefactoring.getTargetContainer(), currentVersion);
@@ -693,9 +669,38 @@ public boolean checkBodyOfMatchedOperations(Version currentVersion, Version pare
// check if it is in the matched
if (isMatched(umlOperationBodyMapper, currentVersion, parentVersion, equalOperator))
return true;
- //Check if is added
+ //check if block got uncommented
+ if(isBlockUncommented(umlOperationBodyMapper, currentVersion, parentVersion, equalOperator)){ //can get renamed to detectUnmatchedBlockResurrection(for detection only. this method can decide)
+ return true;
+ }
+// Check if is added
return isAdded(umlOperationBodyMapper, currentVersion, parentVersion, equalOperator);
}
+ public boolean isBlockUncommented(UMLOperationBodyMapper umlOperationBodyMapper, Version currentVersion, Version parentVersion, Predicate equalOperator) {
+ List currentNonMappedInnerNodes = umlOperationBodyMapper.getNonMappedInnerNodesT2();
+ UMLCommentListDiff umlCommentListDiff = umlOperationBodyMapper.getCommentListDiff();
+ List deletedComments = umlCommentListDiff.getDeletedComments();
+ if(currentNonMappedInnerNodes.size() == 0 || deletedComments == null || deletedComments.isEmpty()) // early return
+ return false;
+ Map deletedCommentsHashMap = generateCommentTextHashMap(deletedComments);
+ for(CompositeStatementObject nonMappedInnerNode : currentNonMappedInnerNodes) {
+ String hashOfNonMappedInnerNode = Util.getSHA512(nonMappedInnerNode.getActualSignature());
+ if (!deletedCommentsHashMap.containsKey(hashOfNonMappedInnerNode))
+ continue;
+ Block blockAfter = Block.of(nonMappedInnerNode, umlOperationBodyMapper.getContainer2(), currentVersion);//Composite expression found in deleted comments. block expression resurrection
+ if (!equalOperator.test(blockAfter))
+ continue;
+ //check block body continuity
+ if (!isBlockBodyUnchangedDuringUncomment(umlOperationBodyMapper, nonMappedInnerNode, deletedCommentsHashMap, blockAfter))
+ continue;
+ Block virtualBlockBefore = Block.of(nonMappedInnerNode, umlOperationBodyMapper.getContainer2(), parentVersion); // add metadata, flag isVirtual = true; this is the heuristic! building the old block with current container
+ blockChangeHistory.addChange(virtualBlockBefore, blockAfter, ChangeFactory.forBlock(Change.Type.UNCOMMENTED_BLOCK));
+ elements.add(virtualBlockBefore);
+ blockChangeHistory.connectRelatedNodes();
+ return true;
+ }
+ return false;
+ }
public boolean isBlockRefactored(Collection refactorings, Version currentVersion, Version parentVersion, Predicate equalOperator) {
Set leftBlockSet = analyseBlockRefactorings(refactorings, currentVersion, parentVersion, equalOperator);
@@ -1137,7 +1142,16 @@ public boolean isMatched(UMLOperationBodyMapper umlOperationBodyMapper, Version
List stringRepresentationBodyBefore = stringRepresentationBefore.subList(1, stringRepresentationBefore.size());
List stringRepresentationBodyAfter = stringRepresentationAfter.subList(1, stringRepresentationAfter.size());
if (!stringRepresentationBodyBefore.equals(stringRepresentationBodyAfter)) {
- blockChangeHistory.addChange(blockBefore, blockAfter, ChangeFactory.forBlock(Change.Type.BODY_CHANGE));
+ Set commentTransitions = DetectCommentTransitionsInsideMatchedBlock(umlOperationBodyMapper, blockBefore, blockAfter, currentVersion, parentVersion);
+ if(!commentTransitions.isEmpty()){
+ for(Change.Type changeType : commentTransitions){
+ blockChangeHistory.addChange(blockBefore, blockAfter, ChangeFactory.forBlock(changeType));
+ }
+ }
+ else{
+ blockChangeHistory.addChange(blockBefore, blockAfter, ChangeFactory.forBlock(Change.Type.BODY_CHANGE));
+ }
+
}
bodyChange = true;
}
diff --git a/src/main/java/org/codetracker/change/Change.java b/src/main/java/org/codetracker/change/Change.java
index 0eca191d9b1..e3ffd031dd2 100644
--- a/src/main/java/org/codetracker/change/Change.java
+++ b/src/main/java/org/codetracker/change/Change.java
@@ -56,8 +56,10 @@ enum Type {
INITIALIZER_ADDED("initializer added"),
INITIALIZER_REMOVED("initializer removed"),
SUPERCLASS_CHANGE("superclass change"),
- INTERFACE_LIST_CHANGE("interface list change");
-
+ INTERFACE_LIST_CHANGE("interface list change"),
+ COMMENTED_OUT_STATEMENT("commented out statement"),
+ UNCOMMENTED_STATEMENT("uncommented statement"),
+ UNCOMMENTED_BLOCK("uncommented Block");
private static final Map lookup = new HashMap<>();
static {
diff --git a/src/main/java/org/codetracker/change/ChangeFactory.java b/src/main/java/org/codetracker/change/ChangeFactory.java
index 405b85aa30d..78cdf3ad153 100644
--- a/src/main/java/org/codetracker/change/ChangeFactory.java
+++ b/src/main/java/org/codetracker/change/ChangeFactory.java
@@ -141,6 +141,19 @@ public AbstractChange build() {
change = new BodyChange();
break;
}
+ case UNCOMMENTED_STATEMENT:{
+ change = new UncommentedCodeInBlock();
+ break;
+ }
+ case COMMENTED_OUT_STATEMENT:{
+ change = new CommentedOutCodeInBlock();
+ break;
+ }
+ case UNCOMMENTED_BLOCK:{
+ change = new UncommentedBlock();
+ break;
+ }
+
case CATCH_BLOCK_CHANGE: {
change = new CatchBlockChange();
break;
diff --git a/src/main/java/org/codetracker/change/block/CommentedOutCodeInBlock.java b/src/main/java/org/codetracker/change/block/CommentedOutCodeInBlock.java
new file mode 100644
index 00000000000..f7469135110
--- /dev/null
+++ b/src/main/java/org/codetracker/change/block/CommentedOutCodeInBlock.java
@@ -0,0 +1,13 @@
+package org.codetracker.change.block;
+
+public class CommentedOutCodeInBlock extends BlockBodyChange {
+ public CommentedOutCodeInBlock() {
+ super(Type.COMMENTED_OUT_STATEMENT);
+ }
+
+ @Override
+ public String toString() {
+ return "Body Change, Contains Commented-out Code";
+ }
+
+}
diff --git a/src/main/java/org/codetracker/change/block/UncommentedBlock.java b/src/main/java/org/codetracker/change/block/UncommentedBlock.java
new file mode 100644
index 00000000000..df8c71c3620
--- /dev/null
+++ b/src/main/java/org/codetracker/change/block/UncommentedBlock.java
@@ -0,0 +1,13 @@
+package org.codetracker.change.block;
+
+public class UncommentedBlock extends BlockChange {
+ public UncommentedBlock() {
+ super(Type.UNCOMMENTED_BLOCK);
+ }
+
+ @Override
+ public String toString() {
+ return "Block Change, Uncommented Block";
+ }
+
+}
diff --git a/src/main/java/org/codetracker/change/block/UncommentedCodeInBlock.java b/src/main/java/org/codetracker/change/block/UncommentedCodeInBlock.java
new file mode 100644
index 00000000000..9f940f99931
--- /dev/null
+++ b/src/main/java/org/codetracker/change/block/UncommentedCodeInBlock.java
@@ -0,0 +1,15 @@
+package org.codetracker.change.block;
+
+import org.codetracker.change.Change;
+
+public class UncommentedCodeInBlock extends BlockBodyChange {
+ public UncommentedCodeInBlock() {
+ super(Change.Type.UNCOMMENTED_STATEMENT);
+ }
+
+ @Override
+ public String toString() {
+ return "Body Change, Contains Uncommented Code";
+ }
+
+}
diff --git a/src/main/java/org/codetracker/util/CommentTransitionAnalyzer.java b/src/main/java/org/codetracker/util/CommentTransitionAnalyzer.java
new file mode 100644
index 00000000000..60b08efa671
--- /dev/null
+++ b/src/main/java/org/codetracker/util/CommentTransitionAnalyzer.java
@@ -0,0 +1,213 @@
+package org.codetracker.util;
+
+import gr.uom.java.xmi.LocationInfo;
+import gr.uom.java.xmi.UMLComment;
+import gr.uom.java.xmi.decomposition.*;
+import org.codetracker.api.Version;
+import org.codetracker.change.Change;
+import org.codetracker.element.Block;
+
+import java.util.*;
+
+public class CommentTransitionAnalyzer {
+
+ public static Set DetectCommentTransitionsInsideMatchedBlock(UMLOperationBodyMapper umlOperationBodyMapper, Block blockBefore, Block blockAfter, Version currentVersion, Version parentVersion) {
+ Set changeTypes = new HashSet<>();
+ List deletedComments = umlOperationBodyMapper.getCommentListDiff().getDeletedComments();
+ List addedComments = umlOperationBodyMapper.getCommentListDiff().getAddedComments();
+ Map deletedCommentsHash;
+ Map addedCommentsHash;
+
+ if (!deletedComments.isEmpty()) {
+ deletedCommentsHash = generateCommentTextHashMap(deletedComments);
+ if (isUncommentedCodeDetected(deletedCommentsHash, umlOperationBodyMapper, blockAfter))
+ changeTypes.add(Change.Type.UNCOMMENTED_STATEMENT);
+ }
+ if (!addedComments.isEmpty()) {
+ addedCommentsHash = generateCommentTextHashMap(addedComments);
+ if (isCommentedOutCodeDetected(addedCommentsHash, umlOperationBodyMapper, blockBefore)) {
+ changeTypes.add(Change.Type.COMMENTED_OUT_STATEMENT);
+ }
+ }
+ return changeTypes;
+ }
+
+ private static boolean isUncommentedCodeDetected(Map deletedCommentsHash, UMLOperationBodyMapper umlOperationBodyMapper, Block blockAfter) {
+ if (deletedCommentsHash.isEmpty())
+ return false;
+ // find unmatched leaf statements afterBlock in deleted comments
+ if (!umlOperationBodyMapper.getNonMappedLeavesT2().isEmpty()) {
+ for (AbstractCodeFragment abstractCodeFragment : umlOperationBodyMapper.getNonMappedLeavesT2()) {
+ if (blockAfter.getComposite().getLeaves().contains(abstractCodeFragment)) { //if nonmappedLeave actually belongs to the target Block
+ String codeFragmentHash = Util.getSHA512(abstractCodeFragment.getString().trim());
+ if (deletedCommentsHash.containsKey(codeFragmentHash)) {
+ return true;
+ }
+ }
+ }
+ }
+ //find unmatched composit statements afterBlock in deleted comments
+ if (!umlOperationBodyMapper.getNonMappedInnerNodesT2().isEmpty()) {
+ for (CompositeStatementObject compositeStatementObject : umlOperationBodyMapper.getNonMappedInnerNodesT2()) {
+ if (((CompositeStatementObject) blockAfter.getComposite()).contains(compositeStatementObject)) { //if nonMappedInnerNode actually belongs to the current block
+ String codeFragmentHash = Util.getSHA512(compositeStatementObject.getActualSignature());
+ if (deletedCommentsHash.containsKey(codeFragmentHash)) {
+ return true;
+ }
+ }
+ }
+ }
+ // RefactoringMiner may create leaf mappings between semantically different statements when textual similarity is high enough,
+ // even when one side is actually a commented/uncommented transition.
+ if (!umlOperationBodyMapper.getMappings().isEmpty()) {
+ for (AbstractCodeMapping mapping : umlOperationBodyMapper.getMappings()) {
+ if (!(mapping instanceof LeafMapping)) // does not happen in composite(i guess)
+ continue;
+ if (!isMappingBelongToTrackedBlock(mapping, blockAfter, true))
+ continue;
+ //this issue only happens with leaf statements and not composites....
+ String codeFragmentHashCurrent = Util.getSHA512(mapping.getFragment2().getString().trim());
+ if (deletedCommentsHash.containsKey(codeFragmentHashCurrent)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isCommentedOutCodeDetected(Map addedCommentsHash, UMLOperationBodyMapper umlOperationBodyMapper, Block blockBefore) {
+ if (addedCommentsHash.isEmpty())
+ return false;
+ if (!umlOperationBodyMapper.getNonMappedLeavesT1().isEmpty()) {
+ //find unmatched leaf statements parent1 in added comments
+ for (AbstractCodeFragment abstractCodeFragment : umlOperationBodyMapper.getNonMappedLeavesT1()) {
+ if (blockBefore.getComposite().getLeaves().contains(abstractCodeFragment)) { // if nonMappedleaf actually belongs to parent block
+ String codeFragmentHash = Util.getSHA512(abstractCodeFragment.getString().trim());
+ if (addedCommentsHash.containsKey(codeFragmentHash)) {
+ return true;
+ }
+ }
+ }
+ }
+ if (!umlOperationBodyMapper.getNonMappedInnerNodesT1().isEmpty()) {
+ //find unmatched composit statements parent1 in added comments
+ for (CompositeStatementObject compositeStatementObject : umlOperationBodyMapper.getNonMappedInnerNodesT1()) {
+ if (((CompositeStatementObject) blockBefore.getComposite()).contains(compositeStatementObject)) { // if nonMappedInnerNode actually belongs to parent block
+ if (!isBracketAndBelongsToStandAloneBlock(compositeStatementObject))
+ continue;
+ String codeFragmentHash = Util.getSHA512(compositeStatementObject.getActualSignature());
+ if (addedCommentsHash.containsKey(codeFragmentHash)) {
+ return true;
+ }
+ }
+ }
+ }
+ // RefactoringMiner may create leaf mappings between semantically different statements when textual similarity is high enough,
+ // even when one side is actually a commented/uncommented transition.
+ if (!umlOperationBodyMapper.getMappings().isEmpty()) {
+ for (AbstractCodeMapping mapping : umlOperationBodyMapper.getMappings()) {
+ if (!(mapping instanceof LeafMapping)) // does not happen in composite(i guess)
+ continue;
+ if (!isMappingBelongToTrackedBlock(mapping, blockBefore, false))
+ continue;
+ //this issue only happens with leaf statements and not composites....
+ String codeFragmentHashParent = Util.getSHA512(mapping.getFragment1().getString().trim());
+ if (!addedCommentsHash.isEmpty() && addedCommentsHash.containsKey(codeFragmentHashParent)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ public static boolean isBlockBodyUnchangedDuringUncomment (UMLOperationBodyMapper umlOperationBodyMapper, CompositeStatementObject uncommentedBlock, Map deletedComments, Block blockAfter) {
+ Set mappedFragments = new HashSet<>();
+ for (AbstractCodeMapping mapping : umlOperationBodyMapper.getMappings()) {
+ if(!isMappingBelongToTrackedBlock(mapping, blockAfter, true))
+ continue;
+ if(mapping instanceof CompositeStatementObjectMapping) {
+ CompositeStatementObject composite = (CompositeStatementObject) mapping.getFragment2();
+ if (!isBracketAndBelongsToStandAloneBlock(composite))
+ continue;
+ mappedFragments.add(Util.getSHA512(composite.getActualSignature()));
+ }
+ else if(mapping instanceof LeafMapping && mapping.getFragment2() instanceof StatementObject){ // mappings belong to the currentblock
+ mappedFragments.add(Util.getSHA512(mapping.getFragment2().getString().trim()));
+ }
+ }
+ for (CompositeStatementObject inner : uncommentedBlock.getInnerNodes()) {
+ if (!isBracketAndBelongsToStandAloneBlock(inner)) {
+ continue;
+ }
+ String hash = Util.getSHA512(inner.getActualSignature().trim());
+ if(!existsAsMappedOrComment(hash, mappedFragments, deletedComments)){
+ return false;
+ }
+ }
+ for (AbstractCodeFragment leaf : uncommentedBlock.getLeaves()) {
+ String hash = Util.getSHA512(((StatementObject) leaf).getActualSignature());
+ if(!existsAsMappedOrComment(hash, mappedFragments, deletedComments))
+ return false;
+ }
+ return true;
+ }
+
+ private static Change.Type partialBlockUncommented(CompositeStatementObject compositeStatementObject, Block blockBefore, Block blockAfter) {
+ //TODO: else/catch/finally uncommented support should be added
+ if (blockBefore.getComposite().getLocationInfo().getCodeElementType().equals(LocationInfo.CodeElementType.IF_STATEMENT) &&
+ blockAfter.getComposite().getLocationInfo().getCodeElementType().equals(LocationInfo.CodeElementType.IF_STATEMENT)) {
+ CompositeStatementObject ifBefore = (CompositeStatementObject) blockBefore.getComposite();
+ CompositeStatementObject ifAfter = (CompositeStatementObject) blockAfter.getComposite();
+ if (ifBefore.getStatements().size() == 1 && ifAfter.getStatements().size() == 2) {
+ return Change.Type.UNCOMMENTED_STATEMENT;
+ }
+ }
+ return null;
+ }
+
+ public static boolean isBracketAndBelongsToStandAloneBlock(CompositeStatementObject compositeStatementObject){ //heuristic
+ if ((compositeStatementObject.getLocationInfo().getCodeElementType() == LocationInfo.CodeElementType.BLOCK) && (!compositeStatementObject.getParent().getActualSignature().equals("{")))
+ return false;
+ return true;
+ }
+ private static boolean isMappingBelongToTrackedBlock(AbstractCodeMapping mapping, Block block, boolean isAfter){
+ if(mapping instanceof CompositeStatementObjectMapping){
+ CompositeStatementObject composite = (CompositeStatementObject) mapping.getFragment2();
+ if(!((CompositeStatementObject) block.getComposite()).contains(composite))
+ return false;
+ }
+ else if(mapping instanceof LeafMapping && (isAfter ? mapping.getFragment2() : mapping.getFragment1()) instanceof StatementObject){
+ if(!block.getComposite().getLeaves().contains(isAfter ? mapping.getFragment2() : mapping.getFragment1()))
+ return false;
+ }
+ return true;
+ }
+ private static boolean existsAsMappedOrComment(String hash, Set mappedFragments, Map comments){
+ return mappedFragments.contains(hash) || comments.containsKey(hash);
+ }
+ public static String normalizeCommentText(String commentText) {
+ if (commentText.startsWith("//")) {
+ commentText = commentText.substring(2);
+ }
+ commentText = commentText.trim();
+ return commentText;
+ }
+
+ public static Map generateCommentTextHashMap(List commentList) {
+ Map commentTextHashMap = new HashMap<>();
+ for (UMLComment comment : commentList) {
+ String text = comment.getText();
+ // remove /* */
+ text = text.replace("/*", "").replace("*/", "");
+ String[] lines = text.split("\\R");
+ for (String line : lines) {
+ line = normalizeCommentText(line);
+ if (line.isBlank()) {
+ continue;
+ }
+ commentTextHashMap.put(Util.getSHA512(line), comment);
+ }
+ }
+ return commentTextHashMap;
+ }
+
+}