Last active
July 15, 2025 15:28
-
-
Save samwightt/04c86b184f8639aa39cd06ae048c0bbb to your computer and use it in GitHub Desktop.
Break down ast-grep results by github codeowners.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { execSync } = require('child_process'); | |
const fs = require('fs'); | |
// Simple glob to regex conversion | |
function globToRegex(glob) { | |
const escaped = glob | |
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars | |
.replace(/\*/g, '.*') // Convert * to .* | |
.replace(/\?/g, '.'); // Convert ? to . | |
return new RegExp(`^${escaped}$`); | |
} | |
// Find the owning team for a file path | |
function findOwner(filePath, codeownersRules) { | |
let bestMatch = ''; | |
let bestPattern = ''; | |
for (const rule of codeownersRules) { | |
let matches = false; | |
// Check if pattern contains wildcards | |
if (rule.pattern.includes('*') || rule.pattern.includes('?')) { | |
// Use regex for wildcard patterns | |
const regex = globToRegex(rule.pattern); | |
matches = regex.test(filePath); | |
} else { | |
// Simple prefix check for directory patterns | |
matches = filePath.startsWith(rule.pattern); | |
} | |
if (matches) { | |
// Use the most specific match (longest pattern) | |
if (rule.pattern.length > bestPattern.length) { | |
bestMatch = rule.owners; | |
bestPattern = rule.pattern; | |
} | |
} | |
} | |
return bestMatch; | |
} | |
// Parse CODEOWNERS file | |
function parseCodeowners() { | |
const codeownersPath = '.github/CODEOWNERS'; | |
if (!fs.existsSync(codeownersPath)) { | |
console.error('CODEOWNERS file not found!'); | |
process.exit(1); | |
} | |
const content = fs.readFileSync(codeownersPath, 'utf8'); | |
const rules = []; | |
content.split('\n').forEach(line => { | |
const trimmed = line.trim(); | |
// Skip comments and empty lines | |
if (!trimmed || trimmed.startsWith('#')) { | |
return; | |
} | |
// Parse pattern and owners | |
const match = trimmed.match(/^(\S+)\s+(.+)$/); | |
if (match) { | |
const pattern = match[1].replace(/\/$/, ''); // Remove trailing slash | |
const owners = match[2] | |
.split(/\s+/) // Split on spaces | |
.filter(owner => owner) // Remove empty strings | |
.join(' '); // Join back | |
rules.push({ pattern, owners }); | |
} | |
}); | |
return rules; | |
} | |
// Main function | |
function main() { | |
// Get rule file from command line arguments | |
const ruleFile = process.argv[2]; | |
if (!ruleFile) { | |
console.error('Usage: node group-violations-by-team.js <rule-file>'); | |
console.error( | |
'Example: node group-violations-by-team.js sg/rules/no-ng-class.yml', | |
); | |
process.exit(1); | |
} | |
console.log(`Running ast-grep scan with rule: ${ruleFile}`); | |
// Run ast-grep with JSON output | |
let astGrepOutput; | |
try { | |
astGrepOutput = execSync( | |
`npx ast-grep scan --rule ${ruleFile} --json=compact`, | |
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }, | |
); | |
} catch (error) { | |
// ast-grep returns non-zero exit code when violations are found | |
astGrepOutput = error.stdout || ''; | |
} | |
if (!astGrepOutput || astGrepOutput.trim() === '[]') { | |
console.log('No violations found!'); | |
return; | |
} | |
// Parse JSON output | |
let violations; | |
try { | |
violations = JSON.parse(astGrepOutput); | |
} catch (error) { | |
console.error('Failed to parse ast-grep JSON output'); | |
console.error(error.message); | |
process.exit(1); | |
} | |
// Extract unique file paths | |
const filePaths = [...new Set(violations.map(v => v.file))].sort(); | |
// Parse CODEOWNERS and match files to teams | |
console.log('Analyzing file ownership...'); | |
const codeownersRules = parseCodeowners(); | |
// Group files by team | |
const teamFiles = {}; | |
filePaths.forEach(filePath => { | |
const owner = findOwner(filePath, codeownersRules); | |
const team = owner || 'UNOWNED'; | |
if (!teamFiles[team]) { | |
teamFiles[team] = []; | |
} | |
teamFiles[team].push(filePath); | |
}); | |
console.log(); | |
console.log('=== VIOLATIONS GROUPED BY TEAM ==='); | |
console.log(); | |
// Sort teams and display results | |
const sortedTeams = Object.keys(teamFiles).sort(); | |
sortedTeams.forEach(team => { | |
console.log(`Team: ${team}`); | |
console.log('Files with violations:'); | |
let teamTotal = 0; | |
teamFiles[team].forEach(filePath => { | |
// Count violations in this file | |
const violationCount = violations.filter(v => v.file === filePath).length; | |
console.log(` - ${filePath} (${violationCount} violations)`); | |
teamTotal += violationCount; | |
}); | |
console.log(`Total violations for ${team}: ${teamTotal}`); | |
console.log(); | |
}); | |
// Summary | |
console.log('=== SUMMARY ==='); | |
console.log(`Total files with violations: ${filePaths.length}`); | |
console.log(`Total violations: ${violations.length}`); | |
console.log(`Teams involved: ${sortedTeams.length}`); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment