@boshu2/vibe-check 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plans/2025-12-29-complexity-driver-plan.md +225 -0
- package/.agents/plans/2025-12-29-complexity-drivers-plan.md +253 -0
- package/.agents/research/2025-12-29-complexity-driver-architecture.md +392 -0
- package/.agents/research/2025-12-29-complexity-drivers.md +227 -0
- package/.beads/issues.jsonl +12 -0
- package/CHANGELOG.md +27 -0
- package/README.md +71 -0
- package/dist/analyzers/complexity.d.ts +92 -0
- package/dist/analyzers/complexity.d.ts.map +1 -0
- package/dist/analyzers/complexity.js +79 -0
- package/dist/analyzers/complexity.js.map +1 -0
- package/dist/analyzers/modularity.d.ts +3 -1
- package/dist/analyzers/modularity.d.ts.map +1 -1
- package/dist/analyzers/modularity.js +32 -6
- package/dist/analyzers/modularity.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/driver.d.ts +18 -0
- package/dist/commands/driver.d.ts.map +1 -0
- package/dist/commands/driver.js +58 -0
- package/dist/commands/driver.js.map +1 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/modularity.d.ts +2 -0
- package/dist/commands/modularity.d.ts.map +1 -1
- package/dist/commands/modularity.js +86 -7
- package/dist/commands/modularity.js.map +1 -1
- package/drivers/README.md +327 -0
- package/drivers/go.sh +131 -0
- package/drivers/java.sh +137 -0
- package/drivers/javascript.sh +134 -0
- package/drivers/php.sh +132 -0
- package/drivers/python.sh +90 -0
- package/drivers/rust.sh +132 -0
- package/package.json +1 -1
package/drivers/go.sh
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# drivers/go.sh
|
|
3
|
+
# Wraps gocyclo to produce standard complexity JSON
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./drivers/go.sh [directory]
|
|
6
|
+
# directory: Path to Go code to analyze (default: current directory)
|
|
7
|
+
#
|
|
8
|
+
# Output: JSON conforming to ComplexityReport schema
|
|
9
|
+
# Exit codes: 0 = success, 1 = error (gocyclo not installed or other failure)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TARGET_DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Check gocyclo is installed
|
|
16
|
+
if ! command -v gocyclo &> /dev/null; then
|
|
17
|
+
echo '{"error": "gocyclo not installed. Run: go install github.com/fzipp/gocyclo/cmd/gocyclo@latest"}' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if target directory exists
|
|
22
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
23
|
+
echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check if there are any Go files
|
|
28
|
+
if ! find "$TARGET_DIR" -name "*.go" ! -path "*/vendor/*" -print -quit 2>/dev/null | grep -q .; then
|
|
29
|
+
# No Go files found, output empty result
|
|
30
|
+
echo '{"tool":"gocyclo","language":"go","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Run gocyclo and transform output
|
|
35
|
+
# gocyclo outputs lines like:
|
|
36
|
+
# 8 main complex /tmp/go-test/main.go:16:1
|
|
37
|
+
# <complexity> <package> <function> <file:row:column>
|
|
38
|
+
|
|
39
|
+
gocyclo "$TARGET_DIR" 2>/dev/null | awk -v target_dir="$TARGET_DIR" '
|
|
40
|
+
BEGIN {
|
|
41
|
+
# Initialize
|
|
42
|
+
file_count = 0
|
|
43
|
+
func_count = 0
|
|
44
|
+
total_complexity = 0
|
|
45
|
+
grade_a = 0; grade_b = 0; grade_c = 0; grade_d = 0; grade_e = 0; grade_f = 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function complexity_to_grade(c) {
|
|
49
|
+
if (c <= 5) return "A"
|
|
50
|
+
else if (c <= 10) return "B"
|
|
51
|
+
else if (c <= 20) return "C"
|
|
52
|
+
else if (c <= 30) return "D"
|
|
53
|
+
else if (c <= 40) return "E"
|
|
54
|
+
else return "F"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function increment_grade(g) {
|
|
58
|
+
if (g == "A") grade_a++
|
|
59
|
+
else if (g == "B") grade_b++
|
|
60
|
+
else if (g == "C") grade_c++
|
|
61
|
+
else if (g == "D") grade_d++
|
|
62
|
+
else if (g == "E") grade_e++
|
|
63
|
+
else grade_f++
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
# Parse: complexity package function file:line:col
|
|
68
|
+
complexity = $1
|
|
69
|
+
pkg = $2
|
|
70
|
+
func_name = $3
|
|
71
|
+
file_line = $4
|
|
72
|
+
|
|
73
|
+
# Split file:line:col
|
|
74
|
+
split(file_line, parts, ":")
|
|
75
|
+
file = parts[1]
|
|
76
|
+
line = parts[2]
|
|
77
|
+
|
|
78
|
+
# Track functions per file
|
|
79
|
+
if (!(file in files)) {
|
|
80
|
+
files[file] = 1
|
|
81
|
+
file_count++
|
|
82
|
+
file_functions[file] = ""
|
|
83
|
+
file_complexities[file] = ""
|
|
84
|
+
file_total[file] = 0
|
|
85
|
+
file_max[file] = 0
|
|
86
|
+
file_func_count[file] = 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func_count++
|
|
90
|
+
total_complexity += complexity
|
|
91
|
+
|
|
92
|
+
grade = complexity_to_grade(complexity)
|
|
93
|
+
|
|
94
|
+
# Append function data (JSON format)
|
|
95
|
+
sep = (file_functions[file] == "") ? "" : ","
|
|
96
|
+
file_functions[file] = file_functions[file] sep sprintf("{\"name\":\"%s\",\"complexity\":%d,\"grade\":\"%s\",\"line\":%d,\"endLine\":null}", func_name, complexity, grade, line)
|
|
97
|
+
|
|
98
|
+
# Track file stats
|
|
99
|
+
file_total[file] += complexity
|
|
100
|
+
file_func_count[file]++
|
|
101
|
+
if (complexity > file_max[file]) {
|
|
102
|
+
file_max[file] = complexity
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
END {
|
|
107
|
+
# Output JSON
|
|
108
|
+
printf "{\"tool\":\"gocyclo\",\"language\":\"go\",\"generatedAt\":\""
|
|
109
|
+
# Get current timestamp
|
|
110
|
+
cmd = "date -u +\"%Y-%m-%dT%H:%M:%SZ\""
|
|
111
|
+
cmd | getline timestamp
|
|
112
|
+
close(cmd)
|
|
113
|
+
printf "%s\",\"files\":{", timestamp
|
|
114
|
+
|
|
115
|
+
first_file = 1
|
|
116
|
+
for (file in files) {
|
|
117
|
+
if (!first_file) printf ","
|
|
118
|
+
first_file = 0
|
|
119
|
+
|
|
120
|
+
avg = (file_func_count[file] > 0) ? file_total[file] / file_func_count[file] : 0
|
|
121
|
+
file_grade = complexity_to_grade(avg)
|
|
122
|
+
increment_grade(file_grade)
|
|
123
|
+
|
|
124
|
+
printf "\"%s\":{\"functions\":[%s],\"avgComplexity\":%.2f,\"maxComplexity\":%d,\"grade\":\"%s\"}", file, file_functions[file], avg, file_max[file], file_grade
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
avg_complexity = (func_count > 0) ? total_complexity / func_count : 0
|
|
128
|
+
|
|
129
|
+
printf "},\"summary\":{\"totalFiles\":%d,\"totalFunctions\":%d,\"avgComplexity\":%.2f,\"gradeDistribution\":{\"A\":%d,\"B\":%d,\"C\":%d,\"D\":%d,\"E\":%d,\"F\":%d}}}", file_count, func_count, avg_complexity, grade_a, grade_b, grade_c, grade_d, grade_e, grade_f
|
|
130
|
+
}
|
|
131
|
+
'
|
package/drivers/java.sh
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# drivers/java.sh
|
|
3
|
+
# Wraps PMD to produce standard complexity JSON
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./drivers/java.sh [directory]
|
|
6
|
+
# directory: Path to Java code to analyze (default: current directory)
|
|
7
|
+
#
|
|
8
|
+
# Output: JSON conforming to ComplexityReport schema
|
|
9
|
+
# Exit codes: 0 = success, 1 = error (pmd not installed or other failure)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TARGET_DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Check pmd is installed
|
|
16
|
+
if ! command -v pmd &> /dev/null; then
|
|
17
|
+
echo '{"error": "pmd not installed. Download from: https://pmd.github.io/"}' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if target directory exists
|
|
22
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
23
|
+
echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check if there are any Java files
|
|
28
|
+
if ! find "$TARGET_DIR" -name "*.java" ! -path "*/target/*" ! -path "*/build/*" -print -quit 2>/dev/null | grep -q .; then
|
|
29
|
+
# No Java files found, output empty result
|
|
30
|
+
echo '{"tool":"pmd","language":"java","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Run PMD and transform output
|
|
35
|
+
# PMD outputs JSON with violations grouped by file
|
|
36
|
+
# We filter for CyclomaticComplexity rule violations
|
|
37
|
+
# Using --no-cache to avoid caching issues
|
|
38
|
+
# --ignore-violations to ensure exit 0 even with violations found
|
|
39
|
+
|
|
40
|
+
pmd check -d "$TARGET_DIR" \
|
|
41
|
+
-R category/java/design.xml/CyclomaticComplexity \
|
|
42
|
+
-f json \
|
|
43
|
+
--no-cache \
|
|
44
|
+
--ignore-violations-on-exit 2>/dev/null | jq -c '
|
|
45
|
+
# Helper function for complexity to grade conversion
|
|
46
|
+
def complexity_to_grade:
|
|
47
|
+
if . <= 5 then "A"
|
|
48
|
+
elif . <= 10 then "B"
|
|
49
|
+
elif . <= 20 then "C"
|
|
50
|
+
elif . <= 30 then "D"
|
|
51
|
+
elif . <= 40 then "E"
|
|
52
|
+
else "F"
|
|
53
|
+
end;
|
|
54
|
+
|
|
55
|
+
# Process PMD violations format
|
|
56
|
+
# PMD JSON format has .files array with violations
|
|
57
|
+
(.files // []) |
|
|
58
|
+
map(
|
|
59
|
+
select(.violations | length > 0) |
|
|
60
|
+
{
|
|
61
|
+
path: .filename,
|
|
62
|
+
violations: [
|
|
63
|
+
.violations[] |
|
|
64
|
+
# PMD description format: "The {class|method} {name} has a {Cyclomatic|Standard} Complexity of {X}."
|
|
65
|
+
# or "The {class|method} '{name}' has a {Cyclomatic|Standard} cyclomatic complexity of {X}."
|
|
66
|
+
{
|
|
67
|
+
description: .description,
|
|
68
|
+
method: (.description | capture("method .(?<name>[^']+).") | .name),
|
|
69
|
+
beginLine: .beginLine,
|
|
70
|
+
endLine: .endLine,
|
|
71
|
+
# Extract complexity number from description
|
|
72
|
+
complexity: (
|
|
73
|
+
.description |
|
|
74
|
+
capture("complexity of (?<num>[0-9]+)") |
|
|
75
|
+
.num | tonumber
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
} |
|
|
80
|
+
select(.violations | length > 0)
|
|
81
|
+
) |
|
|
82
|
+
|
|
83
|
+
# Group by file and calculate metrics
|
|
84
|
+
map({
|
|
85
|
+
key: .path,
|
|
86
|
+
value: {
|
|
87
|
+
functions: (
|
|
88
|
+
.violations | map({
|
|
89
|
+
name: .method,
|
|
90
|
+
complexity: .complexity,
|
|
91
|
+
grade: (.complexity | complexity_to_grade),
|
|
92
|
+
line: .beginLine,
|
|
93
|
+
endLine: .endLine
|
|
94
|
+
})
|
|
95
|
+
),
|
|
96
|
+
avgComplexity: (
|
|
97
|
+
if (.violations | length) > 0 then
|
|
98
|
+
((.violations | map(.complexity) | add) / (.violations | length))
|
|
99
|
+
else 0 end
|
|
100
|
+
),
|
|
101
|
+
maxComplexity: (
|
|
102
|
+
if (.violations | length) > 0 then
|
|
103
|
+
(.violations | map(.complexity) | max)
|
|
104
|
+
else 0 end
|
|
105
|
+
),
|
|
106
|
+
grade: (
|
|
107
|
+
if (.violations | length) > 0 then
|
|
108
|
+
(((.violations | map(.complexity) | add) / (.violations | length)) | complexity_to_grade)
|
|
109
|
+
else "A" end
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}) |
|
|
113
|
+
from_entries as $files |
|
|
114
|
+
|
|
115
|
+
# Build final output
|
|
116
|
+
{
|
|
117
|
+
tool: "pmd",
|
|
118
|
+
language: "java",
|
|
119
|
+
generatedAt: (now | todate),
|
|
120
|
+
files: $files,
|
|
121
|
+
summary: {
|
|
122
|
+
totalFiles: ($files | to_entries | length),
|
|
123
|
+
totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
|
|
124
|
+
avgComplexity: (
|
|
125
|
+
[$files | to_entries[].value.avgComplexity] |
|
|
126
|
+
if length > 0 then (add / length) else 0 end
|
|
127
|
+
),
|
|
128
|
+
gradeDistribution: (
|
|
129
|
+
[$files | to_entries[].value.grade] |
|
|
130
|
+
reduce .[] as $grade (
|
|
131
|
+
{A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
|
|
132
|
+
.[$grade] += 1
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
'
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# drivers/javascript.sh
|
|
3
|
+
# Wraps cyclomatic-complexity to produce standard complexity JSON
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./drivers/javascript.sh [directory]
|
|
6
|
+
# directory: Path to JavaScript/TypeScript code to analyze (default: current directory)
|
|
7
|
+
#
|
|
8
|
+
# Output: JSON conforming to ComplexityReport schema
|
|
9
|
+
# Exit codes: 0 = success, 1 = error (npx not available or other failure)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TARGET_DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Check if npx is available
|
|
16
|
+
if ! command -v npx &> /dev/null; then
|
|
17
|
+
echo '{"error": "npx not found. Install Node.js and npm."}' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if target directory exists
|
|
22
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
23
|
+
echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Find all JS/TS files (excluding common build/dep directories)
|
|
28
|
+
FILES=$(find "$TARGET_DIR" -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) \
|
|
29
|
+
! -path "*/node_modules/*" \
|
|
30
|
+
! -path "*/dist/*" \
|
|
31
|
+
! -path "*/build/*" \
|
|
32
|
+
! -path "*/.next/*" \
|
|
33
|
+
! -path "*/coverage/*" 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
# If no files found, output empty result
|
|
36
|
+
if [ -z "$FILES" ]; then
|
|
37
|
+
echo '{"tool":"cyclomatic-complexity","language":"javascript","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Build glob pattern for cyclomatic-complexity
|
|
42
|
+
# It expects quoted glob patterns like 'src/**/*.ts'
|
|
43
|
+
PATTERN="${TARGET_DIR}/**/*.{js,ts,jsx,tsx}"
|
|
44
|
+
|
|
45
|
+
# Run cyclomatic-complexity and transform output
|
|
46
|
+
# cyclomatic-complexity outputs JSON like:
|
|
47
|
+
# [
|
|
48
|
+
# {
|
|
49
|
+
# "file": "path/to/file.js",
|
|
50
|
+
# "functionComplexities": [
|
|
51
|
+
# {"name": "func", "complexity": 5, "line": 10}
|
|
52
|
+
# ],
|
|
53
|
+
# "complexitySum": 5,
|
|
54
|
+
# "complexityLevel": "ok"
|
|
55
|
+
# }
|
|
56
|
+
# ]
|
|
57
|
+
|
|
58
|
+
npx --yes cyclomatic-complexity "$PATTERN" --json 2>/dev/null | jq -c '
|
|
59
|
+
# Filter out empty files and global scope functions
|
|
60
|
+
map(
|
|
61
|
+
select(.functionComplexities | length > 0) |
|
|
62
|
+
. + {functionComplexities: [.functionComplexities[] | select(.name != "global")]}
|
|
63
|
+
) |
|
|
64
|
+
map(select(.functionComplexities | length > 0)) |
|
|
65
|
+
|
|
66
|
+
# Transform to standard schema
|
|
67
|
+
(
|
|
68
|
+
map({
|
|
69
|
+
key: .file,
|
|
70
|
+
value: {
|
|
71
|
+
functions: (
|
|
72
|
+
.functionComplexities | map({
|
|
73
|
+
name: .name,
|
|
74
|
+
complexity: .complexity,
|
|
75
|
+
grade: (
|
|
76
|
+
if .complexity <= 5 then "A"
|
|
77
|
+
elif .complexity <= 10 then "B"
|
|
78
|
+
elif .complexity <= 20 then "C"
|
|
79
|
+
elif .complexity <= 30 then "D"
|
|
80
|
+
elif .complexity <= 40 then "E"
|
|
81
|
+
else "F"
|
|
82
|
+
end
|
|
83
|
+
),
|
|
84
|
+
line: .line,
|
|
85
|
+
endLine: null
|
|
86
|
+
})
|
|
87
|
+
),
|
|
88
|
+
avgComplexity: (
|
|
89
|
+
if (.functionComplexities | length) > 0 then
|
|
90
|
+
((.functionComplexities | map(.complexity) | add) / (.functionComplexities | length))
|
|
91
|
+
else 0 end
|
|
92
|
+
),
|
|
93
|
+
maxComplexity: (
|
|
94
|
+
if (.functionComplexities | length) > 0 then
|
|
95
|
+
(.functionComplexities | map(.complexity) | max)
|
|
96
|
+
else 0 end
|
|
97
|
+
),
|
|
98
|
+
grade: (
|
|
99
|
+
if (.functionComplexities | length) > 0 then
|
|
100
|
+
((.functionComplexities | map(.complexity) | add) / (.functionComplexities | length)) as $avg |
|
|
101
|
+
if $avg <= 5 then "A"
|
|
102
|
+
elif $avg <= 10 then "B"
|
|
103
|
+
elif $avg <= 20 then "C"
|
|
104
|
+
elif $avg <= 30 then "D"
|
|
105
|
+
elif $avg <= 40 then "E"
|
|
106
|
+
else "F"
|
|
107
|
+
end
|
|
108
|
+
else "A" end
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}) | from_entries
|
|
112
|
+
) as $files |
|
|
113
|
+
{
|
|
114
|
+
tool: "cyclomatic-complexity",
|
|
115
|
+
language: "javascript",
|
|
116
|
+
generatedAt: (now | todate),
|
|
117
|
+
files: $files,
|
|
118
|
+
summary: {
|
|
119
|
+
totalFiles: ($files | to_entries | length),
|
|
120
|
+
totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
|
|
121
|
+
avgComplexity: (
|
|
122
|
+
[$files | to_entries[].value.avgComplexity] |
|
|
123
|
+
if length > 0 then (add / length) else 0 end
|
|
124
|
+
),
|
|
125
|
+
gradeDistribution: (
|
|
126
|
+
[$files | to_entries[].value.grade] |
|
|
127
|
+
reduce .[] as $grade (
|
|
128
|
+
{A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
|
|
129
|
+
.[$grade] += 1
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
'
|
package/drivers/php.sh
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# drivers/php.sh
|
|
3
|
+
# Wraps PHPMD to produce standard complexity JSON
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./drivers/php.sh [directory]
|
|
6
|
+
# directory: Path to PHP code to analyze (default: current directory)
|
|
7
|
+
#
|
|
8
|
+
# Output: JSON conforming to ComplexityReport schema
|
|
9
|
+
# Exit codes: 0 = success, 1 = error (phpmd not installed or other failure)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TARGET_DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Check phpmd is installed
|
|
16
|
+
if ! command -v phpmd &> /dev/null; then
|
|
17
|
+
echo '{"error": "phpmd not installed. Run: composer global require phpmd/phpmd"}' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if target directory exists
|
|
22
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
23
|
+
echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check if there are any PHP files
|
|
28
|
+
if ! find "$TARGET_DIR" -name "*.php" ! -path "*/vendor/*" -print -quit 2>/dev/null | grep -q .; then
|
|
29
|
+
# No PHP files found, output empty result
|
|
30
|
+
echo '{"tool":"phpmd","language":"php","generatedAt":"'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'","files":{},"summary":{"totalFiles":0,"totalFunctions":0,"avgComplexity":0,"gradeDistribution":{"A":0,"B":0,"C":0,"D":0,"E":0,"F":0}}}'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Run PHPMD and transform output
|
|
35
|
+
# PHPMD outputs JSON violations, we need to extract cyclomatic complexity violations
|
|
36
|
+
# and transform them to our schema
|
|
37
|
+
# Using --ignore-violations-on-exit to ensure exit 0 even with violations
|
|
38
|
+
|
|
39
|
+
phpmd "$TARGET_DIR" json codesize --exclude vendor --ignore-violations-on-exit 2>/dev/null | jq -c '
|
|
40
|
+
# Helper function for complexity to grade conversion
|
|
41
|
+
def complexity_to_grade:
|
|
42
|
+
if . <= 5 then "A"
|
|
43
|
+
elif . <= 10 then "B"
|
|
44
|
+
elif . <= 20 then "C"
|
|
45
|
+
elif . <= 30 then "D"
|
|
46
|
+
elif . <= 40 then "E"
|
|
47
|
+
else "F"
|
|
48
|
+
end;
|
|
49
|
+
|
|
50
|
+
# Process PHPMD violations format
|
|
51
|
+
# Extract only CyclomaticComplexity violations
|
|
52
|
+
.files |
|
|
53
|
+
map(
|
|
54
|
+
select(.violations | length > 0) |
|
|
55
|
+
{
|
|
56
|
+
path: .file,
|
|
57
|
+
violations: [
|
|
58
|
+
.violations[] |
|
|
59
|
+
select(.rule == "CyclomaticComplexity") |
|
|
60
|
+
{
|
|
61
|
+
# Parse complexity from description like "The method foo() has a Cyclomatic Complexity of 12."
|
|
62
|
+
description: .description,
|
|
63
|
+
method: .method,
|
|
64
|
+
beginLine: .beginLine,
|
|
65
|
+
endLine: .endLine,
|
|
66
|
+
# Extract complexity number from description using regex
|
|
67
|
+
complexity: (
|
|
68
|
+
.description |
|
|
69
|
+
capture("Cyclomatic Complexity of (?<num>[0-9]+)") |
|
|
70
|
+
.num | tonumber
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
} |
|
|
75
|
+
select(.violations | length > 0)
|
|
76
|
+
) |
|
|
77
|
+
|
|
78
|
+
# Group by file and calculate metrics
|
|
79
|
+
map({
|
|
80
|
+
key: .path,
|
|
81
|
+
value: {
|
|
82
|
+
functions: (
|
|
83
|
+
.violations | map({
|
|
84
|
+
name: .method,
|
|
85
|
+
complexity: .complexity,
|
|
86
|
+
grade: (.complexity | complexity_to_grade),
|
|
87
|
+
line: .beginLine,
|
|
88
|
+
endLine: .endLine
|
|
89
|
+
})
|
|
90
|
+
),
|
|
91
|
+
avgComplexity: (
|
|
92
|
+
if (.violations | length) > 0 then
|
|
93
|
+
((.violations | map(.complexity) | add) / (.violations | length))
|
|
94
|
+
else 0 end
|
|
95
|
+
),
|
|
96
|
+
maxComplexity: (
|
|
97
|
+
if (.violations | length) > 0 then
|
|
98
|
+
(.violations | map(.complexity) | max)
|
|
99
|
+
else 0 end
|
|
100
|
+
),
|
|
101
|
+
grade: (
|
|
102
|
+
if (.violations | length) > 0 then
|
|
103
|
+
(((.violations | map(.complexity) | add) / (.violations | length)) | complexity_to_grade)
|
|
104
|
+
else "A" end
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}) |
|
|
108
|
+
from_entries as $files |
|
|
109
|
+
|
|
110
|
+
# Build final output
|
|
111
|
+
{
|
|
112
|
+
tool: "phpmd",
|
|
113
|
+
language: "php",
|
|
114
|
+
generatedAt: (now | todate),
|
|
115
|
+
files: $files,
|
|
116
|
+
summary: {
|
|
117
|
+
totalFiles: ($files | to_entries | length),
|
|
118
|
+
totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
|
|
119
|
+
avgComplexity: (
|
|
120
|
+
[$files | to_entries[].value.avgComplexity] |
|
|
121
|
+
if length > 0 then (add / length) else 0 end
|
|
122
|
+
),
|
|
123
|
+
gradeDistribution: (
|
|
124
|
+
[$files | to_entries[].value.grade] |
|
|
125
|
+
reduce .[] as $grade (
|
|
126
|
+
{A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
|
|
127
|
+
.[$grade] += 1
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# drivers/python.sh
|
|
3
|
+
# Wraps radon to produce standard complexity JSON
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./drivers/python.sh [directory]
|
|
6
|
+
# directory: Path to Python code to analyze (default: current directory)
|
|
7
|
+
#
|
|
8
|
+
# Output: JSON conforming to ComplexityReport schema
|
|
9
|
+
# Exit codes: 0 = success, 1 = error (radon not installed or other failure)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
TARGET_DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Check radon is installed
|
|
16
|
+
if ! command -v radon &> /dev/null; then
|
|
17
|
+
echo '{"error": "radon not installed. Run: pip install radon"}' >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if target directory exists
|
|
22
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
23
|
+
echo "{\"error\": \"Directory not found: $TARGET_DIR\"}" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Run radon and transform output
|
|
28
|
+
# radon cc outputs JSON like:
|
|
29
|
+
# {
|
|
30
|
+
# "file.py": [
|
|
31
|
+
# {"name": "func", "complexity": 5, "rank": "A", "lineno": 10, ...}
|
|
32
|
+
# ]
|
|
33
|
+
# }
|
|
34
|
+
radon cc "$TARGET_DIR" -j --total-average 2>/dev/null | jq -c '
|
|
35
|
+
(to_entries | map({
|
|
36
|
+
key: .key,
|
|
37
|
+
value: {
|
|
38
|
+
functions: (.value | map({
|
|
39
|
+
name: .name,
|
|
40
|
+
complexity: .complexity,
|
|
41
|
+
grade: .rank,
|
|
42
|
+
line: .lineno,
|
|
43
|
+
endLine: .endline
|
|
44
|
+
})),
|
|
45
|
+
avgComplexity: (
|
|
46
|
+
if (.value | length) > 0 then
|
|
47
|
+
((.value | map(.complexity) | add) / (.value | length))
|
|
48
|
+
else 0 end
|
|
49
|
+
),
|
|
50
|
+
maxComplexity: (
|
|
51
|
+
if (.value | length) > 0 then
|
|
52
|
+
(.value | map(.complexity) | max)
|
|
53
|
+
else 0 end
|
|
54
|
+
),
|
|
55
|
+
grade: (
|
|
56
|
+
if (.value | length) > 0 then
|
|
57
|
+
((.value | map(.complexity) | add) / (.value | length)) as $avg |
|
|
58
|
+
if $avg <= 5 then "A"
|
|
59
|
+
elif $avg <= 10 then "B"
|
|
60
|
+
elif $avg <= 20 then "C"
|
|
61
|
+
elif $avg <= 30 then "D"
|
|
62
|
+
elif $avg <= 40 then "E"
|
|
63
|
+
else "F"
|
|
64
|
+
end
|
|
65
|
+
else "A" end
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}) | from_entries) as $files |
|
|
69
|
+
{
|
|
70
|
+
tool: "radon",
|
|
71
|
+
language: "python",
|
|
72
|
+
generatedAt: (now | todate),
|
|
73
|
+
files: $files,
|
|
74
|
+
summary: {
|
|
75
|
+
totalFiles: ($files | to_entries | length),
|
|
76
|
+
totalFunctions: ([$files | to_entries[].value.functions | length] | add // 0),
|
|
77
|
+
avgComplexity: (
|
|
78
|
+
[$files | to_entries[].value.avgComplexity] |
|
|
79
|
+
if length > 0 then (add / length) else 0 end
|
|
80
|
+
),
|
|
81
|
+
gradeDistribution: (
|
|
82
|
+
[$files | to_entries[].value.grade] |
|
|
83
|
+
reduce .[] as $grade (
|
|
84
|
+
{A: 0, B: 0, C: 0, D: 0, E: 0, F: 0};
|
|
85
|
+
.[$grade] += 1
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
'
|