Last active
March 3, 2022 11:00
-
-
Save sudipto80/c6f527430b89f6db118b to your computer and use it in GitHub Desktop.
Smart Diff For Source Code using Roslyn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//A very basic but smart code comparer for C# | |
//Written using Microsoft.CodeAnalysis.CSharp and LINQ | |
public class SmartComparer | |
{ | |
public static string Code1 { get; set; } | |
public static string Code2 { get; set; } | |
public static IEnumerable<ClassDeclarationSyntax> Classes1 { get; set; } | |
public static IEnumerable<ClassDeclarationSyntax> Classes2 { get; set; } | |
public static SyntaxTree Tree1 { get { return CSharpSyntaxTree.ParseText(Code1); } } | |
public static SyntaxTree Tree2 { get { return CSharpSyntaxTree.ParseText(Code2); } } | |
public static string MethodCompareMessage { get; internal set; } | |
public static string PropertyCompareMessage { get; internal set; } | |
public static bool MethodsMatching { get; set; } | |
public static bool ClassnamesMatching { get; set; } | |
private static List<Dictionary<string, object>> getProperties(IEnumerable<ClassDeclarationSyntax> classes) | |
{ | |
return classes.SelectMany(c => | |
c.Members.OfType<PropertyDeclarationSyntax>() | |
.Select(pds => | |
new Dictionary<string, object> //C# 6.0 Feature { Dictionary Initializer saved the day } | |
{ | |
//The name of the property | |
["ClassName"] = c.Identifier.ValueText, | |
["PropertyName"] = pds.Identifier.ValueText, | |
//The data type of the property | |
["PropertyDataType"] = pds.Type.ToFullString(), | |
//Attributes of the property | |
["Attributes"] = pds.AttributeLists.Select(t => t.Attributes.ToFullString()) | |
.ToList(), | |
//Whether the property is read only or not | |
["ReadOnly"] = pds.DescendantNodesAndTokens() | |
.All(p => !p.Kind().Equals(SyntaxKind.SetKeyword)), | |
//Access modifiers of the property | |
["Modifiers"] = pds.Modifiers.Select(m => m.ValueText) | |
.ToList(), | |
//The body of the set call of the property | |
["SetBody"] = pds.DescendantNodes() | |
.FirstOrDefault(p => p.Kind().Equals(SyntaxKind.SetAccessorDeclaration)) | |
?.GetText() | |
?.Container.CurrentText.Lines | |
.Select(l => l.ToString()) | |
.Where(m => m.Trim().Length != 0) | |
.ToList(), | |
//The body of the get call of the property | |
["GetBody"] = pds.DescendantNodes() | |
.FirstOrDefault(p => p.Kind() == SyntaxKind.GetAccessorDeclaration) | |
?.GetText() | |
?.Container.CurrentText.Lines | |
.Select(l => l.ToString()) | |
.Where(m => m.Trim().Length != 0) | |
.ToList() | |
}).OrderBy(t => t["ClassName"]).ThenBy(t => t["PropertyName"])).ToList(); | |
} | |
private static string expandList(List<string> list) | |
{ | |
if (list.Count == 0) | |
return " empty"; | |
return list.Aggregate((a, b) => a + " " + b); | |
} | |
public static bool DoPropertiesMatch() | |
{ | |
if (DoClassesMatch()) | |
{ | |
var props1 = getProperties(Classes1); | |
var props2 = getProperties(Classes2); | |
StringBuilder propCheckBuilder = new StringBuilder(); | |
for (int i = 0; i < props1.Count; i++) | |
{ | |
var attributes1 = props1[i]["Attributes"] as List<string>; | |
var attributes2 = props2[i]["Attributes"] as List<string>; | |
var modifiers1 = props1[i]["Modifiers"] as List<string>; | |
var modifiers2 = props2[i]["Modifiers"] as List<string>; | |
var getBody1 = props1[i]["GetBody"] as List<string>; | |
var getBody2 = props2[i]["GetBody"] as List<string>; | |
var setBody1 = props1[i]["SetBody"] as List<string>; | |
var setBody2 = props2[i]["SetBody"] as List<string>; | |
var propDataType1 = props1[i]["PropertyDataType"].ToString(); | |
var propDataType2 = props2[i]["PropertyDataType"].ToString(); | |
var readOnly1 = props1[i]["ReadOnly"].ToString(); | |
var readOnly2 = props2[i]["ReadOnly"].ToString(); | |
if (!modifiers1.SequenceEqual(modifiers2)) | |
{ | |
//Another C# 6.0 feature {String interpolation} | |
propCheckBuilder.AppendLine($"Modifiers are not matching for parameter { props1[i]["PropertyName"]} of class {props1[i]["ClassName"]}"); | |
propCheckBuilder.AppendLine($"Ver 1 modifiers were {expandList(modifiers1)} and Ver 2 modifiers are {expandList(modifiers2)}"); | |
} | |
if (!attributes1.SequenceEqual(attributes2)) | |
{ | |
//Another C# 6.0 feature {String interpolation} | |
propCheckBuilder.AppendLine($"Attributes are not matching for parameter { props1[i]["PropertyName"]} of class {props1[i]["ClassName"]}"); | |
propCheckBuilder.AppendLine($"Ver 1 attributes were {expandList(attributes1)} and Ver 2 attributes are {expandList(attributes2)}"); | |
} | |
if (propDataType1 != propDataType2) | |
{ | |
propCheckBuilder.AppendLine($@"Data type of property {props1[i]["PropertyName"]} of class {props1[i]["ClassName"]} has been changed from {props1[i]["PropertyDataType"]} to {props2[i]["PropertyDataType"]}"); | |
} | |
if (readOnly1 != readOnly2) | |
{ | |
if (readOnly1.ToString() == "False") | |
{ | |
propCheckBuilder.AppendLine($@"property {props1[i]["PropertyName"]} of class {props1[i]["ClassName"]} has been changed from readonly to settable property"); | |
} | |
if (readOnly1.ToString() == "True") | |
{ | |
propCheckBuilder.AppendLine($@"property {props1[i]["PropertyName"]} of class {props1[i]["ClassName"]} has been changed from settable to a readonly property"); | |
} | |
} | |
//I left the getbody and setbody for you to implement | |
} | |
PropertyCompareMessage = propCheckBuilder.ToString(); | |
return propCheckBuilder.ToString().Trim().Length == 0; | |
} | |
return false; | |
} | |
public static bool DoMethodsMatch() | |
{ | |
//Finding methods from version 1 along with class names | |
if (DoClassesMatch())//Just in case | |
{ | |
var methodsFrom1 = Classes1.Select(c => new | |
{ | |
ClassName = c.Identifier.ValueText, | |
Methods = c.Members.OfType<MethodDeclarationSyntax>() | |
}).ToList(); | |
//Finding methods from version 2 along with class names | |
var methodsFrom2 = Classes2.Select(c => new | |
{ | |
ClassName = c.Identifier.ValueText, | |
Methods = c.Members.OfType<MethodDeclarationSyntax>() | |
}).ToList(); | |
//Storing class name and the method count for version 1 | |
var classWiseMethodCount1 = methodsFrom1.OrderBy(f => f.ClassName) | |
.ToDictionary(f => f.ClassName, f => f.Methods.Count()); | |
//Storing class name and the method count for version 2 | |
var classWiseMethodCount2 = methodsFrom2.OrderBy(f => f.ClassName) | |
.ToDictionary(f => f.ClassName, f => f.Methods.Count()); | |
//Checking whether methods from code 1 and code 2 match up or not | |
var methodCountMatch = classWiseMethodCount1.Zip(classWiseMethodCount2, | |
(a, b) => | |
/* Checking if the class names match */ | |
a.Key == b.Key && | |
/* Checking if they both have same number of methods or not */ | |
a.Value == b.Value) | |
//If all classes match this condition then the methods match | |
.All(k => k == true); | |
//Count match | |
//Names match | |
bool namesMatch = methodsFrom1.SelectMany(m => m.Methods.Select(z => z.Identifier.ValueText)) | |
.OrderBy(t => t) | |
.SequenceEqual(methodsFrom2.SelectMany(m => m.Methods.Select(z => z.Identifier.ValueText)).OrderBy(t => t)); | |
//Parameters match | |
var params1 = methodsFrom1.SelectMany(m => m.Methods.Select(z => new Dictionary<string, object> | |
{ | |
["ClassName"] = m.ClassName, | |
["MethodName"] = z.Identifier.ValueText, | |
["ReturnType"] = z.ReturnType.ToFullString(), | |
["Modifiers"] = z.Modifiers.Select(j => j.Value.ToString()).ToList(), | |
["Attributes"] = z.AttributeLists.Select(g => g.Attributes.ToString()).ToList(), | |
["ContainsAnnotations"] = z.ContainsAnnotations, | |
["ConstraintClauses"] = z.ConstraintClauses.Select(y => y.ToFullString()), | |
["HasLeadingTrivia"] = z.HasLeadingTrivia, | |
["HasStructuredTrivia"] = z.HasStructuredTrivia, | |
["ParameterInfo"] = z.ParameterList | |
.Parameters | |
.Select(h => new KeyValuePair<string, string>(h.Identifier.ValueText, h.Type.ToFullString())).OrderBy(t => t.Key) | |
.ToDictionary(t => t.Key, t => t.Value) | |
})).ToList(); | |
var params2 = methodsFrom2.SelectMany(m => m.Methods.Select(z => new Dictionary<string, object> | |
{ | |
["ClassName"] = m.ClassName, | |
["MethodName"] = z.Identifier.ValueText, | |
["ReturnType"] = z.ReturnType.ToFullString(), | |
["Modifiers"] = z.Modifiers.Select(j => j.Value.ToString()).ToList(), | |
["Attributes"] = z.AttributeLists.Select(g => g.Attributes.ToString()).ToList(), | |
["ContainsAnnotations"] = z.ContainsAnnotations, | |
["ConstraintClauses"] = z.ConstraintClauses.Select(y => y.ToFullString()), | |
["HasLeadingTrivia"] = z.HasLeadingTrivia, | |
["HasStructuredTrivia"] = z.HasStructuredTrivia, | |
["ParameterInfo"] = z.ParameterList | |
.Parameters | |
.Select(u => new KeyValuePair<string, string>(u.Identifier.ValueText, u.Type.ToFullString())).OrderBy(t => t.Key).ToDictionary(t => t.Key, t => t.Value) | |
})).ToList(); | |
params1 = params1.OrderBy(t => t["ClassName"]).ToList(); | |
params2 = params2.OrderBy(t => t["ClassName"]).ToList(); | |
StringBuilder methodCheckBuilder = new StringBuilder(); | |
for (int i = 0; i < params1.Count; i++) | |
{ | |
var returnType1 = params1[i]["ReturnType"].ToString(); | |
var returnType2 = params2[i]["ReturnType"].ToString(); | |
var modifiers1 = params1[i]["Modifiers"] as List<string>; | |
var modifiers2 = params2[i]["Modifiers"] as List<string>; | |
var attributes1 = params1[i]["Attributes"] as List<string>; | |
var attributes2 = params2[i]["Attributes"] as List<string>; | |
var hasAnnotation1 = params1[i]["ContainsAnnotations"].ToString(); | |
var hasAnnotation2 = params2[i]["ContainsAnnotations"].ToString(); | |
var ConstraintClauses1 = params1[i]["ConstraintClauses"] as List<string>; | |
var ConstraintClauses2 = params2[i]["ConstraintClauses"] as List<string>; | |
var hasLeadingTrivia1 = params1[i]["HasLeadingTrivia"].ToString(); | |
var hasLeadingTrivia2 = params2[i]["HasLeadingTrivia"].ToString(); | |
var hasStructuredTrivia1 = params1[i]["HasStructuredTrivia"].ToString(); | |
var hasStructuredTrivia2 = params2[i]["HasStructuredTrivia"].ToString(); | |
var paramInfo1 = params1[i]["ParameterInfo"] as Dictionary<string, string>; | |
var paramInfo2 = params2[i]["ParameterInfo"] as Dictionary<string, string>; | |
if (returnType1 != returnType2) | |
{ | |
methodCheckBuilder.AppendLine(String.Format("* Return type of method {0} of class {1} has been changed from {2} to {3}", | |
params1[i]["MethodName"], params1[i]["ClassName"], params1[i]["ReturnType"], params2[i]["ReturnType"])); | |
} | |
if (!paramInfo1.Keys.OrderBy(t => t).SequenceEqual(paramInfo2.Keys.OrderBy(t => t))) | |
{ | |
var paramNamesFrom1 = paramInfo1.Select(t => t.Key).ToList(); | |
var paramNamesFrom2 = paramInfo2.Select(t => t.Key).ToList(); | |
string paraList1 = expandList(paramNamesFrom1); | |
string paraList2 = expandList(paramNamesFrom2); | |
methodCheckBuilder.AppendLine(String.Format(@"* Parameters are mismatching for method {0} of class {1}. The following parameters were used before {2} and the current parameter list is {3} ", | |
params1[i]["MethodName"], params1[i]["ClassName"], paraList1, paraList2)); | |
} | |
if (paramInfo1.Keys.OrderBy(t => t).SequenceEqual(paramInfo2.Keys.OrderBy(t => t))) | |
{ | |
foreach (var k in paramInfo1.Keys) | |
{ | |
if (paramInfo1[k] != paramInfo2[k]) | |
{ | |
methodCheckBuilder.AppendLine(String.Format(@"* Data Type for the parameter {0} of method {1} of class {2} has been changed to {3} from {4}", | |
k, params1[i]["MethodName"], params1[i]["ClassName"], paramInfo2[k], paramInfo1[k])); | |
} | |
} | |
} | |
} | |
MethodCompareMessage = methodCheckBuilder.ToString().Trim(); | |
return methodCountMatch && namesMatch && MethodCompareMessage.Length == 0; | |
} | |
else | |
return false; | |
} | |
public static bool DoClassesMatch() | |
{ | |
Classes1 = Tree1.GetRoot() | |
.DescendantNodes() | |
.OfType<ClassDeclarationSyntax>(); | |
Classes2 = Tree2.GetRoot() | |
.DescendantNodes() | |
.OfType<ClassDeclarationSyntax>(); | |
//Checking whether classes from both code branches match or not | |
bool classesMatch = Classes1.Select(c => c.Identifier.ValueText) | |
.All(c => Classes2.Select(cl => cl.Identifier.ValueText).Contains(c)); | |
var bl1 = Classes1.Select(c => new { ClassName = c.Identifier.ValueText, BaseList = c.BaseList?.Types.Select(t => t.Type.ToString()).ToList() }) | |
.ToDictionary(t => t.ClassName); | |
var bl2 = Classes2.Select(c => new { ClassName = c.Identifier.ValueText, BaseList = c.BaseList?.Types.Select(t => t.Type.ToString()).ToList() }) | |
.ToDictionary(t => t.ClassName); | |
bool baseListMatch = true; | |
foreach (string key in bl1.Keys) | |
{ | |
if ((bl1[key].BaseList == null && bl2[key].BaseList != null) | |
|| (bl2[key].BaseList == null && bl1[key].BaseList != null))//exlcusive or | |
{ | |
baseListMatch = false; | |
break; | |
} | |
if (bl1[key].BaseList == null && bl2[key].BaseList == null) | |
continue; | |
if (!bl1[key].BaseList.OrderBy(t => t).SequenceEqual(bl2[key].BaseList.OrderBy(m => m))) | |
{ | |
baseListMatch = false; | |
break; | |
} | |
} | |
return classesMatch && baseListMatch; | |
} | |
public void CompareCode() | |
{ | |
} | |
} | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
string code_1 = @"public class Employee | |
{ | |
private int _Age; | |
private string _Name; | |
public int Age | |
{ | |
get { return _Age; } | |
} | |
public string Name | |
{ | |
get { return _Name; } | |
set { _Name = value; } | |
} | |
[Obsolete] | |
public int AgeOld | |
{ | |
get {return _Age}; | |
} | |
public void GetPayCheck() | |
{ | |
} | |
public void Work() | |
{ | |
} | |
}"; | |
string code_2 = @"public class Employee | |
{ | |
private string _Name; | |
private double _Age; | |
public double Age | |
{ | |
get { return _Age; } | |
set { _Age = value; } | |
} | |
[Obsolete] | |
public int AgeOld | |
{ | |
get {return _Age}; | |
} | |
public string Name | |
{ | |
get { return _Name; } | |
set { _Name = value; } | |
} | |
//Void paycheck :( | |
public void GetPayCheck() | |
{ | |
} | |
public void Work() | |
{ | |
} | |
} | |
"; | |
SmartComparer.Code1 = code_1; | |
SmartComparer.Code2 = code_2; | |
bool classesMatch = SmartComparer.DoClassesMatch(); | |
bool propMatch = SmartComparer.DoPropertiesMatch(); | |
bool methodMatch = SmartComparer.DoMethodsMatch(); | |
bool isMatching = classesMatch && propMatch && methodMatch; | |
if (!isMatching) | |
{ | |
Console.WriteLine("The following differences were found:"); | |
Console.WriteLine(); | |
string mes = SmartComparer.PropertyCompareMessage + Environment.NewLine + SmartComparer.MethodCompareMessage; | |
Console.WriteLine(mes); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment