diff --git a/csharp/Platform.Bot/Program.cs b/csharp/Platform.Bot/Program.cs index 521a6b95..acf52e02 100644 --- a/csharp/Platform.Bot/Program.cs +++ b/csharp/Platform.Bot/Program.cs @@ -95,7 +95,7 @@ private static async Task Main(string[] args) var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename); Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close"); var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName); - var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage)); + var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage), new LibraryMethodAnalysisTrigger(githubStorage)); var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage)); var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations"))); var cancellation = new CancellationTokenSource(); diff --git a/csharp/Platform.Bot/Triggers/LibraryMethodAnalysisTrigger.cs b/csharp/Platform.Bot/Triggers/LibraryMethodAnalysisTrigger.cs new file mode 100644 index 00000000..745a811b --- /dev/null +++ b/csharp/Platform.Bot/Triggers/LibraryMethodAnalysisTrigger.cs @@ -0,0 +1,645 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Interfaces; +using Octokit; +using Storage.Remote.GitHub; + +namespace Platform.Bot.Triggers +{ + using TContext = Issue; + + /// + /// + /// Represents a trigger that analyzes the codebase to find places where existing library methods can be used. + /// + /// + /// + /// + internal class LibraryMethodAnalysisTrigger : ITrigger + { + private readonly GitHubStorage _storage; + + /// + /// + /// Initializes a new instance. + /// + /// + /// + /// + /// A GitHub storage instance. + /// + /// + public LibraryMethodAnalysisTrigger(GitHubStorage storage) + { + _storage = storage; + } + + /// + /// + /// Determines whether this trigger should be activated for the given context. + /// + /// + /// + /// + /// The context. + /// + /// + /// + /// True if the trigger should be activated, false otherwise. + /// + /// + public async Task Condition(TContext context) + { + return context.Title.ToLower().Contains("analyze library usage") || + context.Title.ToLower().Contains("find library methods") || + context.Body?.ToLower().Contains("library method analysis") == true; + } + + /// + /// + /// Performs the analysis and creates a report of potential library method optimizations. + /// + /// + /// + /// + /// The context. + /// + /// + public async Task Action(TContext context) + { + try + { + var analysis = await AnalyzeLibraryUsage(); + var report = GenerateAnalysisReport(analysis); + + await _storage.CreateIssueComment(context.Repository.Id, context.Number, report); + _storage.CloseIssue(context); + } + catch (Exception ex) + { + await _storage.CreateIssueComment(context.Repository.Id, context.Number, + $"Error during library analysis: {ex.Message}"); + } + } + + /// + /// + /// Analyzes the current library usage and identifies potential optimizations. + /// + /// + /// + /// + /// Analysis results containing suggestions and observations. + /// + /// + private async Task AnalyzeLibraryUsage() + { + var result = new LibraryAnalysisResult(); + + // Get all available Octokit client methods + var octokitMethods = GetAvailableOctokitMethods(); + + // Get currently used methods from codebase + var usedMethods = await GetCurrentlyUsedMethods(); + + // Find unused methods + result.UnusedMethods = octokitMethods.Except(usedMethods).ToList(); + + // Find potential optimizations + result.OptimizationSuggestions = await FindOptimizationOpportunities(); + + // Analyze patterns + result.UsagePatterns = AnalyzeUsagePatterns(usedMethods); + + return result; + } + + /// + /// + /// Gets all available methods from the Octokit GitHubClient. + /// + /// + /// + /// + /// List of available method names. + /// + /// + private List GetAvailableOctokitMethods() + { + var methods = new List(); + var clientType = typeof(GitHubClient); + + // Get all public properties that expose API endpoints + var properties = clientType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + if (property.PropertyType.Namespace?.Contains("Octokit") == true) + { + var nestedMethods = GetMethodsFromType(property.PropertyType, property.Name); + methods.AddRange(nestedMethods); + } + } + + return methods.Distinct().ToList(); + } + + /// + /// + /// Gets methods from a specific type, recursively exploring nested API endpoints. + /// + /// + /// + /// + /// The type to analyze. + /// + /// + /// + /// The prefix for the method path. + /// + /// + /// + /// List of method paths. + /// + /// + private List GetMethodsFromType(Type type, string prefix) + { + var methods = new List(); + + // Add direct methods + var directMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => !m.IsSpecialName && m.DeclaringType == type) + .Select(m => $"{prefix}.{m.Name}") + .ToList(); + + methods.AddRange(directMethods); + + // Add nested properties + var nestedProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType.Namespace?.Contains("Octokit") == true); + + foreach (var property in nestedProperties) + { + var nestedMethods = GetMethodsFromType(property.PropertyType, $"{prefix}.{property.Name}"); + methods.AddRange(nestedMethods); + } + + return methods; + } + + /// + /// + /// Scans the codebase to find currently used GitHub API methods. + /// + /// + /// + /// + /// List of currently used method names. + /// + /// + private async Task> GetCurrentlyUsedMethods() + { + var usedMethods = new List(); + var codebaseFiles = Directory.GetFiles("csharp", "*.cs", SearchOption.AllDirectories); + + foreach (var file in codebaseFiles) + { + try + { + var content = await File.ReadAllTextAsync(file); + var methodCalls = ExtractMethodCalls(content); + usedMethods.AddRange(methodCalls); + } + catch (Exception ex) + { + // Skip files that can't be read + Console.WriteLine($"Could not read file {file}: {ex.Message}"); + } + } + + return usedMethods.Distinct().ToList(); + } + + /// + /// + /// Extracts GitHub API method calls from source code content. + /// + /// + /// + /// + /// The source code content. + /// + /// + /// + /// List of method calls found in the content. + /// + /// + private List ExtractMethodCalls(string content) + { + var methodCalls = new List(); + var lines = content.Split('\n'); + + foreach (var line in lines) + { + // Look for patterns like "Client.Repository.GetAll" or "_storage.Client.Issue.Comment.Create" + if (line.Contains("Client.") && !line.Trim().StartsWith("//")) + { + var clientIndex = line.IndexOf("Client."); + if (clientIndex >= 0) + { + var methodCall = ExtractMethodCallFromLine(line, clientIndex); + if (!string.IsNullOrEmpty(methodCall)) + { + methodCalls.Add(methodCall); + } + } + } + } + + return methodCalls; + } + + /// + /// + /// Extracts a complete method call from a line of code. + /// + /// + /// + /// + /// The line of code. + /// + /// + /// + /// The starting index of "Client." + /// + /// + /// + /// The extracted method call or empty string if none found. + /// + /// + private string ExtractMethodCallFromLine(string line, int startIndex) + { + var methodCallBuilder = new StringBuilder(); + var index = startIndex; + + while (index < line.Length) + { + var ch = line[index]; + if (char.IsLetter(ch) || ch == '.' || ch == '_') + { + methodCallBuilder.Append(ch); + } + else if (ch == '(') + { + // Found method call + break; + } + else if (char.IsWhiteSpace(ch)) + { + // Skip whitespace + } + else + { + // End of method chain + break; + } + index++; + } + + var result = methodCallBuilder.ToString(); + return result.StartsWith("Client.") ? result : string.Empty; + } + + /// + /// + /// Finds potential optimization opportunities in the codebase. + /// + /// + /// + /// + /// List of optimization suggestions. + /// + /// + private async Task> FindOptimizationOpportunities() + { + var suggestions = new List(); + + // Check for common patterns that could be optimized + suggestions.AddRange(await FindAsyncAwaitOptimizations()); + suggestions.AddRange(await FindBatchingOpportunities()); + suggestions.AddRange(await FindCachingOpportunities()); + + return suggestions; + } + + /// + /// + /// Finds places where async/await patterns could be improved. + /// + /// + /// + /// + /// List of async/await optimization suggestions. + /// + /// + private async Task> FindAsyncAwaitOptimizations() + { + var suggestions = new List(); + var codebaseFiles = Directory.GetFiles("csharp", "*.cs", SearchOption.AllDirectories); + + foreach (var file in codebaseFiles) + { + try + { + var content = await File.ReadAllTextAsync(file); + var lines = content.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + + // Look for .Result usage + if (line.Contains(".Result") && line.Contains("Client.")) + { + suggestions.Add(new OptimizationSuggestion + { + Type = "Async/Await", + File = file, + LineNumber = i + 1, + Issue = "Using .Result can cause deadlocks", + Suggestion = "Consider using await instead of .Result", + Example = line.Replace(".Result", " // Consider: await " + line.Split('.')[0]) + }); + } + + // Look for .Wait() usage + if (line.Contains(".Wait()") && line.Contains("Client.")) + { + suggestions.Add(new OptimizationSuggestion + { + Type = "Async/Await", + File = file, + LineNumber = i + 1, + Issue = "Using .Wait() can cause deadlocks", + Suggestion = "Consider using await instead of .Wait()", + Example = line.Replace(".Wait()", " // Consider: await " + line.Split('.')[0]) + }); + } + } + } + catch (Exception ex) + { + // Skip files that can't be read + Console.WriteLine($"Could not analyze file {file}: {ex.Message}"); + } + } + + return suggestions; + } + + /// + /// + /// Finds opportunities for batching multiple API calls. + /// + /// + /// + /// + /// List of batching optimization suggestions. + /// + /// + private async Task> FindBatchingOpportunities() + { + var suggestions = new List(); + + // This is a simplified example - in practice, you'd analyze for loops + // that make individual API calls that could be batched + suggestions.Add(new OptimizationSuggestion + { + Type = "Batching", + File = "General", + LineNumber = 0, + Issue = "Multiple individual API calls in loops", + Suggestion = "Consider using batch API methods or GraphQL for multiple operations", + Example = "Instead of multiple GetRepository calls, use GetAllForOrg with pagination" + }); + + return suggestions; + } + + /// + /// + /// Finds opportunities for caching API responses. + /// + /// + /// + /// + /// List of caching optimization suggestions. + /// + /// + private async Task> FindCachingOpportunities() + { + var suggestions = new List(); + + suggestions.Add(new OptimizationSuggestion + { + Type = "Caching", + File = "General", + LineNumber = 0, + Issue = "Repeated calls to get organization members or repositories", + Suggestion = "Consider caching frequently accessed data that doesn't change often", + Example = "Cache organization member lists and repository metadata" + }); + + return suggestions; + } + + /// + /// + /// Analyzes usage patterns in the current codebase. + /// + /// + /// + /// + /// List of currently used methods. + /// + /// + /// + /// Analysis of usage patterns. + /// + /// + private UsagePatternAnalysis AnalyzeUsagePatterns(List usedMethods) + { + var analysis = new UsagePatternAnalysis(); + + analysis.MostUsedMethods = usedMethods + .GroupBy(m => m) + .OrderByDescending(g => g.Count()) + .Take(10) + .ToDictionary(g => g.Key, g => g.Count()); + + analysis.ApiCategories = usedMethods + .Where(m => m.Contains('.')) + .Select(m => m.Split('.')[1]) // Get the category (Repository, Issue, etc.) + .GroupBy(c => c) + .OrderByDescending(g => g.Count()) + .ToDictionary(g => g.Key, g => g.Count()); + + return analysis; + } + + /// + /// + /// Generates a human-readable report from the analysis results. + /// + /// + /// + /// + /// The analysis results. + /// + /// + /// + /// A formatted report string. + /// + /// + private string GenerateAnalysisReport(LibraryAnalysisResult analysis) + { + var report = new StringBuilder(); + + report.AppendLine("# GitHub API Library Usage Analysis Report"); + report.AppendLine(); + + // Usage patterns section + report.AppendLine("## Current Usage Patterns"); + report.AppendLine(); + report.AppendLine("### Most Used API Categories:"); + foreach (var category in analysis.UsagePatterns.ApiCategories.Take(5)) + { + report.AppendLine($"- **{category.Key}**: {category.Value} usages"); + } + report.AppendLine(); + + report.AppendLine("### Most Frequently Called Methods:"); + foreach (var method in analysis.UsagePatterns.MostUsedMethods.Take(5)) + { + report.AppendLine($"- `{method.Key}`: {method.Value} times"); + } + report.AppendLine(); + + // Optimization suggestions section + if (analysis.OptimizationSuggestions.Any()) + { + report.AppendLine("## Optimization Opportunities"); + report.AppendLine(); + + var groupedSuggestions = analysis.OptimizationSuggestions.GroupBy(s => s.Type); + foreach (var group in groupedSuggestions) + { + report.AppendLine($"### {group.Key} Optimizations ({group.Count()} found):"); + foreach (var suggestion in group.Take(3)) // Limit to top 3 per category + { + report.AppendLine($"- **Issue**: {suggestion.Issue}"); + report.AppendLine($" - **Suggestion**: {suggestion.Suggestion}"); + if (!string.IsNullOrEmpty(suggestion.File) && suggestion.File != "General") + { + report.AppendLine($" - **Location**: {suggestion.File}:{suggestion.LineNumber}"); + } + if (!string.IsNullOrEmpty(suggestion.Example)) + { + report.AppendLine($" - **Example**: `{suggestion.Example}`"); + } + report.AppendLine(); + } + } + } + + // Unused methods section + if (analysis.UnusedMethods.Any()) + { + report.AppendLine("## Available but Unused Library Methods"); + report.AppendLine(); + report.AppendLine("The following GitHub API methods are available but not currently used:"); + report.AppendLine(); + + var categorizedUnused = analysis.UnusedMethods + .Where(m => m.Contains('.')) + .GroupBy(m => m.Split('.')[1]) // Group by API category + .OrderBy(g => g.Key); + + foreach (var category in categorizedUnused.Take(5)) // Limit to 5 categories + { + report.AppendLine($"### {category.Key} API:"); + foreach (var method in category.Take(5)) // Limit to 5 methods per category + { + report.AppendLine($"- `{method}`"); + } + report.AppendLine(); + } + + if (analysis.UnusedMethods.Count > 50) + { + report.AppendLine($"*And {analysis.UnusedMethods.Count - 50} more unused methods...*"); + } + } + + // Summary section + report.AppendLine("## Summary"); + report.AppendLine(); + report.AppendLine($"- **Total unused methods**: {analysis.UnusedMethods.Count}"); + report.AppendLine($"- **Optimization opportunities found**: {analysis.OptimizationSuggestions.Count}"); + report.AppendLine($"- **Most used API category**: {analysis.UsagePatterns.ApiCategories.FirstOrDefault().Key ?? "None"}"); + report.AppendLine(); + report.AppendLine("This analysis can help identify opportunities to:"); + report.AppendLine("1. Improve performance by fixing async/await patterns"); + report.AppendLine("2. Reduce API calls through batching and caching"); + report.AppendLine("3. Discover new GitHub API capabilities that could enhance the bot"); + + return report.ToString(); + } + } + + /// + /// + /// Represents the result of library usage analysis. + /// + /// + /// + public class LibraryAnalysisResult + { + public List UnusedMethods { get; set; } = new(); + public List OptimizationSuggestions { get; set; } = new(); + public UsagePatternAnalysis UsagePatterns { get; set; } = new(); + } + + /// + /// + /// Represents an optimization suggestion. + /// + /// + /// + public class OptimizationSuggestion + { + public string Type { get; set; } = string.Empty; + public string File { get; set; } = string.Empty; + public int LineNumber { get; set; } + public string Issue { get; set; } = string.Empty; + public string Suggestion { get; set; } = string.Empty; + public string Example { get; set; } = string.Empty; + } + + /// + /// + /// Represents usage pattern analysis results. + /// + /// + /// + public class UsagePatternAnalysis + { + public Dictionary MostUsedMethods { get; set; } = new(); + public Dictionary ApiCategories { get; set; } = new(); + } +} \ No newline at end of file