Function-level metrics

What things codebeat looks at and why we think they are important

For every supported language, codebeat can calculate a universal set of metrics we believe are directly related to software quality, extensibility and maintainability. Some of these metrics are immediately understandable and some may require further insights. In this article we're explaining how these are calculated and why we believe they are relevant to your code's overall health.

Assignment Branch Condition

Assignment Branch Condition size is a synthetic metric which helps us understand the size of the source code from a structural point of view, i.e. without looking at superficial things like the sheer amount of code. It is computed by counting the number of assignments, branches and conditions for a given section of code. These are - slightly counterintuitively - defined as:

  • Assignment: an explicit transfer of data into a variable, e.g. =, *=, /=, %=, +=, <<=, >>=, &=, |=, ^=, >>>=, ++, -- etc.;
  • Branch: an explicit forward program branch out of scope, e.g. a function call, class method call, or new operator etc.;
  • Condition: a logical/Boolean test, e.g. ==, !=, <=, >=, <, >, else, case, default, try, catch, ?, unary conditionals etc.;

A scalar ABC size value (or aggregate magnitude) is computed as:

|ABC| = sqrt((A*A)+(B*B)+(C*C))

While not intented as a code complexity measure, we can use ABC as an indicator of how much actual work a piece of code is performing. Good design would have developers prefer shorter procedures that are more readily understood, more reusable and more testable than their longer counterparts. Functions and methods with high ABC scores often indicate a lack of up-front design and a certain disregard of code testability and maintainability.

Defaults

codebeat allows the ABC size to be up to to 10 with no penalty, 10-20 will trigger an INFO-level issue, 20-40 will trigger a WARNING, 40-60 - an ERROR and anything above that will lead to a CRITICAL issue. The default setting is thus [10, 20, 40, 60]. This default is relaxed to [15, 25, 50, 70] for Objective-C which is a less terse language.

Cyclomatic complexity

The cyclomatic complexity of a section of source code is the number of linearly independent paths within it. For instance, if the source code contained no control flow statements (conditionals or decision points), such as if statements, the complexity would be 1, since there is only a single path through the code. If the code had one single-condition if statement, there would be two paths through the code: one where the if statement evaluates to true and another one where it evaluates to false, so complexity would be 2 for a single if statement with a single condition. Two nested single-condition ifs, or one if with two conditions, would produce a cyclomatic complexity of 4.

Cyclomatic complexity is instrumental in figuring out how easy it is to test the code. A function with cyclomatic complexity of 2 will generally require 5 times fewer test cases than a function with a score of 10. High scores also indicate code that is difficult for humans to comprehend, as understanding a single statement will require the developer to keep a large stack of 'how I even got here' data in their short-term memory.

Defaults

codebeat allows the cyclomatic complexity to be up to to 10 with no penalty, 10-20 will trigger an INFO-level issue, 20-35 will trigger a WARNING, 35-50 - an ERROR and anything above that will lead to a CRITICAL issue. The default setting is thus [10, 20, 35, 50].

Lines of code

Lines of code refers to non-commentary lines, meaning pure whitespace and lines containing only comments are not included in the metric. It is the most naive and rudimentary code size metric out there and so deserves less attention than more insightful metrics described above. Still, as Hal Abelson said 'Programs must be written for people to read, and only incidentally for machines to execute' and long functions look intimidating to readers.

Long functions that do a lot of work will often be penalized for both high Assignment Branch Condition size and too many lines of code. However, there may be cases where an increased number of lines of code increases readability and maintainability. Take this function as an example:

func (w *workerImpl) getRequestChannel() (<-chan amqp.Delivery, error) {
	return w.channel.Consume(
		w.reqQueue.Name,
		"",    // consumer
		false, // auto-ack
		false, // exclusive
		false, // no-local
		false, // no-wait
		amqpArguments,
	)
}

All it really does is call a single method of an external API which takes a large number of arguments. While we could put all these arguments in a single line, it would make the function harder to understand, especially if we wanted to switch one of the boolean values that it takes. If this function was flagged for a number of lines of code, it would be wise to simply ignore the warning.

As an aside, the necessity for this function to be that long comes from the fact that the external library it is using has rather poor API design where a single method call requires seven (!) arguments.

Defaults

codebeat allows the number of lines of code per function to be up to to 24 with no penalty, 25-39 will trigger an INFO-level issue, 40-60 will trigger a WARNING, 60-80 - an ERROR and anything above that will lead to a CRITICAL issue. The default setting is thus [25, 40, 60, 80]. This default is much more strict ([10, 20, 40, 80]) in Ruby where short functions are a strong community standard. On the other hand, Java is a much more verbose language and the default is more lenient at [30, 45, 70, 100].

Arity

Arity represents a number of arguments that a function takes. Functions with longer lists of parameters are more difficult to use and more cumbersome to test. The example above shows a function with two string arguments, four boolean arguments and an extra free-form argument amqpArguments.

We've already seen that using an API like this has a bad impact on the quality of callers but arguments can also be interpreted as control flow - the more parameters there are the more paths a function can take. This is not captured in cyclomatic complexity but the same reasoning applies, particularly in terms of testability: exhaustively testing the impact of just the four boolean parameters in the function above would require us to have 16 test cases (2 ** 4). Add to that the combined impact of other parameters and you have a monster test suite for just a single function.

Defaults

codebeat allows 3 or fewer parameters with no penalty, 4 will trigger an INFO-level issue, 5 will trigger a WARNING, 6 - an ERROR and anything above that will lead to a CRITICAL issue. The default setting is thus [4, 5, 6, 7]. This default is more relaxed ([5, 6, 7, 8]) for Python where instance methods need the receiver (usually called self) to be passed as their first argument.

Number of return values (Go only)

Go is special in that it allows a single function to return multiple values. This is often used to pass errors in addition to regular return values in absence of more traditional exception handling. However, this clever pattern can be abused by functions returning long lists of values that are hard to understand and hard for the callers to handle. Take this function as an example:

func YoloSwag() (bool, bool, bool, string, bool, error) {
  // do some crazy work
  return false, true, false, "bacon", true, nil
}

Calling this is a nightmare. If you only need to retrieve one or two of these return values you will need to explicitly ignore the others in which case you'd be calling this function like so:

_, _, _, content, _, err := YoloSwag()
if err != nil {
  panic(err)
}
fmt.Println(content)

Without consulting the API for the YoloSwag function, the reader has no chance of figuring out whether the function captures the right thing and what exactly is being ignored. On the other hand, if the caller decides to capture everything, they will now have to use the captured variables somehow, lest the compiler complains about unused variables. So, with a bad API like that, the caller is left choosing between two equally bad alternatives.

Defaults

codebeat allows 3 or fewer return values with no penalty, 4 will trigger an INFO-level issue, 5 will trigger a WARNING, 6 - an ERROR and anything above that will lead to a CRITICAL issue. The default setting is thus [4, 5, 6, 7].

Maximum block nesting

Maximum block nesting calculates how deeply nested the deepest statement in a single function is. If you're using consistent indentation, you can visualize this as the most right-indented line of the function's body. Deep nesting shows a complicated design with too much control flow (in case of if/else nesting), computational complexity (in case of nested for loops) or a combination of both.

Defaults

We follow Linus Torvalds' advice that '... if you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.' Hence, 3 levels of nesting will carry an INFO level issue, 4 - a WARNING level issue, 5 - an ERROR and 6 and more will trigger a CRITICAL issue. The default setting is thus [3, 4, 5, 6].

Code duplication

The DRY (Don't Repeat Yourself) Principle states that every piece of knowledge must have a single, unambiguous, authoritative representation within a system. Our analyzers can detect code duplication through the analysis of code structure, both within and between source files. We can also find very similar code which we treat the same as straight copy-paste jobs.

Duplication (inadvertent or purposeful) points to wasted effort, poor design mindset, inability to see and factor out the patterns within the codebase and a general disregard for good coding practices. It can and will lead to maintenance nightmares whereby changes to duplicated code need to be copied over to all of its instances and missing a single instance may be a source of a serious bug.

We consider code duplication an even more serious issue than the ones described above and codebeat will penalize it more heavily than any of those per-function violations.

Defaults

This setting is not customizable and each language has a different set of defaults based on the look of its' parse tree. If you feel strongly about this please let us know at [email protected] and we can talk it through and try to find a way to meet your needs.