Home > Software engineering >  Rubocop complains about Metrics/AbcSize
Rubocop complains about Metrics/AbcSize

Time:04-25

So I'm trying to take the right path here and understand how to solve this cop, this looks like a small piece of code IMO, why is it complaining?

moving the nested if-else doesn't change anything, any suggestions on how to solve this cop?

 class WebPush::Register

  include Interactor

  # rubocop:disable Metrics/AbcSize
  def call
    user = Learner.find_by(id: context.user_id)

    # return if existing
    if user.web_push_subscription
      context.subscription = user.web_push_subscription
    else
      subscription = WebPushSubscription.new(
        endpoint:   context.push_params[:endpoint],
        auth_key:   context.push_params[:keys][:auth],
        p256dh_key: context.push_params[:keys][:p256dh],
        learner:    user
      )

      if subscription.save
        context.subscription = subscription
      else
        context.error = subscription.errors.full_messages
        context.fail!
      end
    end
  end
  # rubocop:enable Metrics/AbcSize

end

CodePudding user response:

First, you need to understand how ABC is calculated. Nested conditionals do not affect ABC. RuboCop's warning output shows you the calculation results:

Assignment Branch Condition size for call is too high. [<5, 28, 4> 28.72/17]

The <5, 28, 4> is your <Assignments, Branches, Conditionals>, as described in this article.

The overall score is calculated like this: sqr(5^2 28^2 4^2) = 28.72

The default maximum score for the cop is 17.

I've annotated your code below with the ABC scores for each line. Take note that every time you reference context, you add a B-point. This is because context is not a local variable to call, so the ABC metric assumes it's a method call every time.

def call
  user = Learner.find_by(id: context.user_id) # <1, 3, 0>

  if user.web_push_subscription # <0, 1, 1>
    context.subscription = user.web_push_subscription # <1, 3, 0>
  else # <0, 0, 1>
    # this constructor call is the most expensive part
    subscription = WebPushSubscription.new( # <1, 1, 0>
      endpoint:   context.push_params[:endpoint], # <0, 3, 0>
      auth_key:   context.push_params[:keys][:auth], # <0, 4, 0>
      p256dh_key: context.push_params[:keys][:p256dh], # <0, 4, 0>
      learner:    user # <0, 0, 0>
    )

    if subscription.save # <0, 1, 1>
      context.subscription = subscription # <1, 2, 0>
    else # <0, 0, 1>
      context.error = subscription.errors.full_messages # <1, 4, 0>
      context.fail! # <0, 2, 0>
    end
  end
end

if you set the cop option: CountRepeatedAttributes: false (which I recommend), it will bring your score down to 19.1.

In lieu of that, you can bring your score down by extracting the creation of WebPushExtraction into its own method like so:

  def call
    user = Learner.find_by(id: context.user_id)

    if user.web_push_subscription
      context.subscription = user.web_push_subscription
    else
      create_subscription(user)
    end
  end

  private

  def create_subscription(user)
    push_params = context.push_params
    subscription = WebPushSubscription.new(
      endpoint:   push_params[:endpoint],
      auth_key:   push_params.dig(:key, :auth),
      p256dh_key: push_params.dig(:key, :p256dh),
      learner:    user
    )

    if subscription.save
      context.subscription = subscription
    else
      context.error = subscription.errors.full_messages
      context.fail!
    end
  end

This will split the score up between the two methods. Note some additional ABC-saving strategies in create_subscription, like assigning push_params to a variable, and using dig for the nested hash accessors. Final score for create_subscription is between 12 and 16 depending on the cop options you use, and call is between 6 and 8.

Generally all that's required to bring down ABC score is to refactor to smaller methods.

CodePudding user response:

ABC stands for Assignment, Branch, Condition. ABC = sqrt(A^2 B^2 C^2)

Let's count them.

  def call
    user = Learner.find_by(id: context.user_id)  // assignment, branches

    # return if existing
    if user.web_push_subscription // branch, condition
      context.subscription = user.web_push_subscription  // assignment, branches
    else  // condition
      subscription = WebPushSubscription.new(  // assignment, branch
        endpoint:   context.push_params[:endpoint],  // branches
        auth_key:   context.push_params[:keys][:auth],  // branches
        p256dh_key: context.push_params[:keys][:p256dh],  // branches
        learner:    user
      )

      if subscription.save // branch, condition
        context.subscription = subscription  // assignment
      else  // condition
        context.error = subscription.errors.full_messages  // assignment, branches
        context.fail!  // branch
      end
    end
  end
  • A = 5
  • B = 17 (at least)
  • C = 4

This gives 18.2. By default, 17 is the maximum Rubcop considers satisfactory.

We can handle this with a few simple extract methods.

  private def user
    @user ||= Learner.find_by(id: context.user_id)
  end

  private def create_web_push_subscription_from_context
    subscription = WebPushSubscription.new(
      endpoint:   context.push_params[:endpoint],
      auth_key:   context.push_params[:keys][:auth],
      p256dh_key: context.push_params[:keys][:p256dh],
      learner:    user
    )

    if subscription.save
      context.subscription = subscription
    else
      context.error = subscription.errors.full_messages
      context.fail!
    end
  end

  def call
    if user.web_push_subscription
      context.subscription = user.web_push_subscription
    else
      create_web_push_subscription_from_context
    end
  end

After this refactoring the code is simpler, each method does one thing. That's what the ABC metric encourages.

  • Related