class RuboCop::Cop::Flexport::GlobalModelAccessFromEngine
This cop checks for engines reaching directly into app/ models.
With an ActiveRecord object, engine code can perform arbitrary reads and arbitrary writes to models located in the main `app/` directory. This cop helps isolate Rails Engine code to ensure that modular boundaries are respected.
Checks for both access via `MyGlobalModel.foo` and associations.
@example
# bad class MyEngine::MyService m = SomeGlobalModel.find(123) m.any_random_attribute = "whatever i want" m.save end # good class MyEngine::MyService ApiServiceForGlobalModels.perform_a_supported_operation("foo") end
@example
# bad class MyEngine::MyModel < ApplicationModel has_one :some_global_model, class_name: "SomeGlobalModel" end # good class MyEngine::MyModel < ApplicationModel # No direct association to global models. end
This cop will also complain if you try to use global FactoryBot factories in your engine's specs. To disable this behavior for your engine, add it to the `FactoryBotGlobalAccessAllowedEngines` list in .rubocop.yml.
Constants
- MSG
Public Instance Methods
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 96 def check_for_global_factory_bot_usage(node, factory_node) factory = factory_node.children[0] return unless global_factory?(factory) model_class_name = global_factories[factory] add_offense(node, message: message(model_class_name)) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 88 def check_for_rails_association_with_global_model(assocation_hash_args) class_name_node = extract_class_name_node(assocation_hash_args) class_name = class_name_node&.value return unless global_model?(class_name) add_offense(class_name_node, message: message(class_name)) end
Because this cop's behavior depends on the state of external files, we override this method to bust the RuboCop
cache when those files change.
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 107 def external_dependency_checksum if check_for_global_factory_bot? Digest::SHA1.hexdigest((model_dir_paths + global_factories.keys.sort).join) else Digest::SHA1.hexdigest(model_dir_paths.join) end end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 64 def on_const(node) return unless in_enforced_engine_file? return unless global_model_const?(node) # The cop allows access to e.g. MyGlobalModel::MY_CONST. return if child_of_const?(node) return if in_module_or_class_declaration?(node) add_offense(node, message: message(node.source)) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 74 def on_send(node) return unless in_enforced_engine_file? rails_association_hash_args(node) do |assocation_hash_args| check_for_rails_association_with_global_model(assocation_hash_args) end return unless check_for_global_factory_bot? factory_bot_usage(node) do |factory_node| check_for_global_factory_bot_usage(node, factory_node) end end
Private Instance Methods
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 219 def allowed_global_models cop_config['AllowedGlobalModels'] || [] end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 134 def calculate_global_models all_model_paths = model_dir_paths.reject do |path| path.include?('/concerns/') end all_models = all_model_paths.map do |path| # Translates `app/models/foo/bar_baz.rb` to `Foo::BarBaz`. file_name = path.gsub(global_models_path, '').gsub('.rb', '') ActiveSupport::Inflector.classify(file_name) end all_models - allowed_global_models end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 169 def check_for_global_factory_bot? spec_file? && factory_bot_enabled? && factory_bot_global_access_allowed_engines.none? do |engine| processed_source.path.include?(File.join(engines_path, engine, '')) end end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 190 def child_of_const?(node) node.parent.const_type? end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 204 def disabled_engines raw = cop_config['DisabledEngines'] || [] raw.map do |e| ActiveSupport::Inflector.underscore(e) end end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 200 def engines_path cop_config['EnginesPath'] end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 146 def extract_class_name_node(assocation_hash_args) assocation_hash_args.each_pair do |key, value| return value if key.value == :class_name && value.str_type? end nil end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 215 def factory_bot_enabled? cop_config['FactoryBotEnabled'] end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 211 def factory_bot_global_access_allowed_engines cop_config['FactoryBotGlobalAccessAllowedEngines'] || [] end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 125 def global_factories @global_factories ||= find_factories.reject { |path| path.start_with?(engines_path) }.values.reduce(:merge) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 186 def global_factory?(factory_name) global_factories.include?(factory_name) end
class_name is e.g. “FooGlobalModelNamespace::BarModel”
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 182 def global_model?(class_name) global_model_names.include?(class_name) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 175 def global_model_const?(const_node) # Remove leading `::`, if any. class_name = const_node.source.sub(/^:*/, '') global_model?(class_name) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 121 def global_model_names @global_model_names ||= calculate_global_models end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 194 def global_models_path path = cop_config['GlobalModelsPath'] path += '/' unless path.end_with?('/') path end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 161 def in_disabled_engine?(file_path) disabled_engines.any? do |e| # Add trailing / to engine path to avoid incorrectly # matching engines with similar names file_path.include?(File.join(engines_path, e, '')) end end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 153 def in_enforced_engine_file? file_path = processed_source.path return false unless file_path.include?(engines_path) return false if in_disabled_engine?(file_path) true end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 117 def message(model) format(MSG, model: model) end
# File lib/rubocop/cop/flexport/global_model_access_from_engine.rb, line 130 def model_dir_paths Dir[File.join(global_models_path, '**/*.rb')] end