using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Iteratively { class ItlyVerifier { public static readonly string UsageString = "Usage: itly_verifier.dll --instanceName name1 --instanceName name2 [ ...]"; public static readonly string FileLocation = "itly/c-sharp/ItlyVerifier.cs"; public static readonly string NameItlyNamespace = "Iteratively"; public static readonly string NameItlyTrackMethod = "Track"; static void Main(string[] args) { var allReferences = new List(); var allErrors = new List(); try { if (args.Length > 0) { var ampliInstanceNames = new List(); for (int i = 0; i < args.Length; i++) { if (args[i] == "--instanceName") { ampliInstanceNames.Add(args[i+1]); i++; continue; } var filePath = args[i]; try { var fileContents = File.ReadAllText(filePath); // WARNING: This doesn't throw parsing errors // WARNING: instead need to check Diagnostic flags SyntaxTree tree = CSharpSyntaxTree.ParseText(fileContents); var root = (CompilationUnitSyntax)tree.GetRoot(); // Check for parsing errors if (root.ContainsSkippedText || root.ContainsDiagnostics) { var diagnostics = root.GetDiagnostics().ToList().ConvertAll( (d) => Regex.Replace(d.ToString(), "\"", "\\\"") ); allErrors.Add(new VerifierError(filePath, diagnostics)); } var walker = new ItlyReferenceWalker(filePath, ampliInstanceNames); walker.Visit(root); allReferences.AddRange(walker.References); } catch (Exception ex) { // Add to Error list for output to Verifier allErrors.Add(new VerifierError(ex.Message, filePath)); } } } else { // No file path, display usage allErrors.Add(new VerifierError( FileLocation, $"Invalid arguments. No file path(s) provided. {UsageString}" )); } } catch (Exception ex) { allErrors.Add(new VerifierError(FileLocation, "Unhandled exception. " + ex.Message)); } // Output JSON to console Console.WriteLine(JsonHelper.ToJson(MapOutput(allReferences, allErrors))); } private static Dictionary MapOutput(List references, List errors) { return new Dictionary() { ["errors"] = errors.GroupBy(x => x.Location) .ToDictionary( x => x.Key, x => x.ToList() .ConvertAll(e => e.Errors.ToList())[0] as object ), ["references"] = references .GroupBy(x => x.FilePath) .ToDictionary( x => x.Key, x => x.ToList().ConvertAll(r => { var dict = r.ToDict(); dict.Remove("filePath"); return dict as object; }) as object ) }; } } class VerifierError : IToDict { public string Location; public List Errors; public VerifierError(string location, string error) { Location = location; Errors = new List (new string[] { error }); } public VerifierError(string location, List errors) { Location = location; Errors = errors; } public Dictionary ToDict() { return new Dictionary() { ["location"] = this.Location, ["errors"] = this.Errors.ToList() as object, }; } public override string ToString() { return JsonHelper.ToJson(this.ToDict()); } } class ItlyReference : IToDict { public string FilePath; public string Name; public int Row; public int Column; public ItlyReference(string filePath, string name, int row, int column) { FilePath = filePath; Name = name; Row = row; Column = column; } public Dictionary ToDict() { return new Dictionary() { ["filePath"] = this.FilePath, ["name"] = this.Name, ["row"] = this.Row, ["column"] = this.Column, }; } public override string ToString() { return JsonHelper.ToJson(this.ToDict()); } } class FileResults : IToDict { public string FilePath; public List References; public FileResults(string filePath, List references) { FilePath = filePath; References = references; } public Dictionary ToDict() { return new Dictionary() { ["filePath"] = this.FilePath, ["references"] = this.References }; } public override string ToString() { return JsonHelper.ToJson(this.ToDict()); } } class ItlyReferenceWalker : CSharpSyntaxWalker { private readonly string FilePath; public readonly List References; private readonly List AmpliInstanceNames; public ItlyReferenceWalker(string filePath, List ampliInstanceNames) { FilePath = filePath; References = new List(); AmpliInstanceNames = ampliInstanceNames; } public override void VisitUsingDirective(UsingDirectiveSyntax node) { // Check for 'using "Iteratively"' if (node.Name.ToString() == ItlyVerifier.NameItlyNamespace) { var lineSpan = node.SyntaxTree.GetLineSpan(node.Span); this.References.Add(new ItlyReference( this.FilePath, "using " + node.Name.ToString(), lineSpan.StartLinePosition.Line, lineSpan.StartLinePosition.Character )); } } public override void VisitExpressionStatement(ExpressionStatementSyntax node) { // Console.WriteLine("[VisitExpressionStatement]"); // Console.WriteLine(node); var invocationExpression = node.Expression as InvocationExpressionSyntax; if (invocationExpression != null) { var memberAccessExperession = invocationExpression.Expression as MemberAccessExpressionSyntax; if (memberAccessExperession != null) { var names = GetIdentifierValues(memberAccessExperession); // e.g. ["Itly", "Identify"], ["Console", "Log"], etc // Check for Itly.X() if (names.Length > 1 && this.AmpliInstanceNames.IndexOf(names[0]) >= 0) { var functionName = names[1]; // e.g. "Idenitify", "EventNoProperties", "Track", etc // Handle generic Itly.Track() if (functionName == ItlyVerifier.NameItlyTrackMethod) { var argNames = GetIdentifierValues(invocationExpression.ArgumentList); // e.g. ["userId", "EventNoProperties"] if (argNames != null && argNames.Length > 1) { functionName = argNames[1]; } } var lineSpan = node.SyntaxTree.GetLineSpan(node.Span); this.References.Add(new ItlyReference( this.FilePath, functionName, lineSpan.StartLinePosition.Line, lineSpan.StartLinePosition.Character )); } } } } private string[] GetIdentifierValues(SyntaxNode node) { var names = from idName in node.DescendantNodes() .OfType() select idName.Identifier.ValueText; return names.ToArray(); } } internal interface IToDict { Dictionary ToDict(); } internal class JsonHelper { public static String ToJson(object obj) { if (obj == null) return "null"; var aDictionary = obj as Dictionary; var aList = obj as List; var aCollection = obj as ICollection; List items = new List(); if (aDictionary != null) { foreach(var kvp in aDictionary) { items.Add($"\"{kvp.Key}\": {ToJson(kvp.Value)}"); } return $"{{{String.Join(", ", items)}}}"; } if (aList != null) { foreach(var item in aList) items.Add(ToJson(item)); return $"[{String.Join(", ", items)}]"; } if (aCollection != null) { foreach(var item in aCollection) items.Add(ToJson(item)); return $"[{String.Join(", ", items)}]"; } if (obj is string) { return $"\"{obj}\""; } return obj.ToString(); } } }