Skip to content

Instantly share code, notes, and snippets.

@sudipto80
Last active March 3, 2022 11:00
Show Gist options
  • Save sudipto80/c6f527430b89f6db118b to your computer and use it in GitHub Desktop.
Save sudipto80/c6f527430b89f6db118b to your computer and use it in GitHub Desktop.
Smart Diff For Source Code using Roslyn
//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