diff --git a/.rubocop.yml b/.rubocop.yml index d5276cc..3392724 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,9 @@ inherit_from: - default.yml + +AllCops: + inherit_mode: + merge: + - Exclude + Exclude: + - "**/spec/fixtures/**/*" diff --git a/config/default.yml b/config/default.yml index ea13013..513affb 100644 --- a/config/default.yml +++ b/config/default.yml @@ -62,3 +62,31 @@ Discourse/NoMixingMultisiteAndStandardSpecs: Patterns: - _spec.rb - '(?:^|/)spec/' + +Discourse/Plugins/CallRequiresPlugin: + Enabled: true + Include: + - 'app/controllers/**/*' + +Discourse/Plugins/UsePluginInstanceOn: + Enabled: true + +Discourse/Plugins/NamespaceMethods: + Enabled: true + Exclude: + - '**/spec/**/*' + - '**/tasks/**/*.rake' + - '**/db/fixtures/**/*' + +Discourse/Plugins/NamespaceConstants: + Enabled: true + Exclude: + - '**/spec/**/*' + - '**/tasks/**/*.rake' + - '**/db/fixtures/**/*' + +Discourse/Plugins/UseRequireRelative: + Enabled: true + +Discourse/Plugins/NoMonkeyPatching: + Enabled: true diff --git a/lib/rubocop-discourse.rb b/lib/rubocop-discourse.rb index 760f14d..f8ad552 100644 --- a/lib/rubocop-discourse.rb +++ b/lib/rubocop-discourse.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "rubocop" +require "active_support" +require "active_support/core_ext/string/inflections" require_relative "rubocop/discourse" require_relative "rubocop/discourse/inject" diff --git a/lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb b/lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb new file mode 100644 index 0000000..0bc138e --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Plugin controllers must call `requires_plugin` to prevent routes from + # being accessible when the plugin is disabled. + # + # @example + # # bad + # class MyController + # def my_action + # end + # end + # + # # good + # class MyController + # requires_plugin PLUGIN_NAME + # def my_action + # end + # end + # + class CallRequiresPlugin < Base + MSG = + "Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled." + + def_node_matcher :requires_plugin_present?, <<~MATCHER + (class _ _ + { + (begin <(send nil? :requires_plugin _) ...>) + <(send nil? :requires_plugin _) ...> + } + ) + MATCHER + + def on_class(node) + return if requires_plugin_present?(node) + return if requires_plugin_present_in_parent_classes(node) + add_offense(node, message: MSG) + end + + private + + def requires_plugin_present_in_parent_classes(node) + return unless processed_source.path + controller_path = + base_controller_path(node.parent_class&.const_name.to_s) + return unless controller_path + Commissioner + .new([self.class.new(config, @options)]) + .investigate(parse(controller_path.read, controller_path.to_s)) + .offenses + .empty? + end + + def base_controller_path(base_class) + return if base_class.blank? + base_path = "#{base_class.underscore}.rb" + path = Pathname.new("#{processed_source.path}/../").cleanpath + until path.root? + controller_path = path.join(base_path) + return controller_path if controller_path.exist? + path = path.join("..").cleanpath + end + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse/plugins/namespace_constants.rb b/lib/rubocop/cop/discourse/plugins/namespace_constants.rb new file mode 100644 index 0000000..e5c2e0e --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/namespace_constants.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Constants must be defined inside the plugin namespace (module or class). + # + # @example + # # bad + # MY_CONSTANT = :value + # + # # good + # module MyPlugin + # MY_CONSTANT = :value + # end + # + class NamespaceConstants < Base + MSG = "Don’t define constants outside a class or a module." + + def on_casgn(node) + return if inside_namespace?(node) + add_offense(node, message: MSG) + end + + private + + def inside_namespace?(node) + node.each_ancestor.detect { _1.class_type? || _1.module_type? } + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse/plugins/namespace_methods.rb b/lib/rubocop/cop/discourse/plugins/namespace_methods.rb new file mode 100644 index 0000000..ddcce48 --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/namespace_methods.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Methods must be defined inside the plugin namespace (module or class). + # + # @example + # # bad + # def my_method + # end + # + # # good + # module MyPlugin + # def my_method + # end + # end + # + class NamespaceMethods < Base + MSG = "Don’t define methods outside a class or a module." + + def on_def(node) + return if inside_namespace?(node) + add_offense(node, message: MSG) + end + + private + + def inside_namespace?(node) + node.each_ancestor.detect { _1.class_type? || _1.module_type? } + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb b/lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb new file mode 100644 index 0000000..23781c8 --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Don’t monkey-patch classes directly in `plugin.rb`. Instead, define + # additional methods in a dedicated mixin (an ActiveSupport concern for + # example) and use `prepend` (this allows calling `super` from the mixin). + # + # If you’re just adding new methods to an existing serializer, then use + # `add_to_serializer` instead. + # + # @example generic monkey-patching + # # bad + # ::Topic.class_eval do + # has_many :new_items + # + # def new_method + # end + # end + # + # # good + # module MyPlugin::TopicExtension + # extend ActiveSupport::Concern + # + # prepended do + # has_many :new_items + # end + # + # def new_method + # end + # end + # + # reloadable_patch { ::Topic.prepend(MyPlugin::TopicExtension) } + # + # @example for serializers + # # bad + # UserSerializer.class_eval do + # def new_method + # do_processing + # end + # end + # + # # good + # add_to_serializer(:user, :new_method) { do_processing } + # + class NoMonkeyPatching < Base + MSG = + "Don’t reopen existing classes. Instead, create a mixin and use `prepend`." + MSG_CLASS_EVAL = + "Don’t call `class_eval`. Instead, create a mixin and use `prepend`." + MSG_CLASS_EVAL_SERIALIZERS = + "Don’t call `class_eval` on a serializer. If you’re adding new methods, use `add_to_serializer`. Otherwise, create a mixin and use `prepend`." + MSG_SERIALIZERS = + "Don’t reopen serializers. Instead, use `add_to_serializer`." + RESTRICT_ON_SEND = [:class_eval].freeze + + def_node_matcher :existing_class?, <<~MATCHER + (class (const (cbase) _) ...) + MATCHER + + def_node_matcher :serializer?, <<~MATCHER + ({class send} (const _ /Serializer$/) ...) + MATCHER + + def on_send(node) + if serializer?(node) + return add_offense(node, message: MSG_CLASS_EVAL_SERIALIZERS) + end + add_offense(node, message: MSG_CLASS_EVAL) + end + + def on_class(node) + return unless in_plugin_rb_file? + return unless existing_class?(node) + if serializer?(node) + return add_offense(node, message: MSG_SERIALIZERS) + end + add_offense(node, message: MSG) + end + + private + + def in_plugin_rb_file? + processed_source.path.split("/").last == "plugin.rb" + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb b/lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb new file mode 100644 index 0000000..2470abd --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Using `DiscourseEvent.on` leaves the handler enabled when the plugin is disabled. + # + # @example + # # bad + # DiscourseEvent.on(:event) { do_something } + # + # # good + # on(:event) { do_something } + # + class UsePluginInstanceOn < Base + MSG = + "Use `on` instead of `DiscourseEvent.on` as the latter will listen to events even if the plugin is disabled." + NOT_OUTSIDE_PLUGIN_RB = + "Don’t call `DiscourseEvent.on` outside `plugin.rb`." + RESTRICT_ON_SEND = [:on].freeze + + def_node_matcher :discourse_event_on?, <<~MATCHER + (send (const nil? :DiscourseEvent) :on _) + MATCHER + + def on_send(node) + return unless discourse_event_on?(node) + return add_offense(node, message: MSG) if in_plugin_rb_file? + add_offense(node, message: NOT_OUTSIDE_PLUGIN_RB) + end + + private + + def in_plugin_rb_file? + processed_source.path.split("/").last == "plugin.rb" + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse/plugins/use_require_relative.rb b/lib/rubocop/cop/discourse/plugins/use_require_relative.rb new file mode 100644 index 0000000..2cdd5ce --- /dev/null +++ b/lib/rubocop/cop/discourse/plugins/use_require_relative.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Discourse + module Plugins + # Use `require_relative` to load dependencies. + # + # @example + # # bad + # load File.expand_path("../lib/my_file.rb", __FILE__) + # + # # good + # require_relative "lib/my_file" + # + class UseRequireRelative < Base + MSG = "Use `require_relative` instead of `load`." + RESTRICT_ON_SEND = [:load].freeze + + def_node_matcher :load_called?, <<~MATCHER + (send nil? :load _) + MATCHER + + def on_send(node) + return unless load_called?(node) + add_offense(node, message: MSG) + end + end + end + end + end +end diff --git a/lib/rubocop/cop/discourse_cops.rb b/lib/rubocop/cop/discourse_cops.rb index d39500c..7cf81de 100644 --- a/lib/rubocop/cop/discourse_cops.rb +++ b/lib/rubocop/cop/discourse_cops.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -path = File.join(__dir__, "discourse", "*.rb") +path = File.join(__dir__, "discourse", "**/*.rb") Dir[path].each { |file| require file } diff --git a/rubocop-discourse.gemspec b/rubocop-discourse.gemspec index aab6ac2..0d53955 100644 --- a/rubocop-discourse.gemspec +++ b/rubocop-discourse.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = "rubocop-discourse" - s.version = "3.6.1" + s.version = "3.7.0" s.summary = "Custom rubocop cops used by Discourse" s.authors = ["Discourse Team"] s.license = "MIT" @@ -11,6 +11,7 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split($/) s.require_paths = ["lib"] + s.add_runtime_dependency "activesupport", ">= 6.1" s.add_runtime_dependency "rubocop", ">= 1.59.0" s.add_runtime_dependency "rubocop-rspec", ">= 2.25.0" s.add_runtime_dependency "rubocop-factory_bot", ">= 2.0.0" diff --git a/spec/fixtures/controllers/bad_controller.rb b/spec/fixtures/controllers/bad_controller.rb new file mode 100644 index 0000000..ce6668e --- /dev/null +++ b/spec/fixtures/controllers/bad_controller.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BadController < NoRequiresPluginController +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled. +end diff --git a/spec/fixtures/controllers/base_controller.rb b/spec/fixtures/controllers/base_controller.rb new file mode 100644 index 0000000..c7c3edc --- /dev/null +++ b/spec/fixtures/controllers/base_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class BaseController + class << self + def requires_plugin(*) + end + + def requires_login + end + end +end diff --git a/spec/fixtures/controllers/good_controller.rb b/spec/fixtures/controllers/good_controller.rb new file mode 100644 index 0000000..483fa3b --- /dev/null +++ b/spec/fixtures/controllers/good_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class GoodController < RequiresPluginController +end diff --git a/spec/fixtures/controllers/inherit_from_outside_controller.rb b/spec/fixtures/controllers/inherit_from_outside_controller.rb new file mode 100644 index 0000000..4dfacd6 --- /dev/null +++ b/spec/fixtures/controllers/inherit_from_outside_controller.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InheritFromOutsideController < ApplicationController +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled. +end diff --git a/spec/fixtures/controllers/namespaced_parent_controller.rb b/spec/fixtures/controllers/namespaced_parent_controller.rb new file mode 100644 index 0000000..dd0e5ba --- /dev/null +++ b/spec/fixtures/controllers/namespaced_parent_controller.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InheritFromOutsideController < MyPlugin::ApplicationController +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled. +end diff --git a/spec/fixtures/controllers/no_requires_plugin_controller.rb b/spec/fixtures/controllers/no_requires_plugin_controller.rb new file mode 100644 index 0000000..8c5443d --- /dev/null +++ b/spec/fixtures/controllers/no_requires_plugin_controller.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class NoRequiresPluginController < BaseController + requires_login +end diff --git a/spec/fixtures/controllers/requires_plugin_controller.rb b/spec/fixtures/controllers/requires_plugin_controller.rb new file mode 100644 index 0000000..32fc092 --- /dev/null +++ b/spec/fixtures/controllers/requires_plugin_controller.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class RequiresPluginController < BaseController + requires_plugin "my_plugin" + requires_login +end diff --git a/spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb b/spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb new file mode 100644 index 0000000..77adffb --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::CallRequiresPlugin, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when `requires_plugin` is missing" do + it "registers an offense" do + expect_offense(<<~RUBY) + class MyController < ApplicationController + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled. + requires_login + end + RUBY + end + end + + context "when `requires_plugin` is not missing" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + class MyController + requires_plugin MyPlugin::PLUGIN_NAME + requires_login + end + RUBY + end + end + + context "when inheriting" do + let(:controllers_path) do + Pathname.new("#{__dir__}/../../../../fixtures/controllers").cleanpath + end + + before do + # As we’re providing real files, we need to get rid of the default config + # restricting the cop to `app/controllers/*` + configuration.for_cop(cop).delete("Include") + end + + context "when `requires_plugin` is called in a parent controller" do + let(:good_controller) { controllers_path.join("good_controller.rb") } + + it "does not register an offense" do + expect_no_offenses(good_controller.read, good_controller.to_s) + end + end + + context "when `requires_plugin` is not called in a parent controller" do + let(:bad_controller) { controllers_path.join("bad_controller.rb") } + + it "registers an offense" do + expect_offense(bad_controller.read, bad_controller.to_s) + end + end + + context "when parent controller can’t be located" do + context "when parent controller is namespaced" do + let(:controller) do + controllers_path.join("namespaced_parent_controller.rb") + end + + it "registers an offense" do + expect_offense(controller.read, controller.to_s) + end + end + + context "when parent controller is not namespaced" do + let(:controller) do + controllers_path.join("inherit_from_outside_controller.rb") + end + + it "registers an offense" do + expect_offense(controller.read, controller.to_s) + end + end + end + end +end diff --git a/spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb b/spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb new file mode 100644 index 0000000..c083a91 --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::NamespaceConstants, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when defining a constant outside any namespace" do + it "registers an offense" do + expect_offense(<<~RUBY) + MY_CONSTANT = "my_value" + ^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NamespaceConstants: Don’t define constants outside a class or a module. + + class MyClass + MY_CONSTANT = "my_value" + end + RUBY + end + end + + context "when defining a constant inside a class" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + class MyClass + MY_CONSTANT = "my_value" + end + RUBY + end + end + + context "when defining a constant inside a module" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + module MyModule + MY_CONSTANT = "my_value" + end + RUBY + end + end +end diff --git a/spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb b/spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb new file mode 100644 index 0000000..8f799fe --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::NamespaceMethods, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when defining a method outside any namespace" do + it "registers an offense" do + expect_offense(<<~RUBY) + def my_method + ^^^^^^^^^^^^^ Discourse/Plugins/NamespaceMethods: Don’t define methods outside a class or a module. + "my_value" + end + + class MyClass + def my_method + "my_method" + end + end + RUBY + end + end + + context "when defining a method inside a class" do + context "when defining an instance method" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + class MyClass + def my_method + "my_value" + end + end + RUBY + end + end + + context "when defining a class method" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + class MyClass + class << self + def my_method + "my_value" + end + + def another_method + "plop" + end + end + end + RUBY + end + end + end + + context "when defining a method inside a module" do + context "when defining an instance method" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + module MyModule + def my_method + "my_value" + end + end + RUBY + end + end + + context "when defining a class method" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + module MyModule + class << self + def my_method + "my_value" + end + end + end + RUBY + end + end + end +end diff --git a/spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb b/spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb new file mode 100644 index 0000000..3e494fd --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::NoMonkeyPatching, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when outside `plugin.rb`" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY, "my_class.rb") + class ::MyClass + def my_method + "my_value" + end + end + + class AnotherClass + def my_method + "my_value" + end + end + RUBY + end + end + + context "when inside `plugin.rb`" do + context "when opening an existing class" do + it "registers an offense" do + expect_offense(<<~RUBY, "plugin.rb") + after_initialize do + module MyPlugin + class Engine < Rails::Engine + isolate_namespace MyPlugin + end + end + + class ::Topic + ^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t reopen existing classes. [...] + def self.new_method + :new_value + end + + def my_new_method + "my_new_value" + end + end + end + RUBY + end + end + + context "when opening an existing serializer" do + it "registers an offense" do + expect_offense(<<~RUBY, "plugin.rb") + class ::TopicSerializer + ^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t reopen serializers. [...] + def new_attribute + "my_attribute" + end + end + RUBY + end + end + + context "when calling `.class_eval` on a class" do + it "registers an offense" do + expect_offense(<<~RUBY) + User.class_eval do + ^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t call `class_eval`. [...] + def a_new_method + :new_value + end + end + RUBY + end + end + + context "when calling `.class_eval` on a serializer" do + it "registers an offense" do + expect_offense(<<~RUBY) + UserSerializer.class_eval do + ^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t call `class_eval` on a serializer. [...] + def a_new_method + :new_value + end + end + RUBY + end + end + end +end diff --git a/spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb b/spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb new file mode 100644 index 0000000..ac83868 --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::UsePluginInstanceOn, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when outside `plugin.rb`" do + context "when `DiscourseEvent.on` is called" do + it "registers an offense" do + expect_offense(<<~RUBY, "another_file.rb") + DiscourseEvent.on(:topic_status_updated) { do_something } + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UsePluginInstanceOn: Don’t call `DiscourseEvent.on` outside `plugin.rb`. + RUBY + end + end + end + + context "when inside `plugin.rb`" do + context "when `DiscourseEvent.on` is called" do + it "registers an offense" do + expect_offense(<<~RUBY, "plugin.rb") + DiscourseEvent.on(:topic_status_updated) { do_something } + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UsePluginInstanceOn: Use `on` instead of `DiscourseEvent.on` [...] + RUBY + end + end + + context "when `on` is called" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY, "plugin.rb") + on(:topic_status_updated) { do_something } + RUBY + end + end + end +end diff --git a/spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb b/spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb new file mode 100644 index 0000000..93edd46 --- /dev/null +++ b/spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe RuboCop::Cop::Discourse::Plugins::UseRequireRelative, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + context "when using `load`" do + it "registers an offense" do + expect_offense(<<~RUBY) + load File.expand_path("../app/jobs/onceoff/voting_ensure_consistency.rb", __FILE__) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UseRequireRelative: Use `require_relative` instead of `load`. + RUBY + end + end + + context "when using `require_relative`" do + it "does not register an offense" do + expect_no_offenses(<<~RUBY) + require_relative "app/controllers/encrypt_controller.rb" + RUBY + end + end +end