'
142 | return node.toString().substring(15);
143 | }
144 |
145 | /**
146 | * check for edges and inforce mismatch penalty
147 | */
148 | private double computeEdgeMismatchPenalty(NodeMapping mapping) {
149 | double mismatchCost = 0.0;
150 | double edgePenalty = 0.5; // this is complete guess work.
151 |
152 | Map forwardMap = mapping.getNodeMapping();
153 |
154 | // for each mapped edge (n1->m1) in old, see if (n2->m2) exists in new
155 | for (PDGNode oldSrc : forwardMap.keySet()) {
156 | PDGNode newSrc = forwardMap.get(oldSrc);
157 |
158 | for (PDGNode oldTgt : oldSrc.getDependents()) {
159 | PDGNode newTgt = forwardMap.get(oldTgt);
160 | if (newTgt != null) {
161 | // if the new edge does not exist, penalize
162 | if (!newSrc.getDependents().contains(newTgt)) {
163 | mismatchCost += edgePenalty;
164 | }
165 | }
166 | }
167 | }
168 | // todo possibly add checks for edges that are in new pdg, but not in old pdg (vice versa)
169 | // can use mappings.getReverseNodeMapping()
170 |
171 | return mismatchCost;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/matching/DiffEngine.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.matching;
2 |
3 | import org.pdgdiff.edit.ClassMetadataDiffGenerator;
4 | import org.pdgdiff.edit.EditDistanceCalculator;
5 | import org.pdgdiff.edit.EditScriptGenerator;
6 | import org.pdgdiff.edit.RecoveryProcessor;
7 | import org.pdgdiff.edit.model.EditOperation;
8 | import org.pdgdiff.export.DiffGraphExporter;
9 | import org.pdgdiff.graph.CycleDetection;
10 | import org.pdgdiff.graph.GraphTraversal;
11 | import org.pdgdiff.graph.PDG;
12 | import soot.SootClass;
13 |
14 | import soot.SootMethod;
15 |
16 | import java.io.*;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 | import java.util.stream.Collectors;
20 |
21 | import static org.pdgdiff.export.EditScriptExporter.*;
22 |
23 | public class DiffEngine {
24 |
25 | private static final List aggregatedEditScripts = new ArrayList<>();
26 | private static final boolean debug = false; // setting for development
27 |
28 |
29 | public static void difference(List pdgList1, List pdgList2,
30 | StrategySettings strategySettings, String srcSourceFilePath, String dstSourceFilePath) throws IOException {
31 |
32 | GraphMatcher matcher = GraphMatcherFactory.createMatcher(strategySettings.matchingStrategy, pdgList1, pdgList2);
33 | // for each graph print the size and if it has a cycle (debug mode)
34 | if (debug) pdgList1.forEach(pdg -> {
35 | System.out.println("------");
36 | System.out.println(pdg.getCFG().getBody().getMethod().getSignature());
37 | System.out.println("Node count" + GraphTraversal.getNodeCount(pdg));
38 | CycleDetection.hasCycle(pdg);
39 | });
40 | // perform the actual graph matching
41 | System.out.println("-> Beginning matching PDGs using strategy: " + strategySettings.matchingStrategy);
42 | GraphMapping graphMapping = matcher.matchPDGLists();
43 |
44 | // TODO: clean up debug print stmts
45 | System.out.println("--> Graph matching complete using strategy: " + strategySettings.matchingStrategy);
46 |
47 | // handle unmatched graphs, i.e. additions or deletions of methods to the versions
48 | List unmatchedInList1 = pdgList1.stream()
49 | .filter(pdg -> !graphMapping.getGraphMapping().containsKey(pdg))
50 | .collect(Collectors.toList());
51 |
52 | List unmatchedInList2 = pdgList2.stream()
53 | .filter(pdg -> !graphMapping.getGraphMapping().containsValue(pdg))
54 | .collect(Collectors.toList());
55 |
56 | // generate edit scripts for unmatched methods discovered in above statements
57 | generateEditScriptsForUnmatched(unmatchedInList1, unmatchedInList2, srcSourceFilePath, dstSourceFilePath, strategySettings);
58 | exportGraphMappings(graphMapping, pdgList1, pdgList2, "out/");
59 |
60 | DiffGraphExporter.exportDiffPDGs(
61 | graphMapping,
62 | pdgList1,
63 | pdgList2,
64 | "out/delta-graphs/"
65 | );
66 |
67 | graphMapping.getGraphMapping().forEach((srcPDG, dstPDG) -> {
68 | String method1 = srcPDG.getCFG().getBody().getMethod().getSignature();
69 | String method2 = dstPDG.getCFG().getBody().getMethod().getSignature();
70 | System.out.println("---\n> PDG from class 1: " + method1 + " is matched with PDG from class 2: " + method2);
71 | if (debug) {
72 | System.out.println(GraphTraversal.getNodeCount(srcPDG));
73 | CycleDetection.hasCycle(srcPDG);
74 | System.out.println(GraphTraversal.getNodeCount(dstPDG));
75 | CycleDetection.hasCycle(dstPDG);
76 | }
77 | NodeMapping nodeMapping = graphMapping.getNodeMapping(srcPDG);
78 | if (nodeMapping != null) {
79 | System.out.println("--- Node Mapping:");
80 | nodeMapping.printMappings();
81 |
82 | try {
83 | SootMethod srcObj = srcPDG.getCFG().getBody().getMethod();
84 | SootMethod destObj = dstPDG.getCFG().getBody().getMethod();
85 |
86 | List editScript = EditScriptGenerator.generateEditScript(srcPDG, dstPDG, graphMapping,
87 | srcSourceFilePath, dstSourceFilePath, srcObj, destObj);
88 |
89 | List recoveredEditScript = RecoveryProcessor.recoverMappings(editScript, strategySettings.recoveryStrategy);
90 |
91 | int editDistance = EditDistanceCalculator.calculateEditDistance(recoveredEditScript);
92 | System.out.println("--- Edit information ---");
93 | System.out.println("-- Edit Distance: " + editDistance);
94 |
95 | System.out.println("-- Edit Script:");
96 | for (EditOperation op : recoveredEditScript) {
97 | System.out.println(op);
98 | }
99 |
100 | // serialise and export
101 | aggregatedEditScripts.addAll(recoveredEditScript);
102 | exportEditScript(recoveredEditScript, method1, method2, strategySettings);
103 | } catch (Exception e) {
104 | e.printStackTrace();
105 | }
106 | }
107 | });
108 |
109 | // build edit script for class mappings at this point
110 | if (!pdgList1.isEmpty() && !pdgList2.isEmpty()) {
111 | SootClass srcClass = pdgList1.get(0).getCFG().getBody().getMethod().getDeclaringClass();
112 | SootClass dstClass = pdgList2.get(0).getCFG().getBody().getMethod().getDeclaringClass();
113 |
114 | // TODO: if one of these is empty, i need to mark it as an insertion or deletion of the entire class.
115 | // so need to do a INSERT all or DELETE all for class metadata, this is currently not handled and only
116 | // approximate.
117 | List metadataScript = ClassMetadataDiffGenerator.generateClassMetadataDiff(srcClass, dstClass, srcSourceFilePath, dstSourceFilePath);
118 | aggregatedEditScripts.addAll(metadataScript);
119 | exportEditScript(metadataScript, "metadata", "metadata", null);
120 | }
121 |
122 | if (strategySettings.isAggregateRecovery()) {
123 | List recAggregatedEditScripts = RecoveryProcessor.recoverMappings(aggregatedEditScripts, strategySettings.recoveryStrategy);
124 | writeAggregatedEditScript(recAggregatedEditScripts, "out/diff.json", strategySettings);
125 | } else {
126 | writeAggregatedEditScript(aggregatedEditScripts, "out/diff.json", strategySettings);
127 | }
128 | }
129 |
130 | private static void generateEditScriptsForUnmatched(List unmatchedInList1, List unmatchedInList2,
131 | String srcSourceFilePath, String dstSourceFilePath, StrategySettings strategySettings) {
132 | unmatchedInList1.forEach(pdg -> {
133 | try {
134 | SootMethod method = pdg.getCFG().getBody().getMethod();
135 | String methodSignature = pdg.getCFG().getBody().getMethod().getSignature();
136 | System.out.println("Unmatched method in List 1 (to be deleted): " + methodSignature);
137 |
138 | List editScript = EditScriptGenerator.generateDeleteScript(pdg, srcSourceFilePath, method);
139 | List recoveredEditScript = RecoveryProcessor.recoverMappings(editScript, strategySettings.recoveryStrategy);
140 | aggregatedEditScripts.addAll(recoveredEditScript);
141 | exportEditScript(recoveredEditScript, methodSignature, "DELETION", strategySettings);
142 | } catch (Exception e) {
143 | System.err.println("Failed to generate delete script for unmatched method in List 1");
144 | e.printStackTrace();
145 | }
146 | });
147 |
148 | unmatchedInList2.forEach(pdg -> {
149 | try {
150 | SootMethod method = pdg.getCFG().getBody().getMethod();
151 | String methodSignature = pdg.getCFG().getBody().getMethod().getSignature();
152 | System.out.println("Unmatched method in List 2 (to be added): " + methodSignature);
153 |
154 | List editScript = EditScriptGenerator.generateAddScript(pdg, dstSourceFilePath, method);
155 | List recoveredEditScript = RecoveryProcessor.recoverMappings(editScript, strategySettings.recoveryStrategy);
156 | aggregatedEditScripts.addAll(recoveredEditScript);
157 | exportEditScript(recoveredEditScript, "INSERTION", methodSignature, strategySettings);
158 | } catch (Exception e) {
159 | System.err.println("Failed to generate add script for unmatched method in List 2");
160 | e.printStackTrace();
161 | }
162 | });
163 | }
164 |
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/edit/ClassMetadataDiffGenerator.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.edit;
2 |
3 | import org.pdgdiff.edit.model.*;
4 | import org.pdgdiff.util.CodeAnalysisUtils;
5 | import org.pdgdiff.util.SourceCodeMapper;
6 | import soot.Modifier;
7 | import soot.SootClass;
8 | import soot.SootField;
9 | import soot.util.Chain;
10 |
11 | import java.io.IOException;
12 | import java.util.*;
13 |
14 | public class ClassMetadataDiffGenerator {
15 |
16 | public static List generateClassMetadataDiff(
17 | SootClass srcClass,
18 | SootClass dstClass,
19 | String srcSourceFilePath,
20 | String dstSourceFilePath
21 | ) throws IOException {
22 | Set editScriptSet = new HashSet<>();
23 |
24 | SourceCodeMapper srcCodeMapper = new SourceCodeMapper(srcSourceFilePath);
25 | SourceCodeMapper dstCodeMapper = new SourceCodeMapper(dstSourceFilePath);
26 |
27 | // cmp class metadata
28 | compareClassMetadata(srcClass, dstClass, srcCodeMapper, dstCodeMapper, editScriptSet);
29 |
30 | // cmp fields
31 | compareFields(srcClass, dstClass, srcCodeMapper, dstCodeMapper, editScriptSet);
32 |
33 | return new ArrayList<>(editScriptSet);
34 | }
35 |
36 | private static void compareClassMetadata(
37 | SootClass srcClass,
38 | SootClass dstClass,
39 | SourceCodeMapper srcCodeMapper,
40 | SourceCodeMapper dstCodeMapper,
41 | Set editScriptSet
42 | ) {
43 | // compare class modifiers
44 | if (srcClass.getModifiers() != dstClass.getModifiers()) {
45 | int srcClassLineNumber = CodeAnalysisUtils.getClassLineNumber(srcClass, srcCodeMapper);
46 | int dstClassLineNumber = CodeAnalysisUtils.getClassLineNumber(dstClass, dstCodeMapper);
47 |
48 | String srcClassDeclaration = CodeAnalysisUtils.getClassDeclaration(srcClass, srcCodeMapper);
49 | String dstClassDeclaration = CodeAnalysisUtils.getClassDeclaration(dstClass, dstCodeMapper);
50 |
51 | EditOperation classUpdate = new Update(
52 | null, // no node associated
53 | srcClassLineNumber,
54 | dstClassLineNumber,
55 | srcClassDeclaration,
56 | dstClassDeclaration,
57 | new SyntaxDifference("ClassMetadataDiff: Class modifiers differ")
58 | );
59 |
60 | editScriptSet.add(classUpdate);
61 | }
62 | }
63 |
64 |
65 | // in an ideal world this would also be able to compare uses of a field in the entire body, then I would be able to
66 | // account for rename refactors in the code base quite cleverly, maybe somethnig to look into.
67 |
68 | private static void compareFields(
69 | SootClass srcClass,
70 | SootClass dstClass,
71 | SourceCodeMapper srcCodeMapper,
72 | SourceCodeMapper dstCodeMapper,
73 | Set editScriptSet
74 | ) {
75 | Chain srcFields = srcClass.getFields();
76 | Chain dstFields = dstClass.getFields();
77 |
78 | Map srcFieldMap = new HashMap<>();
79 | Map dstFieldMap = new HashMap<>();
80 |
81 | for (SootField field : srcFields) {
82 | srcFieldMap.put(field.getName(), field);
83 | }
84 |
85 | for (SootField field : dstFields) {
86 | dstFieldMap.put(field.getName(), field);
87 | }
88 |
89 | // matching fields by name, type, and modifiers to try and report update instructions where sensible
90 | Set matchedFields = new HashSet<>();
91 |
92 | // firstly attempting to match by name
93 | for (SootField srcField : srcFields) {
94 | String fieldName = srcField.getName();
95 | SootField dstField = dstFieldMap.get(fieldName);
96 |
97 | if (dstField != null) {
98 | matchedFields.add(fieldName);
99 | if (!fieldsAreEqual(srcField, dstField)) {
100 | // update if field types or modifiers differ
101 | int oldLineNumber = CodeAnalysisUtils.getFieldLineNumber(srcField, srcCodeMapper);
102 | int newLineNumber = CodeAnalysisUtils.getFieldLineNumber(dstField, dstCodeMapper);
103 | String oldCodeSnippet = CodeAnalysisUtils.getFieldDeclaration(srcField, srcCodeMapper);
104 | String newCodeSnippet = CodeAnalysisUtils.getFieldDeclaration(dstField, dstCodeMapper);
105 | if (oldCodeSnippet.equals(newCodeSnippet)) {
106 | EditOperation fieldMove = new Move(
107 | null,
108 | oldLineNumber,
109 | newLineNumber,
110 | oldCodeSnippet
111 | );
112 | editScriptSet.add(fieldMove);
113 | } else {
114 | EditOperation fieldUpdate = new Update(
115 | null,
116 | oldLineNumber,
117 | newLineNumber,
118 | oldCodeSnippet,
119 | newCodeSnippet,
120 | new SyntaxDifference("ClassMetadataDiff: Field " + fieldName + " differs")
121 | );
122 | editScriptSet.add(fieldUpdate);
123 | }
124 | }
125 | }
126 | }
127 |
128 | // secondary matching by type / modifier
129 | for (SootField srcField : srcFields) {
130 | String fieldName = srcField.getName();
131 | if (matchedFields.contains(fieldName)) continue;
132 |
133 | // look for a destination field with similar properties
134 | SootField bestMatch = null;
135 | for (SootField dstField : dstFields) {
136 | if (matchedFields.contains(dstField.getName())) continue;
137 |
138 | if (fieldsAreSimilar(srcField, dstField)) {
139 | bestMatch = dstField;
140 | break;
141 | }
142 | }
143 |
144 | if (bestMatch != null) {
145 | // field has a close match, so treat as an update
146 | matchedFields.add(bestMatch.getName());
147 | int oldLineNumber = CodeAnalysisUtils.getFieldLineNumber(srcField, srcCodeMapper);
148 | int newLineNumber = CodeAnalysisUtils.getFieldLineNumber(bestMatch, dstCodeMapper);
149 | String oldCodeSnippet = CodeAnalysisUtils.getFieldDeclaration(srcField, srcCodeMapper);
150 | String newCodeSnippet = CodeAnalysisUtils.getFieldDeclaration(bestMatch, dstCodeMapper);
151 |
152 | EditOperation fieldUpdate = new Update(
153 | null,
154 | oldLineNumber,
155 | newLineNumber,
156 | oldCodeSnippet,
157 | newCodeSnippet,
158 | new SyntaxDifference("ClassMetadataDiff: Field " + fieldName + " differs")
159 | );
160 | editScriptSet.add(fieldUpdate);
161 | } else {
162 | // no similar field found, treat as a delete
163 | int lineNumber = CodeAnalysisUtils.getFieldLineNumber(srcField, srcCodeMapper);
164 | String codeSnippet = CodeAnalysisUtils.getFieldDeclaration(srcField, srcCodeMapper);
165 | editScriptSet.add(new Delete(null, lineNumber, codeSnippet));
166 | }
167 | }
168 |
169 | // cleanup with insertion operations
170 | for (SootField dstField : dstFields) {
171 | if (!matchedFields.contains(dstField.getName())) {
172 | int lineNumber = CodeAnalysisUtils.getFieldLineNumber(dstField, dstCodeMapper);
173 | String codeSnippet = CodeAnalysisUtils.getFieldDeclaration(dstField, dstCodeMapper);
174 | editScriptSet.add(new Insert(null, lineNumber, codeSnippet));
175 | }
176 | }
177 | }
178 |
179 |
180 | private static boolean fieldsAreSimilar(SootField field1, SootField field2) {
181 | // check if same protectness and type
182 | // cannot compare actual objects (getType()) because these are loaded in difference Soot Scenes, and hence dont
183 | // hash as expected with .equals(), so using the string repr of each!!!
184 | // todo check isStatic, isFinal, etc. and consider name, annotations, initial values
185 | return ((field1.getModifiers() & Modifier.PUBLIC) == (field2.getModifiers() & Modifier.PUBLIC) ||
186 | (field1.getModifiers() & Modifier.PRIVATE) == (field2.getModifiers() & Modifier.PRIVATE) ||
187 | (field1.getModifiers() & Modifier.PROTECTED) == (field2.getModifiers() & Modifier.PROTECTED))
188 | & (field1.getType().toString().equals(field2.getType().toString()));
189 | }
190 |
191 | private static boolean fieldsAreEqual(SootField field1, SootField field2) {
192 | // cmp field types
193 | if (!field1.getType().equals(field2.getType())) {
194 | return false;
195 | }
196 | // cmp modifiers
197 | if (field1.getModifiers() != field2.getModifiers()) {
198 | return false;
199 | }
200 | // TODO: cmp annotations or initial values if necessary
201 | return true;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/util/CodeAnalysisUtils.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.util;
2 |
3 | import soot.*;
4 | import soot.tagkit.LineNumberTag;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.regex.Matcher;
9 | import java.util.regex.Pattern;
10 |
11 |
12 | /**
13 | * This class aims to assist with parsing when Soot struggles.
14 | * A lot of these functions are to supplement Soot when it struggles to parse, and have a O(n) complexity. As further
15 | * work this could probably be optimised further.
16 | */
17 | public class CodeAnalysisUtils {
18 |
19 | public static int getClassLineNumber(SootClass sootClass, SourceCodeMapper codeMapper) {
20 | int lineNumber = sootClass.getJavaSourceStartLineNumber();
21 | if (lineNumber > 0) {
22 | return lineNumber;
23 | }
24 |
25 | // if line number is not directly available, search for it
26 | String className = sootClass.getShortName();
27 | String classPattern = String.format(".*\\b(class|interface|enum)\\b\\s+\\b%s\\b.*\\{", Pattern.quote(className));
28 | Pattern pattern = Pattern.compile(classPattern);
29 |
30 | int totalLines = codeMapper.getTotalLines();
31 | for (int i = 1; i <= totalLines; i++) {
32 | String line = codeMapper.getCodeLine(i).trim();
33 | Matcher matcher = pattern.matcher(line);
34 | if (matcher.matches()) {
35 | return i;
36 | }
37 | }
38 |
39 | return -1;
40 | }
41 |
42 | public static String getClassDeclaration(SootClass sootClass, SourceCodeMapper codeMapper) {
43 | int lineNumber = getClassLineNumber(sootClass, codeMapper);
44 | if (lineNumber > 0) {
45 | return codeMapper.getCodeLine(lineNumber).trim();
46 | }
47 | return "";
48 | }
49 |
50 | public static int getFieldLineNumber(SootField field, SourceCodeMapper codeMapper) {
51 | int lineNumber = field.getJavaSourceStartLineNumber();
52 | if (lineNumber > 0) {
53 | return lineNumber;
54 | }
55 |
56 | String fieldName = field.getName();
57 | String fieldType = field.getType().toString();
58 |
59 | // parse simple type name without full package declaration (e.g. String instead of java.lang.String)
60 | String simpleFieldType = fieldType.substring(fieldType.lastIndexOf('.') + 1);
61 | // regex pattern, possibility of missed case here
62 | String fieldPattern = String.format(
63 | ".*\\b(?:public|protected|private|static|final|transient|volatile|abstract|synchronized|native|strictfp|\\s)*\\b%s\\s*(?:<[^>]+>)?\\s+%s\\b.*;",
64 | Pattern.quote(simpleFieldType),
65 | Pattern.quote(fieldName)
66 | );
67 | Pattern pattern = Pattern.compile(fieldPattern);
68 |
69 | int totalLines = codeMapper.getTotalLines();
70 | for (int i = 1; i <= totalLines; i++) {
71 | String line = codeMapper.getCodeLine(i).trim();
72 | Matcher matcher = pattern.matcher(line);
73 | if (matcher.matches()) {
74 | return i;
75 | }
76 | }
77 |
78 | return -1;
79 | }
80 |
81 |
82 | public static String getFieldDeclaration(SootField field, SourceCodeMapper codeMapper) {
83 | int lineNumber = getFieldLineNumber(field, codeMapper);
84 | if (lineNumber > 0) {
85 | return codeMapper.getCodeLine(lineNumber).trim();
86 | }
87 | return "";
88 | }
89 |
90 |
91 | public static int[] getMethodLineRange(SootMethod method, SourceCodeMapper srcCodeMapper) {
92 | int initialLine = method.getJavaSourceStartLineNumber();
93 | if (initialLine <= 0) {
94 | return new int[]{-1, -1};
95 | }
96 |
97 | String methodName = method.getName();
98 | String methodPattern = String.format(".*\\b%s\\b\\s*\\(.*", Pattern.quote(methodName));
99 | Pattern signatureStartPattern = Pattern.compile(methodPattern);
100 |
101 | int totalLines = srcCodeMapper.getTotalLines();
102 | int startLine = initialLine;
103 | int endLine = initialLine;
104 |
105 | for (int i = initialLine; i > 0; i--) {
106 | String line = srcCodeMapper.getCodeLine(i).trim();
107 | if (line.isEmpty()) continue;
108 |
109 | Matcher m = signatureStartPattern.matcher(line);
110 | if (m.matches()) {
111 | startLine = i;
112 | break;
113 | }
114 | }
115 |
116 | boolean foundBrace = false;
117 | for (int i = startLine; i <= totalLines; i++) {
118 | String line = srcCodeMapper.getCodeLine(i).trim();
119 | if (line.contains("{")) {
120 | endLine = i;
121 | break;
122 | }
123 | if (!foundBrace) {
124 | endLine = i;
125 | }
126 | }
127 |
128 | return new int[]{startLine, endLine};
129 | }
130 |
131 | public static List getParamTokensAndLines(
132 | SootMethod method,
133 | SourceCodeMapper mapper,
134 | List paramLinesOut
135 | ) {
136 | paramLinesOut.clear();
137 | List paramTokens = new ArrayList<>();
138 | int[] range = getMethodLineRange(method, mapper);
139 | if (range[0] < 0 || range[1] < 0) {
140 | return paramTokens;
141 | }
142 |
143 | int startLine = range[0];
144 | int endLine = range[1];
145 | int totalLines = mapper.getTotalLines();
146 |
147 | // collect the lines for the signature block
148 | StringBuilder sb = new StringBuilder();
149 | for (int ln = startLine; ln <= Math.min(endLine, totalLines); ln++) {
150 | sb.append(mapper.getCodeLine(ln)).append("\n");
151 | }
152 | String signatureText = sb.toString();
153 |
154 | int openParenIndex = signatureText.indexOf('(');
155 | int closeParenIndex = signatureText.lastIndexOf(')');
156 | if (openParenIndex < 0 || closeParenIndex < 0 || closeParenIndex < openParenIndex) {
157 | return paramTokens; // no parameters
158 | }
159 |
160 | String paramBlock = signatureText.substring(openParenIndex + 1, closeParenIndex).trim();
161 | if (paramBlock.isEmpty()) {
162 | return paramTokens;
163 | }
164 |
165 | // naive split on commas
166 | String[] rawParams = paramBlock.split(",");
167 |
168 | // which line contains the param substr is assigned to be line num of that param
169 | List lines = new ArrayList<>();
170 | for (int ln = startLine; ln <= endLine; ln++) {
171 | lines.add(mapper.getCodeLine(ln));
172 | }
173 |
174 | for (String raw : rawParams) {
175 | String trimmed = raw.trim();
176 | if (trimmed.isEmpty()) {
177 | continue;
178 | }
179 | int bestLine = startLine; // fallback
180 | for (int offset = 0; offset < lines.size(); offset++) {
181 | if (lines.get(offset).contains(trimmed)) {
182 | bestLine = startLine + offset;
183 | break;
184 | }
185 | }
186 | paramTokens.add(trimmed);
187 | paramLinesOut.add(bestLine);
188 | }
189 | return paramTokens;
190 | }
191 |
192 | public static List getMethodAnnotationsWithLines(
193 | SootMethod method,
194 | SourceCodeMapper codeMapper,
195 | List annoLinesOut
196 | ) {
197 | annoLinesOut.clear();
198 | List annoTokens = new ArrayList<>();
199 | int[] range = getMethodLineRange(method, codeMapper);
200 | if (range[0] <= 0 || range[1] <= 0) {
201 | return annoTokens;
202 | }
203 |
204 | int startLine = range[0];
205 | // climb upward until finding lines not starting with '@' i.e. non annotations
206 | int lineNum = startLine - 1;
207 | while (lineNum > 0) {
208 | String line = codeMapper.getCodeLine(lineNum).trim();
209 | if (line.startsWith("@")) {
210 | String[] rawAnnos = line.split("\\s+@");
211 | for (int i = 0; i < rawAnnos.length; i++) {
212 | String annoRaw = (i == 0) ? rawAnnos[i] : "@" + rawAnnos[i];
213 | annoRaw = annoRaw.trim();
214 | if (!annoRaw.isEmpty()) {
215 | annoTokens.add(annoRaw);
216 | annoLinesOut.add(lineNum);
217 | }
218 | }
219 | lineNum--;
220 | } else {
221 | break;
222 | }
223 | }
224 | return annoTokens;
225 | }
226 |
227 | public static List getAnnotationsLineNumbers(SootMethod method, SourceCodeMapper codeMapper) {
228 | List annotationLines = new ArrayList<>();
229 | int[] range = getMethodLineRange(method, codeMapper);
230 | if (range[0] <= 0) {
231 | return annotationLines;
232 | }
233 | int startLine = range[0];
234 |
235 | // crawl upwards until reaching an empty line or a line that does not start with an @ i.e. non annotations
236 | int lineNum = startLine - 1;
237 | while (lineNum > 0) {
238 | String code = codeMapper.getCodeLine(lineNum).trim();
239 | if (code.startsWith("@")) {
240 | annotationLines.add(lineNum);
241 | lineNum--;
242 | } else if (code.isEmpty()) {
243 | break;
244 | } else {
245 | break;
246 | }
247 | }
248 | return annotationLines;
249 | }
250 |
251 | public static int getLineNumber(Unit unit) {
252 | if (unit == null) {
253 | return -1;
254 | }
255 | LineNumberTag tag = (LineNumberTag) unit.getTag("LineNumberTag");
256 | if (tag != null) {
257 | return tag.getLineNumber();
258 | }
259 | return -1;
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/matching/models/vf2/VF2State.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.matching.models.vf2;
2 |
3 | import org.pdgdiff.matching.NodeFeasibility;
4 | import org.pdgdiff.graph.GraphTraversal;
5 | import org.pdgdiff.graph.PDG;
6 | import soot.toolkits.graph.pdg.PDGNode;
7 |
8 | import java.util.*;
9 |
10 | /**
11 | * VF2State class to store the state of the VF2 algorithm. This class contains methods to store the current state
12 | * of the VF2 algorithm and perform operations on the state.
13 | */
14 | class VF2State {
15 | private final PDG srcPdg;
16 | private final PDG dstPdg;
17 | private final Map mapping; // The current partial mapping
18 |
19 | private final Set T1; // Nodes in PDG1 that are in the mapping or adjacent to mapped nodes
20 | private final Set T2; // Same for PDG2
21 |
22 | private final Set unmappedSrcNodes; // Unmapped nodes in PDG1 (the source pdg)
23 | private final Set unmappedDstNodes; // Unmapped nodes in PDG2 (the dest pdg)
24 |
25 | public VF2State(PDG srcPdg, PDG dstPdg) {
26 | this.srcPdg = srcPdg;
27 | this.dstPdg = dstPdg;
28 | this.mapping = new LinkedHashMap<>();
29 |
30 | this.unmappedSrcNodes = new LinkedHashSet<>(GraphTraversal.collectNodesBFS(srcPdg));
31 | this.unmappedDstNodes = new LinkedHashSet<>(GraphTraversal.collectNodesBFS(dstPdg));
32 |
33 | this.T1 = new LinkedHashSet<>();
34 | this.T2 = new LinkedHashSet<>();
35 | }
36 |
37 | public boolean isComplete() {
38 | // once one of the graphs is fully matched (hence this is subgraph isomorphism)
39 | //TODO: consider allowing this:
40 | // return mapping.size() >= Math.min(GraphTraversal.getNodeCount(srcPdg) * 0.5 , GraphTraversal.getNodeCount(dstPdg) * 0.5);
41 | return mapping.size() >= Math.min(GraphTraversal.getNodeCount(srcPdg), GraphTraversal.getNodeCount(dstPdg));
42 | }
43 |
44 | public Map getMapping() {
45 | return mapping;
46 | }
47 |
48 | public List generateCandidates() {
49 | // TODO: If non determinism prevails, consider implementing a sort on these candidates
50 | // TODO: probably need to sort by id e.g. CFGNODE 1 sorta thing. should hopefully work,
51 | // If not implementing this here, possibly need to implement it in the matchRecursvie function.
52 | List candidates = new ArrayList<>();
53 |
54 | if (!T1.isEmpty() && !T2.isEmpty()) {
55 | // Pick nodes from T1 and T2
56 | PDGNode n1 = selectNode(T1);
57 | for (PDGNode n2 : T2) {
58 | if (nodesAreCompatible(n1, n2)) {
59 | candidates.add(new CandidatePair(n1, n2));
60 | }
61 | }
62 | } else {
63 | // If T1 and T2 are empty, pick any unmapped nodes
64 | PDGNode n1 = selectNode(unmappedSrcNodes);
65 | for (PDGNode n2 : unmappedDstNodes) {
66 | if (nodesAreCompatible(n1, n2)) {
67 | candidates.add(new CandidatePair(n1, n2));
68 | }
69 | }
70 | }
71 |
72 | return candidates;
73 | }
74 |
75 | public boolean isFeasible(CandidatePair pair) {
76 | // Implement feasibility checks:
77 | // - Syntactic feasibility: node attributes match
78 | // - Semantic feasibility: the mapping is consistent with the graph structure
79 | // TODO arguably there is no point in doing checkSyntacticFeasibility here,
80 | // as this is already tested when generating the candidates.
81 | return checkSyntacticFeasibility(pair) && checkSemanticFeasibility(pair);
82 | }
83 |
84 | public void addPair(CandidatePair pair) {
85 | mapping.put(pair.n1, pair.n2);
86 | unmappedSrcNodes.remove(pair.n1);
87 | unmappedDstNodes.remove(pair.n2);
88 |
89 | // Update T1 and T2
90 | updateTerminalSets(pair.n1, pair.n2);
91 | }
92 |
93 | public void removePair(CandidatePair pair) {
94 | mapping.remove(pair.n1);
95 | unmappedSrcNodes.add(pair.n1);
96 | unmappedDstNodes.add(pair.n2);
97 |
98 | // Recalculate T1 and T2
99 | recalculateTerminalSets();
100 | }
101 |
102 | // Helper methods...
103 |
104 | private boolean nodesAreCompatible(PDGNode n1, PDGNode n2) {
105 | // check if the nodes are of the same semantic category (Stmt, Decl, etc.), todo should move this into semantic check section.
106 | if (!NodeFeasibility.isSameNodeCategory(n1, n2)) {
107 | return false;
108 | }
109 | // checks from teh following attributes; NORMAL, ENTRY, CONDHEADER, LOOPHEADER
110 | if (!n1.getAttrib().equals(n2.getAttrib())) {
111 | return false;
112 | }
113 |
114 | return true;
115 | }
116 |
117 |
118 | private boolean checkSyntacticFeasibility(CandidatePair pair) {
119 | // Ensure that the nodes can be mapped based on their attributes
120 | return nodesAreCompatible(pair.n1, pair.n2);
121 | }
122 |
123 | private boolean checkSemanticFeasibility(CandidatePair pair) {
124 | // cmp successors in PDG1 vs mapped successors in PDG2
125 | for (PDGNode succInSrcPdg : srcPdg.getSuccsOf(pair.n1)) {
126 | PDGNode succMappedInDstPdg = this.getMapping().get(succInSrcPdg);
127 | if (succMappedInDstPdg != null) {
128 | boolean dataEdge1 = srcPdg.hasDataEdge(pair.n1, succInSrcPdg);
129 | boolean dataEdge2 = dstPdg.hasDataEdge(pair.n2, succMappedInDstPdg);
130 | if (dataEdge1 != dataEdge2) {
131 | return false;
132 | }
133 |
134 | boolean ctrlEdge1 = srcPdg.hasControlEdge(pair.n1, succInSrcPdg);
135 | boolean ctrlEdge2 = dstPdg.hasControlEdge(pair.n2, succMappedInDstPdg);
136 | if (ctrlEdge1 != ctrlEdge2) {
137 | return false;
138 | }
139 | }
140 | }
141 |
142 | // cmp predecessors in PDG1 vs. mapped predecessors in PDG2
143 | for (PDGNode predInSrcPdg : srcPdg.getPredsOf(pair.n1)) {
144 | PDGNode predMappedInDstPdg = this.getMapping().get(predInSrcPdg);
145 | if (predMappedInDstPdg != null) {
146 | boolean dataEdge1 = srcPdg.hasDataEdge(predInSrcPdg, pair.n1);
147 | boolean dataEdge2 = dstPdg.hasDataEdge(predMappedInDstPdg, pair.n2);
148 | if (dataEdge1 != dataEdge2) {
149 | return false;
150 | }
151 |
152 | boolean ctrlEdge1 = srcPdg.hasControlEdge(predInSrcPdg, pair.n1);
153 | boolean ctrlEdge2 = dstPdg.hasControlEdge(predMappedInDstPdg, pair.n2);
154 | if (ctrlEdge1 != ctrlEdge2) {
155 | return false;
156 | }
157 | }
158 | }
159 |
160 | // cross-check every existing mapping pair so that edges from (pair.n1->mappedN1) in PDG1 match edges from (pair.n2->mappedN2) in PDG2.
161 | for (Map.Entry entry : this.getMapping().entrySet()) {
162 | PDGNode alreadyMappedN1 = entry.getKey();
163 | PDGNode alreadyMappedN2 = entry.getValue();
164 |
165 | // Forward edges:
166 | // if PDG1 has data/control edge from (pair.n1 -> alreadyMappedN1), then PDG2 must have the same edge type from (pair.n2 -> alreadyMappedN2)
167 | boolean dataEdge1 = srcPdg.hasDataEdge(pair.n1, alreadyMappedN1);
168 | boolean dataEdge2 = dstPdg.hasDataEdge(pair.n2, alreadyMappedN2);
169 | if (dataEdge1 != dataEdge2) {
170 | return false;
171 | }
172 | boolean ctrlEdge1 = srcPdg.hasControlEdge(pair.n1, alreadyMappedN1);
173 | boolean ctrlEdge2 = dstPdg.hasControlEdge(pair.n2, alreadyMappedN2);
174 | if (ctrlEdge1 != ctrlEdge2) {
175 | return false;
176 | }
177 |
178 | // Reverse edges:
179 | // if PDG1 has data/control edge from (alreadyMappedN1 -> pair.n1), then PDG2 must have the same edge type from (alreadyMappedN2 -> pair.n2).
180 | dataEdge1 = srcPdg.hasDataEdge(alreadyMappedN1, pair.n1);
181 | dataEdge2 = dstPdg.hasDataEdge(alreadyMappedN2, pair.n2);
182 | if (dataEdge1 != dataEdge2) {
183 | return false;
184 | }
185 | ctrlEdge1 = srcPdg.hasControlEdge(alreadyMappedN1, pair.n1);
186 | ctrlEdge2 = dstPdg.hasControlEdge(alreadyMappedN2, pair.n2);
187 | if (ctrlEdge1 != ctrlEdge2) {
188 | return false;
189 | }
190 | }
191 |
192 | return true;
193 |
194 | }
195 |
196 | private void updateTerminalSets(PDGNode n1, PDGNode n2) {
197 | // Add neighbours of n1 to T1 if they are not mapped
198 | for (PDGNode neighbour : n1.getDependents()) {
199 | if (!mapping.containsKey(neighbour)) {
200 | T1.add(neighbour);
201 | }
202 | }
203 | for (PDGNode neighbour : n1.getBackDependets()) {
204 | if (!mapping.containsKey(neighbour)) {
205 | T1.add(neighbour);
206 | }
207 | }
208 |
209 | // Same for n2
210 | for (PDGNode neighbour : n2.getDependents()) {
211 | if (!mapping.containsValue(neighbour)) {
212 | T2.add(neighbour);
213 | }
214 | }
215 | for (PDGNode neighbour : n2.getBackDependets()) {
216 | if (!mapping.containsValue(neighbour)) {
217 | T2.add(neighbour);
218 | }
219 | }
220 |
221 | // Remove n1 and n2 from T1 and T2
222 | T1.remove(n1);
223 | T2.remove(n2);
224 | }
225 |
226 | private void recalculateTerminalSets() {
227 | T1.clear();
228 | T2.clear();
229 | for (PDGNode mappedNode1 : mapping.keySet()) {
230 | for (PDGNode neighbour : mappedNode1.getDependents()) {
231 | if (!mapping.containsKey(neighbour)) {
232 | T1.add(neighbour);
233 | }
234 | }
235 | for (PDGNode neighbour : mappedNode1.getBackDependets()) {
236 | if (!mapping.containsKey(neighbour)) {
237 | T1.add(neighbour);
238 | }
239 | }
240 | }
241 | for (PDGNode mappedNode2 : mapping.values()) {
242 | for (PDGNode neighbour : mappedNode2.getDependents()) {
243 | if (!mapping.containsValue(neighbour)) {
244 | T2.add(neighbour);
245 | }
246 | }
247 | for (PDGNode neighbour : mappedNode2.getBackDependets()) {
248 | if (!mapping.containsValue(neighbour)) {
249 | T2.add(neighbour);
250 | }
251 | }
252 | }
253 | }
254 |
255 | private PDGNode selectNode(Set nodeSet) {
256 | // TODO: implement a more sophisticated node selection strategy here
257 | // ATM return any node from the set
258 | return nodeSet.iterator().next();
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/benchmark/evaluation-scripts/analysis_line_num_granularity.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import ast
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 | import seaborn as sns
6 |
7 | df = pd.read_csv("diff_results_gumtree_indiv_line_nums.csv")
8 |
9 | src_del = 'Deleted Lines (Src) (SootOK)'
10 | src_upd = 'Updated Lines (Src) (SootOk)'
11 | src_move = 'Moved Lines (Src) (SootOk)'
12 |
13 | dest_ins = 'Inserted Lines (Dst) (SootOK)'
14 | dest_upd = 'Updated Lines (Dst) (SootOk)'
15 | dest_move = 'Moved Lines (Dst) (SootOk)'
16 |
17 | # helper function to safely convert a string representation of a list into an actual list
18 | def parse_list(cell):
19 | if pd.isna(cell) or cell == "":
20 | return []
21 | try:
22 | return ast.literal_eval(cell) if isinstance(cell, str) else cell
23 | except Exception:
24 | return []
25 |
26 | for col in [src_del, src_upd, src_move, dest_ins, dest_upd, dest_move]:
27 | df[col] = df[col].apply(parse_list)
28 |
29 | def aggregate_sootok(row):
30 | # for gumtree, consider moved lines as well, for pdg we dont.
31 | if row["Approach"] == "GumTree":
32 | src = row[src_del] + row[src_upd] + row[src_move]
33 | dest = row[dest_ins] + row[dest_upd] + row[dest_move]
34 | else:
35 | src = row[src_del] + row[src_upd]
36 | dest = row[dest_ins] + row[dest_upd]
37 | return pd.Series({
38 | 'Aggregated_Src_SootOk': sorted(set(src)),
39 | 'Aggregated_Dest_SootOk': sorted(set(dest))
40 | })
41 |
42 | df[['Aggregated_Src_SootOk', 'Aggregated_Dest_SootOk']] = df.apply(aggregate_sootok, axis=1)
43 |
44 | results = []
45 |
46 | for (file, commit), group in df.groupby(["Changed File", "Commit ID"]):
47 | baseline = group[group["Approach"] == "GumTree"]
48 | if baseline.empty:
49 | continue
50 | baseline_row = baseline.iloc[0]
51 |
52 | baseline_lines = set(baseline_row["Aggregated_Src_SootOk"]) | set(baseline_row["Aggregated_Dest_SootOk"])
53 | baseline_src = set(baseline_row["Aggregated_Src_SootOk"])
54 | baseline_dest = set(baseline_row["Aggregated_Dest_SootOk"])
55 |
56 | for idx, row in group.iterrows():
57 | if row["Approach"] == "GumTree":
58 | continue # skip the baseline itself
59 |
60 | # union of lines for the current approach
61 | approach_lines = set(row["Aggregated_Src_SootOk"]) | set(row["Aggregated_Dest_SootOk"])
62 | approach_src = set(row["Aggregated_Src_SootOk"])
63 | approach_dest = set(row["Aggregated_Dest_SootOk"])
64 |
65 | misses_src = baseline_src - approach_src
66 | misses_dest = baseline_dest - approach_dest
67 | hallucinations_src = approach_src - baseline_src
68 | hallucinations_dest = approach_dest - baseline_dest
69 |
70 | # lines that GumTree reports but the approach does not: MISS
71 | misses = baseline_lines - approach_lines
72 | # lines that the approach reports but are not in GumTree: HALLUCINATIONS
73 | hallucinations = approach_lines - baseline_lines
74 |
75 | results.append({
76 | "Changed File": file,
77 | "Commit ID": commit,
78 | "Approach": row["Approach"],
79 | "GumTree_Count": len(baseline_lines),
80 | "Approach_Count": len(approach_lines),
81 | "Misses": len(misses),
82 | "Hallucinations": len(hallucinations),
83 | "Misses_Src": len(misses_src),
84 | "Misses_Dest": len(misses_dest),
85 | "Hallucinations_Src": len(hallucinations_src),
86 | "Hallucinations_Dest": len(hallucinations_dest),
87 | })
88 |
89 | diff_df = pd.DataFrame(results)
90 |
91 | hybrid_rows = []
92 | for (file, commit), group in diff_df.groupby(["Changed File", "Commit ID"]):
93 | pdg_vf2 = group[group["Approach"] == "PDGdiff-VF2"] # vf2
94 | pdg_ged = group[group["Approach"] == "PDGdiff-GED"] # ged
95 | if not pdg_vf2.empty and not pdg_ged.empty:
96 | vf2_err = pdg_vf2.iloc[0]["Misses"] + pdg_vf2.iloc[0]["Hallucinations"]
97 | ged_err = pdg_ged.iloc[0]["Misses"] + pdg_ged.iloc[0]["Hallucinations"]
98 | chosen_row = pdg_vf2.iloc[0] if vf2_err <= ged_err else pdg_ged.iloc[0]
99 | chosen_row = chosen_row.copy()
100 | chosen_row["Approach"] = "PDG-Hybrid"
101 | hybrid_rows.append(chosen_row)
102 |
103 | hybrid_df = pd.DataFrame(hybrid_rows)
104 | diff_df = pd.concat([diff_df, hybrid_df], ignore_index=True)
105 |
106 | # agg summary statistics per approach
107 | summary = diff_df.groupby("Approach").agg({
108 | "Misses": ["mean", "sum"],
109 | "Hallucinations": ["mean", "sum"]
110 | })
111 | print("\nSummary statistics by approach:")
112 | print(summary)
113 |
114 | print("\nOverall Misses/Hallucinations describe():")
115 | print(diff_df[["Misses", "Hallucinations"]].describe())
116 |
117 | # sanity check only
118 | print("\nRows with negative Misses or Hallucinations (should be empty):")
119 | print(diff_df[(diff_df["Misses"] < 0) | (diff_df["Hallucinations"] < 0)])
120 |
121 |
122 | for approach in diff_df["Approach"].unique():
123 | group = diff_df[diff_df["Approach"] == approach]
124 | count = group.shape[0]
125 | mean_miss = group["Misses"].mean()
126 | mean_halluc = group["Hallucinations"].mean()
127 | median_miss = group["Misses"].median()
128 | median_halluc = group["Hallucinations"].median()
129 | pct80_miss = group["Misses"].quantile(0.8)
130 | pct80_halluc = group["Hallucinations"].quantile(0.8)
131 | pct90_miss = group["Misses"].quantile(0.9)
132 | pct90_halluc = group["Hallucinations"].quantile(0.9)
133 |
134 | # cmp Pearson correlation with GumTree counts. TODO: arguably this is a bit primitive as we only take counts
135 | pearson_corr = group["Approach_Count"].corr(group["GumTree_Count"])
136 |
137 | print(f"-- {approach} --")
138 | print(f"Count (rows) : {count}")
139 | print(f"Mean Abs Error (Misses) : {mean_miss:.2f}")
140 | print(f"Median Abs Error (Misses) : {median_miss:.2f}")
141 | print(f"80th pct Abs Error (Misses): {pct80_miss:.2f}")
142 | print(f"90th pct Abs Error (Misses): {pct90_miss:.2f}")
143 | print(f"Mean Abs Error (Halluc) : {mean_halluc:.2f}")
144 | print(f"Median Hallucinations : {median_halluc:.2f}")
145 | print(f"80th pct Hallucinations : {pct80_halluc:.2f}")
146 | print(f"90th pct Hallucinations : {pct90_halluc:.2f}")
147 |
148 | print(f"Pearson correlation with GumTree: {pearson_corr:.3f}" if pd.notna(pearson_corr) else
149 | "Pearson correlation with GumTree: N/A (not enough variation or data points)")
150 | print("")
151 |
152 |
153 | approaches = sorted(diff_df["Approach"].unique())
154 |
155 | # prep data for boxplots/violin plots
156 | data_misses = [diff_df[diff_df["Approach"] == app]["Misses"] for app in approaches]
157 | data_halluc = [diff_df[diff_df["Approach"] == app]["Hallucinations"] for app in approaches]
158 |
159 | plt.figure(figsize=(12, 6))
160 |
161 | plt.subplot(1, 2, 1)
162 | sns.violinplot(data=diff_df, x="Approach", y="Misses", inner="quartile", hue="Approach", palette="coolwarm", cut=0)
163 | plt.title("Misses Distribution by Approach")
164 | plt.xticks(rotation=45)
165 |
166 | plt.subplot(1, 2, 2)
167 | sns.violinplot(data=diff_df, x="Approach", y="Hallucinations", inner="quartile", hue="Approach", palette="coolwarm", cut=0)
168 | plt.title("Hallucinations Distribution by Approach")
169 | plt.xticks(rotation=45)
170 |
171 | plt.tight_layout()
172 | plt.savefig("plots/violin.png", dpi=600, bbox_inches='tight')
173 | # plt.show()
174 |
175 | percentiles = np.arange(0, 101)
176 | tick_step = 5
177 |
178 | plt.figure(figsize=(12, 6))
179 | for approach in approaches:
180 | data = diff_df[diff_df["Approach"] == approach]["Misses"]
181 | perc_values = np.percentile(data, percentiles)
182 | sns.lineplot(x=percentiles, y=perc_values, label=approach)
183 | plt.xlabel("Percentile")
184 | plt.ylabel("Misses")
185 | plt.title("Percentile Curve for Misses by Approach")
186 | plt.legend()
187 | ax = plt.gca()
188 | y_max = diff_df["Misses"].max()
189 | ax.set_yticks(np.arange(0, y_max + tick_step, tick_step))
190 | plt.grid(True)
191 | plt.savefig("plots/misses.png", dpi=600, bbox_inches='tight')
192 |
193 | plt.figure(figsize=(12, 6))
194 | for approach in approaches:
195 | data = diff_df[diff_df["Approach"] == approach]["Hallucinations"]
196 | perc_values = np.percentile(data, percentiles)
197 | sns.lineplot(x=percentiles, y=perc_values, label=approach)
198 | plt.xlabel("Percentile")
199 | plt.ylabel("Hallucinations")
200 | plt.title("Percentile Curve for Hallucinations by Approach")
201 | plt.legend()
202 | ax = plt.gca()
203 | y_max = diff_df["Hallucinations"].max()
204 | ax.set_yticks(np.arange(0, y_max + tick_step, tick_step))
205 | plt.grid(True)
206 | plt.savefig("plots/hallucinations.png", dpi=600, bbox_inches='tight')
207 |
208 | summary_src_dest = diff_df.groupby("Approach").agg({
209 | "Misses_Src": "mean",
210 | "Misses_Dest": "mean",
211 | "Hallucinations_Src": "mean",
212 | "Hallucinations_Dest": "mean"
213 | }).reset_index()
214 |
215 | print("\nAverage Values by Approach (Source vs Destination):")
216 | print(summary_src_dest)
217 |
218 | approaches = summary_src_dest["Approach"]
219 | x = np.arange(len(approaches))
220 | width = 0.35
221 |
222 | fig, ax = plt.subplots(figsize=(10,6))
223 | bars_src = ax.bar(x - width/2, summary_src_dest["Misses_Src"], width, label="Source Misses")
224 | bars_dest = ax.bar(x + width/2, summary_src_dest["Misses_Dest"], width, label="Destination Misses")
225 |
226 | ax.set_ylabel("Average Misses (lines)")
227 | ax.set_title("Average Misses by Approach: Source vs Destination")
228 | ax.set_xticks(x)
229 | ax.set_xticklabels(approaches)
230 | ax.legend()
231 | plt.tight_layout()
232 | plt.savefig("plots/avg_misses.png", dpi=600, bbox_inches='tight')
233 |
234 | fig, ax = plt.subplots(figsize=(10,6))
235 | bars_src = ax.bar(x - width/2, summary_src_dest["Hallucinations_Src"], width, label="Source Hallucinations")
236 | bars_dest = ax.bar(x + width/2, summary_src_dest["Hallucinations_Dest"], width, label="Destination Hallucinations")
237 |
238 | ax.set_ylabel("Average Hallucinations (lines)")
239 | ax.set_title("Average Hallucinations by Approach: Source vs Destination")
240 | ax.set_xticks(x)
241 | ax.set_xticklabels(approaches)
242 | ax.legend()
243 | plt.tight_layout()
244 | plt.savefig("plots/avg_hallucinations.png", dpi=600, bbox_inches='tight')
245 |
246 | all_op_cols = [src_del, dest_ins, src_upd, dest_upd, src_move, dest_move]
247 | all_op_labels = ["Deleted (Src)", "Inserted (Dst)", "Updated (Src)", "Updated (Dst)", "Moved (Src)", "Moved (Dst)"]
248 |
249 | # for pdg-based, exclude moves
250 | non_move_op_cols = [src_del, dest_ins, src_upd, dest_upd]
251 | non_move_op_labels = ["Deleted (Src)", "Inserted (Dst)", "Updated (Src)", "Updated (Dst)"]
252 |
253 | def count_lines(series):
254 | return series.apply(lambda x: len(x) if isinstance(x, list) else 0).sum()
255 |
256 | op_stats = []
257 | for approach, group in df.groupby("Approach"):
258 | if approach in ["PDGdiff-GED", "PDGdiff-VF2"]:
259 | op_sum = {}
260 | for col, label in zip(non_move_op_cols, non_move_op_labels):
261 | op_sum[label] = count_lines(group[col])
262 | op_sum["Moved (Src)"] = 0
263 | op_sum["Moved (Dst)"] = 0
264 | total_lines = sum(op_sum.values())
265 | percentages = {label: (op_sum[label] / total_lines) * 100 if total_lines > 0 else 0 for label in all_op_labels}
266 | else:
267 | op_sum = {}
268 | for col, label in zip(all_op_cols, all_op_labels):
269 | op_sum[label] = count_lines(group[col])
270 | total_lines = sum(op_sum.values())
271 | percentages = {label: (op_sum[label] / total_lines) * 100 if total_lines > 0 else 0 for label in all_op_labels}
272 |
273 | record = {"Approach": approach, "Total_Lines": total_lines}
274 | record.update(percentages)
275 | op_stats.append(record)
276 |
277 | op_stats_df = pd.DataFrame(op_stats).set_index("Approach").sort_index()
278 |
279 | print("\n--- Operation Type Percentages by Approach (SootOK columns, excluding moves for GED/VF2) ---")
280 | print(op_stats_df)
281 |
282 | plt.figure(figsize=(10,6))
283 | approaches = op_stats_df.index.tolist()
284 | x = np.arange(len(approaches))
285 | bottom = np.zeros(len(approaches))
286 |
287 | for label in all_op_labels:
288 | perc = op_stats_df[label].values
289 | plt.bar(x, perc, bottom=bottom, label=label)
290 | bottom += perc
291 |
292 | plt.xticks(x, approaches, rotation=45, ha='right')
293 | plt.ylabel("Percentage (%)")
294 | plt.title("Operation Types as Percentage of Total Changed Lines (excluding lines beyond the scope of Soot)")
295 | plt.legend()
296 | plt.tight_layout()
297 | plt.savefig("plots/operation_types_percentage_stacked.png", dpi=600, bbox_inches='tight')
298 | # plt.show()
299 |
300 |
301 |
302 |
303 | operation_cols = [src_del, dest_ins, src_upd, dest_upd]
304 | operation_labels = ["Deleted (Src)", "Inserted (Dst)", "Updated (Src)", "Updated (Dst)"]
305 |
306 | # hlper: count total lines in a column (each cell is a list)
307 | def count_lines(series):
308 | return series.apply(lambda x: len(x) if isinstance(x, list) else 0).sum()
309 |
310 | # calc the total count of each operation type per approach
311 | op_summary = df.groupby("Approach").apply(
312 | lambda group: pd.Series({
313 | label: count_lines(group[col])
314 | for label, col in zip(operation_labels, operation_cols)
315 | })
316 | )
317 |
318 | op_totals = op_summary.sum(axis=1)
319 | op_percentages = op_summary.div(op_totals, axis=0) * 100
320 |
321 | fig, ax = plt.subplots(figsize=(10, 6))
322 | approaches = op_percentages.index.tolist()
323 | x = np.arange(len(approaches))
324 | bottom = np.zeros(len(approaches))
325 |
326 | for label in operation_labels:
327 | percentages = op_percentages[label].values
328 | ax.bar(x, percentages, bottom=bottom, label=label)
329 | bottom += percentages
330 |
331 | ax.set_xlabel("Approach")
332 | ax.set_ylabel("Percentage (%)")
333 | ax.set_title("Operation Types as Percentage of Total Changed Lines (Excluding Moves)")
334 | ax.set_xticks(x)
335 | ax.set_xticklabels(approaches, rotation=45)
336 | ax.legend(title="Operation Type")
337 |
338 | plt.tight_layout()
339 | plt.savefig("plots/percentage_operations_no_moves_all.png", dpi=600, bbox_inches='tight')
340 | plt.show()
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/edit/SignatureDiffGenerator.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.edit;
2 |
3 | import org.pdgdiff.edit.model.*;
4 | import org.pdgdiff.matching.models.heuristic.JaroWinklerSimilarity;
5 | import org.pdgdiff.util.CodeAnalysisUtils;
6 | import org.pdgdiff.util.SourceCodeMapper;
7 | import soot.Modifier;
8 | import soot.SootClass;
9 | import soot.SootMethod;
10 |
11 | import java.io.IOException;
12 | import java.util.*;
13 |
14 | public class SignatureDiffGenerator {
15 |
16 | public static class ParsedSignature {
17 | Set modifiers;
18 | String returnType;
19 | String methodName;
20 | List paramTokens;
21 | List annotations;
22 | List thrownExceptions;
23 |
24 | ParsedSignature(Set modifiers, String returnType, String methodName, List paramTokens, List annotations, List thrownExceptions) {
25 | this.modifiers = modifiers;
26 | this.returnType = returnType;
27 | this.methodName = methodName;
28 | this.paramTokens = paramTokens;
29 | this.annotations = annotations;
30 | this.thrownExceptions = thrownExceptions;
31 | }
32 | }
33 |
34 | public static ParsedSignature parseMethodSignature(SootMethod method, SourceCodeMapper mapper) throws IOException {
35 | // convert integer modifiers to a set of strings: e.g. {"public", "static"}
36 | Set modifierSet = new HashSet<>();
37 | int mods = method.getModifiers();
38 | String modsString = Modifier.toString(mods); // e.g. "public static final"
39 | if (!modsString.isEmpty()) {
40 | // split on whitespace to get indiv tokens
41 | modifierSet.addAll(Arrays.asList(modsString.split("\\s+")));
42 | }
43 |
44 | String retType = method.getReturnType() != null ? method.getReturnType().toString() : "";
45 | String name = method.getName();
46 |
47 | List exceptionClasses = method.getExceptions();
48 | List thrownExceptions = new ArrayList<>();
49 | for (SootClass exception : exceptionClasses) {
50 | thrownExceptions.add(exception.getName());
51 | }
52 |
53 | // to be populated later, no soot native way to get all the info required afaik
54 | List paramLines = new ArrayList<>();
55 | List paramTokens = CodeAnalysisUtils.getParamTokensAndLines(method, mapper, paramLines);
56 |
57 | // Annotation tokens (e.g. "@Override") + line nums for reporting
58 | List annoLines = new ArrayList<>();
59 | List annotations = CodeAnalysisUtils.getMethodAnnotationsWithLines(method, mapper, annoLines);
60 |
61 | return new ParsedSignature(modifierSet, retType, name, paramTokens, annotations, thrownExceptions);
62 | }
63 |
64 |
65 | static List compareSignatures(
66 | ParsedSignature oldSig, ParsedSignature newSig,
67 | SootMethod oldMethod, SootMethod newMethod,
68 | SourceCodeMapper oldMapper, SourceCodeMapper newMapper
69 | ) {
70 | List ops = new ArrayList<>();
71 |
72 | // these are approx'd and could actually return slightly off numbers if hard to parse.
73 | int[] oldRange = CodeAnalysisUtils.getMethodLineRange(oldMethod, oldMapper);
74 | int[] newRange = CodeAnalysisUtils.getMethodLineRange(newMethod, newMapper);
75 |
76 | int oldLine = (oldRange[0] > 0) ? oldRange[0] : -1;
77 | int newLine = (newRange[0] > 0) ? newRange[0] : -1;
78 |
79 | // cmp modifiers todo test this, not sure how useful this is
80 | Set removedModifiers = new HashSet<>(oldSig.modifiers);
81 | removedModifiers.removeAll(newSig.modifiers);
82 |
83 | Set addedModifiers = new HashSet<>(newSig.modifiers);
84 | addedModifiers.removeAll(oldSig.modifiers);
85 |
86 | for (String mod : removedModifiers) {
87 | ops.add(new Delete(
88 | null, oldLine,
89 | "Removed modifier: " + mod
90 | ));
91 | }
92 | for (String mod : addedModifiers) {
93 | ops.add(new Insert(
94 | null, newLine,
95 | "Added modifier: " + mod
96 | ));
97 | }
98 |
99 | // cmp return type
100 | if (!oldSig.returnType.equals(newSig.returnType)) {
101 | SyntaxDifference diff = new SyntaxDifference(
102 | "Return type changed from " + oldSig.returnType + " to " + newSig.returnType
103 | );
104 | ops.add(
105 | new Update(null, oldLine, newLine,
106 | oldSig.returnType, newSig.returnType, diff)
107 | );
108 | }
109 |
110 | // cmp method name
111 | if (!oldSig.methodName.equals(newSig.methodName)) {
112 | SyntaxDifference diff = new SyntaxDifference(
113 | "Method name changed from " + oldSig.methodName + " to " + newSig.methodName
114 | );
115 | ops.add(
116 | new Update(null, oldLine, newLine,
117 | oldSig.methodName, newSig.methodName, diff)
118 | );
119 | }
120 |
121 | List oldParamLines = new ArrayList<>();
122 | List oldParamTokens = CodeAnalysisUtils.getParamTokensAndLines(oldMethod, oldMapper, oldParamLines);
123 |
124 | List newParamLines = new ArrayList<>();
125 | List newParamTokens = CodeAnalysisUtils.getParamTokensAndLines(newMethod, newMapper, newParamLines);;
126 |
127 |
128 | ops.addAll(compareStringListsDP(oldParamTokens, newParamTokens,
129 | oldParamLines, newParamLines,
130 | "Parameter changed"));
131 | //
132 | // if (oldParamLines.size() == 1 && newParamLines.size() == 1) {
133 | // // TODO : avoid accidently marking a inserted param as a insert to the entire line, if the param changed adn multiple params exist on the same li
134 | // This is debatable, if i mark just one side as an insert it will be more equatable with gumtree. However, I do think its less useful as a tool. hard to know.
135 | // if (!oldSig.paramTypes.equals(newSig.paramTypes)) {
136 | // SyntaxDifference diff = new SyntaxDifference("Parameter list changed");
137 | // ops.add(
138 | // new Update(null, oldParamLines.get(0), newParamLines.get(0),
139 | // oldMapper.getCodeLine(oldParamLines.get(0)),newMapper.getCodeLine(newParamLines.get(0)), diff)
140 | // );
141 | // }
142 | // } else {
143 | // // handle multi line parameters;
144 | // ops.addAll(
145 | // compareStringListsDP(oldSig.paramTypes, newSig.paramTypes, oldParamLines, newParamLines)
146 | // );
147 | // }
148 |
149 |
150 | // List oldAnnotationLines = CodeAnalysisUtils.getAnnotationsLineNumbers(oldMethod, oldMapper);
151 | // List newAnnotationLines = CodeAnalysisUtils.getAnnotationsLineNumbers(newMethod, newMapper);
152 | //
153 | // // NB this is not accounting for field annotations. todo fix
154 | // // overwrite annotations using line numbers, unfortunately soot does not provide a way to get annotations
155 | //
156 | // oldSig.annotations = new ArrayList<>();
157 | // newSig.annotations = new ArrayList<>();
158 | // for (int i = 0; i < oldAnnotationLines.size(); i++) {
159 | // oldSig.annotations.add(oldMapper.getCodeLine(oldAnnotationLines.get(i)));
160 | // }
161 | // for (int i = 0; i < newAnnotationLines.size(); i++) {
162 | // newSig.annotations.add(newMapper.getCodeLine(newAnnotationLines.get(i)));
163 | // }
164 | //
165 | //
166 | // if (oldSig.annotations.size() == 1 && newSig.annotations.size() == 1) {
167 | // if (!Objects.equals(oldSig.annotations.get(0), newSig.annotations.get(0))) {
168 | // SyntaxDifference diff = new SyntaxDifference("Annotation changed");
169 | // ops.add(
170 | // new Update(null, oldAnnotationLines.get(0), newAnnotationLines.get(0),
171 | // oldSig.annotations.get(0), newSig.annotations.get(0), diff)
172 | // );
173 | // }
174 | // } else {
175 | // ops.addAll(
176 | // compareStringListsDP(oldSig.annotations, newSig.annotations, oldAnnotationLines, newAnnotationLines)
177 | // );
178 | // }
179 |
180 | List oldAnnoLines = new ArrayList<>();
181 | List oldAnnoTokens = CodeAnalysisUtils.getMethodAnnotationsWithLines(oldMethod, oldMapper, oldAnnoLines);
182 |
183 |
184 | List newAnnoLines = new ArrayList<>();
185 | List newAnnoTokens = CodeAnalysisUtils.getMethodAnnotationsWithLines(newMethod, newMapper, newAnnoLines);
186 |
187 | ops.addAll(compareStringListsDP(oldAnnoTokens, newAnnoTokens,
188 | oldAnnoLines, newAnnoLines,
189 | "Annotation changed"));
190 |
191 |
192 | List oldExceptions = oldSig.thrownExceptions;
193 | List newExceptions = newSig.thrownExceptions;
194 |
195 | // following are being classified as deletes in order to remain more consitent with gumtree, but perhaps
196 | // they should be updates (esp based on how other bits of this impl are treating these sorta changes)
197 |
198 | Set removedExceptions = new HashSet<>(oldExceptions);
199 | removedExceptions.removeAll(newExceptions);
200 | // todo: again should this be deletes or updates...
201 | for (String ex : removedExceptions) {
202 | ops.add(new Delete(null, oldLine, "Removed exception from func sig: " + ex));
203 | }
204 |
205 | Set addedExceptions = new HashSet<>(newExceptions);
206 | addedExceptions.removeAll(oldExceptions);
207 | // todo: again should this be inserts or updates...
208 | for (String ex : addedExceptions) {
209 | ops.add(new Insert(null, newLine, "Added exception from func sig: " + ex));
210 | }
211 |
212 | return ops;
213 | }
214 |
215 | // left to right dynamic programming approach to try and match up parameters (or annos), basically a edit distance optimiation
216 | // nb soot gives parameter types, not names
217 |
218 |
219 | // generic DP function used for params and for annotations
220 | private static List compareStringListsDP(
221 | List oldEntries, // old parameter types or old annotation lines
222 | List newEntries, // new parameter types or new annotation lines
223 | List oldEntriesLines, // old parameter line numbers or old annotation line numbers
224 | List newEntriesLines, // new parameter line numbers or new annotation line numbers
225 | String label
226 | ) {
227 | List ops = new ArrayList<>();
228 | int m = oldEntries.size();
229 | int n = newEntries.size();
230 |
231 | double[][] dp = new double[m + 1][n + 1];
232 | String[][] opsTable = new String[m + 1][n + 1];
233 |
234 | // init DP table
235 | for (int i = 0; i <= m; i++) {
236 | dp[i][0] = i;
237 | opsTable[i][0] = "DELETE";
238 | }
239 | for (int j = 0; j <= n; j++) {
240 | dp[0][j] = j;
241 | opsTable[0][j] = "INSERT";
242 | }
243 | opsTable[0][0] = "NO_CHANGE";
244 |
245 | // fill DP
246 | for (int i = 1; i <= m; i++) {
247 | for (int j = 1; j <= n; j++) {
248 | String oldStr = oldEntries.get(i - 1);
249 | String newStr = newEntries.get(j - 1);
250 |
251 | if (oldStr.equals(newStr)) {
252 | dp[i][j] = dp[i - 1][j - 1];
253 | opsTable[i][j] = "NO_CHANGE";
254 | } else {
255 | double deleteCost = dp[i - 1][j] + 1;
256 | double insertCost = dp[i][j - 1] + 1;
257 |
258 | double similarity = JaroWinklerSimilarity.jaroSimilarity(oldStr, newStr);
259 | double updateCost = dp[i - 1][j - 1] + (1.0 - similarity);
260 |
261 | if (deleteCost <= insertCost && deleteCost <= updateCost) {
262 | dp[i][j] = deleteCost;
263 | opsTable[i][j] = "DELETE";
264 | } else if (insertCost <= deleteCost && insertCost <= updateCost) {
265 | dp[i][j] = insertCost;
266 | opsTable[i][j] = "INSERT";
267 | } else {
268 | dp[i][j] = updateCost;
269 | opsTable[i][j] = "UPDATE";
270 | }
271 | }
272 | }
273 | }
274 |
275 | // backtrack
276 | int i = m, j = n;
277 | while (i > 0 || j > 0) {
278 | String operation = opsTable[i][j];
279 | if ("NO_CHANGE".equals(operation)) {
280 | i--;
281 | j--;
282 | } else if ("DELETE".equals(operation)) {
283 | int oldLineNum = oldEntriesLines.get(i - 1);
284 | String entry = oldEntries.get(i - 1);
285 | ops.add(new Delete(null, oldLineNum, entry));
286 | i--;
287 | } else if ("INSERT".equals(operation)) {
288 | int newLineNum = newEntriesLines.get(j - 1);
289 | String entry = newEntries.get(j - 1);
290 | ops.add(new Insert(null, newLineNum, entry));
291 | j--;
292 | } else if ("UPDATE".equals(operation)) {
293 | int oldLineNum = oldEntriesLines.get(i - 1);
294 | int newLineNum = newEntriesLines.get(j - 1);
295 | String oldEntry = oldEntries.get(i - 1);
296 | String newEntry = newEntries.get(j - 1);
297 |
298 | SyntaxDifference diff = new SyntaxDifference(
299 | label + " from \"" + oldEntry + "\" to \"" + newEntry + "\""
300 | );
301 | ops.add(new Update(null, oldLineNum, newLineNum, oldEntry, newEntry, diff));
302 | i--;
303 | j--;
304 | }
305 | }
306 |
307 | Collections.reverse(ops);
308 | return ops;
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/export/DiffGraphExporter.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.export;
2 |
3 | import org.pdgdiff.graph.GraphGenerator;
4 | import org.pdgdiff.graph.PDG;
5 | import org.pdgdiff.matching.GraphMapping;
6 | import org.pdgdiff.matching.NodeMapping;
7 | import soot.Unit;
8 | import soot.tagkit.LineNumberTag;
9 | import soot.toolkits.graph.pdg.PDGNode;
10 |
11 | import java.io.File;
12 | import java.io.FileWriter;
13 | import java.io.IOException;
14 | import java.io.PrintWriter;
15 | import java.util.*;
16 | import java.util.stream.Collectors;
17 |
18 | public class DiffGraphExporter {
19 |
20 | /**
21 | * This generates a singular 'delta' dot file, i.e. a way of representing the changes that have happeend on one graph and
22 | * taken it to another graph
23 | */
24 | public static void exportDiffPDGs(
25 | GraphMapping graphMapping,
26 | List pdgListSrc,
27 | List pdgListDst,
28 | String outputDir
29 | ) {
30 | File outDir = new File(outputDir);
31 | if (!outDir.exists()) {
32 | outDir.mkdirs();
33 | }
34 |
35 | // one pdg diff's dot file for each matched pair
36 | Map matchedPairs = graphMapping.getGraphMapping();
37 | for (Map.Entry entry : matchedPairs.entrySet()) {
38 | PDG srcPDG = entry.getKey();
39 | PDG dstPDG = entry.getValue();
40 | NodeMapping nodeMapping = graphMapping.getNodeMapping(srcPDG);
41 |
42 | String srcMethod = (srcPDG.getCFG() != null)
43 | ? srcPDG.getCFG().getBody().getMethod().getName()
44 | : "UnknownSrcMethod";
45 | String dstMethod = (dstPDG.getCFG() != null)
46 | ? dstPDG.getCFG().getBody().getMethod().getName()
47 | : "UnknownDstMethod";
48 |
49 | String dotFileName = "diff_" + srcMethod + "_TO_" + dstMethod + ".dot";
50 | File dotFile = new File(outDir, dotFileName);
51 |
52 | exportSingleDiffPDG(srcPDG, dstPDG, nodeMapping, dotFile);
53 | }
54 |
55 | // identify unmatched PDGs in source vs. destination
56 | List unmatchedInSrc = pdgListSrc.stream()
57 | .filter(pdg -> !matchedPairs.containsKey(pdg))
58 | .collect(Collectors.toList());
59 | List unmatchedInDst = pdgListDst.stream()
60 | .filter(pdg -> !matchedPairs.containsValue(pdg))
61 | .collect(Collectors.toList());
62 |
63 |
64 | // NB: if no match, i.e. a graph is inserted or deleted, we can't show a diff and no delta will be made.
65 | }
66 |
67 | /**
68 | * exprts a single .dot file showing the diff between one src PDG and one dst PDG
69 | *
70 | * This aims to follow similar logic to Editscriptgeneration
71 | */
72 | private static void exportSingleDiffPDG(
73 | PDG srcPDG,
74 | PDG dstPDG,
75 | NodeMapping nodeMapping,
76 | File outputDotFile
77 | ) {
78 | try (PrintWriter writer = new PrintWriter(new FileWriter(outputDotFile))) {
79 | writer.println("digraph PDG_DIFF {");
80 | writer.println(" rankdir=TB;");
81 | writer.println(" node [shape=box, style=filled, fontname=Arial];");
82 | writer.println(" edge [fontname=Arial];");
83 |
84 | Map srcToDst = nodeMapping.getNodeMapping();
85 | Map dstToSrc = nodeMapping.getReverseNodeMapping();
86 |
87 | Set srcNodes = new HashSet<>();
88 | srcPDG.iterator().forEachRemaining(srcNodes::add);
89 | Set dstNodes = new HashSet<>();
90 | dstPDG.iterator().forEachRemaining(dstNodes::add);
91 |
92 | // map to store node details (label and color) keyed by their dot id
93 | Map nodeDataMap = new HashMap<>();
94 |
95 | // process nodes from source PDG (matched or deleted nodes)
96 | for (PDGNode srcNode : srcNodes) {
97 | PDGNode dstNode = srcToDst.get(srcNode);
98 | String nodeId = getNodeId(srcNode, true);
99 |
100 | if (dstNode == null) {
101 | // node deleted in dst
102 | String label = removePrefix(srcNode.toString());
103 | String color = "#FFCCCC"; // red for deletion
104 | nodeDataMap.put(nodeId, new NodeData(createNodeLabel(label, srcNode), color));
105 | } else {
106 | // matched (poss either unchanged, moved, or updated)
107 | String label, color;
108 | if (Objects.equals(removePrefix(srcNode.toString()), removePrefix(dstNode.toString()))) {
109 | label = removePrefix(srcNode.toString());
110 | color = "lightgrey"; // unchanged
111 | } else {
112 | label = String.format("%s!NEWLINE!----!NEWLINE!%s",
113 | removePrefix(srcNode.toString()),
114 | removePrefix(dstNode.toString()));
115 | color = "#FFCC99"; // orange for update
116 | }
117 | nodeDataMap.put(nodeId, new NodeData(createNodeLabel(label, srcNode, dstNode), color));
118 | }
119 | }
120 |
121 | // processing nodes added in destination
122 | for (PDGNode dstNode : dstNodes) {
123 | if (!dstToSrc.containsKey(dstNode)) {
124 | String nodeId = getNodeId(dstNode, false);
125 | String label = removePrefix(dstNode.toString());
126 | String color = "#CCFFCC"; // green for added
127 | nodeDataMap.put(nodeId, new NodeData(createNodeLabel(label, dstNode), color));
128 | }
129 | }
130 |
131 | // process edges and record dependency labels
132 | Map> edgeMap = new HashMap<>();
133 | Set connectedNodeIds = new HashSet<>();
134 |
135 | // process edges from the src PDG
136 | for (PDGNode srcNode : srcNodes) {
137 | for (PDGNode succ : srcPDG.getSuccsOf(srcNode)) {
138 | String srcId = getMergedNodeId(srcNode, true, srcToDst);
139 | String tgtId = getMergedNodeId(succ, true, srcToDst);
140 | EdgeKey key = new EdgeKey(srcId, tgtId);
141 |
142 | // get dependency types for the edge in srcPDG.
143 | List depTypes = srcPDG.getEdgeLabels(srcNode, succ);
144 | String depLabel = depTypes.stream()
145 | .map(DiffGraphExporter::mapDependencyType)
146 | .collect(Collectors.joining(","));
147 | edgeMap.computeIfAbsent(key, k -> new HashSet<>()).add("src:" + depLabel);
148 |
149 | connectedNodeIds.add(srcId);
150 | connectedNodeIds.add(tgtId);
151 | }
152 | }
153 |
154 | // process edges from the dest PDG
155 | for (PDGNode dstNode : dstNodes) {
156 | for (PDGNode succ : dstPDG.getSuccsOf(dstNode)) {
157 | String srcId = getMergedNodeId(dstNode, false, dstToSrc);
158 | String tgtId = getMergedNodeId(succ, false, dstToSrc);
159 | EdgeKey key = new EdgeKey(srcId, tgtId);
160 |
161 | List depTypes = dstPDG.getEdgeLabels(dstNode, succ);
162 | String depLabel = depTypes.stream()
163 | .map(DiffGraphExporter::mapDependencyType)
164 | .collect(Collectors.joining(","));
165 | edgeMap.computeIfAbsent(key, k -> new HashSet<>()).add("dst:" + depLabel);
166 |
167 | connectedNodeIds.add(srcId);
168 | connectedNodeIds.add(tgtId);
169 | }
170 | }
171 |
172 | for (String nodeId : connectedNodeIds) {
173 | NodeData data = nodeDataMap.get(nodeId);
174 | if (data != null) {
175 | writer.printf(" %s [label=%s, fillcolor=\"%s\"];%n",
176 | nodeId, data.label, data.color);
177 | }
178 | }
179 |
180 | // write edges with colour and label
181 | for (Map.Entry> entry : edgeMap.entrySet()) {
182 | EdgeKey key = entry.getKey();
183 | Set sources = entry.getValue();
184 | String color;
185 | if (sources.stream().anyMatch(s -> s.startsWith("src:"))
186 | && sources.stream().anyMatch(s -> s.startsWith("dst:"))) {
187 | color = "black";
188 | } else if (sources.stream().anyMatch(s -> s.startsWith("src:"))) {
189 | color = "red";
190 | } else {
191 | color = "green";
192 | }
193 | String edgeLabel = sources.stream()
194 | .map(s -> s.substring(4))
195 | .distinct()
196 | .collect(Collectors.joining("/"));
197 | writer.printf(" %s -> %s [color=%s, label=\"%s\"];%n",
198 | key.srcId, key.tgtId, color, edgeLabel);
199 | }
200 |
201 | writer.println("}");
202 | System.out.println("Created PDG diff: " + outputDotFile.getAbsolutePath());
203 | } catch (IOException e) {
204 | e.printStackTrace();
205 | }
206 | }
207 |
208 | // this is overloaded, depending on update or single-line number operation
209 | private static String createNodeLabel(String originalLabel, PDGNode node) {
210 | return createNodeLabel(originalLabel, node, null);
211 | }
212 |
213 |
214 | private static String createNodeLabel(String originalLabel, PDGNode node1, PDGNode node2) {
215 | int lineNum = getNodeLineNumber(node1);
216 | int lineNum2 = -1;
217 | if (node2 != null) {
218 | lineNum2 = getNodeLineNumber(node2);
219 | }
220 | String safeLabel = escape(originalLabel);
221 |
222 | StringBuilder sb = new StringBuilder();
223 | sb.append("<");
224 | sb.append("").append(safeLabel).append("");
225 | if (lineNum != -1 && lineNum2 == -1) {
226 | sb.append("
")
227 | .append("")
228 | .append("Line: ").append(lineNum)
229 | .append("");
230 | } else if(lineNum != -1) {
231 | sb.append("
")
232 | .append("")
233 | .append("Line: ").append(lineNum)
234 | .append(" -> ")
235 | .append("Line: ").append(lineNum2)
236 | .append("");
237 | }
238 |
239 | sb.append(">");
240 | return sb.toString();
241 | }
242 |
243 | // helper classes and methods
244 |
245 | private static int getNodeLineNumber(PDGNode node) {
246 | if (node.getType() == PDGNode.Type.CFGNODE) {
247 | Object underlying = node.getNode();
248 | if (underlying instanceof Unit) {
249 | Unit unit = (Unit) underlying;
250 | LineNumberTag tag = (LineNumberTag) unit.getTag("LineNumberTag");
251 | if (tag != null) {
252 | return tag.getLineNumber();
253 | }
254 | }
255 | }
256 | return -1;
257 | }
258 |
259 | private static class NodeData {
260 | String label;
261 | String color;
262 | NodeData(String label, String color) {
263 | this.label = label;
264 | this.color = color;
265 | }
266 | }
267 |
268 | private static String mapDependencyType(GraphGenerator.DependencyTypes depType) {
269 | if (depType == GraphGenerator.DependencyTypes.CONTROL_DEPENDENCY) {
270 | return "CTRL_DEP";
271 | } else if (depType == GraphGenerator.DependencyTypes.DATA_DEPENDENCY) {
272 | return "DATA_DEP";
273 | } else {
274 | return "UNKNOWN";
275 | }
276 | }
277 |
278 | // generates a node ID
279 | private static String getNodeId(PDGNode node, boolean isSrc) {
280 | String prefix = isSrc ? "SRC_" : "DST_";
281 | return prefix + System.identityHashCode(node);
282 | }
283 |
284 | // generates a node ID for merged nodes
285 | private static String getMergedNodeId(PDGNode node, boolean isSourceNode, Map mapping) {
286 | PDGNode mappedNode = mapping.get(node);
287 | if (mappedNode != null) {
288 | if (isSourceNode) {
289 | return getNodeId(node, true);
290 | } else {
291 | return getNodeId(mappedNode, true);
292 | }
293 | } else {
294 | return getNodeId(node, isSourceNode);
295 | }
296 | }
297 |
298 | // removes the prefix from a node label
299 | private static String removePrefix(String label) {
300 | String prefix = "Type: CFGNODE: ";
301 | return label.startsWith(prefix) ? label.substring(prefix.length()) : label;
302 | }
303 |
304 | // for dot formatting
305 | private static String escape(String text) {
306 | return text.replace("<", "<")
307 | .replace(">", ">")
308 | .replace("\"", "\\\"")
309 | .replace("!NEWLINE!", "
");
310 |
311 | }
312 |
313 | private static class EdgeKey {
314 | final String srcId;
315 | final String tgtId;
316 |
317 | EdgeKey(String srcId, String tgtId) {
318 | this.srcId = srcId;
319 | this.tgtId = tgtId;
320 | }
321 |
322 | // for comp
323 | @Override
324 | public boolean equals(Object o) {
325 | if (this == o) return true;
326 | if (o == null || getClass() != o.getClass()) return false;
327 | EdgeKey edgeKey = (EdgeKey) o;
328 | return Objects.equals(srcId, edgeKey.srcId) && Objects.equals(tgtId, edgeKey.tgtId);
329 | }
330 |
331 | @Override
332 | public int hashCode() {
333 | return Objects.hash(srcId, tgtId);
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/src/main/java/org/pdgdiff/edit/EditScriptGenerator.java:
--------------------------------------------------------------------------------
1 | package org.pdgdiff.edit;
2 |
3 | import org.pdgdiff.edit.model.*;
4 | import org.pdgdiff.graph.PDG;
5 | import org.pdgdiff.matching.GraphMapping;
6 | import org.pdgdiff.matching.NodeMapping;
7 | import org.pdgdiff.util.CodeAnalysisUtils;
8 | import org.pdgdiff.util.SourceCodeMapper;
9 | import soot.SootMethod;
10 | import soot.Unit;
11 | import soot.toolkits.graph.pdg.PDGNode;
12 |
13 | import java.io.IOException;
14 | import java.util.*;
15 | import java.util.stream.Collectors;
16 |
17 | import org.pdgdiff.edit.SignatureDiffGenerator.ParsedSignature;
18 |
19 | import static org.pdgdiff.edit.SignatureDiffGenerator.compareSignatures;
20 | import static org.pdgdiff.edit.SignatureDiffGenerator.parseMethodSignature;
21 | import static org.pdgdiff.graph.GraphTraversal.collectNodesBFS;
22 |
23 | /**
24 | * Generates edit scripts based on PDG node mappings.
25 | */
26 | public class EditScriptGenerator {
27 |
28 | public static List generateEditScript(
29 | PDG srcPDG,
30 | PDG dstPDG,
31 | GraphMapping graphMapping,
32 | String srcSourceFilePath,
33 | String dstSourceFilePath,
34 | SootMethod srcMethod,
35 | SootMethod destMethod
36 | ) throws IOException {
37 | // using a set to prevent duplicates (order does not matter for now).
38 | Set editScriptSet = new HashSet<>();
39 |
40 | SourceCodeMapper srcCodeMapper = new SourceCodeMapper(srcSourceFilePath);
41 | SourceCodeMapper dstCodeMapper = new SourceCodeMapper(dstSourceFilePath);
42 |
43 | NodeMapping nodeMapping = graphMapping.getNodeMapping(srcPDG);
44 |
45 | Map mappings = nodeMapping.getNodeMapping();
46 | Set srcNodesMapped = mappings.keySet();
47 | Set dstNodesMapped = new HashSet<>(mappings.values());
48 |
49 | Set visitedNodes = new HashSet<>();
50 |
51 | // process mapped nodes for updates or moves
52 | for (PDGNode srcNode : srcNodesMapped) {
53 | PDGNode dstNode = mappings.get(srcNode);
54 |
55 | if (!visitedNodes.contains(srcNode)) {
56 | ComparisonResult compResult = nodesAreEqual(srcNode, dstNode, visitedNodes, srcCodeMapper, dstCodeMapper, nodeMapping);
57 |
58 | if (!compResult.isEqual) {
59 | if (compResult.isMove) {
60 | int oldLineNumber = getNodeLineNumber(srcNode);
61 | int newLineNumber = getNodeLineNumber(dstNode);
62 | String codeSnippet = srcCodeMapper.getCodeLine(oldLineNumber);
63 | editScriptSet.add(new Move(srcNode, oldLineNumber, newLineNumber, codeSnippet));
64 | } else if (!compResult.syntaxDifferences.isEmpty()) {
65 | for (SyntaxDifference syntaxDiff : compResult.syntaxDifferences) {
66 | int oldLineNumber = syntaxDiff.getOldLineNumber();
67 | int newLineNumber = syntaxDiff.getNewLineNumber();
68 | String oldCodeSnippet = syntaxDiff.getOldCodeSnippet();
69 | String newCodeSnippet = syntaxDiff.getNewCodeSnippet();
70 | if (oldCodeSnippet.equals(newCodeSnippet)) {
71 | Move move = new Move(srcNode, oldLineNumber, newLineNumber, oldCodeSnippet);
72 | editScriptSet.add(move);
73 | } else {
74 | Update update = new Update(srcNode, oldLineNumber, newLineNumber, oldCodeSnippet, newCodeSnippet, syntaxDiff);
75 | editScriptSet.add(update);
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | // handle deletions
84 | for (PDGNode srcNode : srcPDG) {
85 | if (!srcNodesMapped.contains(srcNode) && !visitedNodes.contains(srcNode)) {
86 | int lineNumber = getNodeLineNumber(srcNode);
87 | String codeSnippet = srcCodeMapper.getCodeLine(lineNumber);
88 | editScriptSet.add(new Delete(srcNode, lineNumber, codeSnippet));
89 | }
90 | }
91 |
92 | // handle insertions
93 | for (PDGNode dstNode : dstPDG) {
94 | if (!dstNodesMapped.contains(dstNode) && !visitedNodes.contains(dstNode)) {
95 | int lineNumber = getNodeLineNumber(dstNode);
96 | String codeSnippet = dstCodeMapper.getCodeLine(lineNumber);
97 | editScriptSet.add(new Insert(dstNode, lineNumber, codeSnippet));
98 | }
99 | }
100 |
101 | // structural signature diff, happens in every case to account for annotations changing even if signature itself doesnt.
102 | ParsedSignature oldSig = parseMethodSignature(srcMethod, srcCodeMapper);
103 | ParsedSignature newSig = parseMethodSignature(destMethod, dstCodeMapper);
104 |
105 | // misleading naming here , should probably rename to something including annotations
106 | List signatureDiffs =
107 | compareSignatures(oldSig, newSig, srcMethod, destMethod, srcCodeMapper, dstCodeMapper);
108 |
109 | editScriptSet.addAll(signatureDiffs);
110 |
111 | return new ArrayList<>(editScriptSet);
112 | }
113 |
114 |
115 | public static List generateAddScript(PDG pdg, String sourceFilePath, SootMethod method) throws IOException {
116 | SourceCodeMapper codeMapper = new SourceCodeMapper(sourceFilePath);
117 | List editOperations = new ArrayList<>();
118 |
119 | // insert the method signature lines (approx.), handling for annoataions
120 | int[] methodRange = CodeAnalysisUtils.getMethodLineRange(method, codeMapper);
121 | List annotationLines = CodeAnalysisUtils.getAnnotationsLineNumbers(method, codeMapper);
122 | if (!annotationLines.isEmpty() && Collections.min(annotationLines) < methodRange[0]) {
123 | methodRange[0] = Collections.min(annotationLines);
124 | }
125 | if (methodRange[0] > 0 && methodRange[1] >= methodRange[0]) {
126 | for (int i = methodRange[0]; i <= methodRange[1]; i++) {
127 | String signatureLine = codeMapper.getCodeLine(i);
128 | editOperations.add(new Insert(null, i, signatureLine));
129 | }
130 | }
131 |
132 | editOperations.addAll(
133 | collectNodesBFS(pdg).stream()
134 | .map(node -> {
135 | int lineNumber = getNodeLineNumber(node);
136 | String codeSnippet = codeMapper.getCodeLine(lineNumber);
137 | return new Insert(node, lineNumber, codeSnippet);
138 | })
139 | .collect(Collectors.toList())
140 | );
141 |
142 |
143 | // attempt to insert a trailing closing paren
144 | int maxLine = editOperations.stream()
145 | .mapToInt(op -> {
146 | PDGNode node = op.getNode();
147 | return node == null ? -1 : getNodeLineNumber(node);
148 | })
149 | .max()
150 | .orElse(-1);
151 | int nextLine = maxLine + 1;
152 | if (nextLine <= codeMapper.getTotalLines()) {
153 | String content = codeMapper.getCodeLine(nextLine).trim();
154 | if (content.contains("}")) {
155 | editOperations.add(new Insert(null, nextLine, content));
156 | }
157 | }
158 |
159 | return editOperations;
160 | }
161 |
162 | public static List generateDeleteScript(PDG pdg, String sourceFilePath, SootMethod method) throws IOException {
163 | SourceCodeMapper codeMapper = new SourceCodeMapper(sourceFilePath);
164 | List editOperations = new ArrayList<>();
165 |
166 | // delete the method signature lines (approx.)
167 | int[] methodRange = CodeAnalysisUtils.getMethodLineRange(method, codeMapper);
168 | List annotationLines = CodeAnalysisUtils.getAnnotationsLineNumbers(method, codeMapper);
169 | if (!annotationLines.isEmpty() && Collections.min(annotationLines) < methodRange[0]) {
170 | methodRange[0] = Collections.min(annotationLines);
171 | }
172 | if (methodRange[0] > 0 && methodRange[1] >= methodRange[0]) {
173 | for (int i = methodRange[0]; i <= methodRange[1]; i++) {
174 | String signatureLine = codeMapper.getCodeLine(i);
175 | editOperations.add(new Delete(null, i, signatureLine));
176 | }
177 | }
178 |
179 | editOperations.addAll(
180 | collectNodesBFS(pdg).stream()
181 | .map(node -> {
182 | int lineNumber = getNodeLineNumber(node);
183 | String codeSnippet = codeMapper.getCodeLine(lineNumber);
184 | return new Delete(node, lineNumber, codeSnippet);
185 | })
186 | .collect(Collectors.toList())
187 | );
188 |
189 |
190 | // attempt to delete a trailing closing paren
191 | int maxLine = editOperations.stream()
192 | .mapToInt(op -> {
193 | PDGNode node = op.getNode();
194 | return node == null ? -1 : getNodeLineNumber(node);
195 | })
196 | .max()
197 | .orElse(-1);
198 | int nextLine = maxLine + 1;
199 | if (nextLine <= codeMapper.getTotalLines()) {
200 | String content = codeMapper.getCodeLine(nextLine).trim();
201 | if (content.contains("}")) {
202 | editOperations.add(new Delete(null, nextLine, content));
203 | }
204 | }
205 |
206 | return editOperations;
207 | }
208 |
209 |
210 |
211 | private static class ComparisonResult {
212 | public boolean isEqual;
213 | public boolean isMove;
214 | public Set syntaxDifferences;
215 |
216 | public ComparisonResult(boolean isEqual) {
217 | this.isEqual = isEqual;
218 | this.isMove = false;
219 | this.syntaxDifferences = new HashSet<>();
220 | }
221 |
222 | public ComparisonResult(boolean isEqual, boolean isMove, Set syntaxDifferences) {
223 | this.isEqual = isEqual;
224 | this.isMove = isMove;
225 | this.syntaxDifferences = syntaxDifferences;
226 | }
227 | }
228 |
229 |
230 | public static int getNodeLineNumber(PDGNode node) {
231 | if (node.getType() == PDGNode.Type.CFGNODE) {
232 | Unit headUnit = (Unit) node.getNode();
233 | return getLineNumber(headUnit);
234 | }
235 | return -1;
236 | }
237 |
238 | private static ComparisonResult nodesAreEqual(PDGNode n1, PDGNode n2, Set visitedNodes,
239 | SourceCodeMapper srcCodeMapper, SourceCodeMapper dstCodeMapper,
240 | NodeMapping nodeMapping) {
241 | if (visitedNodes.contains(n1)) {
242 | return new ComparisonResult(true);
243 | }
244 | visitedNodes.add(n1);
245 | visitedNodes.add(n2);
246 |
247 | if (!n1.getType().equals(n2.getType())) {
248 | return new ComparisonResult(false);
249 | }
250 |
251 | if (n1.getType() == PDGNode.Type.CFGNODE) {
252 | return compareCFGNodes(n1, n2, srcCodeMapper, dstCodeMapper);
253 | }
254 |
255 | return new ComparisonResult(true);
256 | }
257 |
258 | private static ComparisonResult compareCFGNodes(PDGNode n1, PDGNode n2,
259 | SourceCodeMapper srcCodeMapper, SourceCodeMapper dstCodeMapper) {
260 | Unit unit1 = (Unit) n1.getNode();
261 | Unit unit2 = (Unit) n2.getNode();
262 |
263 | List units1 = Collections.singletonList(unit1);
264 | List units2 = Collections.singletonList(unit2);
265 |
266 | Set differences = compareUnitLists(units1, units2, srcCodeMapper, dstCodeMapper);
267 |
268 | if (!differences.isEmpty()) {
269 | return new ComparisonResult(false, false, differences);
270 | } else {
271 | // check for move operations based on line numbers
272 | int lineNumber1 = getNodeLineNumber(n1);
273 | int lineNumber2 = getNodeLineNumber(n2);
274 | if (lineNumber1 != lineNumber2 && lineNumber1 != -1 && lineNumber2 != -1) {
275 | return new ComparisonResult(false, true, differences);
276 | }
277 | }
278 |
279 | return new ComparisonResult(true);
280 | }
281 |
282 | private static Set compareUnitLists(List units1, List units2,
283 | SourceCodeMapper srcCodeMapper, SourceCodeMapper dstCodeMapper) {
284 | Set differences = new HashSet<>();
285 |
286 | int i = 0, j = 0;
287 | while (i < units1.size() && j < units2.size()) {
288 | Unit unit1 = units1.get(i);
289 | Unit unit2 = units2.get(j);
290 |
291 | if (unitsAreEqual(unit1, unit2)) {
292 | i++;
293 | j++;
294 | } else {
295 | SyntaxDifference diff = new SyntaxDifference(unit1, unit2, srcCodeMapper, dstCodeMapper);
296 | differences.add(diff);
297 | i++;
298 | j++;
299 | }
300 | }
301 |
302 | // handle remaining units in units1 (deletions)
303 | while (i < units1.size()) {
304 | SyntaxDifference diff = new SyntaxDifference(units1.get(i), null, srcCodeMapper, dstCodeMapper);
305 | differences.add(diff);
306 | i++;
307 | }
308 |
309 | // handle remaining units in units2 (insertions)
310 | while (j < units2.size()) {
311 | SyntaxDifference diff = new SyntaxDifference(null, units2.get(j), srcCodeMapper, dstCodeMapper);
312 | differences.add(diff);
313 | j++;
314 | }
315 |
316 | return differences;
317 | }
318 |
319 | private static boolean unitsAreEqual(Unit unit1, Unit unit2) {
320 | if (unit1 == null || unit2 == null) {
321 | return false;
322 | }
323 | // compares the actual body representation of the units
324 | return unit1.toString().equals(unit2.toString());
325 | }
326 |
327 | private static int getLineNumber(Unit unit) {
328 | return CodeAnalysisUtils.getLineNumber(unit);
329 | }
330 | }
331 |
--------------------------------------------------------------------------------