bitHound Blog

Using bitwise operations to lower complexity

At bitHound, we measure "cyclomatic complexity" on a per function basis. Broadly speaking, our analysis counts the total number of logical branches a code path can take in a function.

Some level of complexity will always exist in a function but it's generally a good idea to minimize it where possible.

Complex Maze Simple Maze Source: Wikimedia and IngrimayneType's Maze Pages

Which one is easier to follow?

Being clever with bitwise operations

Convert a poluted if / then / else to an elegant switch

At bitHound, we have several authentication states where a user will be allowed to do different things based on the authorization level.

Viewing a repo has these three main factors involved:

  • Is the user logged in?
  • Has the repo been analyzed already?
  • Does the user have access to view it on gitHub? (open source if not logged in)

Each of these condition matter, which means we would have 2x2x2 = 8 conditions to worry about. Cool -- let's start with the obvious.

function foo(loggedIn, analyzed, hasAccess) {  
  if (loggedIn && !analyzed && !hasAccess) {
    //show not found (to match gitHub's style of response)
  } else if (loggedIn && !analyzed && hasAccess) {
    //go to dashboard
  } else if (loggedIn && analyzed && hasAccess) {
    //go to dashboard
  } else if (loggedIn && analyzed && !hasAccess) {
    //show not found (to match gitHub's style of response)
  } else if (!loggedIn && analyzed && hasAccess) {
    //opensource -- go to dashboard
  } else if (!loggedIn && !analyzed && hasAccess) {
    //opensource but not analyzed yet -- go to dashboard  
  } else if (!loggedIn && !analyzed && !hasAccess) {
    //show not found (to match gitHub's style of response)
  } else if (!loggedIn && analyzed && !hasAccess) {
    //show not found (to match gitHub's style of response)
  }
}

8 cases but lots of similar outcomes! This produces a cyclomatic complexity of 9 and would fail our checks.

Let's organize this into an OR table as the next step.
The OR table looks like this:

Logged in? Analyzed? Has access? Result (binary) Result (decimal)
0 0 0 000 0
0 0 1 001 1
0 1 0 010 2
0 1 1 011 3
1 0 0 100 4
1 0 1 101 5
1 1 0 110 6
1 1 1 111 7

Codifying this OR table results in our new permissionLevel function:

function permissionLevel(isAuthed, isAnalyzed, hasAccess) {  
  isAuthed = isAuthed ? 4 : 0; // 100
  isAnalyzed = isAnalyzed ? 2 : 0; // 010
  hasAccess = hasAccess ? 1 : 0; // 001

  return isAuthed | isSlurped | hasAccess;
}

Awesome! Now what?

We can use this new function with a nice switch statement and nicely combine our branch paths!

switch (permissionLevel(req.isAuthenticated(), !!analyzed, ghStatus == 304 || ghStatus == 200)) {  
  case 1: //Not authed, not analyzed, open source
    //Special case for us. "Sign up to analzye this repo."
  case 3: //Not authed, analyzed, has access
  case 5: //Authed, not analyzed, has access
  case 7: //Authed, analyzed, has access
    next(); //Go to dashboard
    break;
  default: //Unknown access
    render('404', { status: 404 })(req, res);
    break;
}

This is very readable and also brings our cyclomatic complexity score from 9 to 4! This is not a new approach to this problem but one that is worth bringing back once in a while.

bitHound identifies risks and priorities in your Node.js projects.