From 07e015ec1d37f3da477c8f24d5b9f72fff9e6ef9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Sep 2025 21:57:03 +0000 Subject: [PATCH 1/3] Initial plan From e448f4d1aba464188f8911ce5fb6c575b0165c0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:03:31 +0000 Subject: [PATCH 2/3] Add concrete code examples to lint automation solution tab Co-authored-by: BorDevTech <73800053+BorDevTech@users.noreply.github.com> --- scripts/lint-automation/example-issue.md | 35 +++ .../lint-automation/github-issue-creator.ts | 222 +++++++++++++++++- 2 files changed, 256 insertions(+), 1 deletion(-) diff --git a/scripts/lint-automation/example-issue.md b/scripts/lint-automation/example-issue.md index 5b5c1d5..822c5a1 100644 --- a/scripts/lint-automation/example-issue.md +++ b/scripts/lint-automation/example-issue.md @@ -29,6 +29,41 @@ ### đŸ› ī¸ How to Fix +#### 💡 Code Example + +**❌ Before (causes lint error):** +```typescript +import { VetResult } from "@/app/types/vet-result"; + +interface VetRecord { // ← This interface is defined but never used + first_name: string; + last_name: string; + // ... other properties +} + +export async function verify() { + // Implementation without using VetRecord +} +``` + +**✅ After (fixed):** +```typescript +import { VetResult } from "@/app/types/vet-result"; + +// Option 1: Remove the unused interface entirely +export async function verify() { + // Implementation +} + +// Option 2: If you plan to use it later, prefix with underscore +interface _VetRecord { // ← Prefixed to indicate intentionally unused + first_name: string; + last_name: string; + // ... other properties +} +``` + +#### Step-by-Step Instructions: 1. **Review each affected file** listed above 2. **Apply the suggested solution** for each instance 3. **Test the changes** to ensure functionality is preserved diff --git a/scripts/lint-automation/github-issue-creator.ts b/scripts/lint-automation/github-issue-creator.ts index d1c388d..26807e4 100644 --- a/scripts/lint-automation/github-issue-creator.ts +++ b/scripts/lint-automation/github-issue-creator.ts @@ -306,8 +306,16 @@ class GitHubIssueCreator { } } - // Fix instructions + // Fix instructions with code examples body += `### đŸ› ī¸ How to Fix\n\n`; + + // Add specific code examples for this rule + const codeExample = this.generateCodeExample(ruleId, issues[0]); + if (codeExample) { + body += codeExample; + } + + body += `#### Step-by-Step Instructions:\n`; body += `1. **Review each affected file** listed above\n`; body += `2. **Apply the suggested solution** for each instance\n`; body += `3. **Test the changes** to ensure functionality is preserved\n`; @@ -329,6 +337,218 @@ class GitHubIssueCreator { return body; } + private generateCodeExample(ruleId: string, issue: AnalyzedIssue): string { + const examples: Record string> = { + '@typescript-eslint/no-unused-vars': (issue) => { + const fileName = issue.file.split('/').pop() || 'file'; + + if (issue.message.includes('VetRecord')) { + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +import { VetResult } from "@/app/types/vet-result"; + +interface VetRecord { // ← This interface is defined but never used + first_name: string; + last_name: string; + // ... other properties +} + +export async function verify() { + // Implementation without using VetRecord +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +import { VetResult } from "@/app/types/vet-result"; + +// Option 1: Remove the unused interface entirely +export async function verify() { + // Implementation +} + +// Option 2: If you plan to use it later, prefix with underscore +interface _VetRecord { // ← Prefixed to indicate intentionally unused + first_name: string; + last_name: string; + // ... other properties +} +\`\`\` + +`; + } else if (issue.message.includes('searchParams')) { + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); // ← Assigned but never used + // const firstName = searchParams.get("firstname") || ""; + // const lastName = searchParams.get("lastname") || ""; + + const key = "state-name"; + // ... rest of implementation +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +export async function GET(request: NextRequest) { + // Option 1: Remove if truly not needed + const key = "state-name"; + // ... rest of implementation + + // Option 2: If you need it later, use it immediately + const { searchParams } = new URL(request.url); + const firstName = searchParams.get("firstname") || ""; + const lastName = searchParams.get("lastname") || ""; + + // ... use firstName, lastName in implementation +} +\`\`\` + +`; + } else if (issue.message.includes('key')) { + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +export async function GET(request: NextRequest) { + const key = "florida"; // ← Assigned but never used + + // Implementation without using key variable + const response = await fetch(someUrl); + return response; +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +export async function GET(request: NextRequest) { + // Option 1: Remove if not needed + const response = await fetch(someUrl); + return response; + + // Option 2: Use the key variable in implementation + const key = "florida"; + const response = await fetch(\`/api/verify/\${key}\`); + return response; +} +\`\`\` + +`; + } + + // Generic unused variable example + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +function example() { + const unusedVariable = "some value"; // ← Never used + return "result"; +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +function example() { + // Option 1: Remove unused variable + return "result"; + + // Option 2: Use the variable + const usedVariable = "some value"; + return usedVariable; + + // Option 3: Prefix with underscore if intentionally unused + const _unusedVariable = "some value"; + return "result"; +} +\`\`\` + +`; + }, + + '@typescript-eslint/no-explicit-any': (issue) => { + if (issue.message.includes('parseBlob')) { + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +function parseBlob(raw: any): RawVetEntry[] { // ← Using 'any' type + return Array.isArray(raw) ? raw : raw.blob ?? []; +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +// Option 1: Define a proper interface for the raw data +interface ApiResponse { + blob?: RawVetEntry[]; +} + +function parseBlob(raw: ApiResponse | RawVetEntry[]): RawVetEntry[] { + return Array.isArray(raw) ? raw : raw.blob ?? []; +} + +// Option 2: Use 'unknown' for safer type handling +function parseBlob(raw: unknown): RawVetEntry[] { + if (Array.isArray(raw)) return raw; + if (typeof raw === 'object' && raw !== null && 'blob' in raw) { + return (raw as { blob?: RawVetEntry[] }).blob ?? []; + } + return []; +} +\`\`\` + +`; + } + + return `#### 💡 Code Example + +**❌ Before (causes lint error):** +\`\`\`typescript +function processData(data: any) { // ← Using 'any' defeats type safety + return data.someProperty; +} +\`\`\` + +**✅ After (fixed):** +\`\`\`typescript +// Option 1: Define proper interface +interface DataStructure { + someProperty: string; + // ... other known properties +} + +function processData(data: DataStructure) { + return data.someProperty; +} + +// Option 2: Use generic type +function processData(data: T) { + return data.someProperty; +} + +// Option 3: Use 'unknown' for external APIs +function processData(data: unknown) { + if (typeof data === 'object' && data !== null && 'someProperty' in data) { + return (data as { someProperty: string }).someProperty; + } + throw new Error('Invalid data structure'); +} +\`\`\` + +`; + } + }; + + const generator = examples[ruleId]; + return generator ? generator(issue) : ''; + } + private getAdditionalRuleContext(ruleId: string): string { const contexts: Record = { '@typescript-eslint/no-unused-vars': ` From ac11e4cc511a9828e53cc855d1f25f7b4356cef9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Sep 2025 22:15:05 +0000 Subject: [PATCH 3/3] Add intelligent duplicate prevention and issue lifecycle management to lint automation Co-authored-by: BorDevTech <73800053+BorDevTech@users.noreply.github.com> --- scripts/lint-automation/example-issue.md | 12 +- .../lint-automation/github-issue-creator.ts | 189 +++++++++++++++++- 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/scripts/lint-automation/example-issue.md b/scripts/lint-automation/example-issue.md index 822c5a1..016a413 100644 --- a/scripts/lint-automation/example-issue.md +++ b/scripts/lint-automation/example-issue.md @@ -1,4 +1,4 @@ -# Example: Automated GitHub Issue +# Example: Enhanced Automated GitHub Issue with Duplicate Prevention ## 🔧 ESLint Rule Violation: `@typescript-eslint/no-unused-vars` @@ -84,6 +84,16 @@ interface _VetRecord { // ← Prefixed to indicate intentionally unused - **Severity:** error - **Auto-generated:** 2024-12-21T21:30:00.000Z +### 🔄 Enhanced Duplicate Prevention + +This issue uses **intelligent duplicate detection** that: +- ✅ **Prevents duplicate issues** for the same lint rule +- ✅ **Updates existing issues** when violation counts change +- ✅ **Automatically closes issues** when all violations are resolved +- ✅ **Tracks progress** with detailed update comments + +If you see this issue updated automatically, it means new violations of this rule were detected or existing ones were fixed. + --- **Labels:** `lint`, `code-quality`, `bug`, `automated` diff --git a/scripts/lint-automation/github-issue-creator.ts b/scripts/lint-automation/github-issue-creator.ts index 26807e4..1539de7 100644 --- a/scripts/lint-automation/github-issue-creator.ts +++ b/scripts/lint-automation/github-issue-creator.ts @@ -104,9 +104,187 @@ class GitHubIssueCreator { return false; } + async checkExistingRuleIssue(ruleId: string): Promise<{ number: number; title: string; body: string } | null> { + if (!this.token) return null; + + try { + // Search for issues with the specific rule ID in the title + const searchQuery = `repo:${this.owner}/${this.repo}+is:issue+is:open+"Fix ${ruleId} violations"`; + const response = await fetch( + `${this.apiBase}/search/issues?q=${encodeURIComponent(searchQuery)}`, + { + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ClearView-Lint-Automation' + } + } + ); + + if (response.ok) { + const data = await response.json(); + if (data.total_count > 0) { + const issue = data.items[0]; + return { + number: issue.number, + title: issue.title, + body: issue.body + }; + } + } + } catch (error) { + console.warn(`âš ī¸ Could not check existing issues for rule ${ruleId}:`, error); + } + + return null; + } + + async closeResolvedIssues(report: IssueReport): Promise { + if (!this.token) { + console.log('🔍 Would check for resolved lint issues to close'); + return; + } + + try { + // Get all open lint issues created by automation + const response = await fetch( + `${this.apiBase}/search/issues?q=repo:${this.owner}/${this.repo}+is:issue+is:open+label:automated+label:lint`, + { + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ClearView-Lint-Automation' + } + } + ); + + if (!response.ok) return; + + const data = await response.json(); + const openLintIssues = data.items || []; + + // Get current rule IDs from the report + const currentRuleIds = new Set(report.issues.map(issue => issue.ruleId)); + + for (const issue of openLintIssues) { + // Extract rule ID from issue title + const ruleIdMatch = issue.title.match(/Fix (.+?) violations/); + if (!ruleIdMatch) continue; + + const ruleId = ruleIdMatch[1]; + + // If this rule is no longer in the current report, the issue is resolved + if (!currentRuleIds.has(ruleId)) { + await this.closeResolvedIssue(issue.number, ruleId); + } + } + } catch (error) { + console.warn('âš ī¸ Could not check for resolved issues:', error); + } + } + + async closeResolvedIssue(issueNumber: number, ruleId: string): Promise { + if (!this.token) return; + + try { + const response = await fetch(`${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ClearView-Lint-Automation' + }, + body: JSON.stringify({ + state: 'closed', + state_reason: 'completed' + }) + }); + + if (response.ok) { + // Add a comment explaining the closure + await this.addCommentToIssue(issueNumber, `🎉 **Issue Resolved!**\n\nAll instances of \`${ruleId}\` violations have been fixed. This issue is now automatically closed.\n\n*Closed by ClearView Lint Automation on ${new Date().toISOString()}*`); + console.log(`✅ Closed resolved issue #${issueNumber} for rule ${ruleId}`); + } else { + console.warn(`âš ī¸ Failed to close issue #${issueNumber}:`, response.statusText); + } + } catch (error) { + console.warn(`âš ī¸ Could not close resolved issue #${issueNumber}:`, error); + } + } + + async addCommentToIssue(issueNumber: number, comment: string): Promise { + if (!this.token) return; + + try { + await fetch(`${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ClearView-Lint-Automation' + }, + body: JSON.stringify({ body: comment }) + }); + } catch (error) { + console.warn(`âš ī¸ Could not add comment to issue #${issueNumber}:`, error); + } + } + + async updateExistingIssue(existingIssue: { number: number; title: string; body: string }, newGroup: any, ruleId: string): Promise { + if (!this.token) { + console.log(`🔍 Would update existing issue #${existingIssue.number} for rule ${ruleId}`); + return; + } + + try { + // Extract current instance count from existing title + const currentCountMatch = existingIssue.title.match(/\((\d+) instances?\)/); + const currentCount = currentCountMatch ? parseInt(currentCountMatch[1]) : 0; + + // Extract new instance count from new group title + const newCountMatch = newGroup.title.match(/\((\d+) instances?\)/); + const newCount = newCountMatch ? parseInt(newCountMatch[1]) : 0; + + // Only update if the count has changed (indicating new violations or fixes) + if (newCount !== currentCount) { + const updatedTitle = `🔧 Fix ${ruleId} violations (${newCount} instances)`; + const updatedBody = `${newGroup.body}\n\n---\n\n**🔄 Issue Updated:** ${new Date().toISOString()}\n**Previous Count:** ${currentCount} instances\n**Current Count:** ${newCount} instances`; + + const response = await fetch(`${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${existingIssue.number}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ClearView-Lint-Automation' + }, + body: JSON.stringify({ + title: updatedTitle, + body: updatedBody + }) + }); + + if (response.ok) { + console.log(`✅ Updated existing issue #${existingIssue.number} for ${ruleId} (${currentCount} → ${newCount} instances)`); + } else { + console.warn(`âš ī¸ Failed to update issue #${existingIssue.number}:`, response.statusText); + } + } else { + console.log(`â„šī¸ Issue #${existingIssue.number} for ${ruleId} already up to date (${newCount} instances)`); + } + } catch (error) { + console.warn(`âš ī¸ Could not update existing issue #${existingIssue.number}:`, error); + } + } + async createIssuesFromReport(report: IssueReport): Promise { console.log('📝 Creating GitHub issues from lint report...'); + // Check for resolved issues and close them + await this.closeResolvedIssues(report); + // Create a summary issue if there are multiple issues if (report.summary.totalIssues > 5) { await this.createSummaryIssue(report); @@ -116,11 +294,16 @@ class GitHubIssueCreator { const issueGroups = this.groupIssuesForGitHub(report.issues); for (const group of issueGroups) { - const searchTerm = `lint-issue-${group.category.toLowerCase().replace(/\s+/g, '-')}`; - const existingIssue = await this.checkExistingIssues(searchTerm); + // Extract the rule ID from the title for more specific duplicate checking + const ruleIdMatch = group.title.match(/Fix (.+?) violations/); + const ruleId = ruleIdMatch ? ruleIdMatch[1] : group.category; + + const existingIssue = await this.checkExistingRuleIssue(ruleId); if (existingIssue) { - console.log(`â­ī¸ Skipping ${group.category} - issue already exists`); + console.log(`â­ī¸ Skipping ${ruleId} - issue already exists (#${existingIssue.number})`); + // Optionally update the existing issue with new information + await this.updateExistingIssue(existingIssue, group, ruleId); continue; }