230 lines
6.0 KiB
Plaintext
230 lines
6.0 KiB
Plaintext
include::../intro.adoc[]
|
|
|
|
== Why is this an issue?
|
|
|
|
include::../why.adoc[]
|
|
|
|
Note that the calculation of cognitive complexity at function level deviates from the documented process. Given the functional nature of JavaScript, nesting functions is a prevalent practice, especially within frameworks like React.js. Consequently, the cognitive complexity of functions remains independent from one another. This means that the complexity of a nesting function does not increase with the complexity of nested functions.
|
|
|
|
=== What is the potential impact?
|
|
|
|
include::../impact.adoc[]
|
|
|
|
=== Exceptions
|
|
|
|
Cognitive complexity calculations exclude logical expressions using the `||` and `??` operators.
|
|
|
|
[source,javascript]
|
|
----
|
|
function greet(name) {
|
|
name = name || 'Guest';
|
|
console.log('Hello, ' + name + '!');
|
|
}
|
|
----
|
|
|
|
== How to fix it
|
|
|
|
include::../how-with-null-safe.adoc[]
|
|
|
|
=== Code examples
|
|
|
|
**Extraction of a complex condition in a new function.**
|
|
|
|
==== Noncompliant code example
|
|
|
|
The code is using a complex condition and has a cognitive cost of 3.
|
|
[source,javascript,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
function calculateFinalPrice(user, cart) {
|
|
let total = calculateTotal(cart);
|
|
if (user.hasMembership // +1 (if)
|
|
&& user.orders > 10 // +1 (more than one condition)
|
|
&& user.accountActive
|
|
&& !user.hasDiscount
|
|
|| user.orders === 1) { // +1 (change of operator in condition)
|
|
total = applyDiscount(user, total);
|
|
}
|
|
return total;
|
|
}
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
Even if the cognitive complexity of the whole program did not change, it is easier for a reader to understand the code of the `calculateFinalPrice` function, which now only has a cognitive cost of 1.
|
|
|
|
[source,javascript,diff-id=1,diff-type=compliant]
|
|
----
|
|
|
|
function calculateFinalPrice(user, cart) {
|
|
let total = calculateTotal(cart);
|
|
if (isEligibleForDiscount(user)) { // +1 (if)
|
|
total = applyDiscount(user, total);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function isEligibleForDiscount(user) {
|
|
return user.hasMembership
|
|
&& user.orders > 10 // +1 (more than one condition)
|
|
&& user.accountActive
|
|
&& !user.hasDiscount
|
|
|| user.orders === 1 // +1 (change of operator in condition)
|
|
}
|
|
|
|
|
|
----
|
|
|
|
**Break down large functions.**
|
|
|
|
==== Noncompliant code example
|
|
|
|
For example, consider a function that calculates the total price of a shopping cart, including sales tax and shipping. +
|
|
__Note:__ The code is simplified here, to illustrate the purpose. Please imagine there is more happening in the `for` loops.
|
|
|
|
[source,javascript,diff-id=3,diff-type=noncompliant]
|
|
----
|
|
|
|
function calculateTotal(cart) {
|
|
let total = 0;
|
|
for (let i = 0; i < cart.length; i++) { // +1 (for)
|
|
total += cart[i].price;
|
|
}
|
|
|
|
// calculateSalesTax
|
|
for (let i = 0; i < cart.length; i++) { // +1 (for)
|
|
total += 0.2 * cart[i].price;
|
|
}
|
|
|
|
//calculateShipping
|
|
total += 5 * cart.length;
|
|
|
|
return total;
|
|
}
|
|
|
|
----
|
|
|
|
This function could be refactored into smaller functions:
|
|
The complexity is spread over multiple functions and the complex `calculateTotal` has now a complexity score of zero.
|
|
|
|
==== Compliant solution
|
|
|
|
[source,javascript,diff-id=3,diff-type=compliant]
|
|
----
|
|
|
|
function calculateTotal(cart) {
|
|
let total = calculateSubtotal(cart);
|
|
total += calculateSalesTax(cart);
|
|
total += calculateShipping(cart);
|
|
return total;
|
|
}
|
|
|
|
function calculateSubtotal(cart) {
|
|
let subTotal = 0;
|
|
for (const item of cart) { // +1 (for)
|
|
subTotal += item.price;
|
|
}
|
|
return subTotal;
|
|
}
|
|
|
|
function calculateSalesTax(cart) {
|
|
let salesTax = 0;
|
|
for (const item of cart) { // +1 (for)
|
|
salesTax += 0.2 * item.price;
|
|
}
|
|
return salesTax;
|
|
}
|
|
|
|
function calculateShipping(cart) {
|
|
return 5 * cart.length;
|
|
}
|
|
----
|
|
|
|
**Avoid deep nesting by returning early.**
|
|
|
|
|
|
==== Noncompliant code example
|
|
|
|
The below code has a cognitive complexity of 6.
|
|
|
|
[source,javascript,diff-id=4,diff-type=noncompliant]
|
|
----
|
|
function calculateDiscount(price, user) {
|
|
if (isEligibleForDiscount(user)) { // +1 ( if )
|
|
if (user?.hasMembership) { // +2 ( nested if )
|
|
return price * 0.9;
|
|
} else if (user?.orders === 1 ) { // +1 ( else )
|
|
return price * 0.95;
|
|
} else { // +1 ( else )
|
|
return price;
|
|
}
|
|
} else { // +1 ( else )
|
|
return price;
|
|
}
|
|
}
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
Checking for the edge case first flattens the `if` statements and reduces the cognitive complexity to 3.
|
|
|
|
[source,javascript,diff-id=4,diff-type=compliant]
|
|
----
|
|
function calculateDiscount(price, user) {
|
|
if (!isEligibleForDiscount(user)) { // +1 ( if )
|
|
return price;
|
|
}
|
|
if (user?.hasMembership) { // +1 ( if )
|
|
return price * 0.9;
|
|
}
|
|
if (user?.orders === 1) { // +1 ( if )
|
|
return price * 0.95;
|
|
}
|
|
return price;
|
|
}
|
|
----
|
|
|
|
**Use the optional chaining operator to access data.**
|
|
|
|
In the below code, the cognitive complexity is increased due to the multiple checks required to access the manufacturer's name. This can be simplified using the optional chaining operator.
|
|
|
|
==== Noncompliant code example
|
|
|
|
[source,javascript,diff-id=2,diff-type=noncompliant]
|
|
----
|
|
let manufacturerName = null;
|
|
|
|
if (product && product.details && product.details.manufacturer) { // +1 (if) +1 (multiple condition)
|
|
manufacturerName = product.details.manufacturer.name;
|
|
}
|
|
if (manufacturerName) { // +1 (if)
|
|
console.log(manufacturerName);
|
|
} else {
|
|
console.log('Manufacturer name not found');
|
|
}
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
The optional chaining operator will return `undefined` if any reference in the chain is `undefined` or `null`, avoiding multiple checks:
|
|
|
|
[source,javascript,diff-id=2,diff-type=compliant]
|
|
----
|
|
|
|
let manufacturerName = product?.details?.manufacturer?.name;
|
|
|
|
if (manufacturerName) { // +1 (if)
|
|
console.log(manufacturerName);
|
|
} else {
|
|
console.log('Manufacturer name not found');
|
|
}
|
|
----
|
|
|
|
=== Pitfalls
|
|
|
|
As this code is complex, ensure that you have unit tests that cover the code before refactoring.
|
|
|
|
|
|
include::../resources.adoc[]
|
|
|
|
include::../rspecator.adoc[]
|