theme-check 0.8.3 → 0.9.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -0
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +2 -1
  5. data/README.md +4 -1
  6. data/config/default.yml +37 -0
  7. data/docs/checks/content_for_header_modification.md +42 -0
  8. data/docs/checks/parser_blocking_script_tag.md +53 -0
  9. data/exe/theme-check-language-server +1 -2
  10. data/lib/theme_check.rb +8 -1
  11. data/lib/theme_check/analyzer.rb +72 -16
  12. data/lib/theme_check/check.rb +31 -6
  13. data/lib/theme_check/checks.rb +9 -1
  14. data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
  15. data/lib/theme_check/checks/img_width_and_height.rb +18 -49
  16. data/lib/theme_check/checks/missing_template.rb +1 -0
  17. data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
  18. data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
  19. data/lib/theme_check/checks/template_length.rb +3 -0
  20. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  21. data/lib/theme_check/config.rb +2 -0
  22. data/lib/theme_check/disabled_check.rb +6 -4
  23. data/lib/theme_check/disabled_checks.rb +25 -9
  24. data/lib/theme_check/html_check.rb +7 -0
  25. data/lib/theme_check/html_node.rb +52 -0
  26. data/lib/theme_check/html_visitor.rb +36 -0
  27. data/lib/theme_check/json_file.rb +8 -0
  28. data/lib/theme_check/language_server.rb +1 -0
  29. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
  30. data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
  31. data/lib/theme_check/language_server/handler.rb +31 -26
  32. data/lib/theme_check/language_server/server.rb +1 -1
  33. data/lib/theme_check/liquid_check.rb +0 -4
  34. data/lib/theme_check/offense.rb +18 -0
  35. data/lib/theme_check/template.rb +8 -0
  36. data/lib/theme_check/theme.rb +7 -2
  37. data/lib/theme_check/version.rb +1 -1
  38. data/lib/theme_check/visitor.rb +2 -11
  39. metadata +10 -2
@@ -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
@@ -13,7 +13,15 @@ module ThemeCheck
13
13
  end
14
14
 
15
15
  def disableable
16
- 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?))
17
25
  end
18
26
 
19
27
  private
@@ -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
@@ -5,6 +5,7 @@ module ThemeCheck
5
5
  severity :suggestion
6
6
  category :liquid
7
7
  doc docs_url(__FILE__)
8
+ single_file false
8
9
 
9
10
  def on_include(node)
10
11
  template = node.value.template_name_expr
@@ -1,48 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
3
  # Reports errors when trying to use parser-blocking script tags
4
- class ParserBlockingJavaScript < LiquidCheck
5
- include RegexHelpers
4
+ class ParserBlockingJavaScript < HtmlCheck
6
5
  severity :error
7
- categories :liquid, :performance
6
+ categories :html, :performance
8
7
  doc docs_url(__FILE__)
9
8
 
10
- PARSER_BLOCKING_SCRIPT_TAG = %r{
11
- <script # Find the start of a script tag
12
- (?=[^>]+?src=) # Make sure src= is in the script with a lookahead
13
- (?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
14
- /?>
15
- }xim
16
- SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
9
+ def on_script(node)
10
+ return unless node.attributes["src"]
11
+ return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"]&.value == "module"
17
12
 
18
- def on_document(node)
19
- @source = node.template.source
20
- @node = node
21
- record_offenses
22
- end
23
-
24
- private
25
-
26
- def record_offenses
27
- record_offenses_from_regex(
28
- message: "Missing async or defer attribute on script tag",
29
- regex: PARSER_BLOCKING_SCRIPT_TAG,
30
- )
31
- record_offenses_from_regex(
32
- message: "The script_tag filter is parser-blocking. Use a script tag with the async or defer attribute for better performance",
33
- regex: SCRIPT_TAG_FILTER,
34
- )
35
- end
36
-
37
- def record_offenses_from_regex(regex: nil, message: nil)
38
- matches(@source, regex).each do |match|
39
- add_offense(
40
- message,
41
- node: @node,
42
- markup: match[0],
43
- line_number: @source[0...match.begin(0)].count("\n") + 1
44
- )
45
- end
13
+ add_offense("Missing async or defer attribute on script tag", node: node)
46
14
  end
47
15
  end
48
16
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when trying to use parser-blocking script tags
4
+ class ParserBlockingScriptTag < LiquidCheck
5
+ severity :error
6
+ categories :liquid, :performance
7
+ doc docs_url(__FILE__)
8
+
9
+ def on_variable(node)
10
+ used_filters = node.value.filters.map { |name, *_rest| name }
11
+ if used_filters.include?("script_tag")
12
+ add_offense(
13
+ "The script_tag filter is parser-blocking. Use a script tag with the async or defer " \
14
+ "attribute for better performance",
15
+ node: node
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -8,6 +8,9 @@ module ThemeCheck
8
8
  def initialize(max_length: 200, exclude_schema: true)
9
9
  @max_length = max_length
10
10
  @exclude_schema = exclude_schema
11
+ end
12
+
13
+ def on_document(_node)
11
14
  @excluded_lines = 0
12
15
  end
13
16
 
@@ -5,6 +5,7 @@ require 'nokogumbo'
5
5
  module ThemeCheck
6
6
  class ValidHTMLTranslation < JsonCheck
7
7
  severity :suggestion
8
+ category :translation
8
9
  doc docs_url(__FILE__)
9
10
 
10
11
  def on_file(file)
@@ -91,11 +91,13 @@ module ThemeCheck
91
91
 
92
92
  options_for_check = options.transform_keys(&:to_sym)
93
93
  options_for_check.delete(:enabled)
94
+ ignored_patterns = options_for_check.delete(:ignore) || []
94
95
  check = if options_for_check.empty?
95
96
  check_class.new
96
97
  else
97
98
  check_class.new(**options_for_check)
98
99
  end
100
+ check.ignored_patterns = ignored_patterns
99
101
  check.options = options_for_check
100
102
  check
101
103
  end.compact
@@ -4,10 +4,11 @@
4
4
  # We'll use the node position to figure out if the test is disabled or not.
5
5
  module ThemeCheck
6
6
  class DisabledCheck
7
- attr_reader :name, :ranges
7
+ attr_reader :name, :template, :ranges
8
8
  attr_accessor :first_line
9
9
 
10
- def initialize(name)
10
+ def initialize(template, name)
11
+ @template = template
11
12
  @name = name
12
13
  @ranges = []
13
14
  @first_line = false
@@ -24,7 +25,8 @@ module ThemeCheck
24
25
  end
25
26
 
26
27
  def disabled?(index)
27
- ranges.any? { |range| range.cover?(index) }
28
+ index == 0 && first_line ||
29
+ ranges.any? { |range| range.cover?(index) }
28
30
  end
29
31
 
30
32
  def last
@@ -33,7 +35,7 @@ module ThemeCheck
33
35
 
34
36
  def missing_end_index?
35
37
  return false if first_line && ranges.size == 1
36
- last.end.nil?
38
+ last&.end.nil?
37
39
  end
38
40
  end
39
41
  end
@@ -10,28 +10,36 @@ module ThemeCheck
10
10
  ACTION_ENABLE_CHECKS = :enable
11
11
 
12
12
  def initialize
13
- @disabled_checks = {}
13
+ @disabled_checks = Hash.new do |hash, key|
14
+ template, check_name = key
15
+ hash[key] = DisabledCheck.new(template, check_name)
16
+ end
14
17
  end
15
18
 
16
19
  def update(node)
17
20
  text = comment_text(node)
18
21
  if start_disabling?(text)
19
22
  checks_from_text(text).each do |check_name|
20
- @disabled_checks[check_name] ||= DisabledCheck.new(check_name)
21
- @disabled_checks[check_name].start_index = node.start_index
22
- @disabled_checks[check_name].first_line = true if node.line_number == 1
23
+ disabled = @disabled_checks[[node.template, check_name]]
24
+ disabled.start_index = node.start_index
25
+ disabled.first_line = true if node.line_number == 1
23
26
  end
24
27
  elsif stop_disabling?(text)
25
28
  checks_from_text(text).each do |check_name|
26
- next unless @disabled_checks.key?(check_name)
27
- @disabled_checks[check_name].end_index = node.end_index
29
+ disabled = @disabled_checks[[node.template, check_name]]
30
+ next unless disabled
31
+ disabled.end_index = node.end_index
28
32
  end
29
33
  end
30
34
  end
31
35
 
32
- def disabled?(key, index)
33
- @disabled_checks[:all]&.disabled?(index) ||
34
- @disabled_checks[key]&.disabled?(index)
36
+ def disabled?(check, template, check_name, index)
37
+ return true if check.ignored_patterns&.any? do |pattern|
38
+ template.relative_path.fnmatch?(pattern)
39
+ end
40
+
41
+ @disabled_checks[[template, :all]]&.disabled?(index) ||
42
+ @disabled_checks[[template, check_name]]&.disabled?(index)
35
43
  end
36
44
 
37
45
  def checks_missing_end_index
@@ -40,6 +48,14 @@ module ThemeCheck
40
48
  .map(&:name)
41
49
  end
42
50
 
51
+ def remove_disabled_offenses(checks)
52
+ checks.disableable.each do |check|
53
+ check.offenses.reject! do |offense|
54
+ disabled?(check, offense.template, offense.code_name, offense.start_index)
55
+ end
56
+ end
57
+ end
58
+
43
59
  private
44
60
 
45
61
  def comment_text(node)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ class HtmlCheck < Check
5
+ extend ChecksTracking
6
+ end
7
+ end