1# frozen_string_literal: true 2 3require_relative '../../migration_helpers' 4 5module RuboCop 6 module Cop 7 module Migration 8 # Cop that enforces always adding a limit on text columns 9 # 10 # Text columns starting with `encrypted_` are very likely used 11 # by `attr_encrypted` which controls the text length. Those columns 12 # should not add a text limit. 13 class AddLimitToTextColumns < RuboCop::Cop::Cop 14 include MigrationHelpers 15 16 TEXT_LIMIT_ATTRIBUTE_ALLOWED_SINCE = 2021_09_10_00_00_00 17 18 MSG = 'Text columns should always have a limit set (255 is suggested). ' \ 19 'You can add a limit to a `text` column by using `add_text_limit` or by using `.text... limit: 255` inside `create_table`' 20 21 TEXT_LIMIT_ATTRIBUTE_NOT_ALLOWED = 'Text columns should always have a limit set (255 is suggested). Using limit: is not supported in this version. ' \ 22 'You can add a limit to a `text` column by using `add_text_limit` or `.text_limit` inside `create_table`' 23 24 def_node_matcher :reverting?, <<~PATTERN 25 (def :down ...) 26 PATTERN 27 28 def_node_matcher :set_text_limit?, <<~PATTERN 29 (send _ :text_limit ...) 30 PATTERN 31 32 def_node_matcher :add_text_limit?, <<~PATTERN 33 (send _ :add_text_limit ...) 34 PATTERN 35 36 def on_def(node) 37 return unless in_migration?(node) 38 39 # Don't enforce the rule when on down to keep consistency with existing schema 40 return if reverting?(node) 41 42 node.each_descendant(:send) do |send_node| 43 next unless text_operation?(send_node) 44 45 if text_operation_with_limit?(send_node) 46 add_offense(send_node, location: :selector, message: TEXT_LIMIT_ATTRIBUTE_NOT_ALLOWED) if version(node) < TEXT_LIMIT_ATTRIBUTE_ALLOWED_SINCE 47 else 48 # We require a limit for the same table and attribute name 49 if text_limit_missing?(node, *table_and_attribute_name(send_node)) 50 add_offense(send_node, location: :selector) 51 end 52 end 53 end 54 end 55 56 private 57 58 def text_operation_with_limit?(node) 59 migration_method = node.children[1] 60 61 return unless migration_method == :text 62 63 if attributes = node.children[3] 64 attributes.pairs.find { |pair| pair.key.value == :limit }.present? 65 end 66 end 67 68 def text_operation?(node) 69 # Don't complain about text arrays 70 return false if array_column?(node) 71 72 modifier = node.children[0] 73 migration_method = node.children[1] 74 75 if migration_method == :text 76 modifier.type == :lvar 77 elsif ADD_COLUMN_METHODS.include?(migration_method) 78 modifier.nil? && text_column?(node.children[4]) 79 end 80 end 81 82 def text_column?(column_type) 83 column_type.type == :sym && column_type.value == :text 84 end 85 86 # For a given node, find the table and attribute this node is for 87 # 88 # Simple when we have calls to `add_column_XXX` helper methods 89 # 90 # A little bit more tricky when we have attributes defined as part of 91 # a create/change table block: 92 # - The attribute name is available on the node 93 # - Finding the table name requires to: 94 # * go up 95 # * find the first block the attribute def is part of 96 # * go back down to find the create_table node 97 # * fetch the table name from that node 98 def table_and_attribute_name(node) 99 migration_method = node.children[1] 100 table_name, attribute_name = '' 101 102 if migration_method == :text 103 # We are inside a node in a create/change table block 104 block_node = node.each_ancestor(:block).first 105 create_table_node = block_node 106 .children 107 .find { |n| TABLE_METHODS.include?(n.children[1])} 108 109 if create_table_node 110 table_name = create_table_node.children[2].value 111 else 112 # Guard against errors when a new table create/change migration 113 # helper is introduced and warn the author so that it can be 114 # added in TABLE_METHODS 115 table_name = 'unknown' 116 add_offense(block_node, message: 'Unknown table create/change helper') 117 end 118 119 attribute_name = node.children[2].value 120 else 121 # We are in a node for one of the ADD_COLUMN_METHODS 122 table_name = node.children[2].value 123 attribute_name = node.children[3].value 124 end 125 126 [table_name, attribute_name] 127 end 128 129 # Check if there is an `add_text_limit` call for the provided 130 # table and attribute name 131 def text_limit_missing?(node, table_name, attribute_name) 132 return false if encrypted_attribute_name?(attribute_name) 133 134 limit_found = false 135 136 node.each_descendant(:send) do |send_node| 137 if set_text_limit?(send_node) 138 limit_found = matching_set_text_limit?(send_node, attribute_name) 139 elsif add_text_limit?(send_node) 140 limit_found = matching_add_text_limit?(send_node, table_name, attribute_name) 141 end 142 143 break if limit_found 144 end 145 146 !limit_found 147 end 148 149 def matching_set_text_limit?(send_node, attribute_name) 150 limit_attribute = send_node.children[2].value 151 152 limit_attribute == attribute_name 153 end 154 155 def matching_add_text_limit?(send_node, table_name, attribute_name) 156 limit_table = send_node.children[2].value 157 limit_attribute = send_node.children[3].value 158 159 limit_table == table_name && limit_attribute == attribute_name 160 end 161 162 def encrypted_attribute_name?(attribute_name) 163 attribute_name.to_s.start_with?('encrypted_') 164 end 165 end 166 end 167 end 168end 169