From 2183afe3e6f1cce90f52dbe57ac51b0a29aebae7 Mon Sep 17 00:00:00 2001 From: Rafael Gibim <9031589+Drowze@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:49:05 -0300 Subject: [PATCH] Add factory boundaries cop --- config/default.yml | 12 +++ .../cop/overhaul/factory_boundaries.rb | 80 +++++++++++++++++++ lib/rubocop/cop/overhaul_cops.rb | 1 + .../cop/overhaul/factory_boundaries_spec.rb | 50 ++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 lib/rubocop/cop/overhaul/factory_boundaries.rb create mode 100644 spec/rubocop/cop/overhaul/factory_boundaries_spec.rb diff --git a/config/default.yml b/config/default.yml index 44df4ff..02a3d2b 100644 --- a/config/default.yml +++ b/config/default.yml @@ -9,3 +9,15 @@ Overhaul/AssignmentInsteadOfComparison: Description: Detect enumerable comparison blocks returning an assignment. Enabled: true VersionAdded: '0.0.1' +Overhaul/FactoryBoundaries: + Description: Detect usage of factories outside where they are supposed to be + Enabled: false + VersionAdded: '0.0.1' + Include: + - spec/**/*_spec.rb + Factories: {} + # e.g.: + # apple: + # - '**/food_service/**' + # car: + # - '**/cars_service/**' diff --git a/lib/rubocop/cop/overhaul/factory_boundaries.rb b/lib/rubocop/cop/overhaul/factory_boundaries.rb new file mode 100644 index 0000000..42343cc --- /dev/null +++ b/lib/rubocop/cop/overhaul/factory_boundaries.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Overhaul + # Checks whether the used factory is allowed in the current path. + # Requires configuration. + # + # @example + # # with: + # # Factories: {} + # # apple: + # # - '**/food_service/**' + # + # # bad: + # spec/car_service/apple_spec.rb + # create(:apple) + # + # # good: + # spec/food_service/apple_spec.rb + # create(:apple) + class FactoryBoundaries < RuboCop::Cop::Cop + MSG = "Do not use this cop here" + + RESTRICT_ON_SEND = %i[ + attributes_for + attributes_for_list + build + build_list + build_stubbed + build_stubbed_list + create + create_list + generate + generate_list + ].freeze + + # @!method factory_bot_factory(node) + def_node_matcher :factory_bot_factory, <<~PATTERN + (send #factory_call? _ + $(sym _) + ... + ) + PATTERN + + # @!method factory_bot?(node) + def_node_matcher :factory_bot?, <<~PATTERN + (const nil? :FactoryBot) + PATTERN + + def on_send(node) + factory_bot_factory(node) do |value| + allowed_paths = allowed_paths_for(value.value) + file_path = expanded_file_path + + return if allowed_paths.nil? + return if allowed_paths.any? do |path| + file_path.end_with?(path) || # if it's a relative path + File.fnmatch(path, expanded_file_path, File::FNM_PATHNAME) # if it's a glob + end + + add_offense(value) + end + end + + def factory_call?(node) + factory_bot?(node) || node.nil? + end + + def allowed_paths_for(factory) + cop_config["Factories"][factory.to_s] + end + + def expanded_file_path + File.expand_path(processed_source.file_path) + end + end + end + end +end diff --git a/lib/rubocop/cop/overhaul_cops.rb b/lib/rubocop/cop/overhaul_cops.rb index 9097088..2395cff 100644 --- a/lib/rubocop/cop/overhaul_cops.rb +++ b/lib/rubocop/cop/overhaul_cops.rb @@ -2,3 +2,4 @@ require_relative "overhaul/mutable_reform_defaults" require_relative "overhaul/assignment_instead_of_comparison" +require_relative "overhaul/factory_boundaries" diff --git a/spec/rubocop/cop/overhaul/factory_boundaries_spec.rb b/spec/rubocop/cop/overhaul/factory_boundaries_spec.rb new file mode 100644 index 0000000..8e5fc4f --- /dev/null +++ b/spec/rubocop/cop/overhaul/factory_boundaries_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Overhaul::FactoryBoundaries, :config do + let(:cop_config) do + { "Factories" => { "apple" => ["**/food_service/**"] } } + end + + shared_examples "usage of factory outside permitted paths" do + it "flags use of factory outside permitted paths" do + expect_offense(<<~RUBY, "spec/other_service/fac_spec.rb", method: method) + %{method}(:apple) + _{method} ^^^^^^ Do not use this cop here + RUBY + end + end + + shared_examples "usage of factory inside permitted paths" do + it "does not flag usage of factory in permitted paths" do + expect_no_offenses(<<~RUBY, "spec/food_service/fac_spec.rb") + #{method}(:apple) + RUBY + end + end + + described_class::RESTRICT_ON_SEND.flat_map { |method| [method, "FactoryBot.#{method}"] }.each do |method| + context "when method is #{method}" do + let(:method) { method.to_s } + + context "when a factory is configured with globs" do + include_examples "usage of factory outside permitted paths" + include_examples "usage of factory inside permitted paths" + end + + context "when a factory is configured with absolute path" do + let(:cop_config) do + { "Factories" => { "apple" => ["spec/food_service/fac_spec.rb"] } } + end + + include_examples "usage of factory outside permitted paths" + include_examples "usage of factory inside permitted paths" + end + end + end + + it "ignores non-related method calls" do + expect_no_offenses(<<~RUBY, "spec/other_service/fac_spec.rb") + custom_create(:apple) + RUBY + end +end