theme-check 1.5.1 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
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