theme-check 1.4.0 → 1.6.0

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +14 -6
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +42 -0
  5. data/CONTRIBUTING.md +58 -0
  6. data/Gemfile +3 -0
  7. data/config/default.yml +3 -0
  8. data/docs/checks/deprecated_global_app_block_type.md +65 -0
  9. data/docs/flamegraph.svg +18488 -0
  10. data/lib/theme_check/analyzer.rb +5 -0
  11. data/lib/theme_check/asset_file.rb +13 -2
  12. data/lib/theme_check/check.rb +1 -1
  13. data/lib/theme_check/checks/asset_size_css.rb +15 -0
  14. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +18 -1
  15. data/lib/theme_check/checks/convert_include_to_render.rb +2 -1
  16. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +57 -0
  17. data/lib/theme_check/checks/liquid_tag.rb +1 -1
  18. data/lib/theme_check/checks/missing_required_template_files.rb +21 -7
  19. data/lib/theme_check/checks/pagination_size.rb +33 -14
  20. data/lib/theme_check/checks/required_directories.rb +3 -1
  21. data/lib/theme_check/checks/space_inside_braces.rb +47 -24
  22. data/lib/theme_check/checks/translation_key_exists.rb +3 -1
  23. data/lib/theme_check/checks/unused_snippet.rb +3 -1
  24. data/lib/theme_check/cli.rb +32 -6
  25. data/lib/theme_check/corrector.rb +23 -10
  26. data/lib/theme_check/file_system_storage.rb +13 -2
  27. data/lib/theme_check/html_node.rb +4 -4
  28. data/lib/theme_check/html_visitor.rb +20 -8
  29. data/lib/theme_check/in_memory_storage.rb +8 -0
  30. data/lib/theme_check/json_file.rb +9 -4
  31. data/lib/theme_check/json_printer.rb +6 -1
  32. data/lib/theme_check/language_server/document_link_provider.rb +2 -1
  33. data/lib/theme_check/language_server/handler.rb +16 -11
  34. data/lib/theme_check/language_server/server.rb +11 -13
  35. data/lib/theme_check/language_server/uri_helper.rb +37 -0
  36. data/lib/theme_check/language_server.rb +1 -0
  37. data/lib/theme_check/node.rb +118 -11
  38. data/lib/theme_check/offense.rb +26 -0
  39. data/lib/theme_check/position.rb +27 -16
  40. data/lib/theme_check/position_helper.rb +13 -15
  41. data/lib/theme_check/printer.rb +9 -5
  42. data/lib/theme_check/regex_helpers.rb +1 -15
  43. data/lib/theme_check/remote_asset_file.rb +4 -0
  44. data/lib/theme_check/template.rb +5 -19
  45. data/lib/theme_check/template_rewriter.rb +57 -0
  46. data/lib/theme_check/theme_file.rb +18 -1
  47. data/lib/theme_check/version.rb +1 -1
  48. data/lib/theme_check.rb +1 -0
  49. data/theme-check.gemspec +1 -0
  50. metadata +21 -2
@@ -144,6 +144,32 @@ module ThemeCheck
144
144
  corrector = Corrector.new(template: template)
145
145
  correction.call(corrector)
146
146
  end
147
+ rescue => e
148
+ ThemeCheck.bug(<<~EOS)
149
+ Exception while running `Offense#correct`:
150
+ ```
151
+ #{e.class}: #{e.message}
152
+ #{e.backtrace.join("\n ")}
153
+ ```
154
+
155
+ Offense:
156
+ ```
157
+ #{JSON.pretty_generate(to_h)}
158
+ ```
159
+ Check options:
160
+ ```
161
+ #{check.options.pretty_inspect}
162
+ ```
163
+ Markup:
164
+ ```
165
+ #{markup}
166
+ ```
167
+ Node.Markup:
168
+ ```
169
+ #{node&.markup}
170
+ ```
171
+ EOS
172
+ exit(2)
147
173
  end
148
174
 
149
175
  def whole_theme?
@@ -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
@@ -2,14 +2,18 @@
2
2
 
3
3
  module ThemeCheck
4
4
  class Printer
5
+ def initialize(out_stream = STDOUT)
6
+ @out = out_stream
7
+ end
8
+
5
9
  def print(theme, offenses, auto_correct)
6
10
  offenses.each do |offense|
7
11
  print_offense(offense, auto_correct)
8
- puts
12
+ @out.puts
9
13
  end
10
14
 
11
15
  correctable = offenses.select(&:correctable?)
12
- puts "#{theme.all.size} files inspected, #{red(offenses.size.to_s + ' offenses')} detected, \
16
+ @out.puts "#{theme.all.size} files inspected, #{red(offenses.size.to_s + ' offenses')} detected, \
13
17
  #{yellow(correctable.size.to_s + ' offenses')} #{auto_correct ? 'corrected' : 'auto-correctable'}"
14
18
  end
15
19
 
@@ -26,15 +30,15 @@ module ThemeCheck
26
30
  ""
27
31
  end
28
32
 
29
- puts location +
33
+ @out.puts location +
30
34
  colorized_severity(offense.severity) + ": " +
31
35
  yellow(offense.check_name) + ": " +
32
36
  corrected +
33
37
  offense.message + "."
34
38
  if offense.source_excerpt
35
- puts "\t#{offense.source_excerpt}"
39
+ @out.puts "\t#{offense.source_excerpt}"
36
40
  if offense.markup_start_in_excerpt
37
- puts "\t" + (" " * offense.markup_start_in_excerpt) + ("^" * offense.markup.size)
41
+ @out.puts "\t" + (" " * offense.markup_start_in_excerpt) + ("^" * offense.markup.size)
38
42
  end
39
43
  end
40
44
  end
@@ -5,6 +5,7 @@ module ThemeCheck
5
5
  LIQUID_TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
6
6
  LIQUID_VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
7
7
  LIQUID_TAG_OR_VARIABLE = /#{LIQUID_TAG}|#{LIQUID_VARIABLE}/om
8
+ HTML_LIQUID_PLACEHOLDER = /≬[0-9a-z]+#*≬/m
8
9
  START_OR_END_QUOTE = /(^['"])|(['"]$)/
9
10
 
10
11
  def matches(s, re)
@@ -16,20 +17,5 @@ module ThemeCheck
16
17
  end
17
18
  matches
18
19
  end
19
-
20
- def href_to_file_size(href)
21
- # asset_url (+ optional stylesheet_tag) variables
22
- if href =~ /^#{LIQUID_VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
23
- asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
24
- asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
25
- return if asset.nil?
26
- asset.gzipped_size
27
-
28
- # remote URLs
29
- elsif href =~ %r{^(https?:)?//}
30
- asset = RemoteAssetFile.from_src(href)
31
- asset.gzipped_size
32
- end
33
- end
34
20
  end
35
21
  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
@@ -3,10 +3,11 @@
3
3
  module ThemeCheck
4
4
  class Template < ThemeFile
5
5
  def write
6
- content = updated_content
6
+ content = rewriter.to_s
7
7
  if source != content
8
- @storage.write(@relative_path, content)
8
+ @storage.write(@relative_path, content.gsub("\n", @eol))
9
9
  @source = content
10
+ @rewriter = nil
10
11
  end
11
12
  end
12
13
 
@@ -26,19 +27,8 @@ module ThemeCheck
26
27
  name.start_with?('snippets')
27
28
  end
28
29
 
29
- def lines
30
- # Retain trailing newline character
31
- @lines ||= source.split("\n", -1)
32
- end
33
-
34
- # Not entirely obvious but lines is mutable, corrections are to be
35
- # applied on @lines.
36
- def updated_content
37
- lines.join("\n")
38
- end
39
-
40
- def excerpt(line)
41
- lines[line - 1].strip
30
+ def rewriter
31
+ @rewriter ||= TemplateRewriter.new(@relative_path, source)
42
32
  end
43
33
 
44
34
  def source_excerpt(line)
@@ -46,10 +36,6 @@ module ThemeCheck
46
36
  original_lines[line - 1].strip
47
37
  end
48
38
 
49
- def full_line(line)
50
- lines[line - 1]
51
- end
52
-
53
39
  def parse
54
40
  @ast ||= self.class.parse(source)
55
41
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser'
4
+
5
+ module ThemeCheck
6
+ class TemplateRewriter
7
+ def initialize(name, source)
8
+ @buffer = Parser::Source::Buffer.new(name, source: source)
9
+ @rewriter = Parser::Source::TreeRewriter.new(
10
+ @buffer
11
+ )
12
+ end
13
+
14
+ def insert_before(node, content)
15
+ @rewriter.insert_before(
16
+ range(node.start_index, node.end_index),
17
+ content
18
+ )
19
+ end
20
+
21
+ def insert_after(node, content)
22
+ @rewriter.insert_after(
23
+ range(node.start_index, node.end_index),
24
+ content
25
+ )
26
+ end
27
+
28
+ def replace(node, content)
29
+ @rewriter.replace(
30
+ range(node.start_index, node.end_index),
31
+ content
32
+ )
33
+ end
34
+
35
+ def wrap(node, insert_before, insert_after)
36
+ @rewriter.wrap(
37
+ range(node.start_index, node.end_index),
38
+ insert_before,
39
+ insert_after,
40
+ )
41
+ end
42
+
43
+ def to_s
44
+ @rewriter.process
45
+ end
46
+
47
+ private
48
+
49
+ def range(start_index, end_index)
50
+ Parser::Source::Range.new(
51
+ @buffer,
52
+ start_index,
53
+ end_index,
54
+ )
55
+ end
56
+ end
57
+ end
@@ -6,6 +6,8 @@ module ThemeCheck
6
6
  def initialize(relative_path, storage)
7
7
  @relative_path = relative_path
8
8
  @storage = storage
9
+ @source = nil
10
+ @eol = "\n"
9
11
  end
10
12
 
11
13
  def path
@@ -20,8 +22,23 @@ module ThemeCheck
20
22
  relative_path.sub_ext('').to_s
21
23
  end
22
24
 
25
+ # For the corrector to work properly, we should have a
26
+ # simple mental model of the internal representation of eol
27
+ # characters (Windows uses \r\n, Linux uses \n).
28
+ #
29
+ # Parser::Source::Buffer strips the \r from the source file, so if
30
+ # you are autocorrecting the file you might lose that info and
31
+ # cause a git diff. It also makes the node.start_index/end_index
32
+ # calculation break. That's not cool.
33
+ #
34
+ # So in here we track whether the source file has \r\n in it and
35
+ # we'll make sure that the file we write has the same eol as the
36
+ # source file.
23
37
  def source
24
- @source ||= @storage.read(@relative_path)
38
+ return @source if @source
39
+ @source = @storage.read(@relative_path)
40
+ @eol = @source.include?("\r\n") ? "\r\n" : "\n"
41
+ @source = @source.gsub("\r\n", "\n")
25
42
  end
26
43
 
27
44
  def json?
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.4.0"
3
+ VERSION = "1.6.0"
4
4
  end
data/lib/theme_check.rb CHANGED
@@ -34,6 +34,7 @@ require_relative "theme_check/string_helpers"
34
34
  require_relative "theme_check/file_system_storage"
35
35
  require_relative "theme_check/in_memory_storage"
36
36
  require_relative "theme_check/tags"
37
+ require_relative "theme_check/template_rewriter"
37
38
  require_relative "theme_check/template"
38
39
  require_relative "theme_check/theme"
39
40
  require_relative "theme_check/visitor"
data/theme-check.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.add_dependency('liquid', '>= 5.0.1')
28
28
  spec.add_dependency('nokogiri', '>= 1.12')
29
+ spec.add_dependency('parser', '~> 3')
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-30 00:00:00.000000000 Z
11
+ date: 2021-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
41
55
  description:
42
56
  email:
43
57
  - marcandre.cournoyer@shopify.com
@@ -90,6 +104,7 @@ files:
90
104
  - docs/checks/deprecate_bgsizes.md
91
105
  - docs/checks/deprecate_lazysizes.md
92
106
  - docs/checks/deprecated_filter.md
107
+ - docs/checks/deprecated_global_app_block_type.md
93
108
  - docs/checks/html_parsing_error.md
94
109
  - docs/checks/img_lazy_loading.md
95
110
  - docs/checks/img_width_and_height.md
@@ -117,6 +132,7 @@ files:
117
132
  - docs/checks/valid_html_translation.md
118
133
  - docs/checks/valid_json.md
119
134
  - docs/checks/valid_schema.md
135
+ - docs/flamegraph.svg
120
136
  - docs/preview.png
121
137
  - exe/theme-check
122
138
  - exe/theme-check-language-server
@@ -140,6 +156,7 @@ files:
140
156
  - lib/theme_check/checks/deprecate_bgsizes.rb
141
157
  - lib/theme_check/checks/deprecate_lazysizes.rb
142
158
  - lib/theme_check/checks/deprecated_filter.rb
159
+ - lib/theme_check/checks/deprecated_global_app_block_type.rb
143
160
  - lib/theme_check/checks/html_parsing_error.rb
144
161
  - lib/theme_check/checks/img_lazy_loading.rb
145
162
  - lib/theme_check/checks/img_width_and_height.rb
@@ -203,6 +220,7 @@ files:
203
220
  - lib/theme_check/language_server/protocol.rb
204
221
  - lib/theme_check/language_server/server.rb
205
222
  - lib/theme_check/language_server/tokens.rb
223
+ - lib/theme_check/language_server/uri_helper.rb
206
224
  - lib/theme_check/language_server/variable_lookup_finder.rb
207
225
  - lib/theme_check/liquid_check.rb
208
226
  - lib/theme_check/locale_diff.rb
@@ -225,6 +243,7 @@ files:
225
243
  - lib/theme_check/string_helpers.rb
226
244
  - lib/theme_check/tags.rb
227
245
  - lib/theme_check/template.rb
246
+ - lib/theme_check/template_rewriter.rb
228
247
  - lib/theme_check/theme.rb
229
248
  - lib/theme_check/theme_file.rb
230
249
  - lib/theme_check/version.rb