theme-check 0.8.1 → 0.10.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -0
  3. data/CHANGELOG.md +39 -0
  4. data/CONTRIBUTING.md +2 -1
  5. data/README.md +4 -1
  6. data/RELEASING.md +5 -3
  7. data/config/default.yml +46 -1
  8. data/data/shopify_liquid/tags.yml +3 -0
  9. data/docs/checks/asset_url_filters.md +56 -0
  10. data/docs/checks/content_for_header_modification.md +42 -0
  11. data/docs/checks/img_lazy_loading.md +61 -0
  12. data/docs/checks/nested_snippet.md +1 -1
  13. data/docs/checks/parser_blocking_script_tag.md +53 -0
  14. data/docs/checks/space_inside_braces.md +22 -0
  15. data/exe/theme-check-language-server +1 -2
  16. data/lib/theme_check.rb +9 -1
  17. data/lib/theme_check/analyzer.rb +72 -16
  18. data/lib/theme_check/bug.rb +20 -0
  19. data/lib/theme_check/check.rb +31 -6
  20. data/lib/theme_check/checks.rb +49 -4
  21. data/lib/theme_check/checks/asset_url_filters.rb +46 -0
  22. data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
  23. data/lib/theme_check/checks/img_lazy_loading.rb +25 -0
  24. data/lib/theme_check/checks/img_width_and_height.rb +18 -49
  25. data/lib/theme_check/checks/missing_template.rb +1 -0
  26. data/lib/theme_check/checks/nested_snippet.rb +1 -1
  27. data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
  28. data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
  29. data/lib/theme_check/checks/remote_asset.rb +21 -79
  30. data/lib/theme_check/checks/space_inside_braces.rb +5 -5
  31. data/lib/theme_check/checks/template_length.rb +3 -0
  32. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  33. data/lib/theme_check/config.rb +2 -0
  34. data/lib/theme_check/disabled_check.rb +6 -4
  35. data/lib/theme_check/disabled_checks.rb +25 -9
  36. data/lib/theme_check/html_check.rb +7 -0
  37. data/lib/theme_check/html_node.rb +56 -0
  38. data/lib/theme_check/html_visitor.rb +38 -0
  39. data/lib/theme_check/json_file.rb +8 -0
  40. data/lib/theme_check/language_server.rb +2 -0
  41. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
  42. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
  43. data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
  44. data/lib/theme_check/language_server/handler.rb +31 -26
  45. data/lib/theme_check/language_server/server.rb +1 -1
  46. data/lib/theme_check/language_server/variable_lookup_finder.rb +295 -0
  47. data/lib/theme_check/liquid_check.rb +1 -4
  48. data/lib/theme_check/offense.rb +18 -0
  49. data/lib/theme_check/shopify_liquid/tag.rb +13 -0
  50. data/lib/theme_check/template.rb +8 -0
  51. data/lib/theme_check/theme.rb +7 -2
  52. data/lib/theme_check/version.rb +1 -1
  53. data/lib/theme_check/visitor.rb +2 -11
  54. metadata +17 -3
@@ -21,6 +21,10 @@ This check is aimed at eliminating ugly Liquid:
21
21
  <!-- Arround filter pipelines -->
22
22
  {{ url | asset_url | img_tag }}
23
23
  {% assign my_upcase_string = "Hello world"| upcase %}
24
+
25
+ <!-- Arround symbol operators -->
26
+ {%- if target == product and product.price_varies -%}
27
+ {%- if product.featured_media.width >=165 -%}
24
28
  ```
25
29
 
26
30
  :+1: Examples of **correct** code for this check:
@@ -39,6 +43,8 @@ This check is aimed at eliminating ugly Liquid:
39
43
  %}
40
44
  {{ url | asset_url | img_tag }}
41
45
  {% assign my_upcase_string = "Hello world" | upcase %}
46
+ {%- if target == product and product.price_varies -%}
47
+ {%- if product.featured_media.width >= 165 -%}
42
48
  ```
43
49
 
44
50
  ## Check Options
@@ -50,6 +56,22 @@ SpaceInsideBraces:
50
56
  enabled: true
51
57
  ```
52
58
 
59
+ ## Auto-correction
60
+
61
+ This check can automatically trim or add spaces around `{{ ... }}`.
62
+
63
+ ```liquid
64
+ {{ x}}
65
+ {{x}}
66
+ {{ x }}
67
+ ```
68
+
69
+ Can all be auto-corrected with the `--auto-correct` option to:
70
+
71
+ ```liquid
72
+ {{ x }}
73
+ ```
74
+
53
75
  ## When Not To Use It
54
76
 
55
77
  If you don't care about the look of your code.
@@ -6,7 +6,6 @@ require 'theme_check'
6
6
  if ENV["THEME_CHECK_DEBUG"] == "true"
7
7
  $DEBUG = true
8
8
  end
9
- # Force encoding to UTF-8 to fix VSCode
10
- Encoding.default_external = Encoding::UTF_8
9
+
11
10
  status_code = ThemeCheck::LanguageServer.start
12
11
  exit! status_code
data/lib/theme_check.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require "liquid"
3
3
 
4
+ require_relative "theme_check/version"
5
+ require_relative "theme_check/bug"
4
6
  require_relative "theme_check/exceptions"
5
7
  require_relative "theme_check/analyzer"
6
8
  require_relative "theme_check/check"
@@ -34,6 +36,12 @@ require_relative "theme_check/template"
34
36
  require_relative "theme_check/theme"
35
37
  require_relative "theme_check/visitor"
36
38
  require_relative "theme_check/corrector"
37
- require_relative "theme_check/version"
39
+ require_relative "theme_check/html_node"
40
+ require_relative "theme_check/html_visitor"
41
+ require_relative "theme_check/html_check"
38
42
 
39
43
  Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }
44
+
45
+ # UTF-8 is the default internal and external encoding, like in Rails & Shopify.
46
+ Encoding.default_external = Encoding::UTF_8
47
+ Encoding.default_internal = Encoding::UTF_8
@@ -7,6 +7,7 @@ module ThemeCheck
7
7
 
8
8
  @liquid_checks = Checks.new
9
9
  @json_checks = Checks.new
10
+ @html_checks = Checks.new
10
11
 
11
12
  checks.each do |check|
12
13
  check.theme = @theme
@@ -16,33 +17,58 @@ module ThemeCheck
16
17
  @liquid_checks << check
17
18
  when JsonCheck
18
19
  @json_checks << check
20
+ when HtmlCheck
21
+ @html_checks << check
19
22
  end
20
23
  end
21
-
22
- @visitor = Visitor.new(@liquid_checks)
23
24
  end
24
25
 
25
26
  def offenses
26
- @liquid_checks.flat_map(&:offenses) + @json_checks.flat_map(&:offenses)
27
+ @liquid_checks.flat_map(&:offenses) +
28
+ @json_checks.flat_map(&:offenses) +
29
+ @html_checks.flat_map(&:offenses)
27
30
  end
28
31
 
29
- def offenses_clear!
30
- @liquid_checks.each do |check|
31
- check.offenses.clear
32
- end
32
+ def analyze_theme
33
+ reset
33
34
 
34
- @json_checks.each do |check|
35
- check.offenses.clear
35
+ liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
36
+ html_visitor = HtmlVisitor.new(@html_checks)
37
+ @theme.liquid.each do |template|
38
+ liquid_visitor.visit_template(template)
39
+ html_visitor.visit_template(template)
36
40
  end
37
- end
38
41
 
39
- def analyze_theme
40
- offenses_clear!
41
- @theme.liquid.each { |template| @visitor.visit_template(template) }
42
42
  @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
43
- @liquid_checks.call(:on_end)
44
- @json_checks.call(:on_end)
45
- offenses
43
+
44
+ finish
45
+ end
46
+
47
+ def analyze_files(files)
48
+ reset
49
+
50
+ # Call all checks that run on the whole theme
51
+ liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
52
+ html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
53
+ @theme.liquid.each do |template|
54
+ liquid_visitor.visit_template(template)
55
+ html_visitor.visit_template(template)
56
+ end
57
+ @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
58
+
59
+ # Call checks that run on a single files, only on specified file
60
+ liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
61
+ html_visitor = HtmlVisitor.new(@html_checks.single_file)
62
+ files.each do |file|
63
+ if file.liquid?
64
+ liquid_visitor.visit_template(file)
65
+ html_visitor.visit_template(file)
66
+ elsif file.json?
67
+ @json_checks.single_file.call(:on_file, file)
68
+ end
69
+ end
70
+
71
+ finish
46
72
  end
47
73
 
48
74
  def uncorrectable_offenses
@@ -59,5 +85,35 @@ module ThemeCheck
59
85
  @theme.liquid.each(&:write)
60
86
  end
61
87
  end
88
+
89
+ private
90
+
91
+ def reset
92
+ @disabled_checks = DisabledChecks.new
93
+
94
+ @liquid_checks.each do |check|
95
+ check.offenses.clear
96
+ end
97
+
98
+ @html_checks.each do |check|
99
+ check.offenses.clear
100
+ end
101
+
102
+ @json_checks.each do |check|
103
+ check.offenses.clear
104
+ end
105
+ end
106
+
107
+ def finish
108
+ @liquid_checks.call(:on_end)
109
+ @html_checks.call(:on_end)
110
+ @json_checks.call(:on_end)
111
+
112
+ @disabled_checks.remove_disabled_offenses(@liquid_checks)
113
+ @disabled_checks.remove_disabled_offenses(@json_checks)
114
+ @disabled_checks.remove_disabled_offenses(@html_checks)
115
+
116
+ offenses
117
+ end
62
118
  end
63
119
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'theme_check/version'
3
+
4
+ module ThemeCheck
5
+ BUG_POSTAMBLE = <<~EOS
6
+ Theme Check Version: #{VERSION}
7
+ Ruby Version: #{RUBY_VERSION}
8
+ Platform: #{RUBY_PLATFORM}
9
+ Muffin mode: activated
10
+
11
+ ------------------------
12
+ Whoops! It looks like you found a bug in Theme Check.
13
+ Please report it at https://github.com/Shopify/theme-check/issues, and include the message above.
14
+ Or cross your fingers real hard, and try again.
15
+ EOS
16
+
17
+ def self.bug(message)
18
+ abort(message + BUG_POSTAMBLE)
19
+ end
20
+ end
@@ -6,7 +6,7 @@ module ThemeCheck
6
6
  include JsonHelpers
7
7
 
8
8
  attr_accessor :theme
9
- attr_accessor :options
9
+ attr_accessor :options, :ignored_patterns
10
10
  attr_writer :offenses
11
11
 
12
12
  SEVERITIES = [
@@ -19,6 +19,7 @@ module ThemeCheck
19
19
  :liquid,
20
20
  :translation,
21
21
  :performance,
22
+ :html,
22
23
  :json,
23
24
  :performance,
24
25
  ]
@@ -67,12 +68,23 @@ module ThemeCheck
67
68
  end
68
69
  defined?(@can_disable) ? @can_disable : true
69
70
  end
71
+
72
+ def single_file(single_file = nil)
73
+ unless single_file.nil?
74
+ @single_file = single_file
75
+ end
76
+ defined?(@single_file) ? @single_file : !method_defined?(:on_end)
77
+ end
70
78
  end
71
79
 
72
80
  def offenses
73
81
  @offenses ||= []
74
82
  end
75
83
 
84
+ def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
85
+ offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
86
+ end
87
+
76
88
  def severity
77
89
  self.class.severity
78
90
  end
@@ -93,10 +105,6 @@ module ThemeCheck
93
105
  @ignored = true
94
106
  end
95
107
 
96
- def unignore!
97
- @ignored = false
98
- end
99
-
100
108
  def ignored?
101
109
  defined?(@ignored) && @ignored
102
110
  end
@@ -105,9 +113,26 @@ module ThemeCheck
105
113
  self.class.can_disable
106
114
  end
107
115
 
116
+ def single_file?
117
+ self.class.single_file
118
+ end
119
+
120
+ def whole_theme?
121
+ !single_file?
122
+ end
123
+
124
+ def ==(other)
125
+ other.is_a?(Check) && code_name == other.code_name
126
+ end
127
+
108
128
  def to_s
109
129
  s = +"#{code_name}:\n"
110
- properties = { severity: severity, categories: categories, doc: doc }.merge(options)
130
+ properties = {
131
+ severity: severity,
132
+ categories: categories,
133
+ doc: doc,
134
+ ignored_patterns: ignored_patterns,
135
+ }.merge(options)
111
136
  properties.each_pair do |name, value|
112
137
  s << " #{name}: #{value}\n" if value
113
138
  end
@@ -1,16 +1,61 @@
1
1
  # frozen_string_literal: true
2
+ require "pp"
3
+ require "timeout"
4
+
2
5
  module ThemeCheck
3
6
  class Checks < Array
7
+ CHECK_METHOD_TIMEOUT = 5 # sec
8
+
4
9
  def call(method, *args)
5
10
  each do |check|
6
- if check.respond_to?(method) && !check.ignored?
7
- check.send(method, *args)
8
- end
11
+ call_check_method(check, method, *args)
9
12
  end
10
13
  end
11
14
 
12
15
  def disableable
13
- self.class.new(select(&:can_disable?))
16
+ @disableable ||= self.class.new(select(&:can_disable?))
17
+ end
18
+
19
+ def whole_theme
20
+ @whole_theme ||= self.class.new(select(&:whole_theme?))
21
+ end
22
+
23
+ def single_file
24
+ @single_file ||= self.class.new(select(&:single_file?))
25
+ end
26
+
27
+ private
28
+
29
+ def call_check_method(check, method, *args)
30
+ return unless check.respond_to?(method) && !check.ignored?
31
+
32
+ Timeout.timeout(CHECK_METHOD_TIMEOUT) do
33
+ check.send(method, *args)
34
+ end
35
+ rescue Liquid::Error
36
+ # Pass-through Liquid errors
37
+ raise
38
+ rescue => e
39
+ node = args.first
40
+ template = node.respond_to?(:template) ? node.template.relative_path : "?"
41
+ markup = node.respond_to?(:markup) ? node.markup : ""
42
+ node_class = node.respond_to?(:value) ? node.value.class : "?"
43
+
44
+ ThemeCheck.bug(<<~EOS)
45
+ Exception while running `#{check.code_name}##{method}`:
46
+ ```
47
+ #{e.class}: #{e.message}
48
+ #{e.backtrace.join("\n ")}
49
+ ```
50
+
51
+ Template: `#{template}`
52
+ Node: `#{node_class}`
53
+ Markup:
54
+ ```
55
+ #{markup}
56
+ ```
57
+ Check options: `#{check.options.pretty_inspect}`
58
+ EOS
14
59
  end
15
60
  end
16
61
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class AssetUrlFilters < LiquidCheck
4
+ severity :suggestion
5
+ categories :liquid, :performance
6
+ doc docs_url(__FILE__)
7
+
8
+ HTML_FILTERS = [
9
+ 'stylesheet_tag',
10
+ 'script_tag',
11
+ 'img_tag',
12
+ ]
13
+ ASSET_URL_FILTERS = [
14
+ 'asset_url',
15
+ 'asset_img_url',
16
+ 'file_img_url',
17
+ 'file_url',
18
+ 'global_asset_url',
19
+ 'img_url',
20
+ 'payment_type_img_url',
21
+ 'shopify_asset_url',
22
+ ]
23
+
24
+ def on_variable(node)
25
+ record_variable_offense(node)
26
+ end
27
+
28
+ private
29
+
30
+ def record_variable_offense(variable_node)
31
+ # We flag HTML tags with URLs not hosted by Shopify
32
+ return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
33
+ add_offense("Use one of the asset_url filters to serve assets", node: variable_node)
34
+ end
35
+
36
+ def html_resource_drop?(variable_node)
37
+ variable_node.value.filters
38
+ .any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
39
+ end
40
+
41
+ def variable_hosted_by_shopify?(variable_node)
42
+ variable_node.value.filters
43
+ .any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class ContentForHeaderModification < LiquidCheck
4
+ severity :error
5
+ category :liquid
6
+ doc docs_url(__FILE__)
7
+
8
+ def initialize
9
+ @in_assign = false
10
+ @in_capture = false
11
+ end
12
+
13
+ def on_variable(node)
14
+ return unless node.value.name.is_a?(Liquid::VariableLookup)
15
+ return unless node.value.name.name == "content_for_header"
16
+
17
+ if @in_assign || @in_capture || node.value.filters.any?
18
+ add_offense(
19
+ "Do not rely on the content of `content_for_header`",
20
+ node: node,
21
+ )
22
+ end
23
+ end
24
+
25
+ def on_assign(_node)
26
+ @in_assign = true
27
+ end
28
+
29
+ def after_assign(_node)
30
+ @in_assign = false
31
+ end
32
+
33
+ def on_capture(_node)
34
+ @in_capture = true
35
+ end
36
+
37
+ def after_capture(_node)
38
+ @in_capture = false
39
+ end
40
+ end
41
+ end