theme-check 1.5.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60f0635001a9f808d12b55e0328536911bde9ea36160bcf34a63f10114401d18
4
- data.tar.gz: 78365fe98014904e058191934663b75d24701db8a2a2585edac2557192cec5d5
3
+ metadata.gz: 960f944628496ea317205e34678a78ec1e560dbfb48f40cfa33be2d82ebe4770
4
+ data.tar.gz: 66e2abe15609c6e5a40d13d05aa0daedf9fbd783e31226bbfb0c6961c1d89ad6
5
5
  SHA512:
6
- metadata.gz: 0520e083dac843ed6273ba17e0cb44ee0e9e2c412f85a81b176da8b0912a583ab262c0c25b0b580df9c53cabefca305dce468559efa6df786ff37b7583e7ecc4
7
- data.tar.gz: 58b7483ad5935de781eddf850a73c88f04aadbae394416f901fa38e173b44b4af3717947fc72cc009c899866ce1d27cc3f6597d5de7dd7ea1ef6984393d71ff4
6
+ metadata.gz: d920362e7729fc739c0a0202e00a605025db3d80c2fdfac5be9d6d9ea6c74bc4922baa82e093851432417863106c14964f6fdc76f66e55fd7947f33887b989c1
7
+ data.tar.gz: 26eb4ba427e8380ece15160c2c27cd052802ec233bb61a4a17e36dcf7f008cc7bc569b71649c525c6d57a7d79654469af65ec664726665584806928cf7258483
data/CHANGELOG.md CHANGED
@@ -1,4 +1,11 @@
1
1
 
2
+ v1.5.2 / 2021-09-09
3
+ ===================
4
+
5
+ * Handle invalid URIs in RemoteAssetFile ([#418](https://github.com/shopify/theme-check/issues/418), [#438](https://github.com/shopify/theme-check/issues/438))
6
+ * Fix `Bad start_index` error in SpaceInsideBraces ([#423](https://github.com/shopify/theme-check/issues/423))
7
+ * Autocorrect missing directories ([#389](https://github.com/shopify/theme-check/issues/389))
8
+
2
9
  v1.5.1 / 2021-09-08
3
10
  ===================
4
11
 
@@ -13,7 +13,7 @@ module ThemeCheck
13
13
  end
14
14
 
15
15
  def on_tag(node)
16
- if !node.inside_liquid_tag?
16
+ if node.inside_liquid_tag?
17
17
  reset_consecutive_statements
18
18
  # Ignore comments
19
19
  elsif !node.comment?
@@ -18,7 +18,9 @@ module ThemeCheck
18
18
  private
19
19
 
20
20
  def add_missing_directories_offense(directory)
21
- add_offense("Theme is missing '#{directory}' directory")
21
+ add_offense("Theme is missing '#{directory}' directory") do |corrector|
22
+ corrector.mkdir(@theme, directory)
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -15,52 +15,57 @@ module ThemeCheck
15
15
  return if :assign == node.type_name
16
16
 
17
17
  outside_of_strings(node.markup) do |chunk, chunk_start|
18
- chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=) +/) do |_match|
18
+ chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=)( +)/) do |_match|
19
19
  add_offense(
20
20
  "Too many spaces after '#{Regexp.last_match(1)}'",
21
21
  node: node,
22
- markup: Regexp.last_match(0),
23
- node_markup_offset: chunk_start + Regexp.last_match.begin(0)
22
+ markup: Regexp.last_match(2),
23
+ node_markup_offset: chunk_start + Regexp.last_match.begin(2)
24
24
  )
25
25
  end
26
26
  chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
27
27
  add_offense(
28
28
  "Space missing after '#{Regexp.last_match(1)}'",
29
29
  node: node,
30
- markup: Regexp.last_match(0),
30
+ markup: Regexp.last_match(1),
31
31
  node_markup_offset: chunk_start + Regexp.last_match.begin(0),
32
32
  )
33
33
  end
34
- chunk.scan(/ (\||==|<>|<=|>=|<|>|!=)+/) do |_match|
34
+ chunk.scan(/( +)(\||==|<>|<=|>=|<|>|!=)+/) do |_match|
35
35
  add_offense(
36
- "Too many spaces before '#{Regexp.last_match(1)}'",
36
+ "Too many spaces before '#{Regexp.last_match(2)}'",
37
37
  node: node,
38
- markup: Regexp.last_match(0),
39
- node_markup_offset: chunk_start + Regexp.last_match.begin(0)
38
+ markup: Regexp.last_match(1),
39
+ node_markup_offset: chunk_start + Regexp.last_match.begin(1)
40
40
  )
41
41
  end
42
42
  chunk.scan(/(\A|\S)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
43
43
  add_offense(
44
44
  "Space missing before '#{Regexp.last_match(1)}'",
45
45
  node: node,
46
- markup: Regexp.last_match(0),
47
- node_markup_offset: chunk_start + Regexp.last_match.begin(0)
46
+ markup: Regexp.last_match(:match),
47
+ node_markup_offset: chunk_start + Regexp.last_match.begin(:match)
48
48
  )
49
49
  end
50
50
  end
51
51
  end
52
52
 
53
53
  def on_tag(node)
54
- if node.inside_liquid_tag?
55
- markup = if node.whitespace_trimmed?
56
- "-%}"
57
- else
58
- "%}"
59
- end
54
+ unless node.inside_liquid_tag?
60
55
  if node.markup[-1] != " " && node.markup[-1] != "\n"
61
- add_offense("Space missing before '#{markup}'", node: node, markup: node.markup[-1] + markup)
56
+ add_offense(
57
+ "Space missing before '#{node.end_token}'",
58
+ node: node,
59
+ markup: node.markup[-1],
60
+ node_markup_offset: node.markup.size - 1,
61
+ )
62
62
  elsif node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
63
- add_offense("Too many spaces before '#{markup}'", node: node, markup: Regexp.last_match(2) + markup)
63
+ add_offense(
64
+ "Too many spaces before '#{node.end_token}'",
65
+ node: node,
66
+ markup: Regexp.last_match(2),
67
+ node_markup_offset: node.markup.size - Regexp.last_match(2).size
68
+ )
64
69
  end
65
70
  end
66
71
  @ignore = true
@@ -73,22 +78,40 @@ module ThemeCheck
73
78
  def on_variable(node)
74
79
  return if @ignore || node.markup.empty?
75
80
  if node.markup[0] != " "
76
- add_offense("Space missing after '{{'", node: node) do |corrector|
81
+ add_offense(
82
+ "Space missing after '#{node.start_token}'",
83
+ node: node,
84
+ markup: node.markup[0]
85
+ ) do |corrector|
77
86
  corrector.insert_before(node, " ")
78
87
  end
79
88
  end
80
89
  if node.markup[-1] != " " && node.markup[-1] != "\n"
81
- add_offense("Space missing before '}}'", node: node) do |corrector|
90
+ add_offense(
91
+ "Space missing before '#{node.end_token}'",
92
+ node: node,
93
+ markup: node.markup[-1],
94
+ node_markup_offset: node.markup.size - 1,
95
+ ) do |corrector|
82
96
  corrector.insert_after(node, " ")
83
97
  end
84
98
  end
85
- if node.markup[0] == " " && node.markup[1] == " "
86
- add_offense("Too many spaces after '{{'", node: node) do |corrector|
99
+ if node.markup =~ /\A( +)/m
100
+ add_offense(
101
+ "Too many spaces after '#{node.start_token}'",
102
+ node: node,
103
+ markup: Regexp.last_match(1),
104
+ ) do |corrector|
87
105
  corrector.replace(node, " #{node.markup.lstrip}")
88
106
  end
89
107
  end
90
- if node.markup[-1] == " " && node.markup[-2] == " "
91
- add_offense("Too many spaces before '}}'", node: node) do |corrector|
108
+ if node.markup =~ /(\n?)( +)\z/m && Regexp.last_match(1) != "\n"
109
+ add_offense(
110
+ "Too many spaces before '#{node.end_token}'",
111
+ node: node,
112
+ markup: Regexp.last_match(2),
113
+ node_markup_offset: node.markup.size - Regexp.last_match(2).size
114
+ ) do |corrector|
92
115
  corrector.replace(node, "#{node.markup.rstrip} ")
93
116
  end
94
117
  end
@@ -36,5 +36,9 @@ module ThemeCheck
36
36
  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
37
37
  theme.default_locale_json.update_contents('{}')
38
38
  end
39
+
40
+ def mkdir(theme, relative_path)
41
+ theme.storage.mkdir(relative_path)
42
+ end
39
43
  end
40
44
  end
@@ -26,6 +26,12 @@ module ThemeCheck
26
26
  file(relative_path).write(content)
27
27
  end
28
28
 
29
+ def mkdir(relative_path)
30
+ reset_memoizers unless file_exists?(relative_path)
31
+
32
+ file(relative_path).mkpath unless file(relative_path).directory?
33
+ end
34
+
29
35
  def files
30
36
  @file_array ||= glob("**/*")
31
37
  .map { |path| path.relative_path_from(@root).to_s }
@@ -23,6 +23,10 @@ module ThemeCheck
23
23
  @files[relative_path] = content
24
24
  end
25
25
 
26
+ def mkdir(relative_path)
27
+ @files[relative_path] = nil
28
+ end
29
+
26
30
  def files
27
31
  @files.keys
28
32
  end
@@ -10,12 +10,14 @@ module ThemeCheck
10
10
  @value = value
11
11
  @parent = parent
12
12
  @template = template
13
+ @tag_markup = nil
14
+ @line_number_offset = 0
13
15
  end
14
16
 
15
17
  # The original source code of the node. Doesn't contain wrapping braces.
16
18
  def markup
17
19
  if tag?
18
- @value.raw
20
+ tag_markup
19
21
  elsif @value.instance_variable_defined?(:@markup)
20
22
  @value.instance_variable_get(:@markup)
21
23
  end
@@ -64,6 +66,10 @@ module ThemeCheck
64
66
  @value.is_a?(Liquid::Tag)
65
67
  end
66
68
 
69
+ def variable?
70
+ @value.is_a?(Liquid::Variable)
71
+ end
72
+
67
73
  # A {% comment %} block node?
68
74
  def comment?
69
75
  @value.is_a?(Liquid::Comment)
@@ -92,7 +98,12 @@ module ThemeCheck
92
98
 
93
99
  # Most nodes have a line number, but it's not guaranteed.
94
100
  def line_number
95
- @value.line_number if @value.respond_to?(:line_number)
101
+ if tag? && @value.respond_to?(:line_number)
102
+ markup # initialize the line_number_offset
103
+ @value.line_number - @line_number_offset
104
+ elsif @value.respond_to?(:line_number)
105
+ @value.line_number
106
+ end
96
107
  end
97
108
 
98
109
  # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
@@ -101,19 +112,45 @@ module ThemeCheck
101
112
  @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
102
113
  end
103
114
 
115
+ def source
116
+ template&.source
117
+ end
118
+
119
+ WHITESPACE = /\s/
120
+
104
121
  # Is this node inside a `{% liquid ... %}` block?
105
122
  def inside_liquid_tag?
106
- if line_number
107
- template.excerpt(line_number).start_with?("{%")
123
+ # What we're doing here is starting at the start of the tag and
124
+ # backtrack on all the whitespace until we land on something. If
125
+ # that something is {% or %-, then we can safely assume that
126
+ # we're inside a full tag and not a liquid tag.
127
+ @inside_liquid_tag ||= if tag? && line_number && source
128
+ i = 1
129
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
130
+ first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
131
+ first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
132
+ else
133
+ false
134
+ end
135
+ end
136
+
137
+ # Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
138
+ def whitespace_trimmed_start?
139
+ @whitespace_trimmed_start ||= if line_number && source && !inside_liquid_tag?
140
+ i = 1
141
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
142
+ source[start_index - i] == "-"
108
143
  else
109
144
  false
110
145
  end
111
146
  end
112
147
 
113
- # Is this node inside a `{%- ... -%}`
114
- def whitespace_trimmed?
115
- if line_number
116
- template.excerpt(line_number).start_with?("{%-")
148
+ # Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
149
+ def whitespace_trimmed_end?
150
+ @whitespace_trimmed_end ||= if line_number && source && !inside_liquid_tag?
151
+ i = 0
152
+ i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
153
+ source[end_index + i] == "-"
117
154
  else
118
155
  false
119
156
  end
@@ -132,6 +169,24 @@ module ThemeCheck
132
169
  )
133
170
  end
134
171
 
172
+ def start_token
173
+ return "" if inside_liquid_tag?
174
+ output = ""
175
+ output += "{{" if variable?
176
+ output += "{%" if tag?
177
+ output += "-" if whitespace_trimmed_start?
178
+ output
179
+ end
180
+
181
+ def end_token
182
+ return "" if inside_liquid_tag?
183
+ output = ""
184
+ output += "-" if whitespace_trimmed_end?
185
+ output += "}}" if variable?
186
+ output += "%}" if tag?
187
+ output
188
+ end
189
+
135
190
  def start_index
136
191
  position.start_index
137
192
  end
@@ -139,5 +194,62 @@ module ThemeCheck
139
194
  def end_index
140
195
  position.end_index
141
196
  end
197
+
198
+ private
199
+
200
+ # Here we're hacking around a glorious bug in Liquid that makes it so the
201
+ # line_number and markup of a tag is wrong if there's whitespace
202
+ # between the tag_name and the markup of the tag.
203
+ #
204
+ # {%
205
+ # render
206
+ # 'foo'
207
+ # %}
208
+ #
209
+ # Returns a raw value of "render 'foo'\n".
210
+ # The "\n " between render and 'foo' got replaced by a single space.
211
+ #
212
+ # And the line number is the one of 'foo'\n%}. Yay!
213
+ #
214
+ # This breaks any kind of position logic we have since that string
215
+ # does not exist in the template.
216
+ def tag_markup
217
+ return @value.raw if @value.instance_variable_get('@markup').empty?
218
+ return @tag_markup if @tag_markup
219
+
220
+ l = 1
221
+ scanner = StringScanner.new(source)
222
+ scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
223
+ start = scanner.charpos
224
+
225
+ tag_markup = @value.instance_variable_get('@markup')
226
+
227
+ # See https://github.com/Shopify/theme-check/pull/423/files#r701936559 for a detailed explanation
228
+ # of why we're doing the check below.
229
+ #
230
+ # TL;DR it's because line_numbers are not enough to accurately
231
+ # determine the position of the raw markup and because that
232
+ # markup could be present on the same line outside of a Tag. e.g.
233
+ #
234
+ # uhoh {% if uhoh %}
235
+ if (match = /#{@value.tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
236
+ return @tag_markup = match[0]
237
+ end
238
+
239
+ # find the markup
240
+ markup_start = source.index(tag_markup, start)
241
+ markup_end = markup_start + tag_markup.size
242
+
243
+ # go back until you find the tag_name
244
+ tag_start = markup_start
245
+ tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
246
+ tag_start -= @value.tag_name.size
247
+
248
+ # keep track of the error in line_number
249
+ @line_number_offset = source[tag_start...markup_start].count("\n")
250
+
251
+ # return the real raw content
252
+ @tag_markup = source[tag_start...markup_end]
253
+ end
142
254
  end
143
255
  end
@@ -16,22 +16,22 @@ module ThemeCheck
16
16
  @line_number_1_indexed = line_number_1_indexed
17
17
  @node_markup_offset = node_markup_offset
18
18
  @node_markup = node_markup
19
- @strict_position = StrictPosition.new(
20
- needle,
21
- contents,
22
- start_index,
23
- )
24
19
  end
25
20
 
26
21
  def start_line_offset
27
- from_row_column_to_index(contents, line_number, 0)
22
+ @start_line_offset ||= from_row_column_to_index(contents, line_number, 0)
28
23
  end
29
24
 
30
25
  def start_offset
31
- return start_line_offset if @node_markup.nil?
32
- node_markup_start = contents.index(@node_markup, start_line_offset)
33
- return start_line_offset if node_markup_start.nil?
34
- node_markup_start + @node_markup_offset
26
+ @start_offset ||= compute_start_offset
27
+ end
28
+
29
+ def strict_position
30
+ @strict_position ||= StrictPosition.new(
31
+ needle,
32
+ contents,
33
+ start_index,
34
+ )
35
35
  end
36
36
 
37
37
  # 0-indexed, inclusive
@@ -41,39 +41,50 @@ module ThemeCheck
41
41
 
42
42
  # 0-indexed, exclusive
43
43
  def end_index
44
- @strict_position.end_index
44
+ strict_position.end_index
45
45
  end
46
46
 
47
47
  # 0-indexed, inclusive
48
48
  def start_row
49
- @strict_position.start_row
49
+ strict_position.start_row
50
50
  end
51
51
 
52
52
  # 0-indexed, inclusive
53
53
  def start_column
54
- @strict_position.start_column
54
+ strict_position.start_column
55
55
  end
56
56
 
57
57
  # 0-indexed, exclusive (both taken together are) therefore you
58
58
  # might end up on a newline character or the next line
59
59
  def end_row
60
- @strict_position.end_row
60
+ strict_position.end_row
61
61
  end
62
62
 
63
63
  def end_column
64
- @strict_position.end_column
64
+ strict_position.end_column
65
65
  end
66
66
 
67
67
  private
68
68
 
69
+ def compute_start_offset
70
+ return start_line_offset if @node_markup.nil?
71
+ node_markup_start = contents.index(@node_markup, start_line_offset)
72
+ return start_line_offset if node_markup_start.nil?
73
+ node_markup_start + @node_markup_offset
74
+ end
75
+
69
76
  def contents
70
77
  return '' unless @contents.is_a?(String) && !@contents.empty?
71
78
  @contents
72
79
  end
73
80
 
81
+ def content_line_count
82
+ @content_line_count ||= contents.count("\n")
83
+ end
84
+
74
85
  def line_number
75
86
  return 0 if @line_number_1_indexed.nil?
76
- bounded(0, @line_number_1_indexed - 1, contents.lines.size - 1)
87
+ bounded(0, @line_number_1_indexed - 1, content_line_count)
77
88
  end
78
89
 
79
90
  def needle
@@ -7,31 +7,29 @@ module ThemeCheck
7
7
  return 0 unless content.is_a?(String) && !content.empty?
8
8
  return 0 unless row.is_a?(Integer) && col.is_a?(Integer)
9
9
  i = 0
10
- result = 0
11
- safe_row = bounded(0, row, content.lines.size - 1)
12
- lines = content.lines
13
- line_size = lines[i].size
14
- while i < safe_row
15
- result += line_size
16
- i += 1
17
- line_size = lines[i].size
18
- end
19
- result += bounded(0, col, line_size - 1)
20
- result
10
+ safe_row = bounded(0, row, content.count("\n"))
11
+ scanner = StringScanner.new(content)
12
+ scanner.scan_until(/\n/) while i < safe_row && (i += 1)
13
+ result = scanner.charpos || 0
14
+ scanner.scan_until(/\n|\z/)
15
+ bounded(result, result + col, scanner.pre_match.size)
21
16
  end
22
17
 
23
18
  def from_index_to_row_column(content, index)
24
19
  return [0, 0] unless content.is_a?(String) && !content.empty?
25
20
  return [0, 0] unless index.is_a?(Integer)
26
21
  safe_index = bounded(0, index, content.size - 1)
27
- lines = content[0..safe_index].lines
28
- row = lines.size - 1
29
- col = lines.last.size - 1
22
+ content_up_to_index = content[0...safe_index]
23
+ row = content_up_to_index.count("\n")
24
+ col = 0
25
+ col += 1 while (safe_index -= 1) && safe_index >= 0 && content[safe_index] != "\n"
30
26
  [row, col]
31
27
  end
32
28
 
33
29
  def bounded(a, x, b)
34
- [a, [x, b].min].max
30
+ return a if x < a
31
+ return b if x > b
32
+ x
35
33
  end
36
34
  end
37
35
  end
@@ -17,6 +17,8 @@ module ThemeCheck
17
17
 
18
18
  def uri(src)
19
19
  URI.parse(src.sub(%r{^//}, "https://"))
20
+ rescue URI::InvalidURIError
21
+ nil
20
22
  end
21
23
  end
22
24
 
@@ -26,6 +28,7 @@ module ThemeCheck
26
28
  end
27
29
 
28
30
  def content
31
+ return if @uri.nil?
29
32
  return @content unless @content.nil?
30
33
 
31
34
  res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == 'https') do |http|
@@ -41,6 +44,7 @@ module ThemeCheck
41
44
  end
42
45
 
43
46
  def gzipped_size
47
+ return if @uri.nil?
44
48
  @gzipped_size ||= content.bytesize
45
49
  end
46
50
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.5.1"
3
+ VERSION = "1.5.2"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer