theme-check 1.3.0 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +38 -0
  5. data/CONTRIBUTING.md +58 -0
  6. data/Gemfile +3 -0
  7. data/config/default.yml +4 -1
  8. data/data/shopify_liquid/objects.yml +1 -0
  9. data/docs/checks/deprecate_lazysizes.md +0 -3
  10. data/docs/checks/deprecated_global_app_block_type.md +65 -0
  11. data/docs/checks/template_length.md +1 -1
  12. data/docs/flamegraph.svg +18488 -0
  13. data/lib/theme_check/analyzer.rb +1 -0
  14. data/lib/theme_check/checks/default_locale.rb +3 -1
  15. data/lib/theme_check/checks/deprecate_lazysizes.rb +6 -3
  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/pagination_size.rb +33 -14
  19. data/lib/theme_check/checks/remote_asset.rb +2 -2
  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/template_length.rb +1 -1
  23. data/lib/theme_check/cli.rb +28 -5
  24. data/lib/theme_check/corrector.rb +9 -0
  25. data/lib/theme_check/file_system_storage.rb +6 -0
  26. data/lib/theme_check/in_memory_storage.rb +4 -0
  27. data/lib/theme_check/json_file.rb +11 -0
  28. data/lib/theme_check/json_printer.rb +6 -1
  29. data/lib/theme_check/language_server/constants.rb +18 -11
  30. data/lib/theme_check/language_server/document_link_engine.rb +3 -67
  31. data/lib/theme_check/language_server/document_link_provider.rb +71 -0
  32. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  33. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  34. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  35. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  36. data/lib/theme_check/language_server/handler.rb +17 -13
  37. data/lib/theme_check/language_server/server.rb +11 -13
  38. data/lib/theme_check/language_server/uri_helper.rb +37 -0
  39. data/lib/theme_check/language_server.rb +6 -0
  40. data/lib/theme_check/node.rb +120 -8
  41. data/lib/theme_check/position.rb +27 -16
  42. data/lib/theme_check/position_helper.rb +13 -15
  43. data/lib/theme_check/printer.rb +9 -5
  44. data/lib/theme_check/remote_asset_file.rb +4 -0
  45. data/lib/theme_check/theme.rb +2 -1
  46. data/lib/theme_check/version.rb +1 -1
  47. metadata +11 -2
@@ -87,6 +87,7 @@ module ThemeCheck
87
87
  if @auto_correct
88
88
  offenses.each(&:correct)
89
89
  @theme.liquid.each(&:write)
90
+ @theme.json.each(&:write)
90
91
  end
91
92
  end
92
93
 
@@ -7,7 +7,9 @@ module ThemeCheck
7
7
 
8
8
  def on_end
9
9
  return if @theme.default_locale_json
10
- add_offense("Default translation file not found (for example locales/en.default.json)")
10
+ add_offense("Default translation file not found (for example locales/en.default.json)") do |corrector|
11
+ corrector.create_default_locale_json(@theme)
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -7,10 +7,13 @@ module ThemeCheck
7
7
 
8
8
  def on_img(node)
9
9
  class_list = node.attributes["class"]&.split(" ")
10
+ has_loading_lazy = node.attributes["loading"] == "lazy"
11
+ has_native_source = node.attributes["src"] || node.attributes["srcset"]
12
+ return if has_native_source && has_loading_lazy
13
+ has_lazysize_source = node.attributes["data-srcset"] || node.attributes["data-src"]
14
+ has_lazysize_class = class_list&.include?("lazyload")
15
+ return unless has_lazysize_class && has_lazysize_source
10
16
  add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node) if class_list&.include?("lazyload")
11
- add_offense("Use the native srcset attribute instead of data-srcset", node: node) if node.attributes["data-srcset"]
12
- add_offense("Use the native sizes attribute instead of data-sizes", node: node) if node.attributes["data-sizes"]
13
- add_offense("Do not set the data-sizes attribute to auto", node: node) if node.attributes["data-sizes"] == "auto"
14
17
  end
15
18
  end
16
19
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class DeprecatedGlobalAppBlockType < LiquidCheck
4
+ severity :error
5
+ category :liquid
6
+ doc docs_url(__FILE__)
7
+
8
+ INVALID_GLOBAL_APP_BLOCK_TYPE = "@global"
9
+ VALID_GLOBAL_APP_BLOCK_TYPE = "@app"
10
+
11
+ def on_schema(node)
12
+ schema = JSON.parse(node.value.nodelist.join)
13
+
14
+ if block_types_from(schema).include?(INVALID_GLOBAL_APP_BLOCK_TYPE)
15
+ add_offense(
16
+ "Deprecated '#{INVALID_GLOBAL_APP_BLOCK_TYPE}' block type defined in the schema, use '#{VALID_GLOBAL_APP_BLOCK_TYPE}' block type instead.",
17
+ node: node
18
+ )
19
+ end
20
+ rescue JSON::ParserError
21
+ # Ignored, handled in ValidSchema.
22
+ end
23
+
24
+ def on_case(node)
25
+ if node.value == INVALID_GLOBAL_APP_BLOCK_TYPE
26
+ report_offense(node)
27
+ end
28
+ end
29
+
30
+ def on_condition(node)
31
+ if node.value.right == INVALID_GLOBAL_APP_BLOCK_TYPE || node.value.left == INVALID_GLOBAL_APP_BLOCK_TYPE
32
+ report_offense(node)
33
+ end
34
+ end
35
+
36
+ def on_variable(node)
37
+ if node.value.name == INVALID_GLOBAL_APP_BLOCK_TYPE
38
+ report_offense(node)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def report_offense(node)
45
+ add_offense(
46
+ "Deprecated '#{INVALID_GLOBAL_APP_BLOCK_TYPE}' block type, use '#{VALID_GLOBAL_APP_BLOCK_TYPE}' block type instead.",
47
+ node: node
48
+ )
49
+ end
50
+
51
+ def block_types_from(schema)
52
+ schema.fetch("blocks", []).map do |block|
53
+ block.fetch("type", "")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -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?
@@ -36,16 +36,32 @@ module ThemeCheck
36
36
  # Ignored, handled in ValidSchema.
37
37
  end
38
38
 
39
+ ##
40
+ # Section settings look like:
41
+ # #<Liquid::VariableLookup:0x00007fd699c50c48 @name="section", @lookups=["settings", "products_per_page"], @command_flags=0>
42
+ def size_is_a_section_setting?(size)
43
+ size.is_a?(Liquid::VariableLookup) &&
44
+ size.name == 'section' &&
45
+ size.lookups.first == 'settings'
46
+ end
47
+
48
+ ##
49
+ # We'll work with either an explicit value, or the default value of the section setting.
50
+ def get_value(size)
51
+ return size if size.is_a?(Numeric)
52
+ return get_setting_default_value(size) if size_is_a_section_setting?(size)
53
+ end
54
+
39
55
  def after_document(_node)
40
56
  @paginations.each_pair do |size, nodes|
41
- numerical_size = if size.is_a?(Numeric)
42
- size
43
- else
44
- get_setting_default_value(size.lookups.last)
45
- end
46
- if numerical_size.nil?
57
+ # Validate presence of default section setting.
58
+ if size_is_a_section_setting?(size) && !get_setting_default_value(size)
47
59
  nodes.each { |node| add_offense("Default pagination size should be defined in the section settings", node: node) }
48
- elsif numerical_size > @max_size || numerical_size < @min_size || !numerical_size.is_a?(Integer)
60
+ end
61
+
62
+ # Validate if size is within range.
63
+ next unless (numerical_size = get_value(size))
64
+ if numerical_size > @max_size || numerical_size < @min_size || !numerical_size.is_a?(Integer)
49
65
  nodes.each { |node| add_offense("Pagination size must be a positive integer between #{@min_size} and #{@max_size}", node: node) }
50
66
  end
51
67
  end
@@ -53,13 +69,16 @@ module ThemeCheck
53
69
 
54
70
  private
55
71
 
56
- def get_setting_default_value(setting_id)
57
- setting = @schema_settings.select { |s| s['id'] == setting_id }
58
- unless setting.empty?
59
- return setting.last['default']
60
- end
61
- # Setting does not exist
62
- nil
72
+ def get_setting_default_value(variable_lookup)
73
+ setting = @schema_settings.select { |s| s['id'] == variable_lookup.lookups.last }
74
+
75
+ # Setting does not exist.
76
+ return nil if setting.empty?
77
+
78
+ default_value = setting.last['default'].to_i
79
+ return nil if default_value == 0
80
+
81
+ default_value
63
82
  end
64
83
  end
65
84
  end
@@ -23,9 +23,9 @@ module ThemeCheck
23
23
  return if resource_url =~ RELATIVE_PATH
24
24
  return if url_hosted_by_shopify?(resource_url)
25
25
 
26
- # Ignore non-stylesheet rel tags
26
+ # Ignore non-stylesheet link tags
27
27
  rel = node.attributes["rel"]
28
- return if rel && rel != "stylesheet"
28
+ return if node.name == "link" && rel != "stylesheet"
29
29
 
30
30
  add_offense(
31
31
  "Asset should be served by the Shopify CDN for better performance.",
@@ -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
@@ -5,7 +5,7 @@ module ThemeCheck
5
5
  category :liquid
6
6
  doc docs_url(__FILE__)
7
7
 
8
- def initialize(max_length: 500, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
8
+ def initialize(max_length: 600, exclude_schema: true, exclude_stylesheet: true, exclude_javascript: true)
9
9
  @max_length = max_length
10
10
  @exclude_schema = exclude_schema
11
11
  @exclude_stylesheet = exclude_stylesheet
@@ -78,6 +78,15 @@ module ThemeCheck
78
78
  "Print Theme Check version"
79
79
  ) { @command = :version }
80
80
 
81
+ if ENV["THEME_CHECK_DEBUG"]
82
+ @option_parser.separator("")
83
+ @option_parser.separator("Debugging:")
84
+ @option_parser.on(
85
+ "--profile",
86
+ "Output a profile to STDOUT compatible with FlameGraph."
87
+ ) { @command = :profile }
88
+ end
89
+
81
90
  @option_parser.separator("")
82
91
  @option_parser.separator(<<~EOS)
83
92
  Description:
@@ -172,7 +181,7 @@ module ThemeCheck
172
181
  puts option_parser.to_s
173
182
  end
174
183
 
175
- def check
184
+ def check(out_stream = STDOUT)
176
185
  STDERR.puts "Checking #{@config.root} ..."
177
186
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
178
187
  theme = ThemeCheck::Theme.new(storage)
@@ -182,18 +191,32 @@ module ThemeCheck
182
191
  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
183
192
  analyzer.analyze_theme
184
193
  analyzer.correct_offenses
185
- output_with_format(theme, analyzer)
194
+ output_with_format(theme, analyzer, out_stream)
186
195
  raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
187
196
  offense.check.severity_value <= Check.severity_value(@fail_level)
188
197
  end
189
198
  end
190
199
 
191
- def output_with_format(theme, analyzer)
200
+ def profile
201
+ require 'ruby-prof-flamegraph'
202
+
203
+ result = RubyProf.profile do
204
+ check(STDERR)
205
+ end
206
+
207
+ # Print a graph profile to text
208
+ printer = RubyProf::FlameGraphPrinter.new(result)
209
+ printer.print(STDOUT, {})
210
+ rescue LoadError
211
+ STDERR.puts "Profiling is only available in development"
212
+ end
213
+
214
+ def output_with_format(theme, analyzer, out_stream)
192
215
  case @format
193
216
  when :text
194
- ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
217
+ ThemeCheck::Printer.new(out_stream).print(theme, analyzer.offenses, @config.auto_correct)
195
218
  when :json
196
- ThemeCheck::JsonPrinter.new.print(analyzer.offenses)
219
+ ThemeCheck::JsonPrinter.new(out_stream).print(analyzer.offenses)
197
220
  end
198
221
  end
199
222
  end
@@ -31,5 +31,14 @@ module ThemeCheck
31
31
  def create(theme, relative_path, content)
32
32
  theme.storage.write(relative_path, content)
33
33
  end
34
+
35
+ def create_default_locale_json(theme)
36
+ theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
37
+ theme.default_locale_json.update_contents('{}')
38
+ end
39
+
40
+ def mkdir(theme, relative_path)
41
+ theme.storage.mkdir(relative_path)
42
+ end
34
43
  end
35
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
@@ -20,6 +20,17 @@ module ThemeCheck
20
20
  @parser_error
21
21
  end
22
22
 
23
+ def update_contents(new_content = '{}')
24
+ @content = new_content
25
+ end
26
+
27
+ def write
28
+ if source != @content
29
+ @storage.write(@relative_path, content)
30
+ @source = content
31
+ end
32
+ end
33
+
23
34
  def json?
24
35
  true
25
36
  end