gitlab-styles 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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