Compare commits

...

8 Commits

Author SHA1 Message Date
ocasta181 30b8a1cafc run lint complexity from the root directory 2025-04-01 13:30:42 -07:00
ocasta181 3358e6f11e fix scripts path 2025-04-01 13:21:42 -07:00
akfoster dadab51c4c Update .github/scripts/analyze-complexity.js
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-01 13:00:35 -07:00
ocasta181 5587a174ed Update test workflow configuration 2025-04-01 12:58:04 -07:00
ocasta181 6a492bcdd5 Merge main into ocasta181/ENG-226 2025-04-01 12:57:15 -07:00
ocasta181 288372b23b move test out into a script so we can run locally 2025-02-21 17:25:54 -08:00
ocasta181 e9a18cb69d move to maintainability index and complexity score 2025-02-21 16:57:46 -08:00
ocasta181 ac9cade617 add eslint complexity checks 2025-02-21 16:38:40 -08:00
7 changed files with 1020 additions and 256 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add code complexity metrics to ci/cd
+7 -1
View File
@@ -5,8 +5,14 @@
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"plugins": ["@typescript-eslint", "sonarjs"],
"rules": {
"sonarjs/cognitive-complexity": ["warn", 15],
"complexity": ["warn", { "max": 20 }],
"max-depth": ["warn", 4],
"max-lines-per-function": ["warn", { "max": 50, "skipBlankLines": true, "skipComments": true }],
"max-nested-callbacks": ["warn", 3],
"max-params": ["warn", 4],
"@typescript-eslint/naming-convention": [
"warn",
{
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env node
const fs = require("fs")
const { execSync } = require("child_process")
// Get current branch name
function getCurrentBranch() {
return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim()
}
// Analyze a specific branch and return its complexity metrics
function analyzeBranch(branchName) {
// Checkout the branch
execSync(`git checkout ${branchName}`, { stdio: "inherit" })
// Run ESLint complexity check from root directory
execSync("npm run lint:complexity", { stdio: "inherit", cwd: process.cwd() })
const eslintReport = JSON.parse(fs.readFileSync("complexity-report.json", "utf8"))
// Get complexity violations
const totalViolations = eslintReport.length || 0
const cognitiveViolations = eslintReport.filter((msg) => msg.ruleId === "sonarjs/cognitive-complexity").length
// Run maintainability analysis
execSync("node .github/scripts/complexity-analysis.js", { stdio: "inherit" })
const maintainabilityReport = JSON.parse(fs.readFileSync("maintainability-report.json", "utf8"))
const maintainabilityIndex = maintainabilityReport.summary.averageMaintainabilityIndex
return {
violations: totalViolations,
cognitiveViolations,
maintainabilityIndex,
}
}
// Generate a report comparing two sets of metrics
function generateReport(baseMetrics, currentMetrics, baseBranch, currentBranch) {
const getStatus = (base, current, inverse = false) => {
if (inverse) {
return current < base ? "⚠️" : "✅"
}
return current > base ? "⚠️" : "✅"
}
const report = [
"### Code Complexity Analysis Report",
"",
`Comparing '${currentBranch}' with '${baseBranch}'`,
"",
"| Metric | Base Branch | Current Branch | Status |",
"|--------|-------------|----------------|---------|",
`| ESLint Complexity Violations | ${baseMetrics.violations} | ${currentMetrics.violations} | ${getStatus(baseMetrics.violations, currentMetrics.violations)} |`,
`| Cognitive Complexity Violations | ${baseMetrics.cognitiveViolations} | ${currentMetrics.cognitiveViolations} | ${getStatus(baseMetrics.cognitiveViolations, currentMetrics.cognitiveViolations)} |`,
`| Maintainability Index | ${baseMetrics.maintainabilityIndex.toFixed(2)} | ${currentMetrics.maintainabilityIndex.toFixed(2)} | ${getStatus(baseMetrics.maintainabilityIndex, currentMetrics.maintainabilityIndex, true)} |`,
"",
"#### Legend",
"- ✅ No significant increase in complexity",
"- ⚠️ Complexity has increased",
].join("\n")
// In CI, write to file for PR comment
if (process.env.GITHUB_ACTIONS) {
fs.writeFileSync("complexity-report.md", report)
} else {
console.log(report)
}
// Return true if any metrics have degraded
return (
currentMetrics.violations > baseMetrics.violations ||
currentMetrics.cognitiveViolations > baseMetrics.cognitiveViolations ||
currentMetrics.maintainabilityIndex < baseMetrics.maintainabilityIndex
)
}
// Main execution
function main() {
let currentBranch, baseBranch
// Determine if running in GitHub Actions or locally
if (process.env.GITHUB_ACTIONS) {
baseBranch = process.env.GITHUB_BASE_REF
currentBranch = process.env.GITHUB_HEAD_REF
if (!baseBranch || !currentBranch) {
console.error("Required GitHub environment variables not found")
process.exit(1)
}
} else {
currentBranch = getCurrentBranch()
baseBranch = process.argv[2] || "main"
}
console.log(`Comparing complexity metrics between '${currentBranch}' and '${baseBranch}'`)
// Store current branch name to return to it
const originalBranch = currentBranch
try {
// Fetch the base branch
execSync(`git fetch origin ${baseBranch}`, { stdio: "inherit" })
// Analyze base branch
console.log("\nAnalyzing base branch...")
const baseMetrics = analyzeBranch(`origin/${baseBranch}`)
// Analyze current/PR branch
console.log("\nAnalyzing current branch...")
const currentMetrics = analyzeBranch(originalBranch)
// Generate report
console.log("\nGenerating report...")
const hasComplexityIncreased = generateReport(baseMetrics, currentMetrics, baseBranch, currentBranch)
// Set output for GitHub Actions if in CI
if (process.env.GITHUB_ACTIONS) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `complexity_increased=${hasComplexityIncreased}\n`)
}
if (hasComplexityIncreased) {
console.log("\n⚠️ Code complexity has increased")
} else {
console.log("\n✅ Code complexity is stable or has improved")
}
} catch (error) {
console.error("Error:", error.message)
process.exit(1)
} finally {
// Always return to the original branch
execSync(`git checkout ${originalBranch}`, { stdio: "inherit" })
}
}
main()
+94
View File
@@ -0,0 +1,94 @@
const escomplex = require("escomplex")
const fs = require("fs")
const path = require("path")
const glob = require("glob")
function calculateMaintainabilityIndex(halsteadVolume, cyclomaticComplexity, sloc) {
return Math.max(
0,
Math.min(100, ((171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(sloc)) * 100) / 171),
)
}
function analyzeFile(filePath) {
const content = fs.readFileSync(filePath, "utf8")
const report = escomplex.analyse(content, {})
const metrics = {
path: filePath,
maintainabilityIndex: 0,
cyclomaticComplexity: 0,
sloc: 0,
functions: [],
}
// Calculate metrics for each function
report.functions.forEach((func) => {
const mi = calculateMaintainabilityIndex(func.halstead.volume, func.cyclomatic, func.sloc.physical)
metrics.functions.push({
name: func.name,
maintainabilityIndex: mi,
cyclomaticComplexity: func.cyclomatic,
sloc: func.sloc.physical,
})
// Add to file totals
metrics.maintainabilityIndex += mi
metrics.cyclomaticComplexity += func.cyclomatic
metrics.sloc += func.sloc.physical
})
// Average the maintainability index if there are functions
if (report.functions.length > 0) {
metrics.maintainabilityIndex /= report.functions.length
}
return metrics
}
function analyzeProject(pattern) {
const files = glob.sync(pattern)
const results = {
files: [],
summary: {
totalFiles: 0,
averageMaintainabilityIndex: 0,
totalCyclomaticComplexity: 0,
totalSLOC: 0,
},
}
files.forEach((file) => {
try {
const metrics = analyzeFile(file)
results.files.push(metrics)
results.summary.totalFiles++
results.summary.averageMaintainabilityIndex += metrics.maintainabilityIndex
results.summary.totalCyclomaticComplexity += metrics.cyclomaticComplexity
results.summary.totalSLOC += metrics.sloc
} catch (error) {
console.error(`Error analyzing ${file}:`, error.message)
}
})
if (results.summary.totalFiles > 0) {
results.summary.averageMaintainabilityIndex /= results.summary.totalFiles
}
return results
}
// Analyze TypeScript files in src directory
const results = analyzeProject("src/**/*.ts")
// Write results to file
fs.writeFileSync("maintainability-report.json", JSON.stringify(results, null, 2))
// Log summary
console.log("Analysis Summary:")
console.log("Total Files:", results.summary.totalFiles)
console.log("Average Maintainability Index:", results.summary.averageMaintainabilityIndex.toFixed(2))
console.log("Total Cyclomatic Complexity:", results.summary.totalCyclomaticComplexity)
console.log("Total SLOC:", results.summary.totalSLOC)
+36
View File
@@ -181,3 +181,39 @@ jobs:
--verbose
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
complexity-check:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20.15.1
- name: Install dependencies
run: npm ci
- name: Run Complexity Analysis
id: complexity
run: npm run complexity
- name: Post PR Comment
if: always()
run: |
if [ -f complexity-report.md ]; then
gh pr comment ${{ github.event.pull_request.number }} --body-file complexity-report.md
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check Complexity Status
if: steps.complexity.outputs.complexity_increased == 'true'
run: |
echo "::warning::Code complexity metrics have degraded in this PR"
echo "Please review the complexity report in the PR comments"
+741 -255
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -299,6 +299,9 @@
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
"lint:complexity": "eslint src --ext ts --format json -o complexity-report.json || true",
"analyze:complexity": "node .github/scripts/analyze-complexity.js",
"complexity": "node .github/scripts/analyze-complexity.js",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "vscode-test",
@@ -333,7 +336,9 @@
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"esbuild": "^0.25.0",
"escomplex": "^2.0.0-alpha",
"eslint": "^8.57.0",
"eslint-plugin-sonarjs": "^3.0.2",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",