theme-check 0.8.1 → 0.10.0

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