rapid7/metasploit-framework

View on GitHub
lib/rubocop/cop/layout/module_description_indentation.rb

Summary

Maintainability
A
35 mins
Test Coverage
module RuboCop
  module Cop
    module Layout
      class ModuleDescriptionIndentation < Base
        extend AutoCorrector
        include Alignment

        MSG = "Module descriptions should be properly aligned to the 'Description' key, and within %q{ ... }"

        def_node_matcher :find_update_info_node, <<~PATTERN
          (def :initialize _args (begin (super $(send nil? {:update_info :merge_info} (lvar :info) (hash ...))) ...))
        PATTERN

        def_node_matcher :find_nested_update_info_node, <<~PATTERN
          (def :initialize _args (super $(send nil? {:update_info :merge_info} (lvar :info) (hash ...)) ...))
        PATTERN

        def on_def(node)
          update_info_node = find_update_info_node(node) || find_nested_update_info_node(node)
          return if update_info_node.nil?

          hash = update_info_node.arguments.find { |argument| hash_arg?(argument) }
          hash.each_pair do |key, value|
            if key.value == "Description"
              if requires_correction?(key, value)
                add_offense(value.location.end, &autocorrector(value))
              end
            end
          end
        end

        private

        def autocorrector(description_value)
          lambda do |corrector|
            description_key = description_value.parent.key
            new_content = indent_description_value_correctly(description_key, description_value)

            corrector.replace(description_value.source_range, new_content)
          end
        end

        def requires_correction?(description_key, description_value)
          return false if description_value.single_line?

          current_content = description_value.source
          expected_content = indent_description_value_correctly(description_key, description_value)
          expected_content != current_content
        end

        def indent_description_value_correctly(description_key, description_value)
          content_whitespace = indentation(description_key)
          final_line_whitespace = offset(description_key)

          description_lines = node_content(description_value).strip.lines
          indented_description = description_lines.map do |line|
            cleaned_content = line.strip
            if cleaned_content.empty?
              "\n"
            else
              "#{content_whitespace}#{cleaned_content}\n"
            end
          end.join

          new_literal = "%q{\n"
          new_literal <<= indented_description
          new_literal <<= final_line_whitespace
          new_literal <<= '}'

          new_literal
        end

        def node_content(node)
          if node.str_type?
            node.value
          elsif node.dstr_type?
            node.children.map(&:value).join
          else
            raise "Module description should be a string, instead found '#{node.type}'"
          end
        end

        def hash_arg?(node)
          node.type == :hash
        end
      end
    end
  end
end