diff --git a/tools/quality/code/check-code-complexity.ts b/tools/quality/code/check-code-complexity.ts index afb7b2bcb..e80668104 100644 --- a/tools/quality/code/check-code-complexity.ts +++ b/tools/quality/code/check-code-complexity.ts @@ -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()) diff --git a/tools/quality/code/check-code-complexity/analyze-file.ts b/tools/quality/code/check-code-complexity/analyze-file.ts new file mode 100644 index 000000000..39ff080c0 --- /dev/null +++ b/tools/quality/code/check-code-complexity/analyze-file.ts @@ -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 + } +} diff --git a/tools/quality/code/check-code-complexity/build-summary.ts b/tools/quality/code/check-code-complexity/build-summary.ts new file mode 100644 index 000000000..4eaff16ee --- /dev/null +++ b/tools/quality/code/check-code-complexity/build-summary.ts @@ -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() + } +} diff --git a/tools/quality/code/check-code-complexity/calculate-complexity.ts b/tools/quality/code/check-code-complexity/calculate-complexity.ts new file mode 100644 index 000000000..d1df2635c --- /dev/null +++ b/tools/quality/code/check-code-complexity/calculate-complexity.ts @@ -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 +} diff --git a/tools/quality/code/check-code-complexity/calculate-nesting-level.ts b/tools/quality/code/check-code-complexity/calculate-nesting-level.ts new file mode 100644 index 000000000..858f42734 --- /dev/null +++ b/tools/quality/code/check-code-complexity/calculate-nesting-level.ts @@ -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 +} diff --git a/tools/quality/code/check-code-complexity/constants.ts b/tools/quality/code/check-code-complexity/constants.ts new file mode 100644 index 000000000..5bb4f0840 --- /dev/null +++ b/tools/quality/code/check-code-complexity/constants.ts @@ -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'] diff --git a/tools/quality/code/check-code-complexity/extract-function-body.ts b/tools/quality/code/check-code-complexity/extract-function-body.ts new file mode 100644 index 000000000..5436ee5f4 --- /dev/null +++ b/tools/quality/code/check-code-complexity/extract-function-body.ts @@ -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 +} diff --git a/tools/quality/code/check-code-complexity/function-pattern.ts b/tools/quality/code/check-code-complexity/function-pattern.ts new file mode 100644 index 000000000..a95a550a1 --- /dev/null +++ b/tools/quality/code/check-code-complexity/function-pattern.ts @@ -0,0 +1 @@ +export const FUNCTION_PATTERN = /(?:function|const|async\s+(?:function)?|export\s+(?:async\s+)?function)\s+(\w+)/g diff --git a/tools/quality/code/check-code-complexity/index.ts b/tools/quality/code/check-code-complexity/index.ts new file mode 100644 index 000000000..bb84a67d7 --- /dev/null +++ b/tools/quality/code/check-code-complexity/index.ts @@ -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' diff --git a/tools/quality/code/check-code-complexity/run-check-code-complexity.ts b/tools/quality/code/check-code-complexity/run-check-code-complexity.ts new file mode 100644 index 000000000..032c2112b --- /dev/null +++ b/tools/quality/code/check-code-complexity/run-check-code-complexity.ts @@ -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) +} diff --git a/tools/quality/code/check-code-complexity/types.ts b/tools/quality/code/check-code-complexity/types.ts new file mode 100644 index 000000000..2fefd8f1b --- /dev/null +++ b/tools/quality/code/check-code-complexity/types.ts @@ -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 +} diff --git a/tools/quality/code/check-code-complexity/walk-directory.ts b/tools/quality/code/check-code-complexity/walk-directory.ts new file mode 100644 index 000000000..2024070ca --- /dev/null +++ b/tools/quality/code/check-code-complexity/walk-directory.ts @@ -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 +}