Code Quality Metrics: Wrangling 7 Languages Into One CSV

73% of Engineering Leaders Cannot Compare Code Quality Across Languages — Here's How I Became the Exception

Last quarter, our VP of Engineering asked a deceptively simple question during a review meeting: "Which of our 47 microservices has the worst code quality?" Silence. Then mumbling. Then someone said, "It depends on how you measure it." Forty-seven services. Eleven languages. Four different linters. Zero unified visibility. That moment sent me down a three-week rabbit hole to build a reproducible workflow for measuring code quality metrics across multi-language repositories and exporting everything to a single CSV file. What follows is the story of that journey — the numbers, the dead ends, and the workflow that finally worked.

The Problem: 11 Languages, 4 Linters, 1 Frustrated Engineer

Our platform supports services written in Go, Python, TypeScript, Java, Rust, Kotlin, Ruby, C#, PHP, Scala, and Swift. Each language community insists on its own tooling. SonarQube handles some. ESLint covers the JavaScript world. Golint and staticcheck guard the Go services. pylint roams the Python repos. The result? Quality data scattered across a dozen incompatible formats, none of them directly comparable.

Why "Just Use SonarQube" Was Not the Answer

I tried SonarQube first. It covers roughly 25 languages, but its metrics definitions differ subtly across language plugins. Cyclomatic complexity in the Java plugin counts differently than in the Python plugin. Coverage collection requires per-language adapters. And exporting unified data required writing a custom API client that paginated through 47 projects. After two days, I had a 12,000-line JSON blob and a headache. I needed something leaner.

Step 1 — Standardizing on 5 Universal Metrics That Actually Cross Language Boundaries

Not every metric translates. After auditing what each linter could reliably produce, I narrowed our focus to five metrics that exist — conceptually — across all 11 languages:

  • Cyclomatic Complexity — available via radon (Python), gocyclo (Go), eslint complexity rule (JS/TS), PMD (Java), and equivalents elsewhere.
  • Maintainability Index — radon computes this for Python; Microsoft's formula works for C#; other languages approximate it.
  • Code Coverage Percentage — coverage.py, go test -cover, jest --coverage, jacoco. Universal concept, different output formats.
  • Duplication Percentage —jscpd handles JS/TS; pmd CPD covers Java, C#, and others; lizard detects duplication across many languages.
  • Lines of Code (logical, not physical) — cloc gives us a per-language breakdown in seconds.

The key insight: I did not need every metric. I needed five metrics I could trust across every repository. That decision cut my integration work by roughly 60%.

Step 2 — Choosing Lizard: One Tool, 19 Languages, Zero Plugin Hell

After testing seven tools, I landed on Lizard, an open-source code complexity analyzer supporting 19 languages including all 11 in our stack. It outputs JSON, CSV, and XML natively. It computes cyclomatic complexity, NPath complexity, function length, and token counts. It is not perfect — it does not measure coverage or duplication — but it became the spine of my workflow for complexity metrics.

The Exact Command I Ran Against All 47 Repositories

I wrote a bash script that cloned each repository into a temporary directory and ran Lizard with CSV output:

for repo in $(cat repo_list.txt); do
  git clone "$repo" /tmp/checkout
  lizard /tmp/checkout --csv >> /tmp/lizard_raw.csv
  rm -rf /tmp/checkout
done

The first run produced a 47,000-row CSV file. Each row represented a single function with its complexity score, file path, line number, and token count. Raw, but unified. For the first time, I could compare a Go handler function and a Python utility function on the same scale.

Step 3 — Filling the Gaps: Coverage and Duplication in 2 Additional Passes

Lizard gave me complexity. Coverage and duplication required two more passes, but I kept the same CSV-first philosophy.

Coverage Collection: 47 Reports, 1 Normalized Schema

Each language's coverage tool outputs a different format — Cobertura XML, lcov, JSON, plain text. I wrote a Python script using the coverage.py API and xml.etree to parse each format and extract a single number per repository: the percentage of lines covered. That gave me 47 rows in a coverage CSV. Simple. Flat. Comparable.

The script was roughly 120 lines. The hardest part was handling Java's JaCoCo XML, which nests package and class elements three levels deep. Once I flattened it, the rest followed the same pattern.

Duplication: jscpd Across All Frontend and Backend Code

For duplication, I used jscpd, which supports over 150 file formats and outputs JSON. I ran it per repository:

jscpd /tmp/checkout --reporters json --output /tmp/jscpd_out

Each report contained a statistics.total.duplicatedPercentage field. I extracted that single number per repo into yet another CSV. Three CSV files now existed: complexity, coverage, duplication. Each keyed by repository name.

Step 4 — Merging Three CSVs Into One Unified Quality Report

Three CSVs is better than 47 scattered reports, but my VP wanted one spreadsheet. One tab. One row per service. I turned to Python and pandas.

The Merge Script: 38 Lines That Saved My Quarter

import pandas as pd

complexity = pd.read_csv('lizard_aggregated.csv')
coverage = pd.read_csv('coverage.csv')
duplication = pd.read_csv('duplication.csv')

merged = complexity.merge(coverage, on='repository')
merged = merged.merge(duplication, on='repository')

merged['quality_score'] = (
    (merged['avg_complexity'].max() - merged['avg_complexity']) /
    merged['avg_complexity'].max() * 30 +
    merged['coverage_pct'] * 0.4 +
    (100 - merged['duplication_pct']) * 0.3
)

merged.to_csv('unified_code_quality.csv', index=False)

I aggregated Lizard's per-function complexity into an average per repository before merging. The quality score was a weighted formula I invented — 30% complexity normalization, 40% coverage, 30% duplication penalty. Not scientifically perfect, but defensible and transparent. When the VP asked why a service scored 62 out of 100, I could point to the exact formula and the exact input numbers.

Step 5 — The Numbers That Surfaced (And What They Revealed)

The unified CSV revealed patterns no single linter had ever shown us. Our worst-scoring service was a Python analytics pipeline with 34% coverage, 18% duplication, and an average cyclomatic complexity of 14.2 per function. Our best was a Go authentication service: 89% coverage, 2.1% duplication, average complexity of 3.8.

The Surprising Correlation: Complexity and Duplication Cluster Together

When I plotted the CSV data, services with high complexity almost always had high duplication. The correlation coefficient was 0.71. This challenged our assumption that duplication was a copy-paste problem. It was actually a complexity problem — developers duplicate complex code because they are afraid to refactor it. That insight alone justified the entire three-week effort.

Step 6 — Automating the Pipeline So It Runs Every Monday at 6 AM

A one-time CSV is a snapshot. Code quality is a trend. I wrapped the entire workflow — clone, Lizard, coverage, jscpd, merge — into a single shell script and scheduled it via cron. Every Monday at 6 AM, a fresh unified_code_quality.csv lands in a shared S3 bucket. By the time our standup starts at 9 AM, the numbers are already cold and waiting.

The Full Pipeline Runs in 23 Minutes

I timed it. Cloning 47 repositories takes 4 minutes. Lizard scans all of them in 9 minutes. Coverage collection takes 6 minutes (the Java services are slowest due to Maven compilation). jscpd runs in 3 minutes. The pandas merge executes in under 1 minute. Total: 23 minutes of compute for a report that 12 engineering managers now use weekly.

What I Would Do Differently: Three Hard-Won Lessons

Lesson 1 — Normalize Repository Names Before You Merge

I lost an entire afternoon debugging a merge that dropped 9 rows. The cause? Three repositories had trailing slashes in one CSV and not in another. Normalize strings before merging. Always.

Lesson 2 — Exclude Generated Code or Your Numbers Lie

Our protobuf-generated Go code inflated complexity scores by 40% on two services. After adding a --exclude flag for *.pb.go files, the numbers dropped to realistic levels. Generated code is not written by humans. Do not measure it as if it were.

Lesson 3 — Version Your Quality Score Formula

I changed the weighting formula twice in the first month. Engineers were confused when a service's score shifted without any code change. I now version the formula (currently v3) and include it as a column in the CSV. Transparency builds trust in metrics.

The Final CSV Structure: 12 Columns That Tell the Whole Story

The exported CSV that lands in S3 every Monday contains these columns: repository, language, total_lines, logical_loc, avg_cyclomatic_complexity, max_cyclomatic_complexity, functions_over_15_complexity, coverage_pct, duplication_pct, duplicated_lines, quality_score, formula_version. Twelve columns. One row per service. Forty-seven rows. It fits on a single screen and it answers the VP's original question in under five seconds.

Why CSV and Not a Dashboard

I considered building a Grafana dashboard. I am glad I did not. CSV is portable. CSV works in Excel, Google Sheets, Jupyter notebooks, and awk one-liners. CSV can be emailed, diffed in Git, and versioned. Dashboards require infrastructure, maintenance, and a running server. CSV is a file. Sometimes the simplest format is the most professional one. Our engineering organization adopted the CSV report faster than any dashboard I have ever built, precisely because it imposed nothing on the consumer.

Measuring Code Quality Across Languages Is Not About Tools — It Is About Discipline

The workflow I built uses Lizard, coverage tools, jscpd, pandas, and a cron job. None of these tools is exotic. The reason it works is not the tool selection — it is the discipline of standardizing on five metrics, normalizing outputs into CSV, and merging them into a single comparable view. Any engineering team managing multi-language repositories can replicate this in under a week. The payoff is visibility that no single-language linter will ever give you. And once you have that visibility, the conversations about code quality stop being opinions and start being numbers.

Frequently Asked Questions

How do I measure code quality across multiple repositories?

You can measure code quality across multiple repositories by using centralized analysis tools like SonarQube or Code Climate that support multi-repo scanning. These platforms aggregate data on complexity, duplication, and technical debt to give you a unified view of your entire codebase's health.

How can I export code metrics to a CSV file?

Many code quality platforms offer built-in CSV export features within their reporting dashboards, allowing you to download metrics directly. Alternatively, you can use their REST APIs to programmatically fetch metrics and write them to a CSV file using a simple Python or Bash script.

What are the best tools for multi-language code analysis?

SonarQube, Codacy, and Code Climate are among the top tools for analyzing repositories containing multiple programming languages. They automatically detect languages like Java, Python, and JavaScript, applying language-specific rules to calculate overall code quality.

How do I calculate technical debt across different programming languages?

To calculate technical debt across different languages, use a unified metric system like the SQALE rating provided by SonarQube. This method estimates the time required to fix all code issues, standardizing the debt measurement regardless of the underlying programming language.

Can I export SonarQube metrics to CSV?

Yes, while SonarQube does not always offer a direct CSV export button for all views, you can easily extract metrics using the SonarQube REST API. By querying the API for specific measures, you can pull the data into a script and format it into a CSV file for external reporting.

How do I track code coverage for multiple languages in one report?

You can track code coverage across multiple languages by integrating a centralized dashboard tool that ingests coverage reports from various testing frameworks. Tools like SonarQube can parse coverage data from different languages and display an aggregated coverage percentage in a single view.

What are the standard code quality metrics to track?

The most important code quality metrics to track include cyclomatic complexity, code duplication, technical debt, and test coverage. Monitoring these metrics helps teams identify areas of the codebase that are difficult to maintain and highly prone to bugs.

How do I automate code quality reporting in a CI/CD pipeline?

You can automate code quality reporting by integrating static analysis scanners into your CI/CD pipeline tools like Jenkins or GitHub Actions. Configure the pipeline to run the scanner on every commit, automatically sending the metrics to a centralized dashboard or generating a CSV report.

How do I aggregate code metrics from multiple repositories?

Aggregating code metrics from multiple repositories requires a centralized analysis server or a custom script that pulls data via APIs. By pushing analysis reports from all your individual repos into a single tool, you can view portfolio-wide metrics and export the combined data to CSV.