There are many metrics that can indicate some kind of code quality or aspects of readability, but in the end they don't matter. What matters is that the intended audience of the code (i.e. other humans) can understand the code easily. Different audiences have different skill levels, different backgrounds, and different expectations, so there isn't one style that can satisfy them all.
This doesn't mean that metrics are useless, just that we should be aware how they are just proxies for actual readability. One classic example is cyclomatic complexity (CC), which counts the branches in a function. What are various problems with CC?
- In case of a switch/case statement, CC is equal to the number of cases, as if it were written as an if/else-if/else cascade. However, humans tend to perceive a switch/case as much simpler.
- Also, CC cannot really account for exception flow, as every subexpression that could throw is effectively a branch point. Yet, many people perceive exceptions as much simpler than a conditional for every operation that could fail, and exceptions are typically ignored when calculating CC (but how should try/catch be considered?)
- There are also some control flow operators that are often not perceived as control flow, such as short-circuit operators as in
if (a && b)
, or safe navigation operators like foo.?bar
(e.g. in C#).
- Some control flow may be hidden by higher-order functions, e.g. compare the loops
for x in things: foo(x)
with the largely equivalent map(foo, things)
.
There are metrics that try to address some of these concerns, e.g. Cognitive Complexity by SonarQube.
Static analysis tools like linters and style checkers use various metrics to alert you of possible issues. They may operate on a purely syntactic level (e.g. pycodestyle), others perform semantic analysis (e.g. findbugs operates on compiled Java bytecode, not on source code). Many perform a combination, and e.g. operate on a parsed representation of the source code. For dynamic languages, linters might also perform some amount of type checking. A style checker might be able to match your source against known patterns than can be replaced with a more elegant alternative, or when the pattern is likely a bug.
In the development work I'm currently doing (Python), I find the following checks or metrics most helpful:
- a basic style check that enforces consistent formatting and naming (flake8)
- simple semantic checks, such as complaining when a variable is assigned but not used, or when I call a function that doesn't exist (flake8, pylint)
- an external type check (mypy)
- simple test coverage metrics (statement coverage) (pytest-cov)
None of these put a number on how good my code is, but they help me to find and prevent problems. There are similar freely available tools for Java, e.g. checkstyle. These tools typically define policies and not metrics, but a policy might be that a metric should be within a certain bound (e.g. less than 20 local variables in scope).
It is worth reading the documentation for some of these tools to see which aspects of readability they try to measure or which issues they try to detect.