gitlab-styles 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.gitlab-ci.yml +13 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/default.yml +1229 -0
- data/gitlab-styles.gemspec +29 -0
- data/lib/gitlab/styles.rb +6 -0
- data/lib/gitlab/styles/rubocop.rb +30 -0
- data/lib/gitlab/styles/rubocop/cop/active_record_dependent.rb +30 -0
- data/lib/gitlab/styles/rubocop/cop/active_record_serialize.rb +22 -0
- data/lib/gitlab/styles/rubocop/cop/custom_error_class.rb +68 -0
- data/lib/gitlab/styles/rubocop/cop/gem_fetcher.rb +41 -0
- data/lib/gitlab/styles/rubocop/cop/in_batches.rb +20 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_column.rb +56 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_column_with_default_to_large_table.rb +59 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_concurrent_foreign_key.rb +31 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_concurrent_index.rb +38 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_index.rb +52 -0
- data/lib/gitlab/styles/rubocop/cop/migration/add_timestamps.rb +29 -0
- data/lib/gitlab/styles/rubocop/cop/migration/datetime.rb +40 -0
- data/lib/gitlab/styles/rubocop/cop/migration/hash_index.rb +55 -0
- data/lib/gitlab/styles/rubocop/cop/migration/remove_concurrent_index.rb +33 -0
- data/lib/gitlab/styles/rubocop/cop/migration/remove_index.rb +30 -0
- data/lib/gitlab/styles/rubocop/cop/migration/reversible_add_column_with_default.rb +39 -0
- data/lib/gitlab/styles/rubocop/cop/migration/safer_boolean_column.rb +98 -0
- data/lib/gitlab/styles/rubocop/cop/migration/timestamps.rb +31 -0
- data/lib/gitlab/styles/rubocop/cop/migration/update_column_in_batches.rb +45 -0
- data/lib/gitlab/styles/rubocop/cop/polymorphic_associations.rb +27 -0
- data/lib/gitlab/styles/rubocop/cop/project_path_helper.rb +55 -0
- data/lib/gitlab/styles/rubocop/cop/redirect_with_status.rb +48 -0
- data/lib/gitlab/styles/rubocop/cop/rspec/single_line_hook.rb +42 -0
- data/lib/gitlab/styles/rubocop/migration_helpers.rb +15 -0
- data/lib/gitlab/styles/rubocop/model_helpers.rb +15 -0
- data/lib/gitlab/styles/version.rb +5 -0
- metadata +169 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if indexes are added in a concurrent manner.
|
9
|
+
class AddIndex < RuboCop::Cop::Cop
|
10
|
+
include MigrationHelpers
|
11
|
+
|
12
|
+
MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'.freeze
|
13
|
+
|
14
|
+
def on_def(node)
|
15
|
+
return unless in_migration?(node)
|
16
|
+
|
17
|
+
new_tables = []
|
18
|
+
|
19
|
+
node.each_descendant(:send) do |send_node|
|
20
|
+
first_arg = first_argument(send_node)
|
21
|
+
|
22
|
+
# The first argument of "create_table" / "add_index" is the table
|
23
|
+
# name.
|
24
|
+
new_tables << first_arg if create_table?(send_node)
|
25
|
+
|
26
|
+
next if method_name(send_node) != :add_index
|
27
|
+
|
28
|
+
# Using "add_index" is fine for newly created tables as there's no
|
29
|
+
# data in these tables yet.
|
30
|
+
next if new_tables.include?(first_arg)
|
31
|
+
|
32
|
+
add_offense(send_node, :selector)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_table?(node)
|
37
|
+
method_name(node) == :create_table
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_name(node)
|
41
|
+
node.children[1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def first_argument(node)
|
45
|
+
node.children[2] ? node.children[0] : nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if 'add_timestamps' method is called with timezone information.
|
9
|
+
class AddTimestamps < RuboCop::Cop::Cop
|
10
|
+
include MigrationHelpers
|
11
|
+
|
12
|
+
MSG = 'Do not use `add_timestamps`, use `add_timestamps_with_timezone` instead'.freeze
|
13
|
+
|
14
|
+
# Check methods.
|
15
|
+
def on_send(node)
|
16
|
+
return unless in_migration?(node)
|
17
|
+
|
18
|
+
add_offense(node, :selector) if method_name(node) == :add_timestamps
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_name(node)
|
22
|
+
node.children[1]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if datetime data type is added with timezone information.
|
9
|
+
class Datetime < RuboCop::Cop::Cop
|
10
|
+
include MigrationHelpers
|
11
|
+
|
12
|
+
MSG = 'Do not use the `datetime` data type, use `datetime_with_timezone` instead'.freeze
|
13
|
+
|
14
|
+
# Check methods in table creation.
|
15
|
+
def on_def(node)
|
16
|
+
return unless in_migration?(node)
|
17
|
+
|
18
|
+
node.each_descendant(:send) do |send_node|
|
19
|
+
add_offense(send_node, :selector) if method_name(send_node) == :datetime
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check methods.
|
24
|
+
def on_send(node)
|
25
|
+
return unless in_migration?(node)
|
26
|
+
|
27
|
+
node.each_descendant do |descendant|
|
28
|
+
add_offense(node, :expression) if descendant.type == :sym && descendant.children.last == :datetime
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_name(node)
|
33
|
+
node.children[1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'set'
|
2
|
+
require_relative '../../migration_helpers'
|
3
|
+
|
4
|
+
module Gitlab
|
5
|
+
module Styles
|
6
|
+
module Rubocop
|
7
|
+
module Cop
|
8
|
+
module Migration
|
9
|
+
# Cop that prevents the use of hash indexes in database migrations
|
10
|
+
class HashIndex < RuboCop::Cop::Cop
|
11
|
+
include MigrationHelpers
|
12
|
+
|
13
|
+
MSG = 'hash indexes should be avoided at all costs since they are not ' \
|
14
|
+
'recorded in the PostgreSQL WAL, you should use a btree index instead'.freeze
|
15
|
+
|
16
|
+
NAMES = Set.new([:add_index, :index, :add_concurrent_index]).freeze
|
17
|
+
|
18
|
+
def on_send(node)
|
19
|
+
return unless in_migration?(node)
|
20
|
+
|
21
|
+
name = node.children[1]
|
22
|
+
|
23
|
+
return unless NAMES.include?(name)
|
24
|
+
|
25
|
+
opts = node.children.last
|
26
|
+
|
27
|
+
return unless opts&.type == :hash
|
28
|
+
|
29
|
+
opts.each_node(:pair) do |pair|
|
30
|
+
next unless hash_key_type(pair) == :sym &&
|
31
|
+
hash_key_name(pair) == :using
|
32
|
+
|
33
|
+
if hash_key_value(pair).to_s == 'hash'
|
34
|
+
add_offense(pair, :expression)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def hash_key_type(pair)
|
40
|
+
pair.children[0].type
|
41
|
+
end
|
42
|
+
|
43
|
+
def hash_key_name(pair)
|
44
|
+
pair.children[0].children[0]
|
45
|
+
end
|
46
|
+
|
47
|
+
def hash_key_value(pair)
|
48
|
+
pair.children[1].children[0]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if `remove_concurrent_index` is used with `up`/`down` methods
|
9
|
+
# and not `change`.
|
10
|
+
class RemoveConcurrentIndex < RuboCop::Cop::Cop
|
11
|
+
include MigrationHelpers
|
12
|
+
|
13
|
+
MSG = '`remove_concurrent_index` is not reversible so you must manually define ' \
|
14
|
+
'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'.freeze
|
15
|
+
|
16
|
+
def on_send(node)
|
17
|
+
return unless in_migration?(node)
|
18
|
+
return unless node.children[1] == :remove_concurrent_index
|
19
|
+
|
20
|
+
node.each_ancestor(:def) do |def_node|
|
21
|
+
add_offense(def_node, :name) if method_name(def_node) == :change
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_name(node)
|
26
|
+
node.children[0]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if indexes are removed in a concurrent manner.
|
9
|
+
class RemoveIndex < RuboCop::Cop::Cop
|
10
|
+
include MigrationHelpers
|
11
|
+
|
12
|
+
MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'.freeze
|
13
|
+
|
14
|
+
def on_def(node)
|
15
|
+
return unless in_migration?(node)
|
16
|
+
|
17
|
+
node.each_descendant(:send) do |send_node|
|
18
|
+
add_offense(send_node, :selector) if method_name(send_node) == :remove_index
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_name(node)
|
23
|
+
node.children[1]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if `add_column_with_default` is used with `up`/`down` methods
|
9
|
+
# and not `change`.
|
10
|
+
class ReversibleAddColumnWithDefault < RuboCop::Cop::Cop
|
11
|
+
include MigrationHelpers
|
12
|
+
|
13
|
+
def_node_matcher :add_column_with_default?, <<~PATTERN
|
14
|
+
(send nil :add_column_with_default $...)
|
15
|
+
PATTERN
|
16
|
+
|
17
|
+
def_node_matcher :defines_change?, <<~PATTERN
|
18
|
+
(def :change ...)
|
19
|
+
PATTERN
|
20
|
+
|
21
|
+
MSG = '`add_column_with_default` is not reversible so you must manually define ' \
|
22
|
+
'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
|
23
|
+
|
24
|
+
def on_send(node)
|
25
|
+
return unless in_migration?(node)
|
26
|
+
return unless add_column_with_default?(node)
|
27
|
+
|
28
|
+
node.each_ancestor(:def) do |def_node|
|
29
|
+
next unless defines_change?(def_node)
|
30
|
+
|
31
|
+
add_offense(def_node, :name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# This cop requires a default value and disallows nulls for boolean
|
9
|
+
# columns on small tables.
|
10
|
+
#
|
11
|
+
# In general, this prevents 3-state-booleans.
|
12
|
+
# https://robots.thoughtbot.com/avoid-the-threestate-boolean-problem
|
13
|
+
#
|
14
|
+
# In particular, for the `application_settings` table, this ensures that
|
15
|
+
# upgraded installations get a proper default for the new boolean setting.
|
16
|
+
# A developer might otherwise mistakenly assume that a value in
|
17
|
+
# `ApplicationSetting.defaults` is sufficient.
|
18
|
+
#
|
19
|
+
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/2750 for more
|
20
|
+
# information.
|
21
|
+
class SaferBooleanColumn < RuboCop::Cop::Cop
|
22
|
+
include MigrationHelpers
|
23
|
+
|
24
|
+
DEFAULT_OFFENSE = 'Boolean columns on the `%s` table should have a default. You may wish to use `add_column_with_default`.'.freeze
|
25
|
+
NULL_OFFENSE = 'Boolean columns on the `%s` table should disallow nulls.'.freeze
|
26
|
+
DEFAULT_AND_NULL_OFFENSE = 'Boolean columns on the `%s` table should have a default and should disallow nulls. You may wish to use `add_column_with_default`.'.freeze
|
27
|
+
|
28
|
+
SMALL_TABLES = %i[
|
29
|
+
application_settings
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
def_node_matcher :add_column?, <<~PATTERN
|
33
|
+
(send nil :add_column $...)
|
34
|
+
PATTERN
|
35
|
+
|
36
|
+
def on_send(node)
|
37
|
+
return unless in_migration?(node)
|
38
|
+
|
39
|
+
matched = add_column?(node)
|
40
|
+
|
41
|
+
return unless matched
|
42
|
+
|
43
|
+
table, _, type = matched.to_a.take(3).map(&:children).map(&:first)
|
44
|
+
opts = matched[3]
|
45
|
+
|
46
|
+
return unless SMALL_TABLES.include?(table) && type == :boolean
|
47
|
+
|
48
|
+
no_default = no_default?(opts)
|
49
|
+
nulls_allowed = nulls_allowed?(opts)
|
50
|
+
|
51
|
+
offense = if no_default && nulls_allowed
|
52
|
+
DEFAULT_AND_NULL_OFFENSE
|
53
|
+
elsif no_default
|
54
|
+
DEFAULT_OFFENSE
|
55
|
+
elsif nulls_allowed
|
56
|
+
NULL_OFFENSE
|
57
|
+
end
|
58
|
+
|
59
|
+
add_offense(node, :expression, format(offense, table)) if offense
|
60
|
+
end
|
61
|
+
|
62
|
+
def no_default?(opts)
|
63
|
+
return true unless opts
|
64
|
+
|
65
|
+
each_hash_node_pair(opts) do |key, value|
|
66
|
+
return value == 'nil' if key == :default
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def nulls_allowed?(opts)
|
71
|
+
return true unless opts
|
72
|
+
|
73
|
+
each_hash_node_pair(opts) do |key, value|
|
74
|
+
return value != 'false' if key == :null
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_hash_node_pair(hash_node, &block)
|
79
|
+
hash_node.each_node(:pair) do |pair|
|
80
|
+
key = hash_pair_key(pair)
|
81
|
+
value = hash_pair_value(pair)
|
82
|
+
yield(key, value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def hash_pair_key(pair)
|
87
|
+
pair.children[0].children[0]
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash_pair_value(pair)
|
91
|
+
pair.children[1].source
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if 'timestamps' method is called with timezone information.
|
9
|
+
class Timestamps < RuboCop::Cop::Cop
|
10
|
+
include MigrationHelpers
|
11
|
+
|
12
|
+
MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'.freeze
|
13
|
+
|
14
|
+
# Check methods in table creation.
|
15
|
+
def on_def(node)
|
16
|
+
return unless in_migration?(node)
|
17
|
+
|
18
|
+
node.each_descendant(:send) do |send_node|
|
19
|
+
add_offense(send_node, :selector) if method_name(send_node) == :timestamps
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_name(node)
|
24
|
+
node.children[1]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative '../../migration_helpers'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Styles
|
5
|
+
module Rubocop
|
6
|
+
module Cop
|
7
|
+
module Migration
|
8
|
+
# Cop that checks if a spec file exists for any migration using
|
9
|
+
# `update_column_in_batches`.
|
10
|
+
class UpdateColumnInBatches < RuboCop::Cop::Cop
|
11
|
+
include MigrationHelpers
|
12
|
+
|
13
|
+
MSG = 'Migration running `update_column_in_batches` must have a spec file at' \
|
14
|
+
' `%s`.'.freeze
|
15
|
+
|
16
|
+
def on_send(node)
|
17
|
+
return unless in_migration?(node)
|
18
|
+
return unless node.children[1] == :update_column_in_batches
|
19
|
+
|
20
|
+
spec_path = spec_filename(node)
|
21
|
+
|
22
|
+
add_offense(node, :expression, format(MSG, spec_path)) unless File.exist?(File.expand_path(spec_path, rails_root))
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def spec_filename(node)
|
28
|
+
source_name = node.location.expression.source_buffer.name
|
29
|
+
path = Pathname.new(source_name).relative_path_from(rails_root)
|
30
|
+
dirname = File.dirname(path)
|
31
|
+
.sub(%r{\Adb/(migrate|post_migrate)}, 'spec/migrations')
|
32
|
+
filename = File.basename(source_name, '.rb').sub(/\A\d+_/, '')
|
33
|
+
|
34
|
+
File.join(dirname, "#{filename}_spec.rb")
|
35
|
+
end
|
36
|
+
|
37
|
+
def rails_root
|
38
|
+
Pathname.new(File.expand_path('../../..', __dir__))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|