rubocop-discourse 3.1.0 → 3.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/config/default.yml +51 -24
- data/lib/rubocop/cop/discourse/fabricator_shorthand.rb +62 -0
- data/lib/rubocop/cop/discourse/no_add_reference_active_record_migrations.rb +9 -7
- data/lib/rubocop/cop/discourse/no_chdir.rb +1 -1
- data/lib/rubocop/cop/discourse/no_direct_multisite_manipulation.rb +3 -2
- data/lib/rubocop/cop/discourse/no_json_parse_response.rb +2 -4
- data/lib/rubocop/cop/discourse/no_mixing_multisite_and_standard_specs.rb +4 -7
- data/lib/rubocop/cop/discourse/no_mocking_jobs.rb +3 -2
- data/lib/rubocop/cop/discourse/no_nokogiri_html_fragment.rb +1 -1
- data/lib/rubocop/cop/discourse/no_reset_column_information_in_migrations.rb +6 -5
- data/lib/rubocop/cop/discourse/no_time_new_without_args.rb +2 -4
- data/lib/rubocop/cop/discourse/no_uri_escape_encode.rb +11 -8
- data/lib/rubocop/cop/discourse/only_top_level_multisite_specs.rb +2 -6
- data/lib/rubocop/cop/discourse/plugins/call_requires_plugin.rb +71 -0
- data/lib/rubocop/cop/discourse/plugins/namespace_constants.rb +35 -0
- data/lib/rubocop/cop/discourse/plugins/namespace_methods.rb +37 -0
- data/lib/rubocop/cop/discourse/plugins/no_monkey_patching.rb +92 -0
- data/lib/rubocop/cop/discourse/plugins/use_plugin_instance_on.rb +42 -0
- data/lib/rubocop/cop/discourse/plugins/use_require_relative.rb +32 -0
- data/lib/rubocop/cop/discourse/services/empty_lines_around_blocks.rb +114 -0
- data/lib/rubocop/cop/discourse/services/group_keywords.rb +92 -0
- data/lib/rubocop/cop/discourse/time_eq_matcher.rb +2 -4
- data/lib/rubocop/cop/discourse_cops.rb +1 -1
- data/lib/rubocop/discourse.rb +3 -3
- data/lib/rubocop-discourse.rb +4 -0
- data/rubocop-capybara.yml +5 -0
- data/rubocop-core.yml +81 -4
- data/rubocop-discourse.gemspec +15 -10
- data/rubocop-factory_bot.yml +11 -0
- data/rubocop-layout.yml +4 -0
- data/rubocop-rails.yml +14 -0
- data/rubocop-rspec.yml +29 -23
- data/spec/fixtures/controllers/bad_controller.rb +5 -0
- data/spec/fixtures/controllers/base_controller.rb +11 -0
- data/spec/fixtures/controllers/good_controller.rb +4 -0
- data/spec/fixtures/controllers/inherit_from_outside_controller.rb +5 -0
- data/spec/fixtures/controllers/namespaced_parent_controller.rb +5 -0
- data/spec/fixtures/controllers/no_requires_plugin_controller.rb +5 -0
- data/spec/fixtures/controllers/requires_plugin_controller.rb +6 -0
- data/spec/lib/rubocop/cop/discourse/services/empty_lines_around_blocks_spec.rb +309 -0
- data/spec/lib/rubocop/cop/discourse/services/group_keywords_spec.rb +137 -0
- data/spec/lib/rubocop/cop/fabricator_shorthand_spec.rb +47 -0
- data/spec/lib/rubocop/cop/no_add_reference_active_record_migrations_spec.rb +13 -16
- data/spec/lib/rubocop/cop/no_mixing_multisite_and_standard_specs_spec.rb +10 -14
- data/spec/lib/rubocop/cop/no_mocking_jobs_enqueue_spec.rb +8 -12
- data/spec/lib/rubocop/cop/no_reset_column_information_migrations_spec.rb +8 -10
- data/spec/lib/rubocop/cop/only_top_level_multisite_specs_spec.rb +14 -18
- data/spec/lib/rubocop/cop/plugins/call_requires_plugin_spec.rb +79 -0
- data/spec/lib/rubocop/cop/plugins/namespace_constants_spec.rb +40 -0
- data/spec/lib/rubocop/cop/plugins/namespace_methods_spec.rb +84 -0
- data/spec/lib/rubocop/cop/plugins/no_monkey_patching_spec.rb +91 -0
- data/spec/lib/rubocop/cop/plugins/use_plugin_instance_on_spec.rb +37 -0
- data/spec/lib/rubocop/cop/plugins/use_require_relative_spec.rb +24 -0
- data/spec/lib/rubocop/cop/time_eq_matcher_spec.rb +6 -10
- data/spec/spec_helper.rb +4 -4
- data/stree-compat.yml +10 -1
- metadata +110 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4003717b78bca02f538f48bc23fdd1b0e913bb6816c5d6a1e87401c53c167a14
|
4
|
+
data.tar.gz: acba524c9e7e399a5b69974ce94dfefa33fbd61c5c2197ecb425b1ed22418e23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fd47066d7d8066da4adb2f17d95de3420fbaae18599d07925671a28c58a95a2d39b4ba8319813a0daf2106e7ecce4407a99470a93f2d99f335147bad3142269
|
7
|
+
data.tar.gz: 311df91f7d4cf9b5e296d3dcbf625482a0fdea6ac1da33fe389b7db41ec0339b4918434ce97a13c023ba86e3a54eb81edcb512aba7cb9abd0a21b19bb5b5527d
|
data/.github/workflows/ci.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
- name: Setup ruby
|
17
17
|
uses: ruby/setup-ruby@v1
|
18
18
|
with:
|
19
|
-
ruby-version:
|
19
|
+
ruby-version: "3.3"
|
20
20
|
bundler-cache: true
|
21
21
|
|
22
22
|
- name: Rubocop
|
@@ -34,7 +34,7 @@ jobs:
|
|
34
34
|
- uses: actions/checkout@v3
|
35
35
|
|
36
36
|
- name: Release Gem
|
37
|
-
uses: discourse/publish-rubygems-action@
|
37
|
+
uses: discourse/publish-rubygems-action@v3
|
38
38
|
env:
|
39
39
|
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
40
40
|
GIT_EMAIL: team@discourse.org
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require 'spec_helper'
|
data/.rubocop.yml
CHANGED
data/config/default.yml
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
Discourse/NoChdir:
|
2
2
|
Enabled: true
|
3
3
|
Exclude:
|
4
|
-
-
|
5
|
-
- 'plugins/*/spec/**/*'
|
4
|
+
- "**/spec/**/*" # Specs are run sequentially, so chdir can be used
|
6
5
|
|
7
6
|
Discourse/NoTimeNewWithoutArgs:
|
8
7
|
Enabled: true
|
@@ -13,8 +12,8 @@ Discourse/NoURIEscapeEncode:
|
|
13
12
|
Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
|
14
13
|
Enabled: true
|
15
14
|
Include:
|
16
|
-
|
17
|
-
|
15
|
+
- "**/db/migrate/*"
|
16
|
+
- "**/db/post_migrate/*"
|
18
17
|
|
19
18
|
Discourse/NoNokogiriHtmlFragment:
|
20
19
|
Enabled: true
|
@@ -22,43 +21,71 @@ Discourse/NoNokogiriHtmlFragment:
|
|
22
21
|
Discourse/NoResetColumnInformationInMigrations:
|
23
22
|
Enabled: false
|
24
23
|
Include:
|
25
|
-
|
26
|
-
|
24
|
+
- "**/db/migrate/*"
|
25
|
+
- "**/db/post_migrate/*"
|
27
26
|
|
28
27
|
# Specs
|
29
28
|
|
30
29
|
Discourse/NoDirectMultisiteManipulation:
|
31
30
|
Enabled: true
|
32
|
-
|
33
|
-
|
34
|
-
- "(?:^|/)spec/"
|
31
|
+
Include:
|
32
|
+
- "**/spec/**/*"
|
35
33
|
|
36
34
|
Discourse/TimeEqMatcher:
|
37
35
|
Enabled: true
|
38
|
-
|
39
|
-
|
40
|
-
- "(?:^|/)spec/"
|
36
|
+
Include:
|
37
|
+
- "**/spec/**/*"
|
41
38
|
|
42
39
|
Discourse/NoJsonParseResponse:
|
43
40
|
Enabled: false
|
44
|
-
|
45
|
-
|
46
|
-
- "(?:^|/)spec/"
|
41
|
+
Include:
|
42
|
+
- "**/spec/**/*"
|
47
43
|
|
48
44
|
Discourse/NoMockingJobs:
|
49
45
|
Enabled: true
|
50
|
-
|
51
|
-
|
52
|
-
- "(?:^|/)spec/"
|
46
|
+
Include:
|
47
|
+
- "**/spec/**/*"
|
53
48
|
|
54
49
|
Discourse/OnlyTopLevelMultisiteSpecs:
|
55
50
|
Enabled: true
|
56
|
-
|
57
|
-
|
58
|
-
- "(?:^|/)spec/"
|
51
|
+
Include:
|
52
|
+
- "**/spec/**/*"
|
59
53
|
|
60
54
|
Discourse/NoMixingMultisiteAndStandardSpecs:
|
61
55
|
Enabled: true
|
62
|
-
|
63
|
-
|
64
|
-
|
56
|
+
Include:
|
57
|
+
- "**/spec/**/*"
|
58
|
+
|
59
|
+
Discourse/Plugins/CallRequiresPlugin:
|
60
|
+
Enabled: true
|
61
|
+
Include:
|
62
|
+
- "app/controllers/**/*"
|
63
|
+
|
64
|
+
Discourse/Plugins/UsePluginInstanceOn:
|
65
|
+
Enabled: true
|
66
|
+
|
67
|
+
Discourse/Plugins/NamespaceMethods:
|
68
|
+
Enabled: true
|
69
|
+
Exclude:
|
70
|
+
- "**/spec/**/*"
|
71
|
+
- "**/tasks/**/*.rake"
|
72
|
+
- "**/db/fixtures/**/*"
|
73
|
+
|
74
|
+
Discourse/Plugins/NamespaceConstants:
|
75
|
+
Enabled: true
|
76
|
+
Exclude:
|
77
|
+
- "**/spec/**/*"
|
78
|
+
- "**/tasks/**/*.rake"
|
79
|
+
- "**/db/fixtures/**/*"
|
80
|
+
|
81
|
+
Discourse/Plugins/UseRequireRelative:
|
82
|
+
Enabled: true
|
83
|
+
|
84
|
+
Discourse/Plugins/NoMonkeyPatching:
|
85
|
+
Enabled: true
|
86
|
+
|
87
|
+
Discourse/Services/GroupKeywords:
|
88
|
+
Enabled: true
|
89
|
+
|
90
|
+
Discourse/Services/EmptyLinesAroundBlocks:
|
91
|
+
Enabled: true
|
@@ -0,0 +1,62 @@
|
|
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
|
+
extend AutoCorrector
|
30
|
+
include IgnoredNode
|
31
|
+
|
32
|
+
def_node_matcher :offending_fabricator?, <<-MATCHER
|
33
|
+
(block
|
34
|
+
$(send nil? :fab!
|
35
|
+
(sym $_identifier))
|
36
|
+
(args)
|
37
|
+
(send nil? :Fabricate
|
38
|
+
(sym $_identifier)))
|
39
|
+
MATCHER
|
40
|
+
|
41
|
+
def on_block(node)
|
42
|
+
offending_fabricator?(node) do |expression, _identifier|
|
43
|
+
add_offense(
|
44
|
+
node,
|
45
|
+
message: message(expression.source)
|
46
|
+
) do |corrector|
|
47
|
+
next if part_of_ignored_node?(node)
|
48
|
+
corrector.replace(node, expression.source)
|
49
|
+
end
|
50
|
+
ignore_node(node)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def message(expression)
|
57
|
+
"Use the fabricator shorthand: `#{expression}`"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -44,7 +44,7 @@ module RuboCop
|
|
44
44
|
# index_posts_on_image_upload_id ON posts USING btree (image_upload_id)
|
45
45
|
# SQL
|
46
46
|
# end
|
47
|
-
class NoAddReferenceOrAliasesActiveRecordMigration <
|
47
|
+
class NoAddReferenceOrAliasesActiveRecordMigration < Base
|
48
48
|
MSG = <<~MSG
|
49
49
|
AR methods add_reference, add_belongs_to, t.references, and t.belongs_to are
|
50
50
|
high-risk for large tables and have too many background magic operations.
|
@@ -70,12 +70,14 @@ module RuboCop
|
|
70
70
|
MATCHER
|
71
71
|
|
72
72
|
def on_send(node)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
73
|
+
if [
|
74
|
+
using_add_reference?(node),
|
75
|
+
using_add_belongs_to?(node),
|
76
|
+
using_t_references?(node),
|
77
|
+
using_t_belongs_to?(node),
|
78
|
+
].none?
|
79
|
+
return
|
80
|
+
end
|
79
81
|
add_offense(node, message: MSG)
|
80
82
|
end
|
81
83
|
end
|
@@ -16,8 +16,9 @@ module RuboCop
|
|
16
16
|
# it "works", type: :multisite do
|
17
17
|
# do_something
|
18
18
|
# end
|
19
|
-
class NoDirectMultisiteManipulation <
|
20
|
-
MSG =
|
19
|
+
class NoDirectMultisiteManipulation < Base
|
20
|
+
MSG =
|
21
|
+
"Use `type: :multisite` example setting instead of modifying `Rails.configuration.multisite`."
|
21
22
|
|
22
23
|
def_node_matcher :multisite_setter?, <<-MATCHER
|
23
24
|
(send
|
@@ -11,7 +11,7 @@ module RuboCop
|
|
11
11
|
#
|
12
12
|
# # good
|
13
13
|
# expect(response.parsed_body).to eq({})
|
14
|
-
class NoJsonParseResponse <
|
14
|
+
class NoJsonParseResponse < Base
|
15
15
|
MSG = "Use `response.parsed_body` instead of `JSON.parse(response.body)` in specs."
|
16
16
|
|
17
17
|
def_node_matcher :json_parse_body?, <<-MATCHER
|
@@ -28,9 +28,7 @@ module RuboCop
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def autocorrect(node)
|
31
|
-
lambda
|
32
|
-
corrector.replace(node.loc.expression, "response.parsed_body")
|
33
|
-
end
|
31
|
+
lambda { |corrector| corrector.replace(node.loc.expression, "response.parsed_body") }
|
34
32
|
end
|
35
33
|
end
|
36
34
|
end
|
@@ -23,8 +23,9 @@ module RuboCop
|
|
23
23
|
# # x_multisite_spec.rb
|
24
24
|
# describe "x", type: :multisite do
|
25
25
|
# end
|
26
|
-
class NoMixingMultisiteAndStandardSpecs <
|
27
|
-
MSG =
|
26
|
+
class NoMixingMultisiteAndStandardSpecs < Base
|
27
|
+
MSG =
|
28
|
+
"Do not mix multisite and standard specs. Consider moving multisite describes to a separate file."
|
28
29
|
|
29
30
|
def initialize(config = nil, options = nil)
|
30
31
|
super
|
@@ -64,11 +65,7 @@ module RuboCop
|
|
64
65
|
MATCHER
|
65
66
|
|
66
67
|
def top_level?(node)
|
67
|
-
|
68
|
-
node.parent.root?
|
69
|
-
else
|
70
|
-
node.root?
|
71
|
-
end
|
68
|
+
node.parent&.begin_type? ? node.parent.root? : node.root?
|
72
69
|
end
|
73
70
|
end
|
74
71
|
end
|
@@ -3,8 +3,9 @@
|
|
3
3
|
module RuboCop
|
4
4
|
module Cop
|
5
5
|
module Discourse
|
6
|
-
class NoMockingJobs <
|
7
|
-
MSG =
|
6
|
+
class NoMockingJobs < Base
|
7
|
+
MSG =
|
8
|
+
"Use the test helpers provided by Sidekiq instead of mocking `Jobs.expects(:enqueue)`."
|
8
9
|
|
9
10
|
def_node_matcher :mocking_jobs_enqueue?, <<~MATCHER
|
10
11
|
(send (const nil? :Jobs) :expects (:sym :enqueue))
|
@@ -12,7 +12,7 @@ module RuboCop
|
|
12
12
|
#
|
13
13
|
# # good
|
14
14
|
# Nokogiri::HTML5.fragment("<p>test</p>")
|
15
|
-
class NoNokogiriHtmlFragment <
|
15
|
+
class NoNokogiriHtmlFragment < Base
|
16
16
|
MSG = "Nokogiri::HTML.fragment is deprecated and should not be used."
|
17
17
|
|
18
18
|
def_node_matcher :using_nokogiri_html_fragment?, <<-MATCHER
|
@@ -7,11 +7,12 @@ module RuboCop
|
|
7
7
|
# migrations. The method is not thread safe and we run migrations
|
8
8
|
# concurrently for multisites. Also, we don't encourage the use of
|
9
9
|
# ActiveRecord methods in migrations and prefer to write SQL directly.
|
10
|
-
class NoResetColumnInformationInMigrations <
|
11
|
-
MSG =
|
12
|
-
"
|
13
|
-
|
14
|
-
|
10
|
+
class NoResetColumnInformationInMigrations < Base
|
11
|
+
MSG =
|
12
|
+
"ActiveRecord::ModelSchema.reset_column_information is not thread-safe " \
|
13
|
+
"and we run migrations concurrently on multisite clusters. Using this " \
|
14
|
+
"method also means ActiveRecord methods are being used in migration " \
|
15
|
+
"which is discouraged at Discourse. Instead, you should write SQL in your migrations instead."
|
15
16
|
|
16
17
|
def on_send(node)
|
17
18
|
return if node.method_name != :reset_column_information
|
@@ -11,7 +11,7 @@ module RuboCop
|
|
11
11
|
#
|
12
12
|
# # good
|
13
13
|
# now = Time.zone.now
|
14
|
-
class NoTimeNewWithoutArgs <
|
14
|
+
class NoTimeNewWithoutArgs < Base
|
15
15
|
MSG = "Use `Time.zone.now` instead of `Time.new` without arguments."
|
16
16
|
|
17
17
|
def_node_matcher :time_new_without_args?, <<-MATCHER
|
@@ -25,9 +25,7 @@ module RuboCop
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def autocorrect(node)
|
28
|
-
lambda
|
29
|
-
corrector.replace(node.loc.expression, "Time.zone.now")
|
30
|
-
end
|
28
|
+
lambda { |corrector| corrector.replace(node.loc.expression, "Time.zone.now") }
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -15,8 +15,9 @@ module RuboCop
|
|
15
15
|
# # good
|
16
16
|
# UrlHelper.encode("https://a%20a.com?a='a%22")
|
17
17
|
# Addressable::URI.encode("https://a%20a.com?a='a%22")
|
18
|
-
class NoURIEscapeEncode <
|
19
|
-
MSG =
|
18
|
+
class NoURIEscapeEncode < Base
|
19
|
+
MSG =
|
20
|
+
"URI.escape, URI.encode, URI.unescape, URI.decode are deprecated and should not be used."
|
20
21
|
|
21
22
|
def_node_matcher :using_uri_escape?, <<-MATCHER
|
22
23
|
(send (const nil? :URI) :escape ...)
|
@@ -35,12 +36,14 @@ module RuboCop
|
|
35
36
|
MATCHER
|
36
37
|
|
37
38
|
def on_send(node)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
if [
|
40
|
+
using_uri_escape?(node),
|
41
|
+
using_uri_encode?(node),
|
42
|
+
using_uri_unescape?(node),
|
43
|
+
using_uri_decode?(node),
|
44
|
+
].none?
|
45
|
+
return
|
46
|
+
end
|
44
47
|
add_offense(node, message: MSG)
|
45
48
|
end
|
46
49
|
end
|
@@ -25,7 +25,7 @@ module RuboCop
|
|
25
25
|
# it "does X" do
|
26
26
|
# end
|
27
27
|
# end
|
28
|
-
class OnlyTopLevelMultisiteSpecs <
|
28
|
+
class OnlyTopLevelMultisiteSpecs < Base
|
29
29
|
MSG = "Use `type: :multisite` only on a top-level `describe`"
|
30
30
|
|
31
31
|
def on_block(node)
|
@@ -47,11 +47,7 @@ module RuboCop
|
|
47
47
|
MATCHER
|
48
48
|
|
49
49
|
def top_level?(node)
|
50
|
-
|
51
|
-
node.parent.root?
|
52
|
-
else
|
53
|
-
node.root?
|
54
|
-
end
|
50
|
+
node.parent&.begin_type? ? node.parent.root? : node.root?
|
55
51
|
end
|
56
52
|
end
|
57
53
|
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
|