gitlab-styles 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.gitlab-ci.yml +13 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +43 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/default.yml +1229 -0
  14. data/gitlab-styles.gemspec +29 -0
  15. data/lib/gitlab/styles.rb +6 -0
  16. data/lib/gitlab/styles/rubocop.rb +30 -0
  17. data/lib/gitlab/styles/rubocop/cop/active_record_dependent.rb +30 -0
  18. data/lib/gitlab/styles/rubocop/cop/active_record_serialize.rb +22 -0
  19. data/lib/gitlab/styles/rubocop/cop/custom_error_class.rb +68 -0
  20. data/lib/gitlab/styles/rubocop/cop/gem_fetcher.rb +41 -0
  21. data/lib/gitlab/styles/rubocop/cop/in_batches.rb +20 -0
  22. data/lib/gitlab/styles/rubocop/cop/migration/add_column.rb +56 -0
  23. data/lib/gitlab/styles/rubocop/cop/migration/add_column_with_default_to_large_table.rb +59 -0
  24. data/lib/gitlab/styles/rubocop/cop/migration/add_concurrent_foreign_key.rb +31 -0
  25. data/lib/gitlab/styles/rubocop/cop/migration/add_concurrent_index.rb +38 -0
  26. data/lib/gitlab/styles/rubocop/cop/migration/add_index.rb +52 -0
  27. data/lib/gitlab/styles/rubocop/cop/migration/add_timestamps.rb +29 -0
  28. data/lib/gitlab/styles/rubocop/cop/migration/datetime.rb +40 -0
  29. data/lib/gitlab/styles/rubocop/cop/migration/hash_index.rb +55 -0
  30. data/lib/gitlab/styles/rubocop/cop/migration/remove_concurrent_index.rb +33 -0
  31. data/lib/gitlab/styles/rubocop/cop/migration/remove_index.rb +30 -0
  32. data/lib/gitlab/styles/rubocop/cop/migration/reversible_add_column_with_default.rb +39 -0
  33. data/lib/gitlab/styles/rubocop/cop/migration/safer_boolean_column.rb +98 -0
  34. data/lib/gitlab/styles/rubocop/cop/migration/timestamps.rb +31 -0
  35. data/lib/gitlab/styles/rubocop/cop/migration/update_column_in_batches.rb +45 -0
  36. data/lib/gitlab/styles/rubocop/cop/polymorphic_associations.rb +27 -0
  37. data/lib/gitlab/styles/rubocop/cop/project_path_helper.rb +55 -0
  38. data/lib/gitlab/styles/rubocop/cop/redirect_with_status.rb +48 -0
  39. data/lib/gitlab/styles/rubocop/cop/rspec/single_line_hook.rb +42 -0
  40. data/lib/gitlab/styles/rubocop/migration_helpers.rb +15 -0
  41. data/lib/gitlab/styles/rubocop/model_helpers.rb +15 -0
  42. data/lib/gitlab/styles/version.rb +5 -0
  43. 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