theme-check 0.7.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +4 -0
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +43 -0
  5. data/CONTRIBUTING.md +2 -1
  6. data/README.md +4 -1
  7. data/RELEASING.md +5 -3
  8. data/config/default.yml +38 -1
  9. data/data/shopify_liquid/tags.yml +3 -0
  10. data/data/shopify_translation_keys.yml +1 -0
  11. data/dev.yml +1 -1
  12. data/docs/checks/content_for_header_modification.md +42 -0
  13. data/docs/checks/nested_snippet.md +1 -1
  14. data/docs/checks/parser_blocking_script_tag.md +53 -0
  15. data/docs/checks/space_inside_braces.md +28 -0
  16. data/exe/theme-check-language-server +1 -2
  17. data/lib/theme_check.rb +13 -1
  18. data/lib/theme_check/analyzer.rb +79 -13
  19. data/lib/theme_check/bug.rb +20 -0
  20. data/lib/theme_check/check.rb +36 -7
  21. data/lib/theme_check/checks.rb +47 -8
  22. data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
  23. data/lib/theme_check/checks/img_width_and_height.rb +18 -49
  24. data/lib/theme_check/checks/missing_enable_comment.rb +4 -4
  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/space_inside_braces.rb +8 -2
  30. data/lib/theme_check/checks/template_length.rb +3 -0
  31. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  32. data/lib/theme_check/cli.rb +1 -1
  33. data/lib/theme_check/config.rb +8 -2
  34. data/lib/theme_check/disabled_check.rb +41 -0
  35. data/lib/theme_check/disabled_checks.rb +33 -29
  36. data/lib/theme_check/exceptions.rb +32 -0
  37. data/lib/theme_check/html_check.rb +7 -0
  38. data/lib/theme_check/html_node.rb +52 -0
  39. data/lib/theme_check/html_visitor.rb +36 -0
  40. data/lib/theme_check/json_file.rb +13 -1
  41. data/lib/theme_check/language_server.rb +2 -1
  42. data/lib/theme_check/language_server/completion_engine.rb +1 -1
  43. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
  44. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
  45. data/lib/theme_check/language_server/constants.rb +5 -1
  46. data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
  47. data/lib/theme_check/language_server/document_link_engine.rb +2 -2
  48. data/lib/theme_check/language_server/handler.rb +63 -50
  49. data/lib/theme_check/language_server/server.rb +1 -1
  50. data/lib/theme_check/language_server/variable_lookup_finder.rb +295 -0
  51. data/lib/theme_check/liquid_check.rb +0 -4
  52. data/lib/theme_check/node.rb +12 -0
  53. data/lib/theme_check/offense.rb +30 -46
  54. data/lib/theme_check/parsing_helpers.rb +1 -1
  55. data/lib/theme_check/position.rb +77 -0
  56. data/lib/theme_check/position_helper.rb +37 -0
  57. data/lib/theme_check/remote_asset_file.rb +3 -0
  58. data/lib/theme_check/shopify_liquid/tag.rb +13 -0
  59. data/lib/theme_check/template.rb +8 -0
  60. data/lib/theme_check/theme.rb +7 -2
  61. data/lib/theme_check/version.rb +1 -1
  62. data/lib/theme_check/visitor.rb +4 -14
  63. data/theme-check.gemspec +2 -0
  64. metadata +18 -5
  65. data/lib/theme_check/language_server/position_helper.rb +0 -27
@@ -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,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
  require "liquid"
3
3
 
4
+ require_relative "theme_check/version"
5
+ require_relative "theme_check/bug"
6
+ require_relative "theme_check/exceptions"
4
7
  require_relative "theme_check/analyzer"
5
8
  require_relative "theme_check/check"
6
9
  require_relative "theme_check/checks_tracking"
7
10
  require_relative "theme_check/cli"
11
+ require_relative "theme_check/disabled_check"
8
12
  require_relative "theme_check/disabled_checks"
9
13
  require_relative "theme_check/liquid_check"
10
14
  require_relative "theme_check/locale_diff"
@@ -14,6 +18,8 @@ require_relative "theme_check/regex_helpers"
14
18
  require_relative "theme_check/json_check"
15
19
  require_relative "theme_check/json_file"
16
20
  require_relative "theme_check/json_helpers"
21
+ require_relative "theme_check/position_helper"
22
+ require_relative "theme_check/position"
17
23
  require_relative "theme_check/language_server"
18
24
  require_relative "theme_check/checks"
19
25
  require_relative "theme_check/config"
@@ -30,6 +36,12 @@ require_relative "theme_check/template"
30
36
  require_relative "theme_check/theme"
31
37
  require_relative "theme_check/visitor"
32
38
  require_relative "theme_check/corrector"
33
- 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"
34
42
 
35
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
@@ -1,53 +1,119 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
3
  class Analyzer
4
- attr_reader :offenses
5
-
6
4
  def initialize(theme, checks = Check.all.map(&:new), auto_correct = false)
7
5
  @theme = theme
8
- @offenses = []
9
6
  @auto_correct = auto_correct
10
7
 
11
8
  @liquid_checks = Checks.new
12
9
  @json_checks = Checks.new
10
+ @html_checks = Checks.new
13
11
 
14
12
  checks.each do |check|
15
13
  check.theme = @theme
16
- check.offenses = @offenses
17
14
 
18
15
  case check
19
16
  when LiquidCheck
20
17
  @liquid_checks << check
21
18
  when JsonCheck
22
19
  @json_checks << check
20
+ when HtmlCheck
21
+ @html_checks << check
23
22
  end
24
23
  end
24
+ end
25
25
 
26
- @visitor = Visitor.new(@liquid_checks)
26
+ def 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
32
  def analyze_theme
30
- @offenses.clear
31
- @theme.liquid.each { |template| @visitor.visit_template(template) }
33
+ reset
34
+
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)
40
+ end
41
+
32
42
  @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
33
- @liquid_checks.call(:on_end)
34
- @json_checks.call(:on_end)
35
- @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
36
72
  end
37
73
 
38
74
  def uncorrectable_offenses
39
75
  unless @auto_correct
40
- return @offenses
76
+ return offenses
41
77
  end
42
78
 
43
- @offenses.select { |offense| !offense.correctable? }
79
+ offenses.select { |offense| !offense.correctable? }
44
80
  end
45
81
 
46
82
  def correct_offenses
47
83
  if @auto_correct
48
- @offenses.each(&:correct)
84
+ offenses.each(&:correct)
49
85
  @theme.liquid.each(&:write)
50
86
  end
51
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
52
118
  end
53
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,8 +6,8 @@ module ThemeCheck
6
6
  include JsonHelpers
7
7
 
8
8
  attr_accessor :theme
9
- attr_accessor :offenses
10
- attr_accessor :options
9
+ attr_accessor :options, :ignored_patterns
10
+ attr_writer :offenses
11
11
 
12
12
  SEVERITIES = [
13
13
  :error,
@@ -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,6 +68,21 @@ 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
78
+ end
79
+
80
+ def offenses
81
+ @offenses ||= []
82
+ end
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)
70
86
  end
71
87
 
72
88
  def severity
@@ -89,10 +105,6 @@ module ThemeCheck
89
105
  @ignored = true
90
106
  end
91
107
 
92
- def unignore!
93
- @ignored = false
94
- end
95
-
96
108
  def ignored?
97
109
  defined?(@ignored) && @ignored
98
110
  end
@@ -101,9 +113,26 @@ module ThemeCheck
101
113
  self.class.can_disable
102
114
  end
103
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
+
104
128
  def to_s
105
129
  s = +"#{code_name}:\n"
106
- 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)
107
136
  properties.each_pair do |name, value|
108
137
  s << " #{name}: #{value}\n" if value
109
138
  end
@@ -1,22 +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
- def always_enabled
13
- self.class.new(reject(&:can_disable?))
15
+ def disableable
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?))
14
25
  end
15
26
 
16
- def except_for(disabled_checks)
17
- still_enabled = reject { |check| disabled_checks.all.include?(check.code_name) }
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
+ ```
18
50
 
19
- self.class.new((always_enabled + still_enabled).uniq)
51
+ Template: `#{template}`
52
+ Node: `#{node_class}`
53
+ Markup:
54
+ ```
55
+ #{markup}
56
+ ```
57
+ Check options: `#{check.options.pretty_inspect}`
58
+ EOS
20
59
  end
21
60
  end
22
61
  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
@@ -1,41 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
3
  # Reports errors when trying to use parser-blocking script tags
4
- class ImgWidthAndHeight < LiquidCheck
5
- include RegexHelpers
4
+ class ImgWidthAndHeight < HtmlCheck
6
5
  severity :error
7
- categories :liquid, :performance
6
+ categories :html, :performance
8
7
  doc docs_url(__FILE__)
9
8
 
10
- # Not implemented with lookbehinds and lookaheads because performance was shit!
11
- IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
12
- SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
13
- WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
14
- HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
15
-
16
- FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
17
9
  ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
18
10
 
19
- def on_document(node)
20
- @source = node.template.source
21
- @node = node
22
- record_offenses
23
- end
11
+ def on_img(node)
12
+ width = node.attributes["width"]&.value
13
+ height = node.attributes["height"]&.value
24
14
 
25
- private
15
+ record_units_in_field_offenses("width", width, node: node)
16
+ record_units_in_field_offenses("height", height, node: node)
26
17
 
27
- def record_offenses
28
- matches(@source, IMG_TAG).each do |img_match|
29
- next unless img_match[0] =~ SRC_ATTRIBUTE
30
- record_missing_field_offenses(img_match)
31
- record_units_in_field_offenses(img_match)
32
- end
33
- end
34
-
35
- def record_missing_field_offenses(img_match)
36
- width = WIDTH_ATTRIBUTE.match(img_match[0])
37
- height = HEIGHT_ATTRIBUTE.match(img_match[0])
38
- return if width && height
18
+ return if node.attributes["src"].nil? || (width && height)
39
19
  missing_width = width.nil?
40
20
  missing_height = height.nil?
41
21
  error_message = if missing_width && missing_height
@@ -46,29 +26,18 @@ module ThemeCheck
46
26
  "Missing height attribute"
47
27
  end
48
28
 
49
- add_offense(
50
- error_message,
51
- node: @node,
52
- markup: img_match[0],
53
- line_number: @source[0...img_match.begin(0)].count("\n") + 1
54
- )
29
+ add_offense(error_message, node: node)
55
30
  end
56
31
 
57
- def record_units_in_field_offenses(img_match)
58
- FIELDS.each do |field|
59
- field_match = field.match(img_match[0])
60
- next if field_match.nil?
61
- value = field_match[2].gsub(START_OR_END_QUOTE, '')
62
- next unless value =~ ENDS_IN_CSS_UNIT
63
- value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
64
- start = img_match.begin(0) + field_match.begin(2)
65
- add_offense(
66
- "The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
67
- node: @node,
68
- markup: value,
69
- line_number: @source[0...start].count("\n") + 1
70
- )
71
- end
32
+ private
33
+
34
+ def record_units_in_field_offenses(attribute, value, node:)
35
+ return unless value =~ ENDS_IN_CSS_UNIT
36
+ value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
37
+ add_offense(
38
+ "The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\".",
39
+ node: node,
40
+ )
72
41
  end
73
42
  end
74
43
  end