theme-check 1.0.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +2 -6
  3. data/CHANGELOG.md +50 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +39 -0
  6. data/RELEASING.md +34 -2
  7. data/bin/theme-check +29 -0
  8. data/bin/theme-check-language-server +29 -0
  9. data/config/default.yml +28 -1
  10. data/config/nothing.yml +11 -0
  11. data/config/theme_app_extension.yml +168 -0
  12. data/data/shopify_liquid/objects.yml +1 -0
  13. data/docs/checks/app_block_valid_tags.md +40 -0
  14. data/docs/checks/asset_size_app_block_css.md +52 -0
  15. data/docs/checks/asset_size_app_block_javascript.md +57 -0
  16. data/docs/checks/deprecate_lazysizes.md +0 -3
  17. data/docs/checks/missing_template.md +25 -0
  18. data/docs/checks/pagination_size.md +44 -0
  19. data/docs/checks/template_length.md +1 -1
  20. data/docs/checks/undefined_object.md +5 -0
  21. data/lib/theme_check/analyzer.rb +26 -21
  22. data/lib/theme_check/asset_file.rb +3 -15
  23. data/lib/theme_check/bug.rb +3 -1
  24. data/lib/theme_check/check.rb +26 -4
  25. data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
  26. data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
  27. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
  28. data/lib/theme_check/checks/asset_size_css.rb +3 -3
  29. data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
  30. data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
  31. data/lib/theme_check/checks/default_locale.rb +3 -1
  32. data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
  33. data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
  34. data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
  35. data/lib/theme_check/checks/img_width_and_height.rb +3 -3
  36. data/lib/theme_check/checks/missing_template.rb +21 -5
  37. data/lib/theme_check/checks/pagination_size.rb +65 -0
  38. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  39. data/lib/theme_check/checks/remote_asset.rb +3 -3
  40. data/lib/theme_check/checks/space_inside_braces.rb +27 -7
  41. data/lib/theme_check/checks/template_length.rb +1 -1
  42. data/lib/theme_check/checks/undefined_object.rb +1 -1
  43. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  44. data/lib/theme_check/checks.rb +11 -1
  45. data/lib/theme_check/cli.rb +52 -15
  46. data/lib/theme_check/config.rb +56 -10
  47. data/lib/theme_check/corrector.rb +9 -0
  48. data/lib/theme_check/exceptions.rb +29 -27
  49. data/lib/theme_check/file_system_storage.rb +12 -0
  50. data/lib/theme_check/html_check.rb +0 -1
  51. data/lib/theme_check/html_node.rb +37 -16
  52. data/lib/theme_check/html_visitor.rb +17 -3
  53. data/lib/theme_check/json_check.rb +2 -2
  54. data/lib/theme_check/json_file.rb +11 -27
  55. data/lib/theme_check/json_printer.rb +26 -0
  56. data/lib/theme_check/language_server/constants.rb +21 -6
  57. data/lib/theme_check/language_server/document_link_engine.rb +3 -31
  58. data/lib/theme_check/language_server/document_link_provider.rb +70 -0
  59. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  60. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  61. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  62. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  63. data/lib/theme_check/language_server/handler.rb +7 -4
  64. data/lib/theme_check/language_server/server.rb +13 -2
  65. data/lib/theme_check/language_server.rb +5 -0
  66. data/lib/theme_check/node.rb +6 -4
  67. data/lib/theme_check/offense.rb +56 -3
  68. data/lib/theme_check/parsing_helpers.rb +4 -3
  69. data/lib/theme_check/position.rb +98 -14
  70. data/lib/theme_check/regex_helpers.rb +5 -2
  71. data/lib/theme_check/tags.rb +26 -9
  72. data/lib/theme_check/template.rb +3 -32
  73. data/lib/theme_check/theme.rb +3 -0
  74. data/lib/theme_check/theme_file.rb +40 -0
  75. data/lib/theme_check/version.rb +1 -1
  76. data/lib/theme_check.rb +16 -0
  77. data/theme-check.gemspec +1 -1
  78. metadata +24 -6
  79. data/bin/liquid-server +0 -4
@@ -4,13 +4,14 @@ require "forwardable"
4
4
  module ThemeCheck
5
5
  class HtmlNode
6
6
  extend Forwardable
7
- attr_reader :template
7
+ include RegexHelpers
8
+ attr_reader :template, :parent
8
9
 
9
- def_delegators :@value, :content, :attributes
10
-
11
- def initialize(value, template)
10
+ def initialize(value, template, placeholder_values = [], parent = nil)
12
11
  @value = value
13
12
  @template = template
13
+ @placeholder_values = placeholder_values
14
+ @parent = parent
14
15
  end
15
16
 
16
17
  def literal?
@@ -22,35 +23,55 @@ module ThemeCheck
22
23
  end
23
24
 
24
25
  def children
25
- @value.children.map { |child| HtmlNode.new(child, template) }
26
+ @children ||= @value
27
+ .children
28
+ .map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
26
29
  end
27
30
 
28
- def parent
29
- HtmlNode.new(@value.parent, template)
31
+ def attributes
32
+ @attributes ||= @value.attributes
33
+ .map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
34
+ .to_h
30
35
  end
31
36
 
32
- def name
33
- if @value.name == "#document-fragment"
34
- "document"
35
- else
36
- @value.name
37
- end
37
+ def content
38
+ @content ||= replace_placeholders(@value.content)
38
39
  end
39
40
 
41
+ # @value is not forwarded because we _need_ to replace the
42
+ # placeholders for the HtmlNode to make sense.
40
43
  def value
41
44
  if literal?
42
- @value.content
45
+ content
43
46
  else
44
- @value
47
+ markup
48
+ end
49
+ end
50
+
51
+ def name
52
+ if @value.name == "#document-fragment"
53
+ "document"
54
+ else
55
+ @value.name
45
56
  end
46
57
  end
47
58
 
48
59
  def markup
49
- @value.to_html
60
+ @markup ||= replace_placeholders(@value.to_html)
50
61
  end
51
62
 
52
63
  def line_number
53
64
  @value.line
54
65
  end
66
+
67
+ private
68
+
69
+ def replace_placeholders(string)
70
+ # Replace all {%#{i}####%} with the actual content.
71
+ string.gsub(LIQUID_TAG) do |match|
72
+ key = /\d+/.match(match)[0]
73
+ @placeholder_values[key.to_i]
74
+ end
75
+ end
55
76
  end
56
77
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
- require "nokogumbo"
2
+ require "nokogiri"
3
3
  require "forwardable"
4
4
 
5
5
  module ThemeCheck
6
6
  class HtmlVisitor
7
+ include RegexHelpers
7
8
  attr_reader :checks
8
9
 
9
10
  def initialize(checks)
10
11
  @checks = checks
12
+ @placeholder_values = []
11
13
  end
12
14
 
13
15
  def visit_template(template)
14
16
  doc = parse(template)
15
- visit(HtmlNode.new(doc, template))
17
+ visit(HtmlNode.new(doc, template, @placeholder_values))
16
18
  rescue ArgumentError => e
17
19
  call_checks(:on_parse_error, e, template)
18
20
  end
@@ -20,7 +22,19 @@ module ThemeCheck
20
22
  private
21
23
 
22
24
  def parse(template)
23
- Nokogiri::HTML5.fragment(template.source, max_tree_depth: 400, max_attributes: 400)
25
+ parseable_source = +template.source.clone
26
+
27
+ # Replace all liquid tags with {%#{i}######%} to prevent the HTML
28
+ # parser from freaking out. We transparently replace those placeholders in
29
+ # HtmlNode.
30
+ matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
31
+ value = m[0]
32
+ @placeholder_values.push(value)
33
+ key = (@placeholder_values.size - 1).to_s
34
+ parseable_source[m.begin(0)...m.end(0)] = "{%#{key.ljust(m.end(0) - m.begin(0) - 4, '#')}%}"
35
+ end
36
+
37
+ Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400)
24
38
  end
25
39
 
26
40
  def visit(node)
@@ -4,8 +4,8 @@ module ThemeCheck
4
4
  class JsonCheck < Check
5
5
  extend ChecksTracking
6
6
 
7
- def add_offense(message, markup: nil, line_number: nil, template: nil)
8
- offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template)
7
+ def add_offense(message, markup: nil, line_number: nil, template: nil, &block)
8
+ offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, template: template, correction: block)
9
9
  end
10
10
  end
11
11
  end
@@ -1,29 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
- require "pathname"
4
3
 
5
4
  module ThemeCheck
6
- class JsonFile
5
+ class JsonFile < ThemeFile
7
6
  def initialize(relative_path, storage)
8
- @relative_path = relative_path
9
- @storage = storage
7
+ super
10
8
  @loaded = false
11
9
  @content = nil
12
10
  @parser_error = nil
13
11
  end
14
12
 
15
- def path
16
- @storage.path(@relative_path)
17
- end
18
-
19
- def relative_path
20
- @relative_pathname ||= Pathname.new(@relative_path)
21
- end
22
-
23
- def source
24
- @source ||= @storage.read(@relative_path)
25
- end
26
-
27
13
  def content
28
14
  load!
29
15
  @content
@@ -34,22 +20,20 @@ module ThemeCheck
34
20
  @parser_error
35
21
  end
36
22
 
37
- def name
38
- relative_path.sub_ext('').to_s
23
+ def update_contents(new_content = '{}')
24
+ @content = new_content
39
25
  end
40
26
 
41
- def json?
42
- true
43
- end
44
-
45
- def liquid?
46
- false
27
+ def write
28
+ if source != @content
29
+ @storage.write(@relative_path, content)
30
+ @source = content
31
+ end
47
32
  end
48
33
 
49
- def ==(other)
50
- other.is_a?(JsonFile) && relative_path == other.relative_path
34
+ def json?
35
+ true
51
36
  end
52
- alias_method :eql?, :==
53
37
 
54
38
  private
55
39
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module ThemeCheck
5
+ class JsonPrinter
6
+ def print(offenses)
7
+ json = offenses_by_path(offenses)
8
+ puts JSON.dump(json)
9
+ end
10
+
11
+ def offenses_by_path(offenses)
12
+ offenses
13
+ .map(&:to_h)
14
+ .group_by { |offense| offense[:path] }
15
+ .map do |(path, path_offenses)|
16
+ {
17
+ path: path,
18
+ offenses: path_offenses.map { |offense| offense.filter { |k, _v| k != :path } },
19
+ errorCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:error] },
20
+ suggestionCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:suggestion] },
21
+ styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] },
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,13 +2,28 @@
2
2
 
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
- PARTIAL_RENDER = %r{
6
- \{\%-?\s*render\s+'(?<partial>[^']*)'|
7
- \{\%-?\s*render\s+"(?<partial>[^"]*)"|
5
+ def self.partial_tag(tag)
6
+ %r{
7
+ \{\%-?\s*#{tag}\s+'(?<partial>[^']*)'|
8
+ \{\%-?\s*#{tag}\s+"(?<partial>[^"]*)"|
8
9
 
9
- # in liquid tags the whole line is white space until render
10
- ^\s*render\s+'(?<partial>[^']*)'|
11
- ^\s*render\s+"(?<partial>[^"]*)"
10
+ # in liquid tags the whole line is white space until the tag
11
+ ^\s*#{tag}\s+'(?<partial>[^']*)'|
12
+ ^\s*#{tag}\s+"(?<partial>[^"]*)"
13
+ }mix
14
+ end
15
+
16
+ PARTIAL_RENDER = partial_tag('render')
17
+ PARTIAL_INCLUDE = partial_tag('include')
18
+ PARTIAL_SECTION = partial_tag('section')
19
+
20
+ ASSET_INCLUDE = %r{
21
+ \{\{-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
22
+ \{\{-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
23
+
24
+ # in liquid tags the whole line is white space until the asset partial
25
+ ^\s*(?:echo|assign[^=]*\=)\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
26
+ ^\s*(?:echo|assign[^=]*\=)\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
12
27
  }mix
13
28
  end
14
29
  end
@@ -3,46 +3,18 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class DocumentLinkEngine
6
- include PositionHelper
7
- include RegexHelpers
8
-
9
6
  def initialize(storage)
10
7
  @storage = storage
8
+ @providers = DocumentLinkProvider.all.map { |x| x.new(storage) }
11
9
  end
12
10
 
13
11
  def document_links(relative_path)
14
12
  buffer = @storage.read(relative_path)
15
13
  return [] unless buffer
16
- matches(buffer, PARTIAL_RENDER).map do |match|
17
- start_line, start_character = from_index_to_row_column(
18
- buffer,
19
- match.begin(:partial),
20
- )
21
-
22
- end_line, end_character = from_index_to_row_column(
23
- buffer,
24
- match.end(:partial)
25
- )
26
-
27
- {
28
- target: link(match[:partial]),
29
- range: {
30
- start: {
31
- line: start_line,
32
- character: start_character,
33
- },
34
- end: {
35
- line: end_line,
36
- character: end_character,
37
- },
38
- },
39
- }
14
+ @providers.flat_map do |p|
15
+ p.document_links(buffer)
40
16
  end
41
17
  end
42
-
43
- def link(partial)
44
- "file://#{@storage.path('snippets/' + partial + '.liquid')}"
45
- end
46
18
  end
47
19
  end
48
20
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentLinkProvider
6
+ include RegexHelpers
7
+ include PositionHelper
8
+
9
+ class << self
10
+ attr_accessor :partial_regexp, :destination_directory, :destination_postfix
11
+
12
+ def all
13
+ @all ||= []
14
+ end
15
+
16
+ def inherited(subclass)
17
+ all << subclass
18
+ end
19
+ end
20
+
21
+ def initialize(storage = InMemoryStorage.new)
22
+ @storage = storage
23
+ end
24
+
25
+ def partial_regexp
26
+ self.class.partial_regexp
27
+ end
28
+
29
+ def destination_directory
30
+ self.class.destination_directory
31
+ end
32
+
33
+ def destination_postfix
34
+ self.class.destination_postfix
35
+ end
36
+
37
+ def document_links(buffer)
38
+ matches(buffer, partial_regexp).map do |match|
39
+ start_line, start_character = from_index_to_row_column(
40
+ buffer,
41
+ match.begin(:partial),
42
+ )
43
+
44
+ end_line, end_character = from_index_to_row_column(
45
+ buffer,
46
+ match.end(:partial)
47
+ )
48
+
49
+ {
50
+ target: file_link(match[:partial]),
51
+ range: {
52
+ start: {
53
+ line: start_line,
54
+ character: start_character,
55
+ },
56
+ end: {
57
+ line: end_line,
58
+ character: end_character,
59
+ },
60
+ },
61
+ }
62
+ end
63
+ end
64
+
65
+ def file_link(partial)
66
+ "file://#{@storage.path(destination_directory + '/' + partial + destination_postfix)}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class AssetDocumentLinkProvider < DocumentLinkProvider
6
+ @partial_regexp = ASSET_INCLUDE
7
+ @destination_directory = "assets"
8
+ @destination_postfix = ""
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class IncludeDocumentLinkProvider < DocumentLinkProvider
6
+ @partial_regexp = PARTIAL_INCLUDE
7
+ @destination_directory = "snippets"
8
+ @destination_postfix = ".liquid"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class RenderDocumentLinkProvider < DocumentLinkProvider
6
+ @partial_regexp = PARTIAL_RENDER
7
+ @destination_directory = "snippets"
8
+ @destination_postfix = ".liquid"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class SectionDocumentLinkProvider < DocumentLinkProvider
6
+ @partial_regexp = PARTIAL_SECTION
7
+ @destination_directory = "sections"
8
+ @destination_postfix = ".liquid"
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "benchmark"
3
+ require "uri"
4
+ require "cgi"
3
5
 
4
6
  module ThemeCheck
5
7
  module LanguageServer
@@ -53,10 +55,9 @@ module ThemeCheck
53
55
  end
54
56
 
55
57
  def on_text_document_did_open(_id, params)
56
- return unless @diagnostics_tracker.first_run?
57
58
  relative_path = relative_path_from_text_document_uri(params)
58
59
  @storage.write(relative_path, text_document_text(params))
59
- analyze_and_send_offenses(text_document_uri(params))
60
+ analyze_and_send_offenses(text_document_uri(params)) if @diagnostics_tracker.first_run?
60
61
  end
61
62
 
62
63
  def on_text_document_did_save(_id, params)
@@ -98,8 +99,10 @@ module ThemeCheck
98
99
  path_from_uri(params.dig('textDocument', 'uri'))
99
100
  end
100
101
 
101
- def path_from_uri(uri)
102
- uri&.sub('file://', '')
102
+ def path_from_uri(uri_string)
103
+ return if uri_string.nil?
104
+ uri = URI(uri_string)
105
+ CGI.unescape(uri.path)
103
106
  end
104
107
 
105
108
  def relative_path_from_text_document_uri(params)