[ANN] CodeComplexity.jl – Cyclomatic complexity for Julia
I’ve released CodeComplexity.jl, a small package to measure cyclomatic complexity of Julia code. It helps find overly complex functions, set complexity limits in tests or CI, and scan files, directories, or whole packages.
Scopes – Single expression, code string, file, directory (recursive), or package (by name or loaded module).
CI-friendly – check_complexity(pkg; max_complexity=10) throws if any function exceeds the limit, so you can fail tests or CI when complexity gets too high.
Pairs well with LLMs – When using AI coding assistants, complexity metrics give you a concrete threshold to enforce: reject or refactor generated code that exceeds your limit, and use per-function reports to prompt “simplify this” where it matters.
Quick start
using Pkg; Pkg.add("CodeComplexity")
using CodeComplexity
# Per-function report from a file
fc = file_complexity("src/MyModule.jl")
# Scan a package and enforce a limit (e.g. in tests)
using MyPackage
check_complexity(MyPackage; max_complexity=10)
Compatibility: Julia 1.6+, no required dependencies; test suite uses Aqua.jl.
If you care about keeping functions understandable and testable, or want a single check to gate complexity in CI, CodeComplexity.jl is there. Feedback and contributions welcome.
Yeah, that’d probably be better. Honestly, I had looked at the implementation in complex-structure (C901) | Ruff (source), and just asked Opus 4.6 to do this. It was able to one-shot a solution, but took an extra iteration to process Julia Base–and there are some pretty complex functions in there!
I’m too busy to sink much time into this, but I might be able to ask Opus to see if there’s refactoring opportunity with JuliaSyntax. If you don’t see something in the next month, feel free to open a PR!
Yes.
If I tell the agent to prioritise simplicity & clarity during the refactor, then it does so.
Typically, I run CodeComplexity over the refactored code base every few days to check that things haven’t grown out of hand, and so far, they haven’t.
D.
# Julia complexity
function complexity_df(filenames::Vector{String})
rows = []
for fname in filenames
fc = file_complexity(fname)
# Total row
push!(rows, (filename=fname, function_name="TOTAL", complexity=fc.total_complexity))
# Function rows
for func in fc.functions
push!(rows, (filename=fname, function_name=func.name, complexity=func.complexity))
end
end
DataFrame(rows)
end
then
# Directories to scan in order
dirs = [
"src",
"src/common",
"src/models"
]
# Accumulators
df_base = DataFrame()
df_totl = DataFrame()
for dir in dirs
if !isdir(dir)
@warn "Directory not found, skipping: $dir"
continue
end
jfiles = filter(endswith(".jl"), readdir(dir, join=true))
if isempty(jfiles)
@warn "No .jl files found in: $dir"
continue
end
println("Processing directory: $dir ($(length(jfiles)) files)")
for jfile in jfiles
println(" → ", jfile)
try
jtemp = complexity_df([jfile])
# Add source tracking columns
jtemp.source_file = fill(jfile, nrow(jtemp))
jtemp.source_dir = fill(dir, nrow(jtemp))
# Split into base and totals
base = jtemp[jtemp.function_name .!= "TOTAL", :]
totl = jtemp[jtemp.function_name .== "TOTAL", :]
# Append to accumulators
df_base = isempty(df_base) ? base : vcat(df_base, base)
df_totl = isempty(df_totl) ? totl : vcat(df_totl, totl)
catch e
@warn "Failed on $jfile" exception=e
end
end
end
# Sort base results by complexity descending
df_base_sorted = sort(df_base, :complexity, rev=true)
df_totl_sorted = sort(df_totl, :complexity, rev=true)
println("\n── Summary ──────────────────────────────")
println("Total functions analysed : ", nrow(df_base_sorted))
println("Total files analysed : ", nrow(df_totl_sorted))
# println("\nTop 10 most complex functions:")
# println(first(df_base_sorted[:, [:source_file, :function_name, :complexity]], 10))