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,29 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'gitlab/styles/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'gitlab-styles'
7
+ spec.version = Gitlab::Styles::VERSION
8
+ spec.authors = ['GitLab']
9
+ spec.email = ['remy@rymai.me']
10
+
11
+ spec.summary = 'GitLab style guides and shared style configs.'
12
+ spec.homepage = 'https://github.com/gitlab-org/gitlab-styles'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'rubocop', '~> 0.49'
23
+ spec.add_dependency 'rubocop-rspec', '~> 1.15'
24
+ spec.add_dependency 'rubocop-gitlab-security', '~> 0.1.0'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.15'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
29
+ end
@@ -0,0 +1,6 @@
1
+ require 'gitlab/styles/version'
2
+
3
+ module Gitlab
4
+ module Styles
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ require 'gitlab/styles/rubocop/cop/custom_error_class'
2
+ require 'gitlab/styles/rubocop/cop/gem_fetcher'
3
+ require 'gitlab/styles/rubocop/cop/active_record_serialize'
4
+ require 'gitlab/styles/rubocop/cop/redirect_with_status'
5
+ require 'gitlab/styles/rubocop/cop/polymorphic_associations'
6
+ require 'gitlab/styles/rubocop/cop/project_path_helper'
7
+ require 'gitlab/styles/rubocop/cop/active_record_dependent'
8
+ require 'gitlab/styles/rubocop/cop/in_batches'
9
+ require 'gitlab/styles/rubocop/cop/migration/add_column'
10
+ require 'gitlab/styles/rubocop/cop/migration/add_column_with_default_to_large_table'
11
+ require 'gitlab/styles/rubocop/cop/migration/add_concurrent_foreign_key'
12
+ require 'gitlab/styles/rubocop/cop/migration/add_concurrent_index'
13
+ require 'gitlab/styles/rubocop/cop/migration/add_index'
14
+ require 'gitlab/styles/rubocop/cop/migration/add_timestamps'
15
+ require 'gitlab/styles/rubocop/cop/migration/datetime'
16
+ require 'gitlab/styles/rubocop/cop/migration/safer_boolean_column'
17
+ require 'gitlab/styles/rubocop/cop/migration/hash_index'
18
+ require 'gitlab/styles/rubocop/cop/migration/remove_concurrent_index'
19
+ require 'gitlab/styles/rubocop/cop/migration/remove_index'
20
+ require 'gitlab/styles/rubocop/cop/migration/reversible_add_column_with_default'
21
+ require 'gitlab/styles/rubocop/cop/migration/timestamps'
22
+ require 'gitlab/styles/rubocop/cop/migration/update_column_in_batches'
23
+ require 'gitlab/styles/rubocop/cop/rspec/single_line_hook'
24
+
25
+ module Gitlab
26
+ module Styles
27
+ module Rubocop
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../model_helpers'
2
+
3
+ module Gitlab
4
+ module Styles
5
+ module Rubocop
6
+ module Cop
7
+ # Cop that prevents the use of `dependent: ...` in ActiveRecord models.
8
+ class ActiveRecordDependent < RuboCop::Cop::Cop
9
+ include ModelHelpers
10
+
11
+ MSG = 'Do not use `dependent: to remove associated data, ' \
12
+ 'use foreign keys with cascading deletes instead'.freeze
13
+
14
+ METHOD_NAMES = [:has_many, :has_one, :belongs_to].freeze
15
+
16
+ def on_send(node)
17
+ return unless in_model?(node)
18
+ return unless METHOD_NAMES.include?(node.children[1])
19
+
20
+ node.children.last.each_node(:pair) do |pair|
21
+ key_name = pair.children[0].children[0]
22
+
23
+ add_offense(pair, :expression) if key_name == :dependent
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../model_helpers'
2
+
3
+ module Gitlab
4
+ module Styles
5
+ module Rubocop
6
+ module Cop
7
+ # Cop that prevents the use of `serialize` in ActiveRecord models.
8
+ class ActiveRecordSerialize < RuboCop::Cop::Cop
9
+ include ModelHelpers
10
+
11
+ MSG = 'Do not store serialized data in the database, use separate columns and/or tables instead'.freeze
12
+
13
+ def on_send(node)
14
+ return unless in_model?(node)
15
+
16
+ add_offense(node, :selector) if node.children[1] == :serialize
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,68 @@
1
+ module Gitlab
2
+ module Styles
3
+ module Rubocop
4
+ module Cop
5
+ # This cop makes sure that custom error classes, when empty, are declared
6
+ # with Class.new.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class FooError < StandardError
11
+ # end
12
+ #
13
+ # # okish
14
+ # class FooError < StandardError; end
15
+ #
16
+ # # good
17
+ # FooError = Class.new(StandardError)
18
+ class CustomErrorClass < RuboCop::Cop::Cop
19
+ MSG = 'Use `Class.new(SuperClass)` to define an empty custom error class.'.freeze
20
+
21
+ def on_class(node)
22
+ _klass, parent, body = node.children
23
+
24
+ return if body
25
+
26
+ parent_klass = class_name_from_node(parent)
27
+
28
+ return unless parent_klass && parent_klass.to_s.end_with?('Error')
29
+
30
+ add_offense(node, :expression)
31
+ end
32
+
33
+ def autocorrect(node)
34
+ klass, parent, _body = node.children
35
+ replacement = "#{class_name_from_node(klass)} = Class.new(#{class_name_from_node(parent)})"
36
+
37
+ lambda do |corrector|
38
+ corrector.replace(node.source_range, replacement)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # The nested constant `Foo::Bar::Baz` looks like:
45
+ #
46
+ # s(:const,
47
+ # s(:const,
48
+ # s(:const, nil, :Foo), :Bar), :Baz)
49
+ #
50
+ # So recurse through that to get the name as written in the source.
51
+ #
52
+ def class_name_from_node(node, suffix = nil)
53
+ return unless node&.type == :const
54
+
55
+ name = node.children[1].to_s
56
+ name = "#{name}::#{suffix}" if suffix
57
+
58
+ if node.children[0]
59
+ class_name_from_node(node.children[0], name)
60
+ else
61
+ name
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ module Gitlab
2
+ module Styles
3
+ module Rubocop
4
+ module Cop
5
+ # This cop prevents usage of the `git` and `github` arguments to `gem` in a
6
+ # `Gemfile` in order to avoid additional points of failure beyond
7
+ # rubygems.org.
8
+ class GemFetcher < RuboCop::Cop::Cop
9
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'.freeze
10
+
11
+ GIT_KEYS = [:git, :github].freeze
12
+
13
+ def on_send(node)
14
+ return unless gemfile?(node)
15
+
16
+ func_name = node.children[1]
17
+ return unless func_name == :gem
18
+
19
+ node.children.last.each_node(:pair) do |pair|
20
+ key_name = pair.children[0].children[0].to_sym
21
+ if GIT_KEYS.include?(key_name)
22
+ add_offense(node, pair.source_range, MSG)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def gemfile?(node)
30
+ node
31
+ .location
32
+ .expression
33
+ .source_buffer
34
+ .name
35
+ .end_with?("Gemfile")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../model_helpers'
2
+
3
+ module Gitlab
4
+ module Styles
5
+ module Rubocop
6
+ module Cop
7
+ # Cop that prevents the use of `in_batches`
8
+ class InBatches < RuboCop::Cop::Cop
9
+ MSG = 'Do not use `in_batches`, use `each_batch` from the EachBatch module instead'.freeze
10
+
11
+ def on_send(node)
12
+ return unless node.children[1] == :in_batches
13
+
14
+ add_offense(node, :selector)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
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 columns are added in a way that doesn't require
9
+ # downtime.
10
+ class AddColumn < RuboCop::Cop::Cop
11
+ include MigrationHelpers
12
+
13
+ WHITELISTED_TABLES = [:application_settings].freeze
14
+
15
+ MSG = '`add_column` with a default value requires downtime, ' \
16
+ 'use `add_column_with_default` instead'.freeze
17
+
18
+ def on_send(node)
19
+ return unless in_migration?(node)
20
+
21
+ name = node.children[1]
22
+
23
+ return unless name == :add_column
24
+
25
+ # Ignore whitelisted tables.
26
+ return if table_whitelisted?(node.children[2])
27
+
28
+ opts = node.children.last
29
+
30
+ return unless opts&.type == :hash
31
+
32
+ opts.each_node(:pair) do |pair|
33
+ if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
34
+ add_offense(node, :selector)
35
+ end
36
+ end
37
+ end
38
+
39
+ def table_whitelisted?(symbol)
40
+ symbol&.type == :sym &&
41
+ WHITELISTED_TABLES.include?(symbol.children[0])
42
+ end
43
+
44
+ def hash_key_type(pair)
45
+ pair.children[0].type
46
+ end
47
+
48
+ def hash_key_name(pair)
49
+ pair.children[0].children[0]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../../migration_helpers'
2
+
3
+ module Gitlab
4
+ module Styles
5
+ module Rubocop
6
+ module Cop
7
+ module Migration
8
+ # This cop checks for `add_column_with_default` on a table that's been
9
+ # explicitly blacklisted because of its size.
10
+ #
11
+ # Even though this helper performs the update in batches to avoid
12
+ # downtime, using it with tables with millions of rows still causes a
13
+ # significant delay in the deploy process and is best avoided.
14
+ #
15
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
16
+ # information.
17
+ class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop
18
+ include MigrationHelpers
19
+
20
+ MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \
21
+ 'long time to complete, and should be avoided unless absolutely ' \
22
+ 'necessary'.freeze
23
+
24
+ LARGE_TABLES = %i[
25
+ ci_pipelines
26
+ ci_builds
27
+ events
28
+ issues
29
+ merge_request_diff_files
30
+ merge_request_diffs
31
+ merge_requests
32
+ namespaces
33
+ notes
34
+ projects
35
+ routes
36
+ users
37
+ ].freeze
38
+
39
+ def_node_matcher :add_column_with_default?, <<~PATTERN
40
+ (send nil :add_column_with_default $(sym ...) ...)
41
+ PATTERN
42
+
43
+ def on_send(node)
44
+ return unless in_migration?(node)
45
+
46
+ matched = add_column_with_default?(node)
47
+ return unless matched
48
+
49
+ table = matched.to_a.first
50
+ return unless LARGE_TABLES.include?(table)
51
+
52
+ add_offense(node, :expression, format(MSG, table))
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ 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 `add_concurrent_foreign_key` is used instead of
9
+ # `add_foreign_key`.
10
+ class AddConcurrentForeignKey < RuboCop::Cop::Cop
11
+ include MigrationHelpers
12
+
13
+ MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
14
+
15
+ def on_send(node)
16
+ return unless in_migration?(node)
17
+
18
+ name = node.children[1]
19
+
20
+ add_offense(node, :selector) if name == :add_foreign_key
21
+ end
22
+
23
+ def method_name(node)
24
+ node.children.first
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
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_concurrent_index` is used with `up`/`down` methods
9
+ # and not `change`.
10
+ class AddConcurrentIndex < RuboCop::Cop::Cop
11
+ include MigrationHelpers
12
+
13
+ MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
14
+ 'the `up` and `down` methods in your migration class, using `remove_concurrent_index` in `down`'.freeze
15
+
16
+ def on_send(node)
17
+ return unless in_migration?(node)
18
+
19
+ name = node.children[1]
20
+
21
+ return unless name == :add_concurrent_index
22
+
23
+ node.each_ancestor(:def) do |def_node|
24
+ next unless method_name(def_node) == :change
25
+
26
+ add_offense(def_node, :name)
27
+ end
28
+ end
29
+
30
+ def method_name(node)
31
+ node.children.first
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end