diff options
Diffstat (limited to 'rubocop/cop/migration/add_limit_to_text_columns.rb')
-rw-r--r-- | rubocop/cop/migration/add_limit_to_text_columns.rb | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/rubocop/cop/migration/add_limit_to_text_columns.rb b/rubocop/cop/migration/add_limit_to_text_columns.rb new file mode 100644 index 00000000000..15c28bb9266 --- /dev/null +++ b/rubocop/cop/migration/add_limit_to_text_columns.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that enforces always adding a limit on text columns + class AddLimitToTextColumns < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Text columns should always have a limit set (255 is suggested). ' \ + 'You can add a limit to a `text` column by using `add_text_limit`'.freeze + + def_node_matcher :reverting?, <<~PATTERN + (def :down ...) + PATTERN + + def_node_matcher :add_text_limit?, <<~PATTERN + (send _ :add_text_limit ...) + PATTERN + + def on_def(node) + return unless in_migration?(node) + + # Don't enforce the rule when on down to keep consistency with existing schema + return if reverting?(node) + + node.each_descendant(:send) do |send_node| + next unless text_operation?(send_node) + + # We require a limit for the same table and attribute name + if text_limit_missing?(node, *table_and_attribute_name(send_node)) + add_offense(send_node, location: :selector) + end + end + end + + private + + def text_operation?(node) + modifier = node.children[0] + migration_method = node.children[1] + + if migration_method == :text + modifier.type == :lvar + elsif ADD_COLUMN_METHODS.include?(migration_method) + modifier.nil? && text_column?(node.children[4]) + end + end + + def text_column?(column_type) + column_type.type == :sym && column_type.value == :text + end + + # For a given node, find the table and attribute this node is for + # + # Simple when we have calls to `add_column_XXX` helper methods + # + # A little bit more tricky when we have attributes defined as part of + # a create/change table block: + # - The attribute name is available on the node + # - Finding the table name requires to: + # * go up + # * find the first block the attribute def is part of + # * go back down to find the create_table node + # * fetch the table name from that node + def table_and_attribute_name(node) + migration_method = node.children[1] + table_name, attribute_name = '' + + if migration_method == :text + # We are inside a node in a create/change table block + block_node = node.each_ancestor(:block).first + create_table_node = block_node + .children + .find { |n| TABLE_METHODS.include?(n.children[1])} + + if create_table_node + table_name = create_table_node.children[2].value + else + # Guard against errors when a new table create/change migration + # helper is introduced and warn the author so that it can be + # added in TABLE_METHODS + table_name = 'unknown' + add_offense(block_node, message: 'Unknown table create/change helper') + end + + attribute_name = node.children[2].value + else + # We are in a node for one of the ADD_COLUMN_METHODS + table_name = node.children[2].value + attribute_name = node.children[3].value + end + + [table_name, attribute_name] + end + + # Check if there is an `add_text_limit` call for the provided + # table and attribute name + def text_limit_missing?(node, table_name, attribute_name) + limit_found = false + + node.each_descendant(:send) do |send_node| + next unless add_text_limit?(send_node) + + limit_table = send_node.children[2].value + limit_attribute = send_node.children[3].value + + if limit_table == table_name && limit_attribute == attribute_name + limit_found = true + break + end + end + + !limit_found + end + end + end + end +end |