theme-check 0.8.2 → 0.10.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -0
  3. data/CHANGELOG.md +45 -0
  4. data/CONTRIBUTING.md +20 -90
  5. data/README.md +4 -1
  6. data/RELEASING.md +5 -3
  7. data/Rakefile +31 -0
  8. data/config/default.yml +45 -0
  9. data/docs/api/check.md +15 -0
  10. data/docs/api/html_check.md +46 -0
  11. data/docs/api/json_check.md +19 -0
  12. data/docs/api/liquid_check.md +99 -0
  13. data/docs/checks/{CHECK_DOCS_TEMPLATE.md → TEMPLATE.md.erb} +5 -5
  14. data/docs/checks/asset_url_filters.md +56 -0
  15. data/docs/checks/content_for_header_modification.md +42 -0
  16. data/docs/checks/img_lazy_loading.md +61 -0
  17. data/docs/checks/parser_blocking_script_tag.md +53 -0
  18. data/exe/theme-check-language-server +1 -2
  19. data/lib/theme_check.rb +8 -1
  20. data/lib/theme_check/analyzer.rb +72 -16
  21. data/lib/theme_check/bug.rb +1 -0
  22. data/lib/theme_check/check.rb +32 -7
  23. data/lib/theme_check/checks.rb +9 -1
  24. data/lib/theme_check/checks/TEMPLATE.rb.erb +11 -0
  25. data/lib/theme_check/checks/asset_url_filters.rb +46 -0
  26. data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
  27. data/lib/theme_check/checks/img_lazy_loading.rb +25 -0
  28. data/lib/theme_check/checks/img_width_and_height.rb +18 -49
  29. data/lib/theme_check/checks/missing_template.rb +1 -0
  30. data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
  31. data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
  32. data/lib/theme_check/checks/remote_asset.rb +21 -79
  33. data/lib/theme_check/checks/template_length.rb +3 -0
  34. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  35. data/lib/theme_check/config.rb +2 -0
  36. data/lib/theme_check/disabled_check.rb +6 -4
  37. data/lib/theme_check/disabled_checks.rb +25 -9
  38. data/lib/theme_check/html_check.rb +7 -0
  39. data/lib/theme_check/html_node.rb +56 -0
  40. data/lib/theme_check/html_visitor.rb +38 -0
  41. data/lib/theme_check/json_file.rb +13 -0
  42. data/lib/theme_check/language_server.rb +1 -0
  43. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
  44. data/lib/theme_check/language_server/diagnostics_tracker.rb +66 -0
  45. data/lib/theme_check/language_server/handler.rb +31 -26
  46. data/lib/theme_check/language_server/server.rb +1 -1
  47. data/lib/theme_check/liquid_check.rb +1 -4
  48. data/lib/theme_check/offense.rb +18 -0
  49. data/lib/theme_check/template.rb +9 -0
  50. data/lib/theme_check/theme.rb +7 -2
  51. data/lib/theme_check/version.rb +1 -1
  52. data/lib/theme_check/visitor.rb +2 -11
  53. metadata +20 -3
@@ -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
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require "forwardable"
3
+
4
+ module ThemeCheck
5
+ class HtmlNode
6
+ extend Forwardable
7
+ attr_reader :template
8
+
9
+ def_delegators :@value, :content, :attributes
10
+
11
+ def initialize(value, template)
12
+ @value = value
13
+ @template = template
14
+ end
15
+
16
+ def literal?
17
+ @value.name == "text"
18
+ end
19
+
20
+ def element?
21
+ @value.element?
22
+ end
23
+
24
+ def children
25
+ @value.children.map { |child| HtmlNode.new(child, template) }
26
+ end
27
+
28
+ def parent
29
+ HtmlNode.new(@value.parent, template)
30
+ end
31
+
32
+ def name
33
+ if @value.name == "#document-fragment"
34
+ "document"
35
+ else
36
+ @value.name
37
+ end
38
+ end
39
+
40
+ def value
41
+ if literal?
42
+ @value.content
43
+ else
44
+ @value
45
+ end
46
+ end
47
+
48
+ def markup
49
+ @value.to_html
50
+ end
51
+
52
+ def line_number
53
+ @value.line
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require "nokogumbo"
3
+ require "forwardable"
4
+
5
+ module ThemeCheck
6
+ class HtmlVisitor
7
+ attr_reader :checks
8
+
9
+ def initialize(checks)
10
+ @checks = checks
11
+ end
12
+
13
+ def visit_template(template)
14
+ doc = parse(template)
15
+ visit(HtmlNode.new(doc, template))
16
+ end
17
+
18
+ private
19
+
20
+ def parse(template)
21
+ Nokogiri::HTML5.fragment(template.source)
22
+ end
23
+
24
+ def visit(node)
25
+ call_checks(:on_element, node) if node.element?
26
+ call_checks(:"on_#{node.name}", node)
27
+ node.children.each { |child| visit(child) }
28
+ unless node.literal?
29
+ call_checks(:"after_#{node.name}", node)
30
+ call_checks(:after_element, node) if node.element?
31
+ end
32
+ end
33
+
34
+ def call_checks(method, *args)
35
+ checks.call(method, *args)
36
+ end
37
+ end
38
+ end
@@ -38,6 +38,19 @@ module ThemeCheck
38
38
  relative_path.sub_ext('').to_s
39
39
  end
40
40
 
41
+ def json?
42
+ true
43
+ end
44
+
45
+ def liquid?
46
+ false
47
+ end
48
+
49
+ def ==(other)
50
+ other.is_a?(JsonFile) && relative_path == other.relative_path
51
+ end
52
+ alias_method :eql?, :==
53
+
41
54
  private
42
55
 
43
56
  def load!
@@ -9,6 +9,7 @@ require_relative "language_server/completion_helper"
9
9
  require_relative "language_server/completion_provider"
10
10
  require_relative "language_server/completion_engine"
11
11
  require_relative "language_server/document_link_engine"
12
+ require_relative "language_server/diagnostics_tracker"
12
13
 
13
14
  Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
14
15
  require file
@@ -37,6 +37,7 @@ module ThemeCheck
37
37
  partial_match = matches(content, NAMED_FILTER).find do |match|
38
38
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
39
39
  end
40
+ return '' if partial_match.nil?
40
41
  partial_match[1]
41
42
  end
42
43
 
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ require "logger"
3
+
4
+ module ThemeCheck
5
+ module LanguageServer
6
+ class DiagnosticsTracker
7
+ def initialize
8
+ @previously_reported_files = Set.new
9
+ @single_files_offenses = {}
10
+ @first_run = true
11
+ end
12
+
13
+ def first_run?
14
+ @first_run
15
+ end
16
+
17
+ def build_diagnostics(offenses, analyzed_files: nil)
18
+ reported_files = Set.new
19
+ new_single_file_offenses = {}
20
+ analyzed_files = analyzed_files.map { |path| Pathname.new(path) } if analyzed_files
21
+
22
+ offenses.group_by(&:template).each do |template, template_offenses|
23
+ next unless template
24
+ reported_offenses = template_offenses
25
+ previous_offenses = @single_files_offenses[template.path]
26
+ if analyzed_files.nil? || analyzed_files.include?(template.path)
27
+ # We re-analyzed the file, so we know the template_offenses are update to date.
28
+ reported_single_file_offenses = reported_offenses.select(&:single_file?)
29
+ if reported_single_file_offenses.any?
30
+ new_single_file_offenses[template.path] = reported_single_file_offenses
31
+ end
32
+ elsif previous_offenses
33
+ # Merge in the previous ones, if some
34
+ reported_offenses |= previous_offenses
35
+ end
36
+ yield template.path, reported_offenses
37
+ reported_files << template.path
38
+ end
39
+
40
+ @single_files_offenses.each do |path, _|
41
+ # Already reported above, skip
42
+ next if reported_files.include?(path)
43
+
44
+ if analyzed_files.nil? || analyzed_files.include?(path)
45
+ # We re-analyzed this file, if it was not reported, all offenses in it got fixed
46
+ yield path, []
47
+ new_single_file_offenses[path] = nil
48
+ end
49
+ # NOTE: No need to re-report previous offenses as LSP should keep them around until
50
+ # we clear them.
51
+ reported_files << path
52
+ end
53
+
54
+ # Publish diagnostics with empty array if all issues on a previously reported template
55
+ # have been fixed.
56
+ (@previously_reported_files - reported_files).each do |path|
57
+ yield path, []
58
+ end
59
+
60
+ @previously_reported_files = reported_files
61
+ @single_files_offenses.merge!(new_single_file_offenses)
62
+ @first_run = false
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "benchmark"
2
3
 
3
4
  module ThemeCheck
4
5
  module LanguageServer
@@ -19,7 +20,7 @@ module ThemeCheck
19
20
 
20
21
  def initialize(server)
21
22
  @server = server
22
- @previously_reported_files = Set.new
23
+ @diagnostics_tracker = DiagnosticsTracker.new
23
24
  end
24
25
 
25
26
  def on_initialize(id, params)
@@ -52,6 +53,7 @@ module ThemeCheck
52
53
  end
53
54
 
54
55
  def on_text_document_did_open(_id, params)
56
+ return unless @diagnostics_tracker.first_run?
55
57
  relative_path = relative_path_from_text_document_uri(params)
56
58
  @storage.write(relative_path, text_document_text(params))
57
59
  analyze_and_send_offenses(text_document_uri(params))
@@ -124,17 +126,32 @@ module ThemeCheck
124
126
  ignored_patterns: config.ignored_patterns
125
127
  )
126
128
  theme = ThemeCheck::Theme.new(storage)
127
-
128
- offenses = analyze(theme, config)
129
- log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
130
- send_diagnostics(offenses)
131
- end
132
-
133
- def analyze(theme, config)
134
129
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
135
- log("Checking #{config.root}")
136
- analyzer.analyze_theme
137
- analyzer.offenses
130
+
131
+ if @diagnostics_tracker.first_run?
132
+ # Analyze the full theme on first run
133
+ log("Checking #{config.root}")
134
+ offenses = nil
135
+ time = Benchmark.measure do
136
+ offenses = analyzer.analyze_theme
137
+ end
138
+ log("Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s")
139
+ send_diagnostics(offenses)
140
+ else
141
+ # Analyze selected files
142
+ relative_path = Pathname.new(@storage.relative_path(absolute_path))
143
+ file = theme[relative_path]
144
+ # Skip if not a theme file
145
+ if file
146
+ log("Checking #{relative_path}")
147
+ offenses = nil
148
+ time = Benchmark.measure do
149
+ offenses = analyzer.analyze_files([file])
150
+ end
151
+ log("Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s")
152
+ send_diagnostics(offenses, [absolute_path])
153
+ end
154
+ end
138
155
  end
139
156
 
140
157
  def completions(relative_path, line, col)
@@ -145,22 +162,10 @@ module ThemeCheck
145
162
  @document_link_engine.document_links(relative_path)
146
163
  end
147
164
 
148
- def send_diagnostics(offenses)
149
- reported_files = Set.new
150
-
151
- offenses.group_by(&:template).each do |template, template_offenses|
152
- next unless template
153
- send_diagnostic(template.path, template_offenses)
154
- reported_files << template.path
165
+ def send_diagnostics(offenses, analyzed_files = nil)
166
+ @diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
167
+ send_diagnostic(path, diagnostic_offenses)
155
168
  end
156
-
157
- # Publish diagnostics with empty array if all issues on a previously reported template
158
- # have been solved.
159
- (@previously_reported_files - reported_files).each do |path|
160
- send_diagnostic(path, [])
161
- end
162
-
163
- @previously_reported_files = reported_files
164
169
  end
165
170
 
166
171
  def send_diagnostic(path, offenses)
@@ -52,7 +52,7 @@ module ThemeCheck
52
52
  response_body = JSON.dump(response)
53
53
  log(JSON.pretty_generate(response)) if $DEBUG
54
54
 
55
- @out.write("Content-Length: #{response_body.size}\r\n")
55
+ @out.write("Content-Length: #{response_body.bytesize}\r\n")
56
56
  @out.write("\r\n")
57
57
  @out.write(response_body)
58
58
  @out.flush
@@ -6,6 +6,7 @@ module ThemeCheck
6
6
  extend ChecksTracking
7
7
  include ParsingHelpers
8
8
 
9
+ # TODO: remove this once all regex checks are migrate to HtmlCheck# TODO: remove this once all regex checks are migrate to HtmlCheck
9
10
  TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
10
11
  VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
11
12
  START_OR_END_QUOTE = /(^['"])|(['"]$)/
@@ -16,9 +17,5 @@ module ThemeCheck
16
17
  ATTR = /[a-z0-9-]+/i
17
18
  HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
18
19
  HTML_ATTRIBUTES = /(?:#{HTML_ATTRIBUTE}|\s)*/omix
19
-
20
- def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
21
- offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
22
- end
23
20
  end
24
21
  end