theme-check 1.6.1 → 1.6.2
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/CHANGELOG.md +6 -0
- data/docs/api/html_check.md +7 -7
- data/docs/api/liquid_check.md +10 -10
- data/docs/checks/convert_include_to_render.md +1 -1
- data/docs/checks/missing_enable_comment.md +1 -1
- data/lib/theme_check/analyzer.rb +15 -15
- data/lib/theme_check/asset_file.rb +1 -1
- data/lib/theme_check/check.rb +2 -2
- data/lib/theme_check/checks/html_parsing_error.rb +2 -2
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/missing_template.rb +6 -6
- data/lib/theme_check/checks/nested_snippet.rb +2 -2
- data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
- data/lib/theme_check/checks/syntax_error.rb +5 -5
- data/lib/theme_check/checks/template_length.rb +2 -2
- data/lib/theme_check/checks/undefined_object.rb +7 -7
- data/lib/theme_check/checks/unused_assign.rb +4 -4
- data/lib/theme_check/checks/unused_snippet.rb +7 -7
- data/lib/theme_check/checks/valid_json.rb +1 -1
- data/lib/theme_check/checks.rb +2 -2
- data/lib/theme_check/cli.rb +1 -1
- data/lib/theme_check/corrector.rb +6 -6
- data/lib/theme_check/disabled_check.rb +3 -3
- data/lib/theme_check/disabled_checks.rb +9 -9
- data/lib/theme_check/html_node.rb +36 -28
- data/lib/theme_check/html_visitor.rb +6 -6
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
- data/lib/theme_check/{template.rb → liquid_file.rb} +2 -2
- data/lib/theme_check/liquid_node.rb +291 -0
- data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
- data/lib/theme_check/locale_diff.rb +5 -5
- data/lib/theme_check/node.rb +12 -225
- data/lib/theme_check/offense.rb +15 -15
- data/lib/theme_check/position.rb +1 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/{template_rewriter.rb → theme_file_rewriter.rb} +1 -1
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -10
- data/theme-check.gemspec +1 -1
- metadata +8 -7
data/lib/theme_check/checks.rb
CHANGED
@@ -47,7 +47,7 @@ module ThemeCheck
|
|
47
47
|
raise
|
48
48
|
rescue => e
|
49
49
|
node = args.first
|
50
|
-
|
50
|
+
theme_file = node.respond_to?(:theme_file) ? node.theme_file.relative_path : "?"
|
51
51
|
markup = node.respond_to?(:markup) ? node.markup : ""
|
52
52
|
node_class = node.respond_to?(:value) ? node.value.class : "?"
|
53
53
|
line_number = node.respond_to?(:line_number) ? node.line_number : "?"
|
@@ -59,7 +59,7 @@ module ThemeCheck
|
|
59
59
|
#{e.backtrace.join("\n ")}
|
60
60
|
```
|
61
61
|
|
62
|
-
|
62
|
+
Theme File: `#{theme_file}`
|
63
63
|
Node: `#{node_class}`
|
64
64
|
Markup:
|
65
65
|
```
|
data/lib/theme_check/cli.rb
CHANGED
@@ -186,7 +186,7 @@ module ThemeCheck
|
|
186
186
|
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
187
187
|
theme = ThemeCheck::Theme.new(storage)
|
188
188
|
if theme.all.empty?
|
189
|
-
raise Abort, "No
|
189
|
+
raise Abort, "No theme files found."
|
190
190
|
end
|
191
191
|
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
|
192
192
|
analyzer.analyze_theme
|
@@ -2,25 +2,25 @@
|
|
2
2
|
|
3
3
|
module ThemeCheck
|
4
4
|
class Corrector
|
5
|
-
def initialize(
|
6
|
-
@
|
5
|
+
def initialize(theme_file:)
|
6
|
+
@theme_file = theme_file
|
7
7
|
end
|
8
8
|
|
9
9
|
def insert_after(node, content)
|
10
|
-
@
|
10
|
+
@theme_file.rewriter.insert_after(node, content)
|
11
11
|
end
|
12
12
|
|
13
13
|
def insert_before(node, content)
|
14
|
-
@
|
14
|
+
@theme_file.rewriter.insert_before(node, content)
|
15
15
|
end
|
16
16
|
|
17
17
|
def replace(node, content)
|
18
|
-
@
|
18
|
+
@theme_file.rewriter.replace(node, content)
|
19
19
|
node.markup = content
|
20
20
|
end
|
21
21
|
|
22
22
|
def wrap(node, insert_before, insert_after)
|
23
|
-
@
|
23
|
+
@theme_file.rewriter.wrap(node, insert_before, insert_after)
|
24
24
|
end
|
25
25
|
|
26
26
|
def create(theme, relative_path, content)
|
@@ -4,11 +4,11 @@
|
|
4
4
|
# We'll use the node position to figure out if the test is disabled or not.
|
5
5
|
module ThemeCheck
|
6
6
|
class DisabledCheck
|
7
|
-
attr_reader :name, :
|
7
|
+
attr_reader :name, :theme_file, :ranges
|
8
8
|
attr_accessor :first_line
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
10
|
+
def initialize(theme_file, name)
|
11
|
+
@theme_file = theme_file
|
12
12
|
@name = name
|
13
13
|
@ranges = []
|
14
14
|
@first_line = false
|
@@ -11,8 +11,8 @@ module ThemeCheck
|
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
@disabled_checks = Hash.new do |hash, key|
|
14
|
-
|
15
|
-
hash[key] = DisabledCheck.new(
|
14
|
+
theme_file, check_name = key
|
15
|
+
hash[key] = DisabledCheck.new(theme_file, check_name)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -20,26 +20,26 @@ module ThemeCheck
|
|
20
20
|
text = comment_text(node)
|
21
21
|
if start_disabling?(text)
|
22
22
|
checks_from_text(text).each do |check_name|
|
23
|
-
disabled = @disabled_checks[[node.
|
23
|
+
disabled = @disabled_checks[[node.theme_file, check_name]]
|
24
24
|
disabled.start_index = node.start_index
|
25
25
|
disabled.first_line = true if node.line_number == 1
|
26
26
|
end
|
27
27
|
elsif stop_disabling?(text)
|
28
28
|
checks_from_text(text).each do |check_name|
|
29
|
-
disabled = @disabled_checks[[node.
|
29
|
+
disabled = @disabled_checks[[node.theme_file, check_name]]
|
30
30
|
next unless disabled
|
31
31
|
disabled.end_index = node.end_index
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def disabled?(check,
|
36
|
+
def disabled?(check, theme_file, check_name, index)
|
37
37
|
return true if check.ignored_patterns&.any? do |pattern|
|
38
|
-
|
38
|
+
theme_file.relative_path.fnmatch?(pattern)
|
39
39
|
end
|
40
40
|
|
41
|
-
@disabled_checks[[
|
42
|
-
@disabled_checks[[
|
41
|
+
@disabled_checks[[theme_file, :all]]&.disabled?(index) ||
|
42
|
+
@disabled_checks[[theme_file, check_name]]&.disabled?(index)
|
43
43
|
end
|
44
44
|
|
45
45
|
def checks_missing_end_index
|
@@ -51,7 +51,7 @@ module ThemeCheck
|
|
51
51
|
def remove_disabled_offenses(checks)
|
52
52
|
checks.disableable.each do |check|
|
53
53
|
check.offenses.reject! do |offense|
|
54
|
-
disabled?(check, offense.
|
54
|
+
disabled?(check, offense.theme_file, offense.code_name, offense.start_index)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -2,18 +2,50 @@
|
|
2
2
|
require "forwardable"
|
3
3
|
|
4
4
|
module ThemeCheck
|
5
|
-
class HtmlNode
|
5
|
+
class HtmlNode < Node
|
6
6
|
extend Forwardable
|
7
7
|
include RegexHelpers
|
8
|
-
attr_reader :
|
8
|
+
attr_reader :theme_file, :parent
|
9
9
|
|
10
|
-
def initialize(value,
|
10
|
+
def initialize(value, theme_file, placeholder_values = [], parent = nil)
|
11
11
|
@value = value
|
12
|
-
@
|
12
|
+
@theme_file = theme_file
|
13
13
|
@placeholder_values = placeholder_values
|
14
14
|
@parent = parent
|
15
15
|
end
|
16
16
|
|
17
|
+
# @value is not forwarded because we _need_ to replace the
|
18
|
+
# placeholders for the HtmlNode to make sense.
|
19
|
+
def value
|
20
|
+
if literal?
|
21
|
+
content
|
22
|
+
else
|
23
|
+
markup
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def children
|
28
|
+
@children ||= @value
|
29
|
+
.children
|
30
|
+
.map { |child| HtmlNode.new(child, theme_file, @placeholder_values, self) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def markup
|
34
|
+
@markup ||= replace_placeholders(@value.to_html)
|
35
|
+
end
|
36
|
+
|
37
|
+
def line_number
|
38
|
+
@value.line
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_index
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
|
45
|
+
def end_index
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
17
49
|
def literal?
|
18
50
|
@value.name == "text"
|
19
51
|
end
|
@@ -22,12 +54,6 @@ module ThemeCheck
|
|
22
54
|
@value.element?
|
23
55
|
end
|
24
56
|
|
25
|
-
def children
|
26
|
-
@children ||= @value
|
27
|
-
.children
|
28
|
-
.map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
|
29
|
-
end
|
30
|
-
|
31
57
|
def attributes
|
32
58
|
@attributes ||= @value.attributes
|
33
59
|
.map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
|
@@ -38,16 +64,6 @@ module ThemeCheck
|
|
38
64
|
@content ||= replace_placeholders(@value.content)
|
39
65
|
end
|
40
66
|
|
41
|
-
# @value is not forwarded because we _need_ to replace the
|
42
|
-
# placeholders for the HtmlNode to make sense.
|
43
|
-
def value
|
44
|
-
if literal?
|
45
|
-
content
|
46
|
-
else
|
47
|
-
markup
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
67
|
def name
|
52
68
|
if @value.name == "#document-fragment"
|
53
69
|
"document"
|
@@ -56,14 +72,6 @@ module ThemeCheck
|
|
56
72
|
end
|
57
73
|
end
|
58
74
|
|
59
|
-
def markup
|
60
|
-
@markup ||= replace_placeholders(@value.to_html)
|
61
|
-
end
|
62
|
-
|
63
|
-
def line_number
|
64
|
-
@value.line
|
65
|
-
end
|
66
|
-
|
67
75
|
private
|
68
76
|
|
69
77
|
def replace_placeholders(string)
|
@@ -11,18 +11,18 @@ module ThemeCheck
|
|
11
11
|
@checks = checks
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
doc, placeholder_values = parse(
|
16
|
-
visit(HtmlNode.new(doc,
|
14
|
+
def visit_liquid_file(liquid_file)
|
15
|
+
doc, placeholder_values = parse(liquid_file)
|
16
|
+
visit(HtmlNode.new(doc, liquid_file, placeholder_values))
|
17
17
|
rescue ArgumentError => e
|
18
|
-
call_checks(:on_parse_error, e,
|
18
|
+
call_checks(:on_parse_error, e, liquid_file)
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
def parse(
|
23
|
+
def parse(liquid_file)
|
24
24
|
placeholder_values = []
|
25
|
-
parseable_source = +
|
25
|
+
parseable_source = +liquid_file.source.clone
|
26
26
|
|
27
27
|
# Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
|
28
28
|
# parser from freaking out. We transparently replace those placeholders in
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# An in-memory storage is not written to disk. The reasons why you'd
|
4
4
|
# want to do that are your own. The idea is to not write to disk
|
5
|
-
# something that doesn't need to be there. If you have your
|
5
|
+
# something that doesn't need to be there. If you have your theme
|
6
6
|
# as a big hash already, leave it like that and save yourself some IO.
|
7
7
|
module ThemeCheck
|
8
8
|
class InMemoryStorage < Storage
|
@@ -4,8 +4,8 @@ module ThemeCheck
|
|
4
4
|
class JsonCheck < Check
|
5
5
|
extend ChecksTracking
|
6
6
|
|
7
|
-
def add_offense(message, markup: nil, line_number: nil,
|
8
|
-
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number,
|
7
|
+
def add_offense(message, markup: nil, line_number: nil, theme_file: nil, &block)
|
8
|
+
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, theme_file: theme_file, correction: block)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -19,22 +19,22 @@ module ThemeCheck
|
|
19
19
|
new_single_file_offenses = {}
|
20
20
|
analyzed_files = analyzed_files.map { |path| Pathname.new(path) } if analyzed_files
|
21
21
|
|
22
|
-
offenses.group_by(&:
|
23
|
-
next unless
|
22
|
+
offenses.group_by(&:theme_file).each do |theme_file, template_offenses|
|
23
|
+
next unless theme_file
|
24
24
|
reported_offenses = template_offenses
|
25
|
-
previous_offenses = @single_files_offenses[
|
26
|
-
if analyzed_files.nil? || analyzed_files.include?(
|
25
|
+
previous_offenses = @single_files_offenses[theme_file.path]
|
26
|
+
if analyzed_files.nil? || analyzed_files.include?(theme_file.path)
|
27
27
|
# We re-analyzed the file, so we know the template_offenses are update to date.
|
28
28
|
reported_single_file_offenses = reported_offenses.select(&:single_file?)
|
29
29
|
if reported_single_file_offenses.any?
|
30
|
-
new_single_file_offenses[
|
30
|
+
new_single_file_offenses[theme_file.path] = reported_single_file_offenses
|
31
31
|
end
|
32
32
|
elsif previous_offenses
|
33
33
|
# Merge in the previous ones, if some
|
34
34
|
reported_offenses |= previous_offenses
|
35
35
|
end
|
36
|
-
yield
|
37
|
-
reported_files <<
|
36
|
+
yield theme_file.path, reported_offenses
|
37
|
+
reported_files << theme_file.path
|
38
38
|
end
|
39
39
|
|
40
40
|
@single_files_offenses.each do |path, _|
|
@@ -51,7 +51,7 @@ module ThemeCheck
|
|
51
51
|
reported_files << path
|
52
52
|
end
|
53
53
|
|
54
|
-
# Publish diagnostics with empty array if all issues on a previously reported
|
54
|
+
# Publish diagnostics with empty array if all issues on a previously reported theme_file
|
55
55
|
# have been fixed.
|
56
56
|
(@previously_reported_files - reported_files).each do |path|
|
57
57
|
yield path, []
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ThemeCheck
|
4
|
-
class
|
4
|
+
class LiquidFile < ThemeFile
|
5
5
|
def write
|
6
6
|
content = rewriter.to_s
|
7
7
|
if source != content
|
@@ -28,7 +28,7 @@ module ThemeCheck
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def rewriter
|
31
|
-
@rewriter ||=
|
31
|
+
@rewriter ||= ThemeFileRewriter.new(@relative_path, source)
|
32
32
|
end
|
33
33
|
|
34
34
|
def source_excerpt(line)
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
# A node from the Liquid AST, the result of parsing a liquid file.
|
5
|
+
class LiquidNode < Node
|
6
|
+
attr_reader :value, :parent, :theme_file
|
7
|
+
|
8
|
+
def initialize(value, parent, theme_file)
|
9
|
+
raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(LiquidNode)
|
10
|
+
@value = value
|
11
|
+
@parent = parent
|
12
|
+
@theme_file = theme_file
|
13
|
+
@tag_markup = nil
|
14
|
+
@line_number_offset = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# Array of children nodes.
|
18
|
+
def children
|
19
|
+
@children ||= begin
|
20
|
+
nodes =
|
21
|
+
if comment?
|
22
|
+
[]
|
23
|
+
elsif defined?(@value.class::ParseTreeVisitor)
|
24
|
+
@value.class::ParseTreeVisitor.new(@value, {}).children
|
25
|
+
elsif @value.respond_to?(:nodelist)
|
26
|
+
Array(@value.nodelist)
|
27
|
+
else
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
# Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
|
31
|
+
# the args in a hash as children nodes.
|
32
|
+
nodes = nodes.flat_map do |node|
|
33
|
+
case node
|
34
|
+
when Hash
|
35
|
+
node.values
|
36
|
+
else
|
37
|
+
node
|
38
|
+
end
|
39
|
+
end
|
40
|
+
nodes.map { |node| LiquidNode.new(node, self, @theme_file) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# The original source code of the node. Doesn't contain wrapping braces.
|
45
|
+
def markup
|
46
|
+
if tag?
|
47
|
+
tag_markup
|
48
|
+
elsif @value.instance_variable_defined?(:@markup)
|
49
|
+
@value.instance_variable_get(:@markup)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def markup=(markup)
|
54
|
+
if @value.instance_variable_defined?(:@markup)
|
55
|
+
@value.instance_variable_set(:@markup, markup)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Most nodes have a line number, but it's not guaranteed.
|
60
|
+
def line_number
|
61
|
+
if tag? && @value.respond_to?(:line_number)
|
62
|
+
markup # initialize the line_number_offset
|
63
|
+
@value.line_number - @line_number_offset
|
64
|
+
elsif @value.respond_to?(:line_number)
|
65
|
+
@value.line_number
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_index
|
70
|
+
position.start_index
|
71
|
+
end
|
72
|
+
|
73
|
+
def end_index
|
74
|
+
position.end_index
|
75
|
+
end
|
76
|
+
|
77
|
+
# Literals are hard-coded values in the liquid file.
|
78
|
+
def literal?
|
79
|
+
@value.is_a?(String) || @value.is_a?(Integer)
|
80
|
+
end
|
81
|
+
|
82
|
+
# A {% tag %} node?
|
83
|
+
def tag?
|
84
|
+
@value.is_a?(Liquid::Tag)
|
85
|
+
end
|
86
|
+
|
87
|
+
def variable?
|
88
|
+
@value.is_a?(Liquid::Variable)
|
89
|
+
end
|
90
|
+
|
91
|
+
# A {% comment %} block node?
|
92
|
+
def comment?
|
93
|
+
@value.is_a?(Liquid::Comment)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Top level node of every liquid_file.
|
97
|
+
def document?
|
98
|
+
@value.is_a?(Liquid::Document)
|
99
|
+
end
|
100
|
+
alias_method :root?, :document?
|
101
|
+
|
102
|
+
# A {% tag %}...{% endtag %} node?
|
103
|
+
def block_tag?
|
104
|
+
@value.is_a?(Liquid::Block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# The body of blocks
|
108
|
+
def block_body?
|
109
|
+
@value.is_a?(Liquid::BlockBody)
|
110
|
+
end
|
111
|
+
|
112
|
+
# A block of type of node?
|
113
|
+
def block?
|
114
|
+
block_tag? || block_body? || document?
|
115
|
+
end
|
116
|
+
|
117
|
+
# The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
|
118
|
+
# and `after_<type_name>` check methods.
|
119
|
+
def type_name
|
120
|
+
@type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
|
121
|
+
end
|
122
|
+
|
123
|
+
def source
|
124
|
+
theme_file&.source
|
125
|
+
end
|
126
|
+
|
127
|
+
WHITESPACE = /\s/
|
128
|
+
|
129
|
+
# Is this node inside a `{% liquid ... %}` block?
|
130
|
+
def inside_liquid_tag?
|
131
|
+
# What we're doing here is starting at the start of the tag and
|
132
|
+
# backtrack on all the whitespace until we land on something. If
|
133
|
+
# that something is {% or %-, then we can safely assume that
|
134
|
+
# we're inside a full tag and not a liquid tag.
|
135
|
+
@inside_liquid_tag ||= if tag? && start_index && source
|
136
|
+
i = 1
|
137
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
138
|
+
first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
|
139
|
+
first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
|
140
|
+
else
|
141
|
+
false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
|
146
|
+
def whitespace_trimmed_start?
|
147
|
+
@whitespace_trimmed_start ||= if start_index && source && !inside_liquid_tag?
|
148
|
+
i = 1
|
149
|
+
i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
|
150
|
+
source[start_index - i] == "-"
|
151
|
+
else
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
|
157
|
+
def whitespace_trimmed_end?
|
158
|
+
@whitespace_trimmed_end ||= if end_index && source && !inside_liquid_tag?
|
159
|
+
i = 0
|
160
|
+
i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
|
161
|
+
source[end_index + i] == "-"
|
162
|
+
else
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def start_token
|
168
|
+
return "" if inside_liquid_tag?
|
169
|
+
output = ""
|
170
|
+
output += "{{" if variable?
|
171
|
+
output += "{%" if tag?
|
172
|
+
output += "-" if whitespace_trimmed_start?
|
173
|
+
output
|
174
|
+
end
|
175
|
+
|
176
|
+
def end_token
|
177
|
+
return "" if inside_liquid_tag?
|
178
|
+
output = ""
|
179
|
+
output += "-" if whitespace_trimmed_end?
|
180
|
+
output += "}}" if variable?
|
181
|
+
output += "%}" if tag?
|
182
|
+
output
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def position
|
188
|
+
@position ||= Position.new(
|
189
|
+
markup,
|
190
|
+
theme_file&.source,
|
191
|
+
line_number_1_indexed: line_number
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Here we're hacking around a glorious bug in Liquid that makes it so the
|
196
|
+
# line_number and markup of a tag is wrong if there's whitespace
|
197
|
+
# between the tag_name and the markup of the tag.
|
198
|
+
#
|
199
|
+
# {%
|
200
|
+
# render
|
201
|
+
# 'foo'
|
202
|
+
# %}
|
203
|
+
#
|
204
|
+
# Returns a raw value of "render 'foo'\n".
|
205
|
+
# The "\n " between render and 'foo' got replaced by a single space.
|
206
|
+
#
|
207
|
+
# And the line number is the one of 'foo'\n%}. Yay!
|
208
|
+
#
|
209
|
+
# This breaks any kind of position logic we have since that string
|
210
|
+
# does not exist in the theme_file.
|
211
|
+
def tag_markup
|
212
|
+
return @tag_markup if @tag_markup
|
213
|
+
|
214
|
+
l = 1
|
215
|
+
scanner = StringScanner.new(source)
|
216
|
+
scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
|
217
|
+
start = scanner.charpos
|
218
|
+
|
219
|
+
tag_name = @value.tag_name
|
220
|
+
tag_markup = @value.instance_variable_get('@markup')
|
221
|
+
|
222
|
+
# This is tricky, if the tag_markup is empty, then the tag could
|
223
|
+
# either start on a previous line, or the tag could start on the
|
224
|
+
# same line.
|
225
|
+
#
|
226
|
+
# Consider this:
|
227
|
+
# 1 {%
|
228
|
+
# 2 comment
|
229
|
+
# 3 %}{% endcomment %}{%comment%}
|
230
|
+
#
|
231
|
+
# Both comments would markup == "" AND line_number == 3
|
232
|
+
#
|
233
|
+
# There's no way to determine which one is the correct one, but
|
234
|
+
# we'll try our best to at least give you one.
|
235
|
+
#
|
236
|
+
# To screw with you even more, the name of the tag could be
|
237
|
+
# outside of a tag on the same line :) But I won't do anything
|
238
|
+
# about that (yet?).
|
239
|
+
#
|
240
|
+
# {% comment
|
241
|
+
# %}comment{% endcomment %}
|
242
|
+
if tag_markup.empty?
|
243
|
+
eol = source.index("\n", start) || source.size
|
244
|
+
|
245
|
+
# OK here I'm trying one of two things. Either tag_start is on
|
246
|
+
# the same line OR tag_start is on a previous line. The line
|
247
|
+
# number would be at the end of the whitespace after tag_name.
|
248
|
+
unless (tag_start = source.index(tag_name, start)) && tag_start < eol
|
249
|
+
tag_start = start
|
250
|
+
tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
|
251
|
+
tag_start -= @value.tag_name.size
|
252
|
+
|
253
|
+
# keep track of the error in line_number
|
254
|
+
@line_number_offset = source[tag_start...start].count("\n")
|
255
|
+
end
|
256
|
+
tag_end = tag_start + tag_name.size
|
257
|
+
tag_end += 1 while source[tag_end] =~ WHITESPACE
|
258
|
+
|
259
|
+
# return the real raw content
|
260
|
+
@tag_markup = source[tag_start...tag_end]
|
261
|
+
return @tag_markup
|
262
|
+
|
263
|
+
# See https://github.com/Shopify/theme-check/pull/423/files#r701936559 for a detailed explanation
|
264
|
+
# of why we're doing the check below.
|
265
|
+
#
|
266
|
+
# TL;DR it's because line_numbers are not enough to accurately
|
267
|
+
# determine the position of the raw markup and because that
|
268
|
+
# markup could be present on the same line outside of a Tag. e.g.
|
269
|
+
#
|
270
|
+
# uhoh {% if uhoh %}
|
271
|
+
elsif (match = /#{tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
|
272
|
+
return @tag_markup = match[0]
|
273
|
+
end
|
274
|
+
|
275
|
+
# find the markup
|
276
|
+
markup_start = source.index(tag_markup, start)
|
277
|
+
markup_end = markup_start + tag_markup.size
|
278
|
+
|
279
|
+
# go back until you find the tag_name
|
280
|
+
tag_start = markup_start
|
281
|
+
tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
|
282
|
+
tag_start -= tag_name.size
|
283
|
+
|
284
|
+
# keep track of the error in line_number
|
285
|
+
@line_number_offset = source[tag_start...markup_start].count("\n")
|
286
|
+
|
287
|
+
# return the real raw content
|
288
|
+
@tag_markup = source[tag_start...markup_end]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|