lizard parses 26 languages by not resolving imports, and installing it writes a version file back into your source tree
A simple code complexity analyser without caring about the C/C++ header files or Java imports, supports most of the popular languages.
At a glance
- What is it?
- An extensible complexity analyser whose central design decision is to measure how complex code looks rather than how complex it really is, which is what lets it cover Solidity, Zig, GDScript and twenty-three other languages without a working build. The packaging is where it gets interesting: the version comes from a regex over the changelog, and installing rewrites a file in the source tree to match.
- Who is it for?
- lizard is worth using if you need complexity numbers on a codebase you cannot build, because not resolving includes is what lets it parse Solidity, Zig, GDScript and two dozen others, and because its warning-threshold flag is designed as a ratchet for a repository that already fails. Two things to know.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
It measures how complex the code looks, and says so in the readme
There is a paragraph in the readme that is the most honest thing in it:
This tool actually calculates how complex the code looks rather than how complex the code really is. People will need this tool because it is often very hard to get all the included folders and files right when they are complicated. But we do not really need that kind of accuracy for cyclomatic complexity.
That is the design in three sentences. The analyser does not resolve includes or imports. It reads what is in the file in front of it and counts decision points, and the branch it takes on any name that is not defined in that file does not matter.
The payoff is the language list. Twenty-six languages, including Solidity, Zig, GDScript, TTCN-3, Structured Text and Fortran, and for several of those there is no practical dependency resolver to build in the first place. It also means C and C++ work without the header files being present, and Java works without the imports resolving, which the description leads with.
So the approximation is not a shortcut around a limitation. It is the only way the tool can cover the list it covers, and the readme states that rather than implying a fidelity it does not have.
The help lists 17 languages and the list above it lists 26
The page gives you two inventories of the same thing, and they do not match.
The list near the top enumerates twenty-six: a set of general-purpose languages, then C sharp, two C-family entries, an Erlang, a Fortran, a game scripting language, a Java and a Kotlin, a Lua, an Objective-C, a Perl, a PHP, a procedural SQL dialect, an R, a Scala, a Solidity, an industrial structured text, a TTCN dialect, a Vue single-file dialect, and a Zig.
The option help, further down, enumerates the available languages for the selector flag. It names seventeen: the C-family entries, the Java and C sharp, JavaScript, Python, the Objective-C, the test-language dialect, Ruby, PHP, Swift, Scala, the game scripting language, Go, Lua, Rust, TypeScript, and the procedural SQL dialect.
So nine are missing from the help. The industrial structured text, the Solidity, the Zig, the Vue dialect, the Erlang, the Fortran, the Perl, the R, and the Kotlin.
Every one of those is presumably selectable, since the analysis finds them by default with no selector at all. The gap is in the documentation a user reads to find out what to type after the flag, and the most likely explanation is that the help string is a hand-maintained list rather than one derived from the language definitions.
The tree has two directories that back this up: one for the language definitions and one for extensions. Two registries, and one of them is partly reflected in a string in the help text.
Installing rewrites a version file in the source tree, derived from a regex over the changelog
The setup script has an unusual amount of logic above the actual package metadata call.
It starts with a placeholder version of three zeros. It then opens the changelog, compiles a pattern that matches a heading which looks like a version number, and walks the file until the first match. That string becomes the version.
Then it defines a function that opens a version module inside the extensions directory, reads it, finds the current version assignment with a second regex, replaces it with the changelog's version, and writes the file back to disk.
Then it calls that function. The whole sequence sits inside a bare exception handler that falls back to importing the version module.
Two consequences. First, running the setup script, including the version query the release target uses, modifies a source file. The write happens before packaging does anything. Second, the fallback path and the primary path can disagree: if the changelog is unreadable the module import happens after the failed write attempt, and if the file write partially succeeded you have a version module that disagrees with the changelog.
There is a version command class defined as well, which prints the version, which is how the release target asks the question that triggers the write.
None of this is unusual in an old-style script. It is worth knowing before you install from a working tree, because an install is not a read-only operation here.
The default target chain ends in a linter that cannot fail the build
The build file's default goal is a chain: the extensive target, which is the tests and then a style check, and then the full linter.
The style check runs a style checker over the three source directories. The linter runs over the same three with an exit-zero flag and an explicit rcfile. The exit-zero flag means the linter reports and the build continues whatever it finds.
So of the three stages in the default target, one fails on style violations, one fails on test failures, and one cannot fail at all.
The phony declaration and the actual targets disagree in both directions. The declaration lists a dependency target and a publish target that are not defined in the file. The file defines two test targets, an upgrade target, a build target, a release target and four clean targets that the declaration does not mention. And the two test targets are different test runners: one runs pytest under coverage over the test directory and prints a coverage report, the other runs the standard library's runner.
Which of the two is current is not stated, and the phony declaration lists neither of the test target names correctly.
The warning-count flag is designed for a codebase that already fails
One option is worth explaining slowly, because it is the feature that makes this tool usable on an existing repository.
The flag takes a number. If the number of warnings found is equal to or fewer than the number you passed, the tool exits normally. If it is more, the tool exits with an error. And if the number you pass is negative, the tool exits normally regardless of how many warnings there are.
So the semantics are inverted from what most people expect on the first reading: a positive number is a budget, and a negative number disables the check. The readme describes the intended use as being useful in a makefile for legacy code.
Which is the point. A complexity tool that fails immediately on a repository with ten thousand warnings is a tool nobody runs, and this one lets you set the budget to whatever you are down to today and tighten it over time. The negative case lets a legacy build keep running while you fix it somewhere else.
Two other defaults are worth knowing. The complexity threshold is fifteen, and there is a second measure that counts a multi-case switch as one branch rather than several, for codebases where the switch statement would otherwise dominate every number. And the function length threshold is a thousand lines, which is high enough that it will rarely fire.
Four output formats, each named after the CI system that consumes it
The output options are not a grab bag. Each one exists because something reads it.
There is an XML format described as being in the style of a particular C++ tool's reporting, recommended for a report server. There is a CSV format generated as a transform of the default table. There is an HTML report with interactive tables that are sortable, searchable and filterable. And there is a Checkstyle XML format, named for the Java static-analysis convention and described as being for integration with a build server and other tools.
The warnings-only mode is the other half of the story, and it has two spellings. One prints in the diagnostic format used by the two major C compilers, with a link to that tool's documentation. The other prints in the format used by one commercial Windows compiler, with a link to that format's documentation.
So the same warning can be emitted in four dialects across two build systems, and two of those dialects exist so the tool can be dropped into a build that treats warnings as compiler output.
That is a coherent theme with the rest of the tool. It is not trying to be a good citizen in one ecosystem; it is trying to be droppable into whichever build already exists, which is the same reason it does not resolve imports.
The root carries an app engine config, a website, four lock mechanisms and a prompt transcript
The top-level entries are worth listing, because they describe a project of a certain age that has kept growing.
There is the analyser itself as a standalone script, an extensions directory, a language definitions directory, a directory of templates, a test directory, and a documentation directory. Alongside those: a website directory, an HTML entry point, a Python entry point, an XSL transform, a style checker config, a theory document, and a to-do file.
Then the deployment and environment layer, which is five mechanisms for the same job. Two requirement files for development. Two lock files. A Nix flake with its own lock. A shell definition file. A build script. A Makefile. And a bower manifest, which is a JavaScript dependency manager that has been effectively abandoned.
Then the two entries that nobody puts at the root of a released package: an app engine application configuration and its Python configuration file, which is a deployment target from the same era as the bower manifest, and a prompt history file.
The prompt history is the odd one. It is the kind of artefact a working session leaves behind, and it is committed alongside the licence and the readme. Whether it belongs there is a question for the maintainer, but its presence tells you something about how the work gets done.
The build badge points at a service that no longer runs
Three closing details.
The build badge at the top of the readme points at a continuous-integration service's site, and the repository's configuration file for that service is at the root. There is a workflow directory in the tree as well, but the badge is not from it.
The runtime requirement is a Python version floor with a caveat attached: three point eight or above, with earlier versions not verified. For a tool that parses source text rather than importing it, a wide interpreter range is plausible, and it is the one claim in the readme marked as unverified rather than asserted.
And the licence. The package is published under a permissive licence with a licence file at the root, but the repository's own recorded licence carries no recognised identifier. There is a second licence file name at the root as well, alongside the standard one, so the licensing situation is one file for humans and nothing for automated detection.
The version is a healthy one for a project like this: a two dozen minor releases in, and the readme is in reStructuredText with its own formatting conventions, which is consistent with a tool that has been published as a package for a long time.
Editorial conclusion
lizard is worth using if you need complexity numbers on a codebase you cannot build, because not resolving includes is what lets it parse Solidity, Zig, GDScript and two dozen others, and because its warning-threshold flag is designed as a ratchet for a repository that already fails. Two things to know. It says plainly that it measures apparent rather than real complexity, so treat the numbers as comparable across a codebase rather than comparable across projects. And a proper install rewrites a version file inside the source tree, derived from the changelog by pattern match, so install from a wheel or a clean checkout rather than from a working tree you have local edits in.
Frequently asked questions
What is the lizard complexity analyser?
An extensible cyclomatic complexity analyser that counts lines without comments, the complexity number, and token and parameter counts per function, and also does copy-paste and duplicate detection. It parses twenty-six languages, from C sharp and C through Zig and Solidity, and sets thresholds for complexity and parameter count that produce warnings and a non-zero exit code.
Why does lizard not resolve includes and imports?
On purpose, and the readme says so: the tool calculates how complex the code looks rather than how complex it really is, because getting every included folder and file right is often infeasible, and that accuracy is not needed for a complexity number. That is what lets it cover C and C++ without header files and Java without resolving imports.
How many programming languages does lizard support?
The list at the top of the readme names twenty-six. The option help for the language selector names seventeen of them, omitting the structured-text, Solidity, Zig, Vue, Erlang, Fortran, Perl, R and Kotlin entries. The repository holds separate directories for language definitions and for extensions.
Can lizard be used on a codebase that already has thousands of warnings?
That is what the warning-count option is for. It takes a number: warnings at or below it exit normally, more than it exits with an error, and a negative number disables the check entirely. The readme describes it as useful in a makefile for legacy code, which makes it a ratchet rather than a gate.
What happens when you install lizard from a source checkout?
The setup script derives the version by pattern-matching the first version heading in the changelog, then opens a version module inside the extensions directory, replaces its version assignment and writes the file back before packaging does anything. So a setup script run, including the version query the release target performs, modifies a file in the source tree.
What output formats does lizard produce?
Four, each aimed at a consumer: an XML format in the style of a C++ reporting tool, a CSV transform of the default table, an HTML report with interactive sortable and filterable tables, and Checkstyle XML for build-server integration. Warnings-only output has two dialects, matching the diagnostic format of the major C compilers and the format used by one commercial Windows compiler.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/terryyin-lizard)