summaryrefslogtreecommitdiff
path: root/rubocop
diff options
context:
space:
mode:
Diffstat (limited to 'rubocop')
-rw-r--r--rubocop/cop/database/multiple_databases.rb13
-rw-r--r--rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb4
-rw-r--r--rubocop/cop/graphql/graphql_name_position.rb40
-rw-r--r--rubocop/formatter/todo_formatter.rb141
-rw-r--r--rubocop/todo_dir.rb81
5 files changed, 277 insertions, 2 deletions
diff --git a/rubocop/cop/database/multiple_databases.rb b/rubocop/cop/database/multiple_databases.rb
index fb6e81f9845..f20348d9d1f 100644
--- a/rubocop/cop/database/multiple_databases.rb
+++ b/rubocop/cop/database/multiple_databases.rb
@@ -17,6 +17,10 @@ module RuboCop
https://docs.gitlab.com/ee/development/database/transaction_guidelines.html
EOF
+ ALLOWED_METHODS = %i[
+ no_touching
+ ].freeze
+
def_node_matcher :active_record_base_method_is_used?, <<~PATTERN
(send (const (const nil? :ActiveRecord) :Base) $_)
PATTERN
@@ -24,8 +28,17 @@ module RuboCop
def on_send(node)
return unless active_record_base_method_is_used?(node)
+ active_record_base_method = node.children[1]
+ return if method_is_allowed?(active_record_base_method)
+
add_offense(node, location: :expression, message: AR_BASE_MESSAGE)
end
+
+ private
+
+ def method_is_allowed?(method_name)
+ ALLOWED_METHODS.include?(method_name.to_sym)
+ end
end
end
end
diff --git a/rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb b/rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb
index 599371aa5a1..0795f96732d 100644
--- a/rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb
+++ b/rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb
@@ -4,7 +4,7 @@ module RuboCop
module Cop
module Gitlab
# This cop checks for `UploadedFile.from_params` usage.
- # See https://docs.gitlab.com/ee/development/uploads.html#how-to-add-a-new-upload-route
+ # See https://docs.gitlab.com/ee/development/uploads/working_with_uploads.html
#
# @example
#
@@ -34,7 +34,7 @@ module RuboCop
# end
# end
class AvoidUploadedFileFromParams < RuboCop::Cop::Cop
- MSG = 'Use the `UploadedFile` set by `multipart.rb` instead of calling `UploadedFile.from_params` directly. See https://docs.gitlab.com/ee/development/uploads.html#how-to-add-a-new-upload-route'
+ MSG = 'Use the `UploadedFile` set by `multipart.rb` instead of calling `UploadedFile.from_params` directly. See https://docs.gitlab.com/ee/development/uploads/working_with_uploads.html'
def_node_matcher :calling_uploaded_file_from_params?, <<~PATTERN
(send (const nil? :UploadedFile) :from_params ...)
diff --git a/rubocop/cop/graphql/graphql_name_position.rb b/rubocop/cop/graphql/graphql_name_position.rb
new file mode 100644
index 00000000000..f18d65588cc
--- /dev/null
+++ b/rubocop/cop/graphql/graphql_name_position.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# This cop ensures that if a class uses `graphql_name`, then
+# it's the first line of the class
+#
+# @example
+#
+# # bad
+# class AwfulClass
+# field :some_field, GraphQL::Types::JSON
+# graphql_name 'AwfulClass'
+# end
+#
+# # good
+# class GreatClass
+# graphql_name 'AwfulClass'
+# field :some_field, GraphQL::Types::String
+# end
+
+module RuboCop
+ module Cop
+ module Graphql
+ class GraphqlNamePosition < RuboCop::Cop::Cop
+ MSG = '`graphql_name` should be the first line of the class: '\
+ 'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#naming-conventions'
+
+ def_node_search :graphql_name?, <<~PATTERN
+ (send nil? :graphql_name ...)
+ PATTERN
+
+ def on_class(node)
+ return unless graphql_name?(node)
+ return if node.body.single_line?
+
+ add_offense(node, location: :expression) unless graphql_name?(node.body.children.first)
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/formatter/todo_formatter.rb b/rubocop/formatter/todo_formatter.rb
new file mode 100644
index 00000000000..14b4242063e
--- /dev/null
+++ b/rubocop/formatter/todo_formatter.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'set'
+require 'rubocop'
+require 'yaml'
+
+require_relative '../todo_dir'
+
+module RuboCop
+ module Formatter
+ # This formatter dumps a YAML configuration file per cop rule
+ # into `.rubocop_todo/**/*.yml` which contains detected offenses.
+ #
+ # For example, this formatter stores offenses for `RSpec/VariableName`
+ # in `.rubocop_todo/rspec/variable_name.yml`.
+ class TodoFormatter < BaseFormatter
+ # Disable a cop which exceeds this limit. This way we ensure that we
+ # don't enable a cop by accident when moving it from
+ # .rubocop_todo.yml to .rubocop_todo/.
+ # We keep the cop disabled if it has been disabled previously explicitly
+ # via `Enabled: false` in .rubocop_todo.yml or .rubocop_todo/.
+ MAX_OFFENSE_COUNT = 15
+
+ class Todo
+ attr_reader :cop_name, :files, :offense_count
+
+ def initialize(cop_name)
+ @cop_name = cop_name
+ @files = Set.new
+ @offense_count = 0
+ @cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
+ end
+
+ def record(file, offense_count)
+ @files << file
+ @offense_count += offense_count
+ end
+
+ def autocorrectable?
+ @cop_class&.support_autocorrect?
+ end
+ end
+
+ def initialize(output, options = {})
+ directory = options.delete(:rubocop_todo_dir) || TodoDir::DEFAULT_TODO_DIR
+ @todos = Hash.new { |hash, cop_name| hash[cop_name] = Todo.new(cop_name) }
+ @todo_dir = TodoDir.new(directory)
+ @config_inspect_todo_dir = load_config_inspect_todo_dir(directory)
+ @config_old_todo_yml = load_config_old_todo_yml(directory)
+ check_multiple_configurations!
+
+ super
+ end
+
+ def file_finished(file, offenses)
+ return if offenses.empty?
+
+ file = relative_path(file)
+
+ offenses.map(&:cop_name).tally.each do |cop_name, offense_count|
+ @todos[cop_name].record(file, offense_count)
+ end
+ end
+
+ def finished(_inspected_files)
+ @todos.values.sort_by(&:cop_name).each do |todo|
+ yaml = to_yaml(todo)
+ path = @todo_dir.write(todo.cop_name, yaml)
+
+ output.puts "Written to #{relative_path(path)}\n"
+ end
+ end
+
+ private
+
+ def relative_path(path)
+ parent = File.expand_path('..', @todo_dir.directory)
+ path.delete_prefix("#{parent}/")
+ end
+
+ def to_yaml(todo)
+ yaml = []
+ yaml << '---'
+ yaml << '# Cop supports --auto-correct.' if todo.autocorrectable?
+ yaml << "#{todo.cop_name}:"
+
+ if previously_disabled?(todo) && offense_count_exceeded?(todo)
+ yaml << " # Offense count: #{todo.offense_count}"
+ yaml << ' # Temporarily disabled due to too many offenses'
+ yaml << ' Enabled: false'
+ end
+
+ yaml << ' Exclude:'
+
+ files = todo.files.sort.map { |file| " - '#{file}'" }
+ yaml.concat files
+ yaml << ''
+
+ yaml.join("\n")
+ end
+
+ def offense_count_exceeded?(todo)
+ todo.offense_count > MAX_OFFENSE_COUNT
+ end
+
+ def check_multiple_configurations!
+ cop_names = @config_inspect_todo_dir.keys & @config_old_todo_yml.keys
+ return if cop_names.empty?
+
+ list = cop_names.sort.map { |cop_name| "- #{cop_name}" }.join("\n")
+ raise "Multiple configurations found for cops:\n#{list}\n"
+ end
+
+ def previously_disabled?(todo)
+ cop_name = todo.cop_name
+
+ config = @config_old_todo_yml[cop_name] ||
+ @config_inspect_todo_dir[cop_name] || {}
+ return false if config.empty?
+
+ config['Enabled'] == false
+ end
+
+ def load_config_inspect_todo_dir(directory)
+ @todo_dir.list_inspect.each_with_object({}) do |path, combined|
+ config = YAML.load_file(path)
+ combined.update(config) if Hash === config
+ end
+ end
+
+ # Load YAML configuration from `.rubocop_todo.yml`.
+ # We consider this file already old, obsolete, and to be removed soon.
+ def load_config_old_todo_yml(directory)
+ path = File.expand_path(File.join(directory, '../.rubocop_todo.yml'))
+ config = YAML.load_file(path) if File.exist?(path)
+
+ config || {}
+ end
+ end
+ end
+end
diff --git a/rubocop/todo_dir.rb b/rubocop/todo_dir.rb
new file mode 100644
index 00000000000..4aca4454a06
--- /dev/null
+++ b/rubocop/todo_dir.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'active_support/inflector/inflections'
+
+module RuboCop
+ # Helper class to manage file access to RuboCop TODOs in .rubocop_todo directory.
+ class TodoDir
+ DEFAULT_TODO_DIR = File.expand_path('../.rubocop_todo', __dir__)
+
+ # Suffix to indicate TODOs being inspected right now.
+ SUFFIX_INSPECT = '.inspect'
+
+ attr_reader :directory
+
+ def initialize(directory, inflector: ActiveSupport::Inflector)
+ @directory = directory
+ @inflector = inflector
+ end
+
+ def read(cop_name, suffix = nil)
+ read_suffixed(cop_name)
+ end
+
+ def write(cop_name, content)
+ path = path_for(cop_name)
+
+ FileUtils.mkdir_p(File.dirname(path))
+ File.write(path, content)
+
+ path
+ end
+
+ def inspect(cop_name)
+ path = path_for(cop_name)
+
+ if File.exist?(path)
+ FileUtils.mv(path, "#{path}#{SUFFIX_INSPECT}")
+ true
+ else
+ false
+ end
+ end
+
+ def inspect_all
+ pattern = File.join(@directory, '**/*.yml')
+
+ Dir.glob(pattern).count do |path|
+ FileUtils.mv(path, "#{path}#{SUFFIX_INSPECT}")
+ end
+ end
+
+ def list_inspect
+ pattern = File.join(@directory, "**/*.yml.inspect")
+
+ Dir.glob(pattern)
+ end
+
+ def delete_inspected
+ pattern = File.join(@directory, '**/*.yml.inspect')
+
+ Dir.glob(pattern).count do |path|
+ File.delete(path)
+ end
+ end
+
+ private
+
+ def read_suffixed(cop_name, suffix = nil)
+ path = path_for(cop_name, suffix)
+
+ File.read(path) if File.exist?(path)
+ end
+
+ def path_for(cop_name, suffix = nil)
+ todo_path = "#{@inflector.underscore(cop_name)}.yml#{suffix}"
+
+ File.join(@directory, todo_path)
+ end
+ end
+end