Skip to content

Instantly share code, notes, and snippets.

@samwightt
Last active July 15, 2025 15:28
Show Gist options
  • Save samwightt/04c86b184f8639aa39cd06ae048c0bbb to your computer and use it in GitHub Desktop.
Save samwightt/04c86b184f8639aa39cd06ae048c0bbb to your computer and use it in GitHub Desktop.
Break down ast-grep results by github codeowners.
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