rubocop-discourse 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/config/default.yml +28 -0
  4. data/lib/rubocop/cop/discourse/fabricator_shorthand.rb +52 -0
  5. data/lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb +71 -0
  6. data/lib/rubocop/cop/discourse/plugins/namespace_constants.rb +35 -0
  7. data/lib/rubocop/cop/discourse/plugins/namespace_methods.rb +37 -0
  8. data/lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb +92 -0
  9. data/lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb +42 -0
  10. data/lib/rubocop/cop/discourse/plugins/use_require_relative.rb +32 -0
  11. data/lib/rubocop/cop/discourse_cops.rb +1 -1
  12. data/lib/rubocop-discourse.rb +2 -0
  13. data/rubocop-capybara.yml +5 -0
  14. data/rubocop-discourse.gemspec +4 -1
  15. data/rubocop-factory_bot.yml +11 -0
  16. data/rubocop-rspec.yml +0 -15
  17. data/spec/fixtures/controllers/bad_controller.rb +5 -0
  18. data/spec/fixtures/controllers/base_controller.rb +11 -0
  19. data/spec/fixtures/controllers/good_controller.rb +4 -0
  20. data/spec/fixtures/controllers/inherit_from_outside_controller.rb +5 -0
  21. data/spec/fixtures/controllers/namespaced_parent_controller.rb +5 -0
  22. data/spec/fixtures/controllers/no_requires_plugin_controller.rb +5 -0
  23. data/spec/fixtures/controllers/requires_plugin_controller.rb +6 -0
  24. data/spec/lib/rubocop/cop/fabricator_shorthand_spec.rb +34 -0
  25. data/spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb +81 -0
  26. data/spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb +42 -0
  27. data/spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb +86 -0
  28. data/spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb +93 -0
  29. data/spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb +39 -0
  30. data/spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb +26 -0
  31. data/stree-compat.yml +8 -0
  32. metadata +67 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e9fb235d758a713e2af0dda30c4985b71f05cb86f1660c4c386e10ca53bd5ce
4
- data.tar.gz: 84304eb1c797ded1fa2af6d4fc57e8de585ae584b631db88f4eeccad84c48728
3
+ metadata.gz: 3d347009b4a77b64450a20ef3afc5ca102dd361c60e3fbde2e5fd0ea976469cd
4
+ data.tar.gz: c98b24bd73f783f154320e337e44f6859e050c43ff9219cfa0be6f63a891fc13
5
5
  SHA512:
6
- metadata.gz: 1c851bc6c2e619647677b52e51d3df9cbe38e413a6bc050532a76db80722c451407a27c045511e52f652c30c128110358e0663ae719b83878d672941d7e1fd6e
7
- data.tar.gz: e4f3f2227473855ad627d347777eeb2ca964cca0918e9d76986f2cf3593c7d26a9bcbe442a3e1c810424f86df2da3ba85e4a84f33faf48ed5df5b5d3068c3f78
6
+ metadata.gz: 98c56b4b42703f8fc1fe0b3f68344507e2001d7a71d5530c89b8f21a29b5276fc518542b445e9dd9a902cb30be38ce0d4df18218b2cf08bfb50465411d7c7d77
7
+ data.tar.gz: f7f5f50325374fe530a26c20ed113b4cb827ed7541a8a9f6dbae1a81c0dca29be2cc7073c8f543b5200dab2e2238617984a7f4dbd87d838ff8a1d4cfa8da0db6
data/.rubocop.yml CHANGED
@@ -1,2 +1,9 @@
1
1
  inherit_from:
2
2
  - default.yml
3
+
4
+ AllCops:
5
+ inherit_mode:
6
+ merge:
7
+ - Exclude
8
+ Exclude:
9
+ - "**/spec/fixtures/**/*"
data/config/default.yml CHANGED
@@ -62,3 +62,31 @@ Discourse/NoMixingMultisiteAndStandardSpecs:
62
62
  Patterns:
63
63
  - _spec.rb
64
64
  - '(?:^|/)spec/'
65
+
66
+ Discourse/Plugins/CallRequiresPlugin:
67
+ Enabled: true
68
+ Include:
69
+ - 'app/controllers/**/*'
70
+
71
+ Discourse/Plugins/UsePluginInstanceOn:
72
+ Enabled: true
73
+
74
+ Discourse/Plugins/NamespaceMethods:
75
+ Enabled: true
76
+ Exclude:
77
+ - '**/spec/**/*'
78
+ - '**/tasks/**/*.rake'
79
+ - '**/db/fixtures/**/*'
80
+
81
+ Discourse/Plugins/NamespaceConstants:
82
+ Enabled: true
83
+ Exclude:
84
+ - '**/spec/**/*'
85
+ - '**/tasks/**/*.rake'
86
+ - '**/db/fixtures/**/*'
87
+
88
+ Discourse/Plugins/UseRequireRelative:
89
+ Enabled: true
90
+
91
+ Discourse/Plugins/NoMonkeyPatching:
92
+ Enabled: true
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ # When fabricating a record without custom attributes, we can use the
7
+ # fabricator shorthand as long as the identifier matches the fabricator
8
+ # name.
9
+ #
10
+ # @example
11
+ #
12
+ # # bad
13
+ # fab!(:user) { Fabricate(:user) }
14
+ #
15
+ # # good
16
+ # fab!(:user)
17
+ #
18
+ # When using custom attributes or the identifier doesn't match, the
19
+ # shorthand can't be used.
20
+ #
21
+ # @example
22
+ #
23
+ # # good
24
+ # fab!(:user) { Fabricate(:user, trust_level: TrustLevel[0]) }
25
+ #
26
+ # # good
27
+ # fab!(:another_user) { Fabricate(:user) }
28
+ class FabricatorShorthand < Base
29
+ def_node_matcher :offending_fabricator?, <<-MATCHER
30
+ (block
31
+ (send nil? :fab!
32
+ (sym $_identifier))
33
+ (args)
34
+ (send nil? :Fabricate
35
+ (sym $_identifier)))
36
+ MATCHER
37
+
38
+ def on_block(node)
39
+ offending_fabricator?(node) do |identifier|
40
+ add_offense(node, message: message(identifier))
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def message(identifier)
47
+ "Use the fabricator shorthand: `fab!(:#{identifier})`"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Plugin controllers must call `requires_plugin` to prevent routes from
8
+ # being accessible when the plugin is disabled.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # class MyController
13
+ # def my_action
14
+ # end
15
+ # end
16
+ #
17
+ # # good
18
+ # class MyController
19
+ # requires_plugin PLUGIN_NAME
20
+ # def my_action
21
+ # end
22
+ # end
23
+ #
24
+ class CallRequiresPlugin < Base
25
+ MSG =
26
+ "Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled."
27
+
28
+ def_node_matcher :requires_plugin_present?, <<~MATCHER
29
+ (class _ _
30
+ {
31
+ (begin <(send nil? :requires_plugin _) ...>)
32
+ <(send nil? :requires_plugin _) ...>
33
+ }
34
+ )
35
+ MATCHER
36
+
37
+ def on_class(node)
38
+ return if requires_plugin_present?(node)
39
+ return if requires_plugin_present_in_parent_classes(node)
40
+ add_offense(node, message: MSG)
41
+ end
42
+
43
+ private
44
+
45
+ def requires_plugin_present_in_parent_classes(node)
46
+ return unless processed_source.path
47
+ controller_path =
48
+ base_controller_path(node.parent_class&.const_name.to_s)
49
+ return unless controller_path
50
+ Commissioner
51
+ .new([self.class.new(config, @options)])
52
+ .investigate(parse(controller_path.read, controller_path.to_s))
53
+ .offenses
54
+ .empty?
55
+ end
56
+
57
+ def base_controller_path(base_class)
58
+ return if base_class.blank?
59
+ base_path = "#{base_class.underscore}.rb"
60
+ path = Pathname.new("#{processed_source.path}/../").cleanpath
61
+ until path.root?
62
+ controller_path = path.join(base_path)
63
+ return controller_path if controller_path.exist?
64
+ path = path.join("..").cleanpath
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Constants must be defined inside the plugin namespace (module or class).
8
+ #
9
+ # @example
10
+ # # bad
11
+ # MY_CONSTANT = :value
12
+ #
13
+ # # good
14
+ # module MyPlugin
15
+ # MY_CONSTANT = :value
16
+ # end
17
+ #
18
+ class NamespaceConstants < Base
19
+ MSG = "Don’t define constants outside a class or a module."
20
+
21
+ def on_casgn(node)
22
+ return if inside_namespace?(node)
23
+ add_offense(node, message: MSG)
24
+ end
25
+
26
+ private
27
+
28
+ def inside_namespace?(node)
29
+ node.each_ancestor.detect { _1.class_type? || _1.module_type? }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Methods must be defined inside the plugin namespace (module or class).
8
+ #
9
+ # @example
10
+ # # bad
11
+ # def my_method
12
+ # end
13
+ #
14
+ # # good
15
+ # module MyPlugin
16
+ # def my_method
17
+ # end
18
+ # end
19
+ #
20
+ class NamespaceMethods < Base
21
+ MSG = "Don’t define methods outside a class or a module."
22
+
23
+ def on_def(node)
24
+ return if inside_namespace?(node)
25
+ add_offense(node, message: MSG)
26
+ end
27
+
28
+ private
29
+
30
+ def inside_namespace?(node)
31
+ node.each_ancestor.detect { _1.class_type? || _1.module_type? }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Don’t monkey-patch classes directly in `plugin.rb`. Instead, define
8
+ # additional methods in a dedicated mixin (an ActiveSupport concern for
9
+ # example) and use `prepend` (this allows calling `super` from the mixin).
10
+ #
11
+ # If you’re just adding new methods to an existing serializer, then use
12
+ # `add_to_serializer` instead.
13
+ #
14
+ # @example generic monkey-patching
15
+ # # bad
16
+ # ::Topic.class_eval do
17
+ # has_many :new_items
18
+ #
19
+ # def new_method
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ # module MyPlugin::TopicExtension
25
+ # extend ActiveSupport::Concern
26
+ #
27
+ # prepended do
28
+ # has_many :new_items
29
+ # end
30
+ #
31
+ # def new_method
32
+ # end
33
+ # end
34
+ #
35
+ # reloadable_patch { ::Topic.prepend(MyPlugin::TopicExtension) }
36
+ #
37
+ # @example for serializers
38
+ # # bad
39
+ # UserSerializer.class_eval do
40
+ # def new_method
41
+ # do_processing
42
+ # end
43
+ # end
44
+ #
45
+ # # good
46
+ # add_to_serializer(:user, :new_method) { do_processing }
47
+ #
48
+ class NoMonkeyPatching < Base
49
+ MSG =
50
+ "Don’t reopen existing classes. Instead, create a mixin and use `prepend`."
51
+ MSG_CLASS_EVAL =
52
+ "Don’t call `class_eval`. Instead, create a mixin and use `prepend`."
53
+ MSG_CLASS_EVAL_SERIALIZERS =
54
+ "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`."
55
+ MSG_SERIALIZERS =
56
+ "Don’t reopen serializers. Instead, use `add_to_serializer`."
57
+ RESTRICT_ON_SEND = [:class_eval].freeze
58
+
59
+ def_node_matcher :existing_class?, <<~MATCHER
60
+ (class (const (cbase) _) ...)
61
+ MATCHER
62
+
63
+ def_node_matcher :serializer?, <<~MATCHER
64
+ ({class send} (const _ /Serializer$/) ...)
65
+ MATCHER
66
+
67
+ def on_send(node)
68
+ if serializer?(node)
69
+ return add_offense(node, message: MSG_CLASS_EVAL_SERIALIZERS)
70
+ end
71
+ add_offense(node, message: MSG_CLASS_EVAL)
72
+ end
73
+
74
+ def on_class(node)
75
+ return unless in_plugin_rb_file?
76
+ return unless existing_class?(node)
77
+ if serializer?(node)
78
+ return add_offense(node, message: MSG_SERIALIZERS)
79
+ end
80
+ add_offense(node, message: MSG)
81
+ end
82
+
83
+ private
84
+
85
+ def in_plugin_rb_file?
86
+ processed_source.path.split("/").last == "plugin.rb"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Using `DiscourseEvent.on` leaves the handler enabled when the plugin is disabled.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # DiscourseEvent.on(:event) { do_something }
12
+ #
13
+ # # good
14
+ # on(:event) { do_something }
15
+ #
16
+ class UsePluginInstanceOn < Base
17
+ MSG =
18
+ "Use `on` instead of `DiscourseEvent.on` as the latter will listen to events even if the plugin is disabled."
19
+ NOT_OUTSIDE_PLUGIN_RB =
20
+ "Don’t call `DiscourseEvent.on` outside `plugin.rb`."
21
+ RESTRICT_ON_SEND = [:on].freeze
22
+
23
+ def_node_matcher :discourse_event_on?, <<~MATCHER
24
+ (send (const nil? :DiscourseEvent) :on _)
25
+ MATCHER
26
+
27
+ def on_send(node)
28
+ return unless discourse_event_on?(node)
29
+ return add_offense(node, message: MSG) if in_plugin_rb_file?
30
+ add_offense(node, message: NOT_OUTSIDE_PLUGIN_RB)
31
+ end
32
+
33
+ private
34
+
35
+ def in_plugin_rb_file?
36
+ processed_source.path.split("/").last == "plugin.rb"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Plugins
7
+ # Use `require_relative` to load dependencies.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # load File.expand_path("../lib/my_file.rb", __FILE__)
12
+ #
13
+ # # good
14
+ # require_relative "lib/my_file"
15
+ #
16
+ class UseRequireRelative < Base
17
+ MSG = "Use `require_relative` instead of `load`."
18
+ RESTRICT_ON_SEND = [:load].freeze
19
+
20
+ def_node_matcher :load_called?, <<~MATCHER
21
+ (send nil? :load _)
22
+ MATCHER
23
+
24
+ def on_send(node)
25
+ return unless load_called?(node)
26
+ add_offense(node, message: MSG)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- path = File.join(__dir__, "discourse", "*.rb")
3
+ path = File.join(__dir__, "discourse", "**/*.rb")
4
4
  Dir[path].each { |file| require file }
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rubocop"
4
+ require "active_support"
5
+ require "active_support/core_ext/string/inflections"
4
6
  require_relative "rubocop/discourse"
5
7
  require_relative "rubocop/discourse/inject"
6
8
 
@@ -0,0 +1,5 @@
1
+ require:
2
+ - rubocop-capybara
3
+
4
+ Capybara/CurrentPathExpectation:
5
+ Enabled: true
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "rubocop-discourse"
5
- s.version = "3.6.1"
5
+ s.version = "3.7.0"
6
6
  s.summary = "Custom rubocop cops used by Discourse"
7
7
  s.authors = ["Discourse Team"]
8
8
  s.license = "MIT"
@@ -11,8 +11,11 @@ Gem::Specification.new do |s|
11
11
  s.files = `git ls-files`.split($/)
12
12
  s.require_paths = ["lib"]
13
13
 
14
+ s.add_runtime_dependency "activesupport", ">= 6.1"
14
15
  s.add_runtime_dependency "rubocop", ">= 1.59.0"
15
16
  s.add_runtime_dependency "rubocop-rspec", ">= 2.25.0"
17
+ s.add_runtime_dependency "rubocop-factory_bot", ">= 2.0.0"
18
+ s.add_runtime_dependency "rubocop-capybara", ">= 2.0.0"
16
19
 
17
20
  s.add_development_dependency "rake", "~> 13.1.0"
18
21
  s.add_development_dependency "rspec", "~> 3.12.0"
@@ -0,0 +1,11 @@
1
+ require:
2
+ - rubocop-factory_bot
3
+
4
+ FactoryBot/AttributeDefinedStatically:
5
+ Enabled: true
6
+
7
+ FactoryBot/CreateList:
8
+ Enabled: true
9
+
10
+ FactoryBot/FactoryClassName:
11
+ Enabled: true
data/rubocop-rspec.yml CHANGED
@@ -223,23 +223,8 @@ RSpec/VoidExpect:
223
223
  RSpec/Yield:
224
224
  Enabled: true
225
225
 
226
- Capybara/CurrentPathExpectation:
227
- Enabled: true
228
-
229
226
  RSpec/Capybara/FeatureMethods:
230
227
  Enabled: true
231
228
 
232
- Capybara/VisibilityMatcher:
233
- Enabled: true
234
-
235
- FactoryBot/AttributeDefinedStatically:
236
- Enabled: true
237
-
238
- FactoryBot/CreateList:
239
- Enabled: true
240
-
241
- FactoryBot/FactoryClassName:
242
- Enabled: true
243
-
244
229
  RSpec/Rails/HttpStatus:
245
230
  Enabled: true
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BadController < NoRequiresPluginController
4
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled.
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BaseController
4
+ class << self
5
+ def requires_plugin(*)
6
+ end
7
+
8
+ def requires_login
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GoodController < RequiresPluginController
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InheritFromOutsideController < ApplicationController
4
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled.
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InheritFromOutsideController < MyPlugin::ApplicationController
4
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled.
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NoRequiresPluginController < BaseController
4
+ requires_login
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RequiresPluginController < BaseController
4
+ requires_plugin "my_plugin"
5
+ requires_login
6
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe RuboCop::Cop::Discourse::FabricatorShorthand, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ it "registers an offense when not using the fabricator shorthand" do
11
+ expect_offense(<<~RUBY)
12
+ RSpec.describe "Foo" do
13
+ fab!(:foo) { Fabricate(:foo) }
14
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/FabricatorShorthand: Use the fabricator shorthand: `fab!(:foo)`
15
+ end
16
+ RUBY
17
+ end
18
+
19
+ it "does not register an offense when the fabricator has attributes" do
20
+ expect_no_offenses(<<~RUBY)
21
+ RSpec.describe "Foo" do
22
+ fab!(:foo) { Fabricate(:foo, bar: 1) }
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ it "does not register an offense when the identifier doesn't match" do
28
+ expect_no_offenses(<<~RUBY)
29
+ RSpec.describe "Foo" do
30
+ fab!(:bar) { Fabricate(:foo) }
31
+ end
32
+ RUBY
33
+ end
34
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::CallRequiresPlugin, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when `requires_plugin` is missing" do
11
+ it "registers an offense" do
12
+ expect_offense(<<~RUBY)
13
+ class MyController < ApplicationController
14
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/CallRequiresPlugin: Use `requires_plugin` in controllers to prevent routes from being accessible when plugin is disabled.
15
+ requires_login
16
+ end
17
+ RUBY
18
+ end
19
+ end
20
+
21
+ context "when `requires_plugin` is not missing" do
22
+ it "does not register an offense" do
23
+ expect_no_offenses(<<~RUBY)
24
+ class MyController
25
+ requires_plugin MyPlugin::PLUGIN_NAME
26
+ requires_login
27
+ end
28
+ RUBY
29
+ end
30
+ end
31
+
32
+ context "when inheriting" do
33
+ let(:controllers_path) do
34
+ Pathname.new("#{__dir__}/../../../../fixtures/controllers").cleanpath
35
+ end
36
+
37
+ before do
38
+ # As we’re providing real files, we need to get rid of the default config
39
+ # restricting the cop to `app/controllers/*`
40
+ configuration.for_cop(cop).delete("Include")
41
+ end
42
+
43
+ context "when `requires_plugin` is called in a parent controller" do
44
+ let(:good_controller) { controllers_path.join("good_controller.rb") }
45
+
46
+ it "does not register an offense" do
47
+ expect_no_offenses(good_controller.read, good_controller.to_s)
48
+ end
49
+ end
50
+
51
+ context "when `requires_plugin` is not called in a parent controller" do
52
+ let(:bad_controller) { controllers_path.join("bad_controller.rb") }
53
+
54
+ it "registers an offense" do
55
+ expect_offense(bad_controller.read, bad_controller.to_s)
56
+ end
57
+ end
58
+
59
+ context "when parent controller can’t be located" do
60
+ context "when parent controller is namespaced" do
61
+ let(:controller) do
62
+ controllers_path.join("namespaced_parent_controller.rb")
63
+ end
64
+
65
+ it "registers an offense" do
66
+ expect_offense(controller.read, controller.to_s)
67
+ end
68
+ end
69
+
70
+ context "when parent controller is not namespaced" do
71
+ let(:controller) do
72
+ controllers_path.join("inherit_from_outside_controller.rb")
73
+ end
74
+
75
+ it "registers an offense" do
76
+ expect_offense(controller.read, controller.to_s)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::NamespaceConstants, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when defining a constant outside any namespace" do
11
+ it "registers an offense" do
12
+ expect_offense(<<~RUBY)
13
+ MY_CONSTANT = "my_value"
14
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NamespaceConstants: Don’t define constants outside a class or a module.
15
+
16
+ class MyClass
17
+ MY_CONSTANT = "my_value"
18
+ end
19
+ RUBY
20
+ end
21
+ end
22
+
23
+ context "when defining a constant inside a class" do
24
+ it "does not register an offense" do
25
+ expect_no_offenses(<<~RUBY)
26
+ class MyClass
27
+ MY_CONSTANT = "my_value"
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+
33
+ context "when defining a constant inside a module" do
34
+ it "does not register an offense" do
35
+ expect_no_offenses(<<~RUBY)
36
+ module MyModule
37
+ MY_CONSTANT = "my_value"
38
+ end
39
+ RUBY
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::NamespaceMethods, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when defining a method outside any namespace" do
11
+ it "registers an offense" do
12
+ expect_offense(<<~RUBY)
13
+ def my_method
14
+ ^^^^^^^^^^^^^ Discourse/Plugins/NamespaceMethods: Don’t define methods outside a class or a module.
15
+ "my_value"
16
+ end
17
+
18
+ class MyClass
19
+ def my_method
20
+ "my_method"
21
+ end
22
+ end
23
+ RUBY
24
+ end
25
+ end
26
+
27
+ context "when defining a method inside a class" do
28
+ context "when defining an instance method" do
29
+ it "does not register an offense" do
30
+ expect_no_offenses(<<~RUBY)
31
+ class MyClass
32
+ def my_method
33
+ "my_value"
34
+ end
35
+ end
36
+ RUBY
37
+ end
38
+ end
39
+
40
+ context "when defining a class method" do
41
+ it "does not register an offense" do
42
+ expect_no_offenses(<<~RUBY)
43
+ class MyClass
44
+ class << self
45
+ def my_method
46
+ "my_value"
47
+ end
48
+
49
+ def another_method
50
+ "plop"
51
+ end
52
+ end
53
+ end
54
+ RUBY
55
+ end
56
+ end
57
+ end
58
+
59
+ context "when defining a method inside a module" do
60
+ context "when defining an instance method" do
61
+ it "does not register an offense" do
62
+ expect_no_offenses(<<~RUBY)
63
+ module MyModule
64
+ def my_method
65
+ "my_value"
66
+ end
67
+ end
68
+ RUBY
69
+ end
70
+ end
71
+
72
+ context "when defining a class method" do
73
+ it "does not register an offense" do
74
+ expect_no_offenses(<<~RUBY)
75
+ module MyModule
76
+ class << self
77
+ def my_method
78
+ "my_value"
79
+ end
80
+ end
81
+ end
82
+ RUBY
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::NoMonkeyPatching, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when outside `plugin.rb`" do
11
+ it "does not register an offense" do
12
+ expect_no_offenses(<<~RUBY, "my_class.rb")
13
+ class ::MyClass
14
+ def my_method
15
+ "my_value"
16
+ end
17
+ end
18
+
19
+ class AnotherClass
20
+ def my_method
21
+ "my_value"
22
+ end
23
+ end
24
+ RUBY
25
+ end
26
+ end
27
+
28
+ context "when inside `plugin.rb`" do
29
+ context "when opening an existing class" do
30
+ it "registers an offense" do
31
+ expect_offense(<<~RUBY, "plugin.rb")
32
+ after_initialize do
33
+ module MyPlugin
34
+ class Engine < Rails::Engine
35
+ isolate_namespace MyPlugin
36
+ end
37
+ end
38
+
39
+ class ::Topic
40
+ ^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t reopen existing classes. [...]
41
+ def self.new_method
42
+ :new_value
43
+ end
44
+
45
+ def my_new_method
46
+ "my_new_value"
47
+ end
48
+ end
49
+ end
50
+ RUBY
51
+ end
52
+ end
53
+
54
+ context "when opening an existing serializer" do
55
+ it "registers an offense" do
56
+ expect_offense(<<~RUBY, "plugin.rb")
57
+ class ::TopicSerializer
58
+ ^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t reopen serializers. [...]
59
+ def new_attribute
60
+ "my_attribute"
61
+ end
62
+ end
63
+ RUBY
64
+ end
65
+ end
66
+
67
+ context "when calling `.class_eval` on a class" do
68
+ it "registers an offense" do
69
+ expect_offense(<<~RUBY)
70
+ User.class_eval do
71
+ ^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t call `class_eval`. [...]
72
+ def a_new_method
73
+ :new_value
74
+ end
75
+ end
76
+ RUBY
77
+ end
78
+ end
79
+
80
+ context "when calling `.class_eval` on a serializer" do
81
+ it "registers an offense" do
82
+ expect_offense(<<~RUBY)
83
+ UserSerializer.class_eval do
84
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/NoMonkeyPatching: Don’t call `class_eval` on a serializer. [...]
85
+ def a_new_method
86
+ :new_value
87
+ end
88
+ end
89
+ RUBY
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::UsePluginInstanceOn, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when outside `plugin.rb`" do
11
+ context "when `DiscourseEvent.on` is called" do
12
+ it "registers an offense" do
13
+ expect_offense(<<~RUBY, "another_file.rb")
14
+ DiscourseEvent.on(:topic_status_updated) { do_something }
15
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UsePluginInstanceOn: Don’t call `DiscourseEvent.on` outside `plugin.rb`.
16
+ RUBY
17
+ end
18
+ end
19
+ end
20
+
21
+ context "when inside `plugin.rb`" do
22
+ context "when `DiscourseEvent.on` is called" do
23
+ it "registers an offense" do
24
+ expect_offense(<<~RUBY, "plugin.rb")
25
+ DiscourseEvent.on(:topic_status_updated) { do_something }
26
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UsePluginInstanceOn: Use `on` instead of `DiscourseEvent.on` [...]
27
+ RUBY
28
+ end
29
+ end
30
+
31
+ context "when `on` is called" do
32
+ it "does not register an offense" do
33
+ expect_no_offenses(<<~RUBY, "plugin.rb")
34
+ on(:topic_status_updated) { do_something }
35
+ RUBY
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe RuboCop::Cop::Discourse::Plugins::UseRequireRelative, :config do
6
+ subject(:cop) { described_class.new(config) }
7
+
8
+ let(:config) { RuboCop::Config.new }
9
+
10
+ context "when using `load`" do
11
+ it "registers an offense" do
12
+ expect_offense(<<~RUBY)
13
+ load File.expand_path("../app/jobs/onceoff/voting_ensure_consistency.rb", __FILE__)
14
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Discourse/Plugins/UseRequireRelative: Use `require_relative` instead of `load`.
15
+ RUBY
16
+ end
17
+ end
18
+
19
+ context "when using `require_relative`" do
20
+ it "does not register an offense" do
21
+ expect_no_offenses(<<~RUBY)
22
+ require_relative "app/controllers/encrypt_controller.rb"
23
+ RUBY
24
+ end
25
+ end
26
+ end
data/stree-compat.yml CHANGED
@@ -3,10 +3,13 @@ require:
3
3
 
4
4
  inherit_from:
5
5
  - ./rubocop-core.yml
6
+ - ./rubocop-capybara.yml
7
+ - ./rubocop-factory_bot.yml
6
8
  - ./rubocop-rspec.yml
7
9
 
8
10
  AllCops:
9
11
  TargetRubyVersion: 3.2
12
+ SuggestExtensions: false
10
13
  DisabledByDefault: true
11
14
  Exclude:
12
15
  - 'db/schema.rb'
@@ -19,3 +22,8 @@ AllCops:
19
22
 
20
23
  Discourse:
21
24
  Enabled: true
25
+
26
+ Discourse/FabricatorShorthand:
27
+ Enabled: true
28
+ Include:
29
+ - 'spec/**/*_spec.rb'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-discourse
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-01 00:00:00.000000000 Z
11
+ date: 2024-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rubocop
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,34 @@ dependencies:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: 2.25.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-factory_bot
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-capybara
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.0.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 2.0.0
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: rake
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -82,6 +124,7 @@ files:
82
124
  - config/default.yml
83
125
  - default.yml
84
126
  - lib/rubocop-discourse.rb
127
+ - lib/rubocop/cop/discourse/fabricator_shorthand.rb
85
128
  - lib/rubocop/cop/discourse/no_add_reference_active_record_migrations.rb
86
129
  - lib/rubocop/cop/discourse/no_chdir.rb
87
130
  - lib/rubocop/cop/discourse/no_direct_multisite_manipulation.rb
@@ -93,19 +136,41 @@ files:
93
136
  - lib/rubocop/cop/discourse/no_time_new_without_args.rb
94
137
  - lib/rubocop/cop/discourse/no_uri_escape_encode.rb
95
138
  - lib/rubocop/cop/discourse/only_top_level_multisite_specs.rb
139
+ - lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb
140
+ - lib/rubocop/cop/discourse/plugins/namespace_constants.rb
141
+ - lib/rubocop/cop/discourse/plugins/namespace_methods.rb
142
+ - lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb
143
+ - lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb
144
+ - lib/rubocop/cop/discourse/plugins/use_require_relative.rb
96
145
  - lib/rubocop/cop/discourse/time_eq_matcher.rb
97
146
  - lib/rubocop/cop/discourse_cops.rb
98
147
  - lib/rubocop/discourse.rb
99
148
  - lib/rubocop/discourse/inject.rb
149
+ - rubocop-capybara.yml
100
150
  - rubocop-core.yml
101
151
  - rubocop-discourse.gemspec
152
+ - rubocop-factory_bot.yml
102
153
  - rubocop-layout.yml
103
154
  - rubocop-rspec.yml
155
+ - spec/fixtures/controllers/bad_controller.rb
156
+ - spec/fixtures/controllers/base_controller.rb
157
+ - spec/fixtures/controllers/good_controller.rb
158
+ - spec/fixtures/controllers/inherit_from_outside_controller.rb
159
+ - spec/fixtures/controllers/namespaced_parent_controller.rb
160
+ - spec/fixtures/controllers/no_requires_plugin_controller.rb
161
+ - spec/fixtures/controllers/requires_plugin_controller.rb
162
+ - spec/lib/rubocop/cop/fabricator_shorthand_spec.rb
104
163
  - spec/lib/rubocop/cop/no_add_reference_active_record_migrations_spec.rb
105
164
  - spec/lib/rubocop/cop/no_mixing_multisite_and_standard_specs_spec.rb
106
165
  - spec/lib/rubocop/cop/no_mocking_jobs_enqueue_spec.rb
107
166
  - spec/lib/rubocop/cop/no_reset_column_information_migrations_spec.rb
108
167
  - spec/lib/rubocop/cop/only_top_level_multisite_specs_spec.rb
168
+ - spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb
169
+ - spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb
170
+ - spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb
171
+ - spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb
172
+ - spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb
173
+ - spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb
109
174
  - spec/lib/rubocop/cop/time_eq_matcher_spec.rb
110
175
  - spec/spec_helper.rb
111
176
  - stree-compat.yml