Code metrics for Flix projects, counted by the compiler rather than by
reading the text. It installs as metrics and is a plugin for flixw, measuring
your code with the exact compiler your project already pinned.
Experimental, third-party, unaffiliated with the Flix project. A plugin is ordinary code running as you — see Safety before installing anything.
./flixw plugin install metrics 0.1.8 \
https://github.com/wstein/flixw-metrics/releases/download/v0.1.8/plugin.jar \
--sha256 bd8707afb5a06a37d26f1bd9b9d3bc3b3892a73e1617b177328a4f5ff7d7c67fThat digest is published here, not taken from the download, which is the point of passing it: flixw re-checks those exact bytes on every run, not only at install.
Needs flixw 0.25.10 or newer, and a project with a pinned compiler.
It installs as metrics, and declares the verb metrics in its jar manifest — so
./flixw metrics works afterwards, in any project on the machine. ./flixw plugin metrics
is the long form and always works; use it if your pinned compiler implements metrics
itself, since the compiler always wins.
Installing is enough to run it. A [plugins.metrics] entry in a project's lock.toml —
which plugin install writes — pins the version, which is what you want for CI and for a
colleague's clone.
$ ./flixw metrics
files: 3
modules: 2
definitions: 5
localDefinitions: 1
effectfulDefinitions: 2
cognitive: 27
traits: 0
instances: 0
enums: 1
structs: 0
effects: 0
typeAliases: 0
lines: 48
codeLines: 37
commentLines: 1
docCommentLines: 6
blankLines: 4
commentPercent: 15
longestLine: 122
linesOverLimit: 3
datalogRules: 0
datalogFacts: 0
widestReturn: 1
tests: 1
docCoveragePercent: 25
purityPercent: 75
where each measure peaks
longest 11 lines Json.encode (src/Json.flix:15)
densest 1.1 complexity/line Json.size (src/Json.flix:33)
most-complex 11 complexity Json.size (src/Json.flix:33)
deepest 3 levels nested Json.size (src/Json.flix:33)
widest 2 parameters Json.size (src/Json.flix:33)
crammed-line 43 tokens on one line Json.encode (src/Json.flix:24)
most-coupled 3 modules used, instability 1.00 Json
smells: 10
src/Json.flix:24 crammed-line (43 tokens, over 30 [Json.encode])
src/Json.flix:24 line-too-long (122 columns, over 100)
src/Json.flix:27 undocumented-public (public with no doc comment [Json.depth])
src/Json.flix:33 dense (1.1 complexity per line, over 1 [Json.size])
src/Json.flix:38 crammed-line (37 tokens, over 30 [Json.size.loop])(Abridged — each ranking lists its top few, not one.)
Every finding names what was exceeded and by how much, so the threshold is arguable rather than mysterious. Nothing here fails your build.
Four output formats:
| for | |
|---|---|
--format text |
a terminal (the default) |
--format json |
a program — full per-definition and per-module lists, schemaVersion at the top |
--format md |
pasting into a pull request, ordered as a work plan |
--format sarif |
GitHub code scanning, so findings land inline on the diff |
./flixw metrics report --format md
./flixw metrics report --format sarif > metrics.sarifOnly the report goes to stdout — the compiler's own dependency-resolution chatter goes to
stderr — so redirecting --format json gives you a file that parses.
Three things get measured, and where each comes from is deliberate:
- From the compiler's typed AST — definitions, modules, local definitions, declared
effects, branch complexity, nesting depth, return shape, Datalog rules and facts, module
coupling. Counting
defby scanning text gets comments, strings and nested definitions wrong, which is the reason this plugin exists. - From the compiler's lexer — code, comment, doc-comment and blank lines, and tokens per line. A line with code and a trailing comment is code; a line inside a block comment is not.
- From thresholds over both — the findings.
Two measures are worth knowing about because they catch what totals hide:
Cognitive complexity is nesting-weighted. Five nested conditions cost more than five
consecutive ones. Divided by lines, it separates long from hard: a hundred readable
lines and ten dense ones can total the same, and the second is the one worth opening. That
ratio is the densest ranking and the dense finding.
Parameters and crammed lines are attributed to the local definition that owns them. In
the sample above, Json.size.loop is blamed for its own crammed line — not Json.size, the
enclosing definition, which is where a text scanner would send you.
Tests are judged differently rather than exempted: a long test is usually a table of cases, and an undocumented test is not a gap in a public API.
Full details of which number comes from where: docs/COMPILER-SDK.md. API docs: wstein.github.io/flixw-metrics.
A cold run is a few seconds — about 5 on the sample above — because the compiler type-checks its own standard library before it reaches your code. A warm run is about 0.4s, which is mostly JVM startup.
What is cached is the measurements, never the report: they are facts about your source
and cannot go stale while the key holds. Findings and formatting are recomputed every run,
so an adjusted threshold takes effect immediately instead of at the next cache miss. The key
covers your sources, flix.toml, the pinned compiler and this plugin's version — change any
of them and it recomputes. Every cache failure is a miss, never a stale answer.
Entries live where flixw says (FLIXW_PLUGIN_CACHE) and go away with the plugin.
A SHA-256 proves an artifact has the bytes you expected. It does not make a plugin safe: plugins are unsandboxed code running as their caller, and flixw prints that warning on every invocation, not only at install.
This plugin reads your source through the compiler flixw already verified. It does not download a compiler, choose one, or run anything else.
Flix 0.75.x. The engine reads the compiler's internal AST, which carries no compatibility promise, so it is compiled against one release and checks what is actually in front of it before running:
$ ./flixw metrics capabilities
{
"compilerJar": "/Users/you/Library/Caches/flixw/compilers/flix-0.75.3-bf123cdb....jar",
"hasFlixApi": true,
"hasReflectionApi": true,
"hasNativeMetrics": false,
"missing": []
}An unsupported compiler gets a sentence naming what is missing, rather than a wrong number or a stack trace. Supporting another Flix generation is one adapter class — see docs/COMPILER-SDK.md.
sh scripts/test.sh # lint, build, tests
sh scripts/package.sh 0.1.7 # dist/plugin.jar and dist/SHA256SUMS./mill bootstraps the pinned build tool, and scripts/fetch-flix.sh downloads the Flix
release the engine is written against and checks its digest — so "it builds here" means
something.
A jar is not byte-reproducible across machines, because jar records timestamps: a local
build will not have the digest the release does. The digest under Install is the
published artifact's, and is what you should verify against.
Apache-2.0. See LICENSE.