theme-check 1.5.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +12 -4
  3. data/CHANGELOG.md +35 -0
  4. data/docs/api/html_check.md +7 -7
  5. data/docs/api/liquid_check.md +10 -10
  6. data/docs/checks/convert_include_to_render.md +1 -1
  7. data/docs/checks/missing_enable_comment.md +1 -1
  8. data/lib/theme_check/analyzer.rb +20 -15
  9. data/lib/theme_check/asset_file.rb +13 -2
  10. data/lib/theme_check/check.rb +3 -3
  11. data/lib/theme_check/checks/asset_size_css.rb +15 -0
  12. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +18 -1
  13. data/lib/theme_check/checks/convert_include_to_render.rb +2 -1
  14. data/lib/theme_check/checks/html_parsing_error.rb +2 -2
  15. data/lib/theme_check/checks/liquid_tag.rb +1 -1
  16. data/lib/theme_check/checks/matching_translations.rb +1 -1
  17. data/lib/theme_check/checks/missing_required_template_files.rb +21 -7
  18. data/lib/theme_check/checks/missing_template.rb +6 -6
  19. data/lib/theme_check/checks/nested_snippet.rb +2 -2
  20. data/lib/theme_check/checks/required_directories.rb +3 -1
  21. data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
  22. data/lib/theme_check/checks/space_inside_braces.rb +47 -24
  23. data/lib/theme_check/checks/syntax_error.rb +5 -5
  24. data/lib/theme_check/checks/template_length.rb +2 -2
  25. data/lib/theme_check/checks/translation_key_exists.rb +3 -1
  26. data/lib/theme_check/checks/undefined_object.rb +7 -7
  27. data/lib/theme_check/checks/unused_assign.rb +4 -4
  28. data/lib/theme_check/checks/unused_snippet.rb +8 -6
  29. data/lib/theme_check/checks/valid_json.rb +1 -1
  30. data/lib/theme_check/checks.rb +4 -2
  31. data/lib/theme_check/cli.rb +7 -4
  32. data/lib/theme_check/corrector.rb +25 -12
  33. data/lib/theme_check/disabled_check.rb +3 -3
  34. data/lib/theme_check/disabled_checks.rb +9 -9
  35. data/lib/theme_check/file_system_storage.rb +13 -2
  36. data/lib/theme_check/html_node.rb +40 -32
  37. data/lib/theme_check/html_visitor.rb +24 -12
  38. data/lib/theme_check/in_memory_storage.rb +9 -1
  39. data/lib/theme_check/json_check.rb +2 -2
  40. data/lib/theme_check/json_file.rb +9 -4
  41. data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
  42. data/lib/theme_check/{template.rb → liquid_file.rb} +6 -20
  43. data/lib/theme_check/liquid_node.rb +291 -0
  44. data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
  45. data/lib/theme_check/locale_diff.rb +5 -5
  46. data/lib/theme_check/node.rb +12 -118
  47. data/lib/theme_check/offense.rb +41 -15
  48. data/lib/theme_check/position.rb +28 -17
  49. data/lib/theme_check/position_helper.rb +13 -15
  50. data/lib/theme_check/regex_helpers.rb +1 -15
  51. data/lib/theme_check/remote_asset_file.rb +4 -0
  52. data/lib/theme_check/theme.rb +1 -1
  53. data/lib/theme_check/theme_file.rb +18 -1
  54. data/lib/theme_check/theme_file_rewriter.rb +57 -0
  55. data/lib/theme_check/version.rb +1 -1
  56. data/lib/theme_check.rb +11 -9
  57. data/theme-check.gemspec +2 -1
  58. metadata +22 -6
@@ -26,12 +26,12 @@ module ThemeCheck
26
26
  end
27
27
 
28
28
  def on_document(node)
29
- @templates[node.template.name] = TemplateInfo.new(Set.new)
29
+ @templates[node.theme_file.name] = TemplateInfo.new(Set.new)
30
30
  end
31
31
 
32
32
  def on_include(node)
33
33
  if node.value.template_name_expr.is_a?(String)
34
- @templates[node.template.name].includes << node
34
+ @templates[node.theme_file.name].includes << node
35
35
  end
36
36
  end
37
37
  alias_method :on_render, :on_include
@@ -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
@@ -14,7 +14,7 @@ module ThemeCheck
14
14
  end
15
15
 
16
16
  def on_document(node)
17
- @layout_theme_node = node if node.template.name == LAYOUT_FILENAME
17
+ @layout_theme_node = node if node.theme_file.name == LAYOUT_FILENAME
18
18
  end
19
19
 
20
20
  def on_variable(node)
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  end
26
26
 
27
27
  def after_document(node)
28
- return unless node.template.name == LAYOUT_FILENAME
28
+ return unless node.theme_file.name == LAYOUT_FILENAME
29
29
 
30
30
  add_missing_object_offense("content_for_layout") unless @content_for_layout_found
31
31
  add_missing_object_offense("content_for_header") unless @content_for_header_found
@@ -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
@@ -7,23 +7,23 @@ module ThemeCheck
7
7
  doc docs_url(__FILE__)
8
8
 
9
9
  def on_document(node)
10
- node.template.warnings.each do |warning|
11
- add_exception_as_offense(warning, template: node.template)
10
+ node.theme_file.warnings.each do |warning|
11
+ add_exception_as_offense(warning, theme_file: node.theme_file)
12
12
  end
13
13
  end
14
14
 
15
15
  def on_error(exception)
16
- add_exception_as_offense(exception, template: theme[exception.template_name])
16
+ add_exception_as_offense(exception, theme_file: theme[exception.template_name])
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def add_exception_as_offense(exception, template:)
21
+ def add_exception_as_offense(exception, theme_file:)
22
22
  add_offense(
23
23
  exception.to_s(false).sub(/ in ".*"$/, ''),
24
24
  line_number: exception.line_number,
25
25
  markup: exception.markup_context&.sub(/^in "(.*)"$/, '\1'),
26
- template: template,
26
+ theme_file: theme_file,
27
27
  )
28
28
  end
29
29
  end
@@ -29,9 +29,9 @@ module ThemeCheck
29
29
  end
30
30
 
31
31
  def after_document(node)
32
- lines = node.template.source.count("\n") - @excluded_lines
32
+ lines = node.theme_file.source.count("\n") - @excluded_lines
33
33
  if lines > @max_length
34
- add_offense("Template has too many lines [#{lines}/#{@max_length}]", template: node.template)
34
+ add_offense("Template has too many lines [#{lines}/#{@max_length}]", theme_file: node.theme_file)
35
35
  end
36
36
  end
37
37
 
@@ -29,7 +29,9 @@ module ThemeCheck
29
29
  "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
30
30
  node: node,
31
31
  markup: key_node.value,
32
- )
32
+ ) do |corrector|
33
+ corrector.add_default_translation_key(@theme.default_locale_json, key_node.value.split("."), "TODO")
34
+ end
33
35
  end
34
36
  end
35
37
 
@@ -62,22 +62,22 @@ module ThemeCheck
62
62
 
63
63
  def on_document(node)
64
64
  return if ignore?(node)
65
- @files[node.template.name] = TemplateInfo.new
65
+ @files[node.theme_file.name] = TemplateInfo.new
66
66
  end
67
67
 
68
68
  def on_assign(node)
69
69
  return if ignore?(node)
70
- @files[node.template.name].all_assigns[node.value.to] = node
70
+ @files[node.theme_file.name].all_assigns[node.value.to] = node
71
71
  end
72
72
 
73
73
  def on_capture(node)
74
74
  return if ignore?(node)
75
- @files[node.template.name].all_captures[node.value.instance_variable_get('@to')] = node
75
+ @files[node.theme_file.name].all_captures[node.value.instance_variable_get('@to')] = node
76
76
  end
77
77
 
78
78
  def on_for(node)
79
79
  return if ignore?(node)
80
- @files[node.template.name].all_forloops[node.value.variable_name] = node
80
+ @files[node.theme_file.name].all_forloops[node.value.variable_name] = node
81
81
  end
82
82
 
83
83
  def on_include(_node)
@@ -90,7 +90,7 @@ module ThemeCheck
90
90
  return unless node.value.template_name_expr.is_a?(String)
91
91
 
92
92
  snippet_name = "snippets/#{node.value.template_name_expr}"
93
- @files[node.template.name].add_render(
93
+ @files[node.theme_file.name].add_render(
94
94
  name: snippet_name,
95
95
  node: node,
96
96
  )
@@ -98,7 +98,7 @@ module ThemeCheck
98
98
 
99
99
  def on_variable_lookup(node)
100
100
  return if ignore?(node)
101
- @files[node.template.name].add_variable_lookup(
101
+ @files[node.theme_file.name].add_variable_lookup(
102
102
  name: node.value.name,
103
103
  node: node,
104
104
  )
@@ -130,7 +130,7 @@ module ThemeCheck
130
130
  private
131
131
 
132
132
  def ignore?(node)
133
- @exclude_snippets && node.template.snippet?
133
+ @exclude_snippets && node.theme_file.snippet?
134
134
  end
135
135
 
136
136
  def each_template
@@ -25,21 +25,21 @@ module ThemeCheck
25
25
  end
26
26
 
27
27
  def on_document(node)
28
- @templates[node.template.name] = TemplateInfo.new(Set.new, {}, Set.new)
28
+ @templates[node.theme_file.name] = TemplateInfo.new(Set.new, {}, Set.new)
29
29
  end
30
30
 
31
31
  def on_assign(node)
32
- @templates[node.template.name].assign_nodes[node.value.to] = node
32
+ @templates[node.theme_file.name].assign_nodes[node.value.to] = node
33
33
  end
34
34
 
35
35
  def on_include(node)
36
36
  if node.value.template_name_expr.is_a?(String)
37
- @templates[node.template.name].includes << "snippets/#{node.value.template_name_expr}"
37
+ @templates[node.theme_file.name].includes << "snippets/#{node.value.template_name_expr}"
38
38
  end
39
39
  end
40
40
 
41
41
  def on_variable_lookup(node)
42
- @templates[node.template.name].used_assigns << node.value.name
42
+ @templates[node.theme_file.name].used_assigns << node.value.name
43
43
  end
44
44
 
45
45
  def on_end
@@ -8,28 +8,30 @@ module ThemeCheck
8
8
  doc docs_url(__FILE__)
9
9
 
10
10
  def initialize
11
- @used_templates = Set.new
11
+ @used_snippets = Set.new
12
12
  end
13
13
 
14
14
  def on_include(node)
15
15
  if node.value.template_name_expr.is_a?(String)
16
- @used_templates << "snippets/#{node.value.template_name_expr}"
16
+ @used_snippets << "snippets/#{node.value.template_name_expr}"
17
17
  else
18
18
  # Can't reliably track unused snippets if an expression is used, ignore this check
19
- @used_templates.clear
19
+ @used_snippets.clear
20
20
  ignore!
21
21
  end
22
22
  end
23
23
  alias_method :on_render, :on_include
24
24
 
25
25
  def on_end
26
- missing_snippets.each do |template|
27
- add_offense("This template is not used", template: template)
26
+ missing_snippets.each do |theme_file|
27
+ add_offense("This snippet is not used", theme_file: theme_file) do |corrector|
28
+ corrector.remove(@theme, theme_file.relative_path.to_s)
29
+ end
28
30
  end
29
31
  end
30
32
 
31
33
  def missing_snippets
32
- theme.snippets.reject { |t| @used_templates.include?(t.name) }
34
+ theme.snippets.reject { |t| @used_snippets.include?(t.name) }
33
35
  end
34
36
  end
35
37
  end
@@ -8,7 +8,7 @@ module ThemeCheck
8
8
  def on_file(file)
9
9
  if file.parse_error
10
10
  message = format_json_parse_error(file.parse_error)
11
- add_offense(message, template: file)
11
+ add_offense(message, theme_file: file)
12
12
  end
13
13
  end
14
14
  end
@@ -47,9 +47,10 @@ module ThemeCheck
47
47
  raise
48
48
  rescue => e
49
49
  node = args.first
50
- template = node.respond_to?(:template) ? node.template.relative_path : "?"
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
+ line_number = node.respond_to?(:line_number) ? node.line_number : "?"
53
54
 
54
55
  ThemeCheck.bug(<<~EOS)
55
56
  Exception while running `#{check.code_name}##{method}`:
@@ -58,12 +59,13 @@ module ThemeCheck
58
59
  #{e.backtrace.join("\n ")}
59
60
  ```
60
61
 
61
- Template: `#{template}`
62
+ Theme File: `#{theme_file}`
62
63
  Node: `#{node_class}`
63
64
  Markup:
64
65
  ```
65
66
  #{markup}
66
67
  ```
68
+ Line number: #{line_number}
67
69
  Check options: `#{check.options.pretty_inspect}`
68
70
  EOS
69
71
  end
@@ -49,7 +49,7 @@ module ThemeCheck
49
49
  "Automatically fix offenses"
50
50
  ) { @auto_correct = true }
51
51
  @option_parser.on(
52
- "--fail-level SEVERITY", Check::SEVERITIES,
52
+ "--fail-level SEVERITY", [:crash] + Check::SEVERITIES,
53
53
  "Minimum severity (error|suggestion|style) for exit with error code"
54
54
  ) do |severity|
55
55
  @fail_level = severity.to_sym
@@ -186,12 +186,15 @@ 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 templates found."
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
193
193
  analyzer.correct_offenses
194
- output_with_format(theme, analyzer, out_stream)
194
+ print_with_format(theme, analyzer, out_stream)
195
+ # corrections are committed after printing so that the
196
+ # source_excerpts are still pointing to the uncorrected source.
197
+ analyzer.write_corrections
195
198
  raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
196
199
  offense.check.severity_value <= Check.severity_value(@fail_level)
197
200
  end
@@ -211,7 +214,7 @@ module ThemeCheck
211
214
  STDERR.puts "Profiling is only available in development"
212
215
  end
213
216
 
214
- def output_with_format(theme, analyzer, out_stream)
217
+ def print_with_format(theme, analyzer, out_stream)
215
218
  case @format
216
219
  when :text
217
220
  ThemeCheck::Printer.new(out_stream).print(theme, analyzer.offenses, @config.auto_correct)
@@ -2,30 +2,25 @@
2
2
 
3
3
  module ThemeCheck
4
4
  class Corrector
5
- def initialize(template:)
6
- @template = template
5
+ def initialize(theme_file:)
6
+ @theme_file = theme_file
7
7
  end
8
8
 
9
9
  def insert_after(node, content)
10
- line = @template.full_line(node.line_number)
11
- line.insert(node.range[1] + 1, content)
10
+ @theme_file.rewriter.insert_after(node, content)
12
11
  end
13
12
 
14
13
  def insert_before(node, content)
15
- line = @template.full_line(node.line_number)
16
- line.insert(node.range[0], content)
14
+ @theme_file.rewriter.insert_before(node, content)
17
15
  end
18
16
 
19
17
  def replace(node, content)
20
- line = @template.full_line(node.line_number)
21
- line[node.range[0]..node.range[1]] = content
18
+ @theme_file.rewriter.replace(node, content)
22
19
  node.markup = content
23
20
  end
24
21
 
25
22
  def wrap(node, insert_before, insert_after)
26
- line = @template.full_line(node.line_number)
27
- line.insert(node.range[0], insert_before)
28
- line.insert(node.range[1] + 1 + insert_before.length, insert_after)
23
+ @theme_file.rewriter.wrap(node, insert_before, insert_after)
29
24
  end
30
25
 
31
26
  def create(theme, relative_path, content)
@@ -34,7 +29,25 @@ module ThemeCheck
34
29
 
35
30
  def create_default_locale_json(theme)
36
31
  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
37
- theme.default_locale_json.update_contents('{}')
32
+ theme.default_locale_json.update_contents({})
33
+ end
34
+
35
+ def remove(theme, relative_path)
36
+ theme.storage.remove(relative_path)
37
+ end
38
+
39
+ def mkdir(theme, relative_path)
40
+ theme.storage.mkdir(relative_path)
41
+ end
42
+
43
+ def add_default_translation_key(file, key, value)
44
+ hash = file.content
45
+ key.reduce(hash) do |pointer, token|
46
+ return pointer[token] = value if token == key.last
47
+ pointer[token] = {} unless pointer.key?(token)
48
+ pointer[token]
49
+ end
50
+ file.update_contents(hash)
38
51
  end
39
52
  end
40
53
  end