diff options
Diffstat (limited to 'rubocop')
-rw-r--r-- | rubocop/cop/database/multiple_databases.rb | 13 | ||||
-rw-r--r-- | rubocop/cop/gitlab/avoid_uploaded_file_from_params.rb | 4 | ||||
-rw-r--r-- | rubocop/cop/graphql/graphql_name_position.rb | 40 | ||||
-rw-r--r-- | rubocop/formatter/todo_formatter.rb | 141 | ||||
-rw-r--r-- | rubocop/todo_dir.rb | 81 |
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 |