1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
# Cop that enforces always adding a limit on text columns
#
# Text columns starting with `encrypted_` are very likely used
# by `attr_encrypted` which controls the text length. Those columns
# should not add a text limit.
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`'
def_node_matcher :reverting?, <<~PATTERN
(def :down ...)
PATTERN
def_node_matcher :set_text_limit?, <<~PATTERN
(send _ :text_limit ...)
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)
# Don't complain about text arrays
return false if array_column?(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)
return false if encrypted_attribute_name?(attribute_name)
limit_found = false
node.each_descendant(:send) do |send_node|
if set_text_limit?(send_node)
limit_found = matching_set_text_limit?(send_node, attribute_name)
elsif add_text_limit?(send_node)
limit_found = matching_add_text_limit?(send_node, table_name, attribute_name)
end
break if limit_found
end
!limit_found
end
def matching_set_text_limit?(send_node, attribute_name)
limit_attribute = send_node.children[2].value
limit_attribute == attribute_name
end
def matching_add_text_limit?(send_node, table_name, attribute_name)
limit_table = send_node.children[2].value
limit_attribute = send_node.children[3].value
limit_table == table_name && limit_attribute == attribute_name
end
def encrypted_attribute_name?(attribute_name)
attribute_name.to_s.start_with?('encrypted_')
end
end
end
end
end
|