mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor: modularize complexity checker
This commit is contained in:
@@ -1,175 +1,5 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { readdirSync, readFileSync, statSync } from 'fs'
|
||||
import { join, extname } from 'path'
|
||||
import { runCheckCodeComplexity } from './check-code-complexity'
|
||||
|
||||
interface ComplexityMetrics {
|
||||
file: string
|
||||
functions: Array<{
|
||||
name: string
|
||||
complexity: number
|
||||
lines: number
|
||||
}>
|
||||
averageComplexity: number
|
||||
maxComplexity: number
|
||||
violations: string[]
|
||||
}
|
||||
|
||||
const MAX_CYCLOMATIC_COMPLEXITY = 10
|
||||
const MAX_COGNITIVE_COMPLEXITY = 15
|
||||
const MAX_NESTING = 4
|
||||
|
||||
function analyzeComplexity(): ComplexityMetrics[] {
|
||||
const results: ComplexityMetrics[] = []
|
||||
const srcDir = 'src'
|
||||
|
||||
function walkDir(dir: string) {
|
||||
const files = readdirSync(dir)
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = join(dir, file)
|
||||
const stat = statSync(fullPath)
|
||||
|
||||
if (stat.isDirectory() && !['node_modules', '.next', 'dist', 'build'].includes(file)) {
|
||||
walkDir(fullPath)
|
||||
} else if (['.ts', '.tsx'].includes(extname(file))) {
|
||||
analyzeFile(fullPath, results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(srcDir)
|
||||
return results
|
||||
}
|
||||
|
||||
function analyzeFile(filePath: string, results: ComplexityMetrics[]) {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
const metrics: ComplexityMetrics = {
|
||||
file: filePath,
|
||||
functions: [],
|
||||
averageComplexity: 0,
|
||||
maxComplexity: 0,
|
||||
violations: []
|
||||
}
|
||||
|
||||
// Simple regex-based analysis (production would use AST parser)
|
||||
const functionPattern = /(?:function|const|async\s+(?:function)?|export\s+(?:async\s+)?function)\s+(\w+)/g
|
||||
let match
|
||||
|
||||
while ((match = functionPattern.exec(content)) !== null) {
|
||||
const startIndex = match.index
|
||||
const functionName = match[1]
|
||||
|
||||
// Calculate complexity by counting conditionals
|
||||
const functionContent = extractFunctionBody(content, startIndex)
|
||||
const complexity = calculateComplexity(functionContent)
|
||||
const nestingLevel = calculateNestingLevel(functionContent)
|
||||
|
||||
metrics.functions.push({
|
||||
name: functionName,
|
||||
complexity,
|
||||
lines: functionContent.split('\n').length
|
||||
})
|
||||
|
||||
metrics.maxComplexity = Math.max(metrics.maxComplexity, complexity)
|
||||
|
||||
if (complexity > MAX_CYCLOMATIC_COMPLEXITY) {
|
||||
metrics.violations.push(
|
||||
`Function "${functionName}" has complexity ${complexity} (max: ${MAX_CYCLOMATIC_COMPLEXITY})`
|
||||
)
|
||||
}
|
||||
|
||||
if (nestingLevel > MAX_NESTING) {
|
||||
metrics.violations.push(
|
||||
`Function "${functionName}" has nesting level ${nestingLevel} (max: ${MAX_NESTING})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.functions.length > 0) {
|
||||
metrics.averageComplexity =
|
||||
metrics.functions.reduce((sum, f) => sum + f.complexity, 0) / metrics.functions.length
|
||||
}
|
||||
|
||||
if (metrics.violations.length > 0) {
|
||||
results.push(metrics)
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip files that can't be analyzed
|
||||
}
|
||||
}
|
||||
|
||||
function extractFunctionBody(content: string, startIndex: number): string {
|
||||
let braceCount = 0
|
||||
let inFunction = false
|
||||
let result = ''
|
||||
|
||||
for (let i = startIndex; i < content.length; i++) {
|
||||
const char = content[i]
|
||||
|
||||
if (char === '{') {
|
||||
inFunction = true
|
||||
braceCount++
|
||||
}
|
||||
|
||||
if (inFunction) {
|
||||
result += char
|
||||
|
||||
if (char === '}') {
|
||||
braceCount--
|
||||
if (braceCount === 0) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function calculateComplexity(code: string): number {
|
||||
let complexity = 1
|
||||
complexity += (code.match(/if\s*\(/g) || []).length
|
||||
complexity += (code.match(/\?.*:/g) || []).length
|
||||
complexity += (code.match(/case\s+/g) || []).length
|
||||
complexity += (code.match(/catch\s*\(/g) || []).length
|
||||
complexity += (code.match(/for\s*\(/g) || []).length
|
||||
complexity += (code.match(/while\s*\(/g) || []).length
|
||||
complexity += (code.match(/&&/g) || []).length * 0.1
|
||||
complexity += (code.match(/\|\|/g) || []).length * 0.1
|
||||
return Math.round(complexity * 10) / 10
|
||||
}
|
||||
|
||||
function calculateNestingLevel(code: string): number {
|
||||
let maxNesting = 0
|
||||
let currentNesting = 0
|
||||
|
||||
for (const char of code) {
|
||||
if (char === '{') {
|
||||
currentNesting++
|
||||
maxNesting = Math.max(maxNesting, currentNesting)
|
||||
} else if (char === '}') {
|
||||
currentNesting--
|
||||
}
|
||||
}
|
||||
|
||||
return maxNesting
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const results = analyzeComplexity()
|
||||
|
||||
const summary = {
|
||||
totalFilesAnalyzed: results.length,
|
||||
violatingFiles: results.length,
|
||||
totalViolations: results.reduce((sum, r) => sum + r.violations.length, 0),
|
||||
avgMaxComplexity: results.length > 0
|
||||
? results.reduce((sum, r) => sum + r.maxComplexity, 0) / results.length
|
||||
: 0,
|
||||
details: results,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2))
|
||||
console.log(runCheckCodeComplexity())
|
||||
|
||||
66
tools/quality/code/check-code-complexity/analyze-file.ts
Normal file
66
tools/quality/code/check-code-complexity/analyze-file.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import { MAX_CYCLOMATIC_COMPLEXITY, MAX_NESTING } from './constants'
|
||||
import { calculateComplexity } from './calculate-complexity'
|
||||
import { calculateNestingLevel } from './calculate-nesting-level'
|
||||
import { extractFunctionBody } from './extract-function-body'
|
||||
import { FUNCTION_PATTERN } from './function-pattern'
|
||||
import { ComplexityMetrics } from './types'
|
||||
|
||||
export const analyzeFile = (filePath: string): ComplexityMetrics | null => {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8')
|
||||
|
||||
const metrics: ComplexityMetrics = {
|
||||
file: filePath,
|
||||
functions: [],
|
||||
averageComplexity: 0,
|
||||
maxComplexity: 0,
|
||||
violations: []
|
||||
}
|
||||
|
||||
let match
|
||||
|
||||
while ((match = FUNCTION_PATTERN.exec(content)) !== null) {
|
||||
const startIndex = match.index
|
||||
const functionName = match[1]
|
||||
|
||||
const functionContent = extractFunctionBody(content, startIndex)
|
||||
const complexity = calculateComplexity(functionContent)
|
||||
const nestingLevel = calculateNestingLevel(functionContent)
|
||||
|
||||
metrics.functions.push({
|
||||
name: functionName,
|
||||
complexity,
|
||||
lines: functionContent.split('\n').length
|
||||
})
|
||||
|
||||
metrics.maxComplexity = Math.max(metrics.maxComplexity, complexity)
|
||||
|
||||
if (complexity > MAX_CYCLOMATIC_COMPLEXITY) {
|
||||
metrics.violations.push(
|
||||
`Function "${functionName}" has complexity ${complexity} (max: ${MAX_CYCLOMATIC_COMPLEXITY})`
|
||||
)
|
||||
}
|
||||
|
||||
if (nestingLevel > MAX_NESTING) {
|
||||
metrics.violations.push(
|
||||
`Function "${functionName}" has nesting level ${nestingLevel} (max: ${MAX_NESTING})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.functions.length > 0) {
|
||||
metrics.averageComplexity =
|
||||
metrics.functions.reduce((sum, func) => sum + func.complexity, 0) / metrics.functions.length
|
||||
}
|
||||
|
||||
if (metrics.violations.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return metrics
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
15
tools/quality/code/check-code-complexity/build-summary.ts
Normal file
15
tools/quality/code/check-code-complexity/build-summary.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ComplexityMetrics, ComplexitySummary } from './types'
|
||||
|
||||
export const buildSummary = (results: ComplexityMetrics[]): ComplexitySummary => {
|
||||
return {
|
||||
totalFilesAnalyzed: results.length,
|
||||
violatingFiles: results.length,
|
||||
totalViolations: results.reduce((sum, record) => sum + record.violations.length, 0),
|
||||
avgMaxComplexity:
|
||||
results.length > 0
|
||||
? results.reduce((sum, record) => sum + record.maxComplexity, 0) / results.length
|
||||
: 0,
|
||||
details: results,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const calculateComplexity = (code: string): number => {
|
||||
let complexity = 1
|
||||
complexity += (code.match(/if\s*\(/g) || []).length
|
||||
complexity += (code.match(/\?.*:/g) || []).length
|
||||
complexity += (code.match(/case\s+/g) || []).length
|
||||
complexity += (code.match(/catch\s*\(/g) || []).length
|
||||
complexity += (code.match(/for\s*\(/g) || []).length
|
||||
complexity += (code.match(/while\s*\(/g) || []).length
|
||||
complexity += (code.match(/&&/g) || []).length * 0.1
|
||||
complexity += (code.match(/\|\|/g) || []).length * 0.1
|
||||
return Math.round(complexity * 10) / 10
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export const calculateNestingLevel = (code: string): number => {
|
||||
let maxNesting = 0
|
||||
let currentNesting = 0
|
||||
|
||||
for (const char of code) {
|
||||
if (char === '{') {
|
||||
currentNesting++
|
||||
maxNesting = Math.max(maxNesting, currentNesting)
|
||||
} else if (char === '}') {
|
||||
currentNesting--
|
||||
}
|
||||
}
|
||||
|
||||
return maxNesting
|
||||
}
|
||||
6
tools/quality/code/check-code-complexity/constants.ts
Normal file
6
tools/quality/code/check-code-complexity/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const MAX_CYCLOMATIC_COMPLEXITY = 10
|
||||
export const MAX_COGNITIVE_COMPLEXITY = 15
|
||||
export const MAX_NESTING = 4
|
||||
|
||||
export const DEFAULT_SRC_DIR = 'src'
|
||||
export const IGNORED_DIRECTORIES = ['node_modules', '.next', 'dist', 'build']
|
||||
@@ -0,0 +1,25 @@
|
||||
export const extractFunctionBody = (content: string, startIndex: number): string => {
|
||||
let braceCount = 0
|
||||
let inFunction = false
|
||||
let result = ''
|
||||
|
||||
for (let i = startIndex; i < content.length; i++) {
|
||||
const char = content[i]
|
||||
|
||||
if (char === '{') {
|
||||
inFunction = true
|
||||
braceCount++
|
||||
}
|
||||
|
||||
if (inFunction) {
|
||||
result += char
|
||||
|
||||
if (char === '}') {
|
||||
braceCount--
|
||||
if (braceCount === 0) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const FUNCTION_PATTERN = /(?:function|const|async\s+(?:function)?|export\s+(?:async\s+)?function)\s+(\w+)/g
|
||||
10
tools/quality/code/check-code-complexity/index.ts
Normal file
10
tools/quality/code/check-code-complexity/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './analyze-file'
|
||||
export * from './build-summary'
|
||||
export * from './calculate-complexity'
|
||||
export * from './calculate-nesting-level'
|
||||
export * from './constants'
|
||||
export * from './extract-function-body'
|
||||
export * from './function-pattern'
|
||||
export * from './run-check-code-complexity'
|
||||
export * from './types'
|
||||
export * from './walk-directory'
|
||||
@@ -0,0 +1,8 @@
|
||||
import { analyzeComplexity } from './walk-directory'
|
||||
import { buildSummary } from './build-summary'
|
||||
|
||||
export const runCheckCodeComplexity = (): string => {
|
||||
const results = analyzeComplexity()
|
||||
const summary = buildSummary(results)
|
||||
return JSON.stringify(summary, null, 2)
|
||||
}
|
||||
22
tools/quality/code/check-code-complexity/types.ts
Normal file
22
tools/quality/code/check-code-complexity/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface ComplexityFunctionMetrics {
|
||||
name: string
|
||||
complexity: number
|
||||
lines: number
|
||||
}
|
||||
|
||||
export interface ComplexityMetrics {
|
||||
file: string
|
||||
functions: ComplexityFunctionMetrics[]
|
||||
averageComplexity: number
|
||||
maxComplexity: number
|
||||
violations: string[]
|
||||
}
|
||||
|
||||
export interface ComplexitySummary {
|
||||
totalFilesAnalyzed: number
|
||||
violatingFiles: number
|
||||
totalViolations: number
|
||||
avgMaxComplexity: number
|
||||
details: ComplexityMetrics[]
|
||||
timestamp: string
|
||||
}
|
||||
37
tools/quality/code/check-code-complexity/walk-directory.ts
Normal file
37
tools/quality/code/check-code-complexity/walk-directory.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { readdirSync, statSync } from 'fs'
|
||||
import { extname, join } from 'path'
|
||||
|
||||
import { DEFAULT_SRC_DIR, IGNORED_DIRECTORIES } from './constants'
|
||||
import { analyzeFile } from './analyze-file'
|
||||
import { ComplexityMetrics } from './types'
|
||||
|
||||
const shouldAnalyzeDirectory = (directoryName: string): boolean => {
|
||||
return !IGNORED_DIRECTORIES.includes(directoryName)
|
||||
}
|
||||
|
||||
export const analyzeComplexity = (): ComplexityMetrics[] => {
|
||||
const results: ComplexityMetrics[] = []
|
||||
|
||||
const walkDir = (dir: string) => {
|
||||
const files = readdirSync(dir)
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = join(dir, file)
|
||||
const stat = statSync(fullPath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (shouldAnalyzeDirectory(file)) {
|
||||
walkDir(fullPath)
|
||||
}
|
||||
} else if (['.ts', '.tsx'].includes(extname(file))) {
|
||||
const metrics = analyzeFile(fullPath)
|
||||
if (metrics) {
|
||||
results.push(metrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(DEFAULT_SRC_DIR)
|
||||
return results
|
||||
}
|
||||
Reference in New Issue
Block a user