Skip to content

Commit 26db705

Browse files
authored
Merge pull request #580 from nobodywasishere/nobody/heredoc-escape
2 parents 5d7116f + 3897016 commit 26db705

File tree

4 files changed

+207
-6
lines changed

4 files changed

+207
-6
lines changed

spec/ameba/ast/util_spec.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ module Ameba::AST
9999
end
100100

101101
it "does not report source of node which has incorrect location" do
102-
s = <<-'CRYSTAL'
102+
s = <<-CRYSTAL
103103
module MyModule
104104
macro conditional_error_for_inline_callbacks
105-
\{%
105+
{%
106106
raise ""
107107
%}
108108
end
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
require "../../../spec_helper"
2+
3+
module Ameba::Rule::Style
4+
describe HeredocEscape do
5+
subject = HeredocEscape.new
6+
7+
it "passes if a heredoc doesn't contain interpolation" do
8+
expect_no_issues subject, <<-CRYSTAL
9+
<<-HEREDOC
10+
foo
11+
HEREDOC
12+
CRYSTAL
13+
end
14+
15+
it "passes if a heredoc contains interpolation" do
16+
expect_no_issues subject, <<-'CRYSTAL'
17+
<<-HEREDOC
18+
foo #{:bar}
19+
HEREDOC
20+
CRYSTAL
21+
end
22+
23+
it "passes if a heredoc contains normal and escaped interpolation" do
24+
expect_no_issues subject, <<-'CRYSTAL'
25+
<<-HEREDOC
26+
foo \#{:bar} #{:baz}
27+
HEREDOC
28+
CRYSTAL
29+
end
30+
31+
it "passes if a heredoc contains an escape sequence and escaped interpolation" do
32+
expect_no_issues subject, <<-'CRYSTAL'
33+
<<-HEREDOC
34+
foo \377 \xFF \uFFFF \u{0} \t \n \#{:baz}
35+
HEREDOC
36+
CRYSTAL
37+
end
38+
39+
it "passes if a heredoc contains an escaped escape sequence and interpolation" do
40+
expect_no_issues subject, <<-'CRYSTAL'
41+
<<-HEREDOC
42+
foo \\377 \\xFF \\uFFFF \\u{0} \\t \\n #{:baz}
43+
HEREDOC
44+
CRYSTAL
45+
end
46+
47+
it "fails if a heredoc contains escaped interpolation" do
48+
expect_issue subject, <<-'CRYSTAL'
49+
<<-HEREDOC
50+
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
51+
foo \#{:bar}
52+
HEREDOC
53+
CRYSTAL
54+
end
55+
56+
it "fails if a heredoc contains escaped interpolation and escaped escape sequences" do
57+
expect_issue subject, <<-'CRYSTAL'
58+
<<-HEREDOC
59+
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
60+
foo \\t \#{:bar}
61+
HEREDOC
62+
CRYSTAL
63+
end
64+
65+
it "passes if a heredoc contains normal and escaped escape sequences" do
66+
expect_no_issues subject, <<-'CRYSTAL'
67+
<<-HEREDOC
68+
foo \t \n | \\t \\n
69+
HEREDOC
70+
CRYSTAL
71+
end
72+
73+
it "fails if a heredoc contains escaped escape sequences" do
74+
expect_issue subject, <<-'CRYSTAL'
75+
<<-HEREDOC
76+
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
77+
\\t \\n
78+
HEREDOC
79+
CRYSTAL
80+
end
81+
82+
it "passes if an escaped heredoc contains interpolation" do
83+
expect_no_issues subject, <<-'CRYSTAL'
84+
<<-'HEREDOC'
85+
foo #{:bar}
86+
HEREDOC
87+
CRYSTAL
88+
end
89+
90+
it "passes if an escaped heredoc contains escaped interpolation" do
91+
expect_no_issues subject, <<-'CRYSTAL'
92+
<<-'HEREDOC'
93+
foo \#{:bar}
94+
HEREDOC
95+
CRYSTAL
96+
end
97+
98+
it "passes if an escaped heredoc contains escape sequences" do
99+
expect_no_issues subject, <<-'CRYSTAL'
100+
<<-'HEREDOC'
101+
foo \377 \xFF \uFFFF \u{0} \t \n
102+
HEREDOC
103+
CRYSTAL
104+
end
105+
106+
it "passes if an escaped heredoc contains escaped escape sequences" do
107+
expect_no_issues subject, <<-'CRYSTAL'
108+
<<-'HEREDOC'
109+
foo \\377 \\xFF \\uFFFF \\u{0} \\t \\n
110+
HEREDOC
111+
CRYSTAL
112+
end
113+
114+
it "fails if an escaped heredoc doesn't contain interpolation" do
115+
expect_issue subject, <<-CRYSTAL
116+
<<-'HEREDOC'
117+
# ^^^^^^^^^^ error: Use an unescaped heredoc marker: `<<-HEREDOC`
118+
foo
119+
HEREDOC
120+
CRYSTAL
121+
end
122+
end
123+
end

spec/ameba/rule/style/redundant_self_spec.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,23 +249,23 @@ module Ameba::Rule::Style
249249
end
250250

251251
it "reports if there is redundant `self` used in a string interpolation" do
252-
source = expect_issue subject, <<-CRYSTAL
252+
source = expect_issue subject, <<-'CRYSTAL'
253253
class Foo
254254
def foo; end
255255

256256
def foo!
257-
"\#{self.foo || 42}"
257+
"#{self.foo || 42}"
258258
# ^^^^ error: Redundant `self` detected
259259
end
260260
end
261261
CRYSTAL
262262

263-
expect_correction source, <<-CRYSTAL
263+
expect_correction source, <<-'CRYSTAL'
264264
class Foo
265265
def foo; end
266266

267267
def foo!
268-
"\#{foo || 42}"
268+
"#{foo || 42}"
269269
end
270270
end
271271
CRYSTAL
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
module Ameba::Rule::Style
2+
# A rule that enforces heredoc variant that escapes interpolation or control
3+
# chars in a heredoc body. The opposite is enforced too - i.e. regular heredoc
4+
# variant that doesn't escape interpolation or control chars in a heredoc body,
5+
# when there is no need to escape it.
6+
#
7+
# For example, this is considered invalid:
8+
#
9+
# ```
10+
# <<-DOC
11+
# This is an escaped \#{:interpolated} string \\n
12+
# DOC
13+
# ```
14+
#
15+
# And should be written as:
16+
#
17+
# ```
18+
# <<-'DOC'
19+
# This is an escaped #{:interpolated} string \n
20+
# DOC
21+
# ```
22+
#
23+
# YAML configuration example:
24+
#
25+
# ```
26+
# Style/HeredocEscape:
27+
# Enabled: true
28+
# ```
29+
class HeredocEscape < Base
30+
include AST::Util
31+
32+
properties do
33+
since_version "1.7.0"
34+
description "Recommends using the heredoc variant that escapes interpolation or control chars in a heredoc body"
35+
end
36+
37+
MSG_ESCAPE_NEEDED = "Use an escaped heredoc marker: `<<-'%s'`"
38+
MSG_ESCAPE_NOT_NEEDED = "Use an unescaped heredoc marker: `<<-%s`"
39+
40+
ESCAPE_SEQUENCE_PATTERN =
41+
/\\(?:[abefnrtv]|[0-7]{1,3}|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\{[0-9a-fA-F]{1,6}\})/
42+
43+
def test(source, node : Crystal::StringInterpolation)
44+
# Heredocs without interpolations have always size of 1
45+
return unless node.expressions.size == 1
46+
return unless expr = node.expressions.first.as?(Crystal::StringLiteral)
47+
48+
return unless code = node_source(node, source.lines)
49+
return unless code.starts_with?("<<-")
50+
51+
body = code.lines[1..-2].join('\n')
52+
53+
if code.starts_with?("<<-'")
54+
return if has_escape_sequence?(expr.value) || has_escaped_escape_sequence?(body)
55+
56+
marker = code.lchop("<<-'").match!(/^(\w+)/)[1]
57+
msg = MSG_ESCAPE_NOT_NEEDED % marker
58+
else
59+
return if !has_escape_sequence?(expr.value) || has_escape_sequence?(body)
60+
61+
marker = code.lchop("<<-").match!(/^(\w+)/)[1]
62+
msg = MSG_ESCAPE_NEEDED % marker
63+
end
64+
65+
issue_for node, msg
66+
end
67+
68+
private def has_escape_sequence?(value : String)
69+
value.matches? /(?<!\\)(?:#\{|#{ESCAPE_SEQUENCE_PATTERN})/,
70+
options: :no_utf_check
71+
end
72+
73+
private def has_escaped_escape_sequence?(value : String)
74+
value.matches? /(?<!\\)(?:\\)+(?:#\{|#{ESCAPE_SEQUENCE_PATTERN})/,
75+
options: :no_utf_check
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)