Skip to content

Commit 7a5b7cc

Browse files
authored
Merge pull request #78 from github/render_local_literal_keys
Enforce local keys being literals
2 parents d910021 + 54f3e11 commit 7a5b7cc

File tree

5 files changed

+223
-52
lines changed

5 files changed

+223
-52
lines changed

lib/rubocop/cop/github/rails_controller_render_literal.rb

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,15 @@
11
# frozen_string_literal: true
22

33
require "rubocop"
4+
require "rubocop/cop/github/render_literal_helpers"
45

56
module RuboCop
67
module Cop
78
module GitHub
89
class RailsControllerRenderLiteral < Cop
9-
MSG = "render must be used with a string literal or an instance of a Class"
10-
11-
def_node_matcher :literal?, <<-PATTERN
12-
({str sym true false nil?} ...)
13-
PATTERN
14-
15-
def_node_matcher :render?, <<-PATTERN
16-
(send nil? {:render :render_to_string} ...)
17-
PATTERN
18-
19-
def_node_matcher :render_literal?, <<-PATTERN
20-
(send nil? {:render :render_to_string} ({str sym} $_) $...)
21-
PATTERN
22-
23-
def_node_matcher :render_const?, <<-PATTERN
24-
(send nil? {:render :render_to_string} (const _ _) ...)
25-
PATTERN
26-
27-
def_node_matcher :render_inst?, <<-PATTERN
28-
(send nil? {:render :render_to_string} (send _ :new ...) ...)
29-
PATTERN
10+
include RenderLiteralHelpers
3011

31-
def_node_matcher :render_with_options?, <<-PATTERN
32-
(send nil? {:render :render_to_string} (hash $...))
33-
PATTERN
12+
MSG = "render must be used with a string literal or an instance of a Class"
3413

3514
def_node_matcher :ignore_key?, <<-PATTERN
3615
(pair (sym {
@@ -68,10 +47,16 @@ class RailsControllerRenderLiteral < Cop
6847
}) ...)
6948
PATTERN
7049

50+
def_node_matcher :render_const?, <<-PATTERN
51+
(send nil? {:render :render_to_string} (const _ _) ...)
52+
PATTERN
53+
7154
def on_send(node)
7255
return unless render?(node)
7356

74-
if render_literal?(node) || render_inst?(node) || render_const?(node)
57+
return if render_view_component?(node) || render_const?(node)
58+
59+
if render_literal?(node)
7560
elsif option_pairs = render_with_options?(node)
7661
option_pairs = option_pairs.reject { |pair| options_key?(pair) }
7762

@@ -82,18 +67,40 @@ def on_send(node)
8267
if template_node = option_pairs.map { |pair| template_key?(pair) }.compact.first
8368
if !literal?(template_node)
8469
add_offense(node, location: :expression)
70+
return
8571
end
8672
else
8773
add_offense(node, location: :expression)
74+
return
8875
end
8976

9077
if layout_node = option_pairs.map { |pair| layout_key?(pair) }.compact.first
9178
if !literal?(layout_node)
9279
add_offense(node, location: :expression)
80+
return
9381
end
9482
end
9583
else
9684
add_offense(node, location: :expression)
85+
return
86+
end
87+
88+
if render_literal?(node)
89+
option_hash = node.arguments[1]
90+
if option_hash && !option_hash.hash_type?
91+
add_offense(node, location: :expression)
92+
return
93+
end
94+
option_pairs = option_hash && option_hash.pairs
95+
else
96+
option_pairs = node.arguments[0].pairs
97+
end
98+
99+
if option_pairs
100+
locals = option_pairs.map { |pair| locals_key?(pair) }.compact.first
101+
if locals && (!locals.hash_type? || !hash_with_literal_keys?(locals))
102+
add_offense(node, location: :expression)
103+
end
97104
end
98105
end
99106
end

lib/rubocop/cop/github/rails_view_render_literal.rb

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,15 @@
11
# frozen_string_literal: true
22

33
require "rubocop"
4+
require "rubocop/cop/github/render_literal_helpers"
45

56
module RuboCop
67
module Cop
78
module GitHub
89
class RailsViewRenderLiteral < Cop
9-
MSG = "render must be used with a string literal or an instance of a Class"
10+
include RenderLiteralHelpers
1011

11-
def_node_matcher :literal?, <<-PATTERN
12-
({str sym true false nil?} ...)
13-
PATTERN
14-
15-
def_node_matcher :render?, <<-PATTERN
16-
(send nil? {:render :render_to_string} $...)
17-
PATTERN
18-
19-
def_node_matcher :render_literal?, <<-PATTERN
20-
(send nil? {:render :render_to_string} ({str sym} $_) $...)
21-
PATTERN
22-
23-
def_node_matcher :render_inst?, <<-PATTERN
24-
(send nil? {:render :render_to_string} (send _ :new ...) ...)
25-
PATTERN
26-
27-
def_node_matcher :render_collection?, <<-PATTERN
28-
(send nil? {:render :render_to_string} (send _ :with_collection ...) ...)
29-
PATTERN
30-
31-
def_node_matcher :render_with_options?, <<-PATTERN
32-
(send nil? {:render :render_to_string} (hash $...) ...)
33-
PATTERN
12+
MSG = "render must be used with a literal template and use literals for locals keys"
3413

3514
def_node_matcher :ignore_key?, <<-PATTERN
3615
(pair (sym {
@@ -50,7 +29,10 @@ class RailsViewRenderLiteral < Cop
5029
def on_send(node)
5130
return unless render?(node)
5231

53-
if render_literal?(node) || render_inst?(node) || render_collection?(node)
32+
# Ignore "component"-style renders
33+
return if render_view_component?(node)
34+
35+
if render_literal?(node)
5436
elsif option_pairs = render_with_options?(node)
5537
if option_pairs.any? { |pair| ignore_key?(pair) }
5638
return
@@ -59,12 +41,31 @@ def on_send(node)
5941
if partial_node = option_pairs.map { |pair| partial_key?(pair) }.compact.first
6042
if !literal?(partial_node)
6143
add_offense(node, location: :expression)
44+
return
6245
end
6346
else
6447
add_offense(node, location: :expression)
48+
return
6549
end
6650
else
6751
add_offense(node, location: :expression)
52+
return
53+
end
54+
55+
if render_literal?(node) && node.arguments.count > 1
56+
locals = node.arguments[1]
57+
elsif options_pairs = render_with_options?(node)
58+
locals = option_pairs.map { |pair| locals_key?(pair) }.compact.first
59+
end
60+
61+
if locals
62+
if locals.hash_type?
63+
if !hash_with_literal_keys?(locals)
64+
add_offense(node, location: :expression)
65+
end
66+
else
67+
add_offense(node, location: :expression)
68+
end
6869
end
6970
end
7071
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
#
3+
require "rubocop"
4+
5+
module RuboCop
6+
module Cop
7+
module GitHub
8+
module RenderLiteralHelpers
9+
extend NodePattern::Macros
10+
11+
def_node_matcher :literal?, <<-PATTERN
12+
({str sym true false nil?} ...)
13+
PATTERN
14+
15+
def_node_matcher :render?, <<-PATTERN
16+
(send nil? {:render :render_to_string} ...)
17+
PATTERN
18+
19+
def_node_matcher :render_literal?, <<-PATTERN
20+
(send nil? {:render :render_to_string} ({str sym} $_) $...)
21+
PATTERN
22+
23+
def_node_matcher :render_inst?, <<-PATTERN
24+
(send nil? {:render :render_to_string} (send _ :new ...) ...)
25+
PATTERN
26+
27+
def_node_matcher :render_with_options?, <<-PATTERN
28+
(send nil? {:render :render_to_string} (hash $...) ...)
29+
PATTERN
30+
31+
def_node_matcher :render_view_component_instance?, <<-PATTERN
32+
(send nil? {:render :render_to_string} (send _ :new ...) ...)
33+
PATTERN
34+
35+
def_node_matcher :render_view_component_collection?, <<-PATTERN
36+
(send nil? {:render :render_to_string} (send _ :with_collection ...) ...)
37+
PATTERN
38+
39+
def_node_matcher :locals_key?, <<-PATTERN
40+
(pair (sym :locals) $_)
41+
PATTERN
42+
43+
def hash_with_literal_keys?(hash)
44+
hash.pairs.all? { |pair| literal?(pair.key) }
45+
end
46+
47+
def render_view_component?(node)
48+
render_view_component_instance?(node) ||
49+
render_view_component_collection?(node)
50+
end
51+
end
52+
end
53+
end
54+
end

test/test_rails_controller_render_literal.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,65 @@ def index
393393
assert_equal 1, cop.offenses.count
394394
assert_equal "render must be used with a string literal or an instance of a Class", cop.offenses[0].message
395395
end
396+
397+
def test_render_shorthand_static_locals_no_offsense
398+
investigate cop, <<-RUBY, "app/controllers/products_controller.rb"
399+
class ProductsController < ActionController::Base
400+
def index
401+
render "products/index", locals: { product: product }
402+
end
403+
end
404+
RUBY
405+
406+
assert_equal 0, cop.offenses.count
407+
end
408+
409+
def test_render_partial_static_locals_no_offsense
410+
investigate cop, <<-RUBY, "app/controllers/products_controller.rb"
411+
class ProductsController < ActionController::Base
412+
def index
413+
render partial: "products/index", locals: { product: product }
414+
end
415+
end
416+
RUBY
417+
418+
assert_equal 0, cop.offenses.count
419+
end
420+
421+
def test_render_literal_dynamic_options_offense
422+
investigate cop, <<-RUBY, "app/controllers/products_controller.rb"
423+
class ProductsController < ActionController::Base
424+
def index
425+
render "products/product", options
426+
end
427+
end
428+
RUBY
429+
430+
assert_equal 1, cop.offenses.count
431+
end
432+
433+
def test_render_literal_dynamic_locals_offense
434+
investigate cop, <<-RUBY, "app/controllers/products_controller.rb"
435+
class ProductsController < ActionController::Base
436+
def index
437+
render "products/product", locals: locals
438+
end
439+
end
440+
RUBY
441+
442+
assert_equal 1, cop.offenses.count
443+
end
444+
445+
446+
def test_render_literal_dynamic_local_key_offense
447+
investigate cop, <<-RUBY, "app/controllers/products_controller.rb"
448+
class ProductsController < ActionController::Base
449+
def index
450+
render "products/product", locals: { product_key => product }
451+
end
452+
end
453+
RUBY
454+
455+
assert_equal 1, cop.offenses.count
456+
end
396457
end

test/test_rails_view_render_literal.rb

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_render_variable_offense
4343
ERB
4444

4545
assert_equal 2, cop.offenses.count
46-
assert_equal "render must be used with a string literal or an instance of a Class", cop.offenses[0].message
46+
assert_equal "render must be used with a literal template and use literals for locals keys", cop.offenses[0].message
4747
end
4848

4949
def test_render_component_no_offense
@@ -94,7 +94,7 @@ def test_render_layout_variable_literal_no_offense
9494
ERB
9595

9696
assert_equal 1, cop.offenses.count
97-
assert_equal "render must be used with a string literal or an instance of a Class", cop.offenses[0].message
97+
assert_equal "render must be used with a literal template and use literals for locals keys", cop.offenses[0].message
9898
end
9999

100100
def test_render_inline_no_offense
@@ -112,4 +112,52 @@ def test_render_template_literal_no_offense
112112

113113
assert_equal 0, cop.offenses.count
114114
end
115+
116+
def test_render_literal_static_locals_no_offense
117+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
118+
<%= render "products/product", product: product %>
119+
ERB
120+
121+
assert_equal 0, cop.offenses.count
122+
end
123+
124+
def test_render_literal_dynamic_locals_offense
125+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
126+
<%= render "products/product", locals %>
127+
ERB
128+
129+
assert_equal 1, cop.offenses.count
130+
end
131+
132+
def test_render_literal_dynamic_local_key_offense
133+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
134+
<%= render "products/product", { product_key => product } %>
135+
ERB
136+
137+
assert_equal 1, cop.offenses.count
138+
end
139+
140+
def test_render_options_static_locals_no_offense
141+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
142+
<%= render partial: "products/product", locals: { product: product } %>
143+
ERB
144+
145+
assert_equal 0, cop.offenses.count
146+
end
147+
148+
def test_render_options_dynamic_locals_offense
149+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
150+
<%= render partial: "products/product", locals: locals %>
151+
ERB
152+
153+
assert_equal 1, cop.offenses.count
154+
end
155+
156+
def test_render_options_dynamic_local_key_offense
157+
erb_investigate cop, <<-ERB, "app/views/products/index.html.erb"
158+
<%= render partial: "products/product", locals: { product_key => product } %>
159+
ERB
160+
161+
assert_equal 1, cop.offenses.count
162+
end
115163
end

0 commit comments

Comments
 (0)