using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Codelyzer.Analysis.Model; using CTA.Rules.Common.Extensions; using CTA.Rules.Config; using CTA.Rules.Models; using CTA.Rules.Models.Tokens; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CTA.Rules.Analyzer { /// /// Object to use for creating an analysis based on a code analysis and a list of rules /// public class RulesAnalysis : IRulesAnalysis { private readonly RootNodes _rootNodes; private readonly List _sourceFileResults; private readonly ProjectActions _projectActions; private readonly ProjectType _projectType; /// /// Initializes an RulesAnalysis instance /// /// List of analyzed code files /// List of rules to be applied to the code files public RulesAnalysis(List sourceFileResults, RootNodes rootNodes) { _projectActions = new ProjectActions(); _sourceFileResults = sourceFileResults; _rootNodes = rootNodes; _projectType = ProjectType.ClassLibrary; } /// /// Initializes a RulesAnalysis instance /// /// List of analyzed code files /// List of rules to be applied to the code files /// Type of project public RulesAnalysis(List sourceFileResults, RootNodes rootNodes, ProjectType projectType = ProjectType.ClassLibrary) { _projectActions = new ProjectActions(); _sourceFileResults = sourceFileResults; _rootNodes = rootNodes; _projectType = projectType; } /// /// Runs the Rules Analysis /// /// public ProjectActions Analyze() { var options = new ParallelOptions() { MaxDegreeOfParallelism = Constants.ThreadCount }; Parallel.ForEach(_sourceFileResults, options, result => { var fileAction = new FileActions() { FilePath = result.FileFullPath }; if (AnalyzeChildren(fileAction, result.Children, 0)) { _projectActions.FileActions.Add(fileAction); } }); AddPackages(_rootNodes.ProjectTokens.Where(p => p.FullKey == _projectType.ToString())?.SelectMany(p => p.PackageActions)?.Distinct()?.ToList(), null); return _projectActions; } public ProjectActions AnalyzeFiles(ProjectActions projectActions, List updatedFiles) { var options = new ParallelOptions() { MaxDegreeOfParallelism = Constants.ThreadCount }; var selectedSourceFileResults = _sourceFileResults.Where(s => updatedFiles.Contains(s.FileFullPath)); Parallel.ForEach(selectedSourceFileResults, options, result => { var fileAction = new FileActions() { FilePath = result.FileFullPath }; if (AnalyzeChildren(fileAction, result.Children, 0)) { var existingFileAction = _projectActions.FileActions.FirstOrDefault(f => f.FilePath == fileAction.FilePath); if (existingFileAction != null) { existingFileAction = fileAction; } else { _projectActions.FileActions.Add(fileAction); } } }); return _projectActions; } /// /// Analyzes children of nodes in a particular file /// /// The object containing the actions to run on the file /// List of child nodes to check /// Recursion level to avoid stack overflows private bool AnalyzeChildren(FileActions fileAction, UstList children, int level, string parentNamespace = "", string parentClass = "") { bool containsActions = false; if (children == null || level > Constants.MaxRecursionDepth) { return false; } foreach (var child in children) { try { switch (child.NodeType) { case IdConstants.AnnotationIdName: { var annotation = (Annotation)child; var compareToken = new AttributeToken() { Key = annotation.Identifier, Namespace = annotation.Reference.Namespace, Type = annotation.SemanticClassType }; _rootNodes.Attributetokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } break; } case IdConstants.UsingDirectiveIdName: { string overrideKey = string.Empty; var compareToken = new UsingDirectiveToken() { Key = child.Identifier }; _rootNodes.Usingdirectivetokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } //Attempt a wildcard search, if applicable. This is using directive specific because it might want to include all sub-namespaces if (token == null) { var wildcardMatches = _rootNodes.Usingdirectivetokens.Where(i => i.Key.Contains("*")); if (wildcardMatches.Any()) { token = wildcardMatches.FirstOrDefault(i => compareToken.Key.WildcardEquals(i.Key)); if (token != null) { //We set the key so that we don't do another wildcard search during replacement, we just use the name as it was declared in the code overrideKey = compareToken.Key; } } } if (token != null) { AddActions(fileAction, token, child.TextSpan, overrideKey); containsActions = true; } break; } case IdConstants.NamespaceIdName: { var compareToken = new NamespaceToken() { Key = child.Identifier }; _rootNodes.NamespaceTokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, child.Identifier)) { containsActions = true; } break; } case IdConstants.ClassIdName: { var classType = (ClassDeclaration)child; var baseToken = new ClassDeclarationToken() { FullKey = classType.BaseType }; _rootNodes.Classdeclarationtokens.TryGetValue(baseToken, out var token); if (token != null) { //In case of class declarations, add actions on the class by name, instead of property AddNamedActions(fileAction, token, classType.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } token = null; string name = string.Concat(classType.Reference != null ? string.Concat(classType.Reference.Namespace, ".") : string.Empty, classType.Identifier); var nameToken = new ClassDeclarationToken() { FullKey = name }; _rootNodes.Classdeclarationtokens.TryGetValue(nameToken, out token); if (token != null) { //In case of class declarations, add actions on the class by name, instead of property AddNamedActions(fileAction, token, classType.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } token = null; // In case base list is null, we skip the block by substituting an empty enumerab le // This case may potentially occur when a class implements no interfaces foreach (string interfaceName in classType.BaseList ?? Enumerable.Empty()) { var baseListToken = new ClassDeclarationToken() { FullKey = interfaceName }; _rootNodes.Classdeclarationtokens.TryGetValue(baseListToken, out token); if (token != null) { AddNamedActions(fileAction, token, classType.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } token = null; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, classType.Identifier)) { containsActions = true; } break; } case IdConstants.InterfaceIdName: { var interfaceType = (InterfaceDeclaration)child; var baseToken = new InterfaceDeclarationToken() { FullKey = interfaceType.BaseType }; InterfaceDeclarationToken token = null; if (!string.IsNullOrEmpty(interfaceType.BaseType)) { _rootNodes.InterfaceDeclarationTokens.TryGetValue(baseToken, out token); } if (token != null) { //In case of interface declarations, add actions on the interface by name, instead of property AddNamedActions(fileAction, token, interfaceType.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } token = null; string name = string.Concat(interfaceType.Reference != null ? string.Concat(interfaceType.Reference.Namespace, ".") : string.Empty, interfaceType.Identifier); var nameToken = new InterfaceDeclarationToken() { FullKey = name }; _rootNodes.InterfaceDeclarationTokens.TryGetValue(nameToken, out token); if (token != null) { //In case of interface declarations, add actions on the interface by name, instead of property AddNamedActions(fileAction, token, interfaceType.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace)) { containsActions = true; } break; } case IdConstants.MethodIdName: { var compareToken = new MethodDeclarationToken() { FullKey = string.Concat(child.Identifier) }; _rootNodes.MethodDeclarationTokens.TryGetValue(compareToken, out var token); if (token != null) { AddNamedActions(fileAction, token, child.Identifier, child.TextSpan); AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } case IdConstants.InvocationIdName: { string overrideKey = string.Empty; InvocationExpression invocationExpression = (InvocationExpression)child; ////If we don't have a semantic analysis, we dont want to replace invocation expressions, otherwise we'll be replacing expressions regardless of their class/namespace if (string.IsNullOrEmpty(invocationExpression.SemanticOriginalDefinition)) break; var compareToken = new InvocationExpressionToken() { Key = invocationExpression.SemanticOriginalDefinition, Namespace = invocationExpression.Reference.Namespace, Type = invocationExpression.SemanticClassType }; _rootNodes.Invocationexpressiontokens.TryGetValue(compareToken, out var token); //Attempt a wildcard search, if applicable. This is invocation expression specific because it has to look inside the invocation expressions only if(token == null) { var wildcardMatches = _rootNodes.Invocationexpressiontokens.Where(i => i.Key.Contains("*")); if (wildcardMatches.Any()) { token = wildcardMatches.FirstOrDefault(i => compareToken.Key.WildcardEquals(i.Key) && compareToken.Namespace == i.Namespace && compareToken.Type == i.Type); if (token != null) { //We set the key so that we don't do another wildcard search during replacement, we just use the name as it was declared in the code overrideKey = compareToken.Key; } } //If the semanticClassType is too specific to apply to all TData types if(token == null) { if(invocationExpression.SemanticClassType.Contains('<')) { string semanticClassType = invocationExpression.SemanticClassType.Substring(0,invocationExpression.SemanticClassType.IndexOf('<')); compareToken = new InvocationExpressionToken() { Key = invocationExpression.SemanticOriginalDefinition, Namespace = invocationExpression.Reference.Namespace, Type = semanticClassType }; _rootNodes.Invocationexpressiontokens.TryGetValue(compareToken, out token); } } } if (token != null) { AddActions(fileAction, token, child.TextSpan, overrideKey); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } case IdConstants.ElementAccessIdName: { ElementAccess elementAccess = (ElementAccess)child; var compareToken = new ElementAccessToken() { Key = elementAccess.Expression, FullKey = GetFullKey(elementAccess.Reference?.Namespace, elementAccess.SemanticClassType, elementAccess.Expression), Type = elementAccess.SemanticClassType, Namespace = elementAccess.Reference?.Namespace }; _rootNodes.ElementAccesstokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } case IdConstants.MemberAccessIdName: { MemberAccess memberAccess = (MemberAccess)child; var compareToken = new MemberAccessToken() { Key = memberAccess.Name, FullKey = GetFullKey(memberAccess.Reference?.Namespace, memberAccess.SemanticClassType, memberAccess.Name), Type = memberAccess.SemanticClassType, Namespace = memberAccess.Reference?.Namespace }; _rootNodes.MemberAccesstokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } case IdConstants.DeclarationNodeIdName: { var declarationNode = (DeclarationNode)child; var compareToken = new IdentifierNameToken() { Key = string.Concat(declarationNode.Reference.Namespace, ".", declarationNode.Identifier), Namespace = declarationNode.Reference.Namespace }; _rootNodes.Identifiernametokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } case IdConstants.ObjectCreationIdName: { var objectCreationNode = (ObjectCreationExpression)child; //Rules based on Object Creation Parent Hierarchy var compareToken = new ObjectCreationExpressionToken() { Key = objectCreationNode.Identifier, Namespace = objectCreationNode.Reference?.Namespace, Type = objectCreationNode.SemanticClassType }; _rootNodes.ObjectCreationExpressionTokens.TryGetValue(compareToken, out var token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } //Rules based on Object Creation location within code var compareTokenLocation = new ObjectCreationExpressionToken() { Key = objectCreationNode.Identifier, Namespace = parentNamespace, Type = parentClass }; _rootNodes.ObjectCreationExpressionTokens.TryGetValue(compareTokenLocation, out var tokenLocation); if (tokenLocation != null) { AddActions(fileAction, tokenLocation, child.TextSpan); containsActions = true; } token = null; if(!string.IsNullOrEmpty(objectCreationNode.SemanticOriginalDefinition)) { var nameToken = new ObjectCreationExpressionToken() { Key = objectCreationNode.SemanticOriginalDefinition, Namespace = objectCreationNode.SemanticNamespace, Type = objectCreationNode.SemanticClassType }; _rootNodes.ObjectCreationExpressionTokens.TryGetValue(nameToken, out token); if (token != null) { AddActions(fileAction, token, child.TextSpan); containsActions = true; } } if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } default: { if (AnalyzeChildren(fileAction, child.Children, ++level, parentNamespace, parentClass)) { containsActions = true; } break; } } } catch (Exception ex) { LogHelper.LogError(ex, "Error loading actions for item {0} of type {1}", child.Identifier, child.NodeType); } } return containsActions; } private string GetFullKey(string containingNamespace, string containingClass, string key) { if (string.IsNullOrEmpty(containingNamespace)) { return key; } else { if (!string.IsNullOrEmpty(containingClass)) { return $"{containingNamespace}.{containingClass}.{key}"; } return $"{containingNamespace}.{key}"; } } /// /// Add actions matching the token /// /// The file to run actions on /// The token that matched the file private void AddActions(FileActions fileAction, CsharpNodeToken token, TextSpan textSpan, string overrideKey = "") { fileAction.AttributeActions.UnionWith(token.AttributeActions.Select(a => new AttributeAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, AttributeActionFunc = a.AttributeActionFunc, AttributeListActionFunc = a.AttributeListActionFunc }).ToList()); fileAction.AttributeActions.UnionWith(token.AttributeListActions.Select(a => new AttributeAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, AttributeActionFunc = a.AttributeActionFunc, AttributeListActionFunc = a.AttributeListActionFunc }).ToList()); fileAction.IdentifierNameActions.UnionWith(token.IdentifierNameActions.Select(a => new IdentifierNameAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, IdentifierNameActionFunc = a.IdentifierNameActionFunc, }).ToList()); fileAction.InvocationExpressionActions.UnionWith(token.InvocationExpressionActions.Select(a => new InvocationExpressionAction() { Key = !string.IsNullOrEmpty(overrideKey) ? overrideKey : a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, InvocationExpressionActionFunc = a.InvocationExpressionActionFunc }).ToList()); fileAction.ElementAccessActions.UnionWith(token.ElementAccessActions.Select(a => new ElementAccessAction() { Key = (token is ElementAccessToken) ? token.FullKey : a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, ElementAccessExpressionActionFunc = a.ElementAccessExpressionActionFunc }).ToList()); fileAction.MemberAccessActions.UnionWith(token.MemberAccessActions.Select(a => new MemberAccessAction() { Key = (token is MemberAccessToken) ? token.FullKey : a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, MemberAccessActionFunc = a.MemberAccessActionFunc }).ToList()); fileAction.Usingactions.UnionWith(token.UsingActions.Select(a => new UsingAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, UsingActionFunc = a.UsingActionFunc, NamespaceUsingActionFunc = a.NamespaceUsingActionFunc, }).ToList()); fileAction.NamespaceActions.UnionWith(token.NamespaceActions.Select(a => new NamespaceAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, NamespaceActionFunc = a.NamespaceActionFunc }).ToList()); fileAction.ObjectCreationExpressionActions.UnionWith(token.ObjectCreationExpressionActions.Select(a => new ObjectCreationExpressionAction() { Key = a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, ObjectCreationExpressionGenericActionFunc = a.ObjectCreationExpressionGenericActionFunc }).ToList()); fileAction.ExpressionActions.UnionWith(token.ExpressionActions.Select(a => new ExpressionAction() { Key = !string.IsNullOrEmpty(overrideKey) ? overrideKey : a.Key, Description = a.Description, Value = a.Value, Name = a.Name, Type = a.Type, TextSpan = textSpan, ActionValidation = a.ActionValidation, ExpressionActionFunc = a.ExpressionActionFunc }).ToList()); if (fileAction.AttributeActions.Any() || fileAction.IdentifierNameActions.Any() || fileAction.InvocationExpressionActions.Any() || fileAction.ExpressionActions.Any() || fileAction.ElementAccessActions.Any() || fileAction.MemberAccessActions.Any() || fileAction.Usingactions.Any() || fileAction.NamespaceActions.Any() || fileAction.ObjectCreationExpressionActions.Any()) { var nodeToken = token.Clone(); nodeToken.TextSpan = textSpan; fileAction.NodeTokens.Add(nodeToken); } AddPackages(token.PackageActions, textSpan); } /// /// Adds a list of packages to the project actions /// /// List of package actions based on the rules private void AddPackages(List packageActions, TextSpan textSpan) { if (packageActions != null && packageActions.Count > 0) { packageActions.ForEach((p) => { if (!_projectActions.PackageActions.Contains(p)) { _projectActions.PackageActions.Add(new PackageAction() { Name = p.Name, Version = p.Version, TextSpan = textSpan }); } }); } } /// /// Add actions using the identifier of the object matching the token /// /// /// /// private void AddNamedActions(FileActions fileAction, CsharpNodeToken token, string identifier, TextSpan textSpan) { fileAction.ClassDeclarationActions.UnionWith(token.ClassDeclarationActions .Select(c => new ClassDeclarationAction() { Key = identifier, Value = c.Value, Description = c.Description, Name = c.Name, Type = c.Type, TextSpan = textSpan, ActionValidation = c.ActionValidation, ClassDeclarationActionFunc = c.ClassDeclarationActionFunc })); fileAction.InterfaceDeclarationActions.UnionWith(token.InterfaceDeclarationActions .Select(c => new InterfaceDeclarationAction() { Key = identifier, Value = c.Value, Name = c.Name, Type = c.Type, Description = c.Description, TextSpan = textSpan, ActionValidation = c.ActionValidation, InterfaceDeclarationActionFunc = c.InterfaceDeclarationActionFunc })); fileAction.MethodDeclarationActions.UnionWith(token.MethodDeclarationActions .Select(c => new MethodDeclarationAction() { Key = identifier, Value = c.Value, Description = c.Description, Name = c.Name, Type = c.Type, TextSpan = textSpan, ActionValidation = c.ActionValidation, MethodDeclarationActionFunc = c.MethodDeclarationActionFunc })); if (fileAction.ClassDeclarationActions.Any() || fileAction.InterfaceDeclarationActions.Any() || fileAction.MethodDeclarationActions.Any() || fileAction.ObjectCreationExpressionActions.Any()) { var nodeToken = token.Clone(); nodeToken.TextSpan = textSpan; nodeToken.AllActions.ForEach(action => { action.Key = identifier; }); fileAction.NodeTokens.Add(nodeToken); } } } }