theme-check 0.3.3 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +10 -3
  3. data/.rubocop.yml +12 -3
  4. data/CHANGELOG.md +42 -0
  5. data/CONTRIBUTING.md +5 -2
  6. data/Gemfile +5 -3
  7. data/LICENSE.md +2 -0
  8. data/README.md +12 -4
  9. data/RELEASING.md +10 -3
  10. data/Rakefile +6 -0
  11. data/config/default.yml +16 -0
  12. data/data/shopify_liquid/tags.yml +27 -0
  13. data/data/shopify_translation_keys.yml +850 -0
  14. data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
  15. data/docs/checks/asset_size_css.md +52 -0
  16. data/docs/checks/asset_size_javascript.md +79 -0
  17. data/docs/checks/convert_include_to_render.md +48 -0
  18. data/docs/checks/default_locale.md +46 -0
  19. data/docs/checks/deprecated_filter.md +46 -0
  20. data/docs/checks/img_width_and_height.md +79 -0
  21. data/docs/checks/liquid_tag.md +65 -0
  22. data/docs/checks/matching_schema_translations.md +93 -0
  23. data/docs/checks/matching_translations.md +72 -0
  24. data/docs/checks/missing_enable_comment.md +50 -0
  25. data/docs/checks/missing_required_template_files.md +26 -0
  26. data/docs/checks/missing_template.md +40 -0
  27. data/docs/checks/nested_snippet.md +69 -0
  28. data/docs/checks/parser_blocking_javascript.md +97 -0
  29. data/docs/checks/remote_asset.md +82 -0
  30. data/docs/checks/required_directories.md +25 -0
  31. data/docs/checks/required_layout_theme_object.md +28 -0
  32. data/docs/checks/space_inside_braces.md +63 -0
  33. data/docs/checks/syntax_error.md +49 -0
  34. data/docs/checks/template_length.md +50 -0
  35. data/docs/checks/translation_key_exists.md +63 -0
  36. data/docs/checks/undefined_object.md +53 -0
  37. data/docs/checks/unknown_filter.md +45 -0
  38. data/docs/checks/unused_assign.md +47 -0
  39. data/docs/checks/unused_snippet.md +32 -0
  40. data/docs/checks/valid_html_translation.md +53 -0
  41. data/docs/checks/valid_json.md +60 -0
  42. data/docs/checks/valid_schema.md +50 -0
  43. data/lib/theme_check.rb +4 -0
  44. data/lib/theme_check/asset_file.rb +34 -0
  45. data/lib/theme_check/check.rb +20 -10
  46. data/lib/theme_check/checks/asset_size_css.rb +89 -0
  47. data/lib/theme_check/checks/asset_size_javascript.rb +68 -0
  48. data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
  49. data/lib/theme_check/checks/default_locale.rb +1 -0
  50. data/lib/theme_check/checks/deprecated_filter.rb +1 -1
  51. data/lib/theme_check/checks/img_width_and_height.rb +74 -0
  52. data/lib/theme_check/checks/liquid_tag.rb +3 -3
  53. data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
  54. data/lib/theme_check/checks/matching_translations.rb +2 -1
  55. data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
  56. data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
  57. data/lib/theme_check/checks/missing_template.rb +1 -0
  58. data/lib/theme_check/checks/nested_snippet.rb +1 -0
  59. data/lib/theme_check/checks/parser_blocking_javascript.rb +8 -15
  60. data/lib/theme_check/checks/remote_asset.rb +98 -0
  61. data/lib/theme_check/checks/required_directories.rb +1 -1
  62. data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
  63. data/lib/theme_check/checks/space_inside_braces.rb +1 -0
  64. data/lib/theme_check/checks/syntax_error.rb +1 -0
  65. data/lib/theme_check/checks/template_length.rb +1 -0
  66. data/lib/theme_check/checks/translation_key_exists.rb +14 -1
  67. data/lib/theme_check/checks/undefined_object.rb +11 -5
  68. data/lib/theme_check/checks/unknown_filter.rb +1 -0
  69. data/lib/theme_check/checks/unused_assign.rb +1 -0
  70. data/lib/theme_check/checks/unused_snippet.rb +1 -0
  71. data/lib/theme_check/checks/valid_html_translation.rb +2 -1
  72. data/lib/theme_check/checks/valid_json.rb +1 -0
  73. data/lib/theme_check/checks/valid_schema.rb +1 -0
  74. data/lib/theme_check/cli.rb +49 -13
  75. data/lib/theme_check/config.rb +5 -2
  76. data/lib/theme_check/disabled_checks.rb +2 -2
  77. data/lib/theme_check/in_memory_storage.rb +13 -8
  78. data/lib/theme_check/language_server.rb +12 -0
  79. data/lib/theme_check/language_server/completion_engine.rb +38 -0
  80. data/lib/theme_check/language_server/completion_helper.rb +25 -0
  81. data/lib/theme_check/language_server/completion_provider.rb +28 -0
  82. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +51 -0
  83. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
  84. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
  85. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
  86. data/lib/theme_check/language_server/constants.rb +10 -0
  87. data/lib/theme_check/language_server/document_link_engine.rb +48 -0
  88. data/lib/theme_check/language_server/handler.rb +105 -10
  89. data/lib/theme_check/language_server/position_helper.rb +27 -0
  90. data/lib/theme_check/language_server/protocol.rb +41 -0
  91. data/lib/theme_check/language_server/server.rb +9 -4
  92. data/lib/theme_check/language_server/tokens.rb +55 -0
  93. data/lib/theme_check/liquid_check.rb +11 -0
  94. data/lib/theme_check/node.rb +1 -2
  95. data/lib/theme_check/offense.rb +52 -17
  96. data/lib/theme_check/packager.rb +1 -1
  97. data/lib/theme_check/regex_helpers.rb +15 -0
  98. data/lib/theme_check/releaser.rb +39 -0
  99. data/lib/theme_check/remote_asset_file.rb +44 -0
  100. data/lib/theme_check/shopify_liquid.rb +1 -0
  101. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +10 -8
  102. data/lib/theme_check/shopify_liquid/filter.rb +3 -5
  103. data/lib/theme_check/shopify_liquid/object.rb +2 -6
  104. data/lib/theme_check/shopify_liquid/tag.rb +14 -0
  105. data/lib/theme_check/storage.rb +3 -3
  106. data/lib/theme_check/string_helpers.rb +47 -0
  107. data/lib/theme_check/tags.rb +1 -2
  108. data/lib/theme_check/theme.rb +7 -1
  109. data/lib/theme_check/version.rb +1 -1
  110. data/packaging/homebrew/theme_check.base.rb +1 -1
  111. data/theme-check.gemspec +1 -2
  112. metadata +57 -18
@@ -1,20 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
  require 'json'
3
3
  require 'stringio'
4
- require 'active_support/core_ext/string/inflections'
5
4
 
6
5
  module ThemeCheck
7
6
  module LanguageServer
8
7
  class DoneStreaming < StandardError; end
8
+
9
9
  class IncompatibleStream < StandardError; end
10
10
 
11
11
  class Server
12
12
  attr_reader :handler
13
+ attr_reader :should_raise_errors
13
14
 
14
15
  def initialize(
15
16
  in_stream: STDIN,
16
17
  out_stream: STDOUT,
17
- err_stream: $DEBUG ? File.open('/tmp/lsp.log', 'a') : STDERR
18
+ err_stream: STDERR,
19
+ should_raise_errors: false
18
20
  )
19
21
  validate!([in_stream, out_stream, err_stream])
20
22
 
@@ -25,6 +27,8 @@ module ThemeCheck
25
27
 
26
28
  @out.sync = true # do not buffer
27
29
  @err.sync = true # do not buffer
30
+
31
+ @should_raise_errors = should_raise_errors
28
32
  end
29
33
 
30
34
  def listen
@@ -37,6 +41,7 @@ module ThemeCheck
37
41
  return 0
38
42
 
39
43
  rescue Exception => e # rubocop:disable Lint/RescueException
44
+ raise e if should_raise_errors
40
45
  log(e)
41
46
  log(e.backtrace)
42
47
  return 1
@@ -45,7 +50,7 @@ module ThemeCheck
45
50
 
46
51
  def send_response(response)
47
52
  response_body = JSON.dump(response)
48
- log(response_body) if $DEBUG
53
+ log(JSON.pretty_generate(response)) if $DEBUG
49
54
 
50
55
  @out.write("Content-Length: #{response_body.size}\r\n")
51
56
  @out.write("\r\n")
@@ -93,7 +98,7 @@ module ThemeCheck
93
98
  end
94
99
 
95
100
  def to_snake_case(method_name)
96
- method_name.gsub(/[^\w]/, '_').underscore
101
+ StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
97
102
  end
98
103
 
99
104
  def initial_line
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ Token = Struct.new(
5
+ :content,
6
+ :start, # inclusive
7
+ :end, # exclusive
8
+ )
9
+
10
+ TAG_START = Liquid::TagStart
11
+ TAG_END = Liquid::TagEnd
12
+ VARIABLE_START = Liquid::VariableStart
13
+ VARIABLE_END = Liquid::VariableEnd
14
+ SPLITTER = %r{
15
+ (?=(?:#{TAG_START}|#{VARIABLE_START}))| # positive lookahead on tag/variable start
16
+ (?<=(?:#{TAG_END}|#{VARIABLE_END})) # positive lookbehind on tag/variable end
17
+ }xom
18
+
19
+ # Implemented as an Enumerable so we stop iterating on the find once
20
+ # we have what we want. Kind of a perf thing.
21
+ class Tokens
22
+ include Enumerable
23
+
24
+ def initialize(buffer)
25
+ @buffer = buffer
26
+ end
27
+
28
+ def each(&block)
29
+ return to_enum(:each) unless block_given?
30
+
31
+ chunks = @buffer.split(SPLITTER)
32
+ chunks.shift if chunks[0]&.empty?
33
+
34
+ prev = Token.new('', 0, 0)
35
+ curr = Token.new('', 0, 0)
36
+
37
+ while (content = chunks.shift)
38
+
39
+ curr.start = prev.end
40
+ curr.end = curr.start + content.size
41
+
42
+ block.call(Token.new(
43
+ content,
44
+ curr.start,
45
+ curr.end,
46
+ ))
47
+
48
+ # recycling structs
49
+ tmp = prev
50
+ prev = curr
51
+ curr = tmp
52
+ end
53
+ end
54
+ end
55
+ end
@@ -6,6 +6,17 @@ module ThemeCheck
6
6
  extend ChecksTracking
7
7
  include ParsingHelpers
8
8
 
9
+ TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
10
+ VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
11
+ START_OR_END_QUOTE = /(^['"])|(['"]$)/
12
+ QUOTED_LIQUID_ATTRIBUTE = %r{
13
+ '(?:#{TAG}|#{VARIABLE}|[^'])*'| # any combination of tag/variable or non straight quote inside straight quotes
14
+ "(?:#{TAG}|#{VARIABLE}|[^"])*" # any combination of tag/variable or non double quotes inside double quotes
15
+ }omix
16
+ ATTR = /[a-z0-9-]+/i
17
+ HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
18
+ HTML_ATTRIBUTES = /(?:#{HTML_ATTRIBUTE}|\s)*/omix
19
+
9
20
  def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
10
21
  offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
11
22
  end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'active_support/core_ext/string/inflections'
3
2
 
4
3
  module ThemeCheck
5
4
  # A node from the Liquid AST, the result of parsing a template.
@@ -101,7 +100,7 @@ module ThemeCheck
101
100
  # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
102
101
  # and `after_<type_name>` check methods.
103
102
  def type_name
104
- @type_name ||= @value.class.name.demodulize.underscore.to_sym
103
+ @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
105
104
  end
106
105
 
107
106
  # Is this node inside a `{% liquid ... %}` block?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
+ Position = Struct.new(:line, :column)
4
+
3
5
  class Offense
4
6
  MAX_SOURCE_EXCERPT_SIZE = 120
5
7
 
@@ -35,6 +37,9 @@ module ThemeCheck
35
37
  elsif @node
36
38
  @node.line_number
37
39
  end
40
+
41
+ @start_position = nil
42
+ @end_position = nil
38
43
  end
39
44
 
40
45
  def source_excerpt
@@ -50,29 +55,19 @@ module ThemeCheck
50
55
  end
51
56
 
52
57
  def start_line
53
- return 0 unless line_number
54
- line_number - 1
58
+ start_position.line
55
59
  end
56
60
 
57
- def end_line
58
- if markup&.ends_with?("\n")
59
- start_line + markup.count("\n") - 1
60
- elsif markup
61
- start_line + markup.count("\n")
62
- else
63
- start_line
64
- end
61
+ def start_column
62
+ start_position.column
65
63
  end
66
64
 
67
- def start_column
68
- return 0 unless line_number && markup
69
- template.full_line(start_line + 1).index(markup.split("\n", 2).first)
65
+ def end_line
66
+ end_position.line
70
67
  end
71
68
 
72
69
  def end_column
73
- return 0 unless line_number && markup
74
- markup_end = markup.split("\n").last
75
- template.full_line(end_line + 1).index(markup_end) + markup_end.size
70
+ end_position.column
76
71
  end
77
72
 
78
73
  def code_name
@@ -88,7 +83,7 @@ module ThemeCheck
88
83
  end
89
84
 
90
85
  def check_name
91
- check.class.name.demodulize
86
+ StringHelpers.demodulize(check.class.name)
92
87
  end
93
88
 
94
89
  def doc
@@ -118,5 +113,45 @@ module ThemeCheck
118
113
  message
119
114
  end
120
115
  end
116
+
117
+ private
118
+
119
+ def full_line(line)
120
+ # Liquid::Template is 1-indexed.
121
+ template.full_line(line + 1)
122
+ end
123
+
124
+ def lines_of_content
125
+ @lines ||= markup.lines.map { |x| x.sub(/\n$/, '') }
126
+ end
127
+
128
+ # 0-indexed, inclusive
129
+ def start_position
130
+ return @start_position if @start_position
131
+ return @start_position = Position.new(0, 0) unless line_number && markup
132
+
133
+ position = Position.new
134
+ position.line = line_number - 1
135
+ position.column = full_line(position.line).index(lines_of_content.first) || 0
136
+
137
+ @start_position = position
138
+ end
139
+
140
+ # 0-indexed, exclusive. It's the line + col that are exclusive.
141
+ # This is why it doesn't make sense to calculate them separately.
142
+ def end_position
143
+ return @end_position if @end_position
144
+ return @end_position = Position.new(0, 0) unless line_number && markup
145
+
146
+ position = Position.new
147
+ position.line = start_line + lines_of_content.size - 1
148
+ position.column = if start_line == position.line
149
+ start_column + markup.size
150
+ else
151
+ lines_of_content.last.size
152
+ end
153
+
154
+ @end_position = position
155
+ end
121
156
  end
122
157
  end
@@ -24,7 +24,7 @@ module ThemeCheck
24
24
  puts "Grabbing sha256 checksum from Rubygems.org"
25
25
  require 'digest/sha2'
26
26
  require 'open-uri'
27
- gem_checksum = open("https://rubygems.org/downloads/theme-check-#{ThemeCheck::VERSION}.gem") do |io|
27
+ gem_checksum = URI.open("https://rubygems.org/downloads/theme-check-#{ThemeCheck::VERSION}.gem") do |io|
28
28
  Digest::SHA256.new.hexdigest(io.read)
29
29
  end
30
30
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module RegexHelpers
5
+ def matches(s, re)
6
+ start_at = 0
7
+ matches = []
8
+ while (m = s.match(re, start_at))
9
+ matches.push(m)
10
+ start_at = m.end(0)
11
+ end
12
+ matches
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'theme_check/version'
3
+
4
+ module ThemeCheck
5
+ class Releaser
6
+ ROOT = File.expand_path('../../..', __FILE__)
7
+ LIB = File.join(ROOT, 'lib')
8
+
9
+ class VersionError < StandardError; end
10
+
11
+ def release(version)
12
+ raise VersionError, "Missing version argument." if version.nil?
13
+ raise VersionError, "Version should be a string." unless version.is_a?(String)
14
+ raise VersionError, "Version should be a valid semver version." unless version =~ /^\d+\.\d+.\d+$/
15
+ update_docs(version)
16
+ update_version(version)
17
+ end
18
+
19
+ def update_version(version)
20
+ version_file_path = File.join(LIB, 'theme_check/version.rb')
21
+ version_file = File.read(version_file_path)
22
+ updated_version_file = version_file.gsub(ThemeCheck::VERSION, version)
23
+
24
+ return if updated_version_file == version_file
25
+ puts "Updating version to #{version} in #{version_file_path}."
26
+ File.write(version_file_path, updated_version_file)
27
+ end
28
+
29
+ def update_docs(version)
30
+ Dir[ROOT + '/docs/checks/*.md'].each do |filename|
31
+ doc_content = File.read(filename)
32
+ updated_doc_content = doc_content.gsub('THEME_CHECK_VERSION', version)
33
+ next if updated_doc_content == doc_content
34
+ puts "Replacing `THEME_CHECK_VERSION` with #{version} in #{Pathname.new(filename).relative_path_from(ROOT)}"
35
+ File.write(filename, updated_doc_content)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ require "net/http"
3
+ require "pathname"
4
+
5
+ module ThemeCheck
6
+ class RemoteAssetFile
7
+ class << self
8
+ def cache
9
+ @cache ||= {}
10
+ end
11
+
12
+ def from_src(src)
13
+ key = uri(src).to_s
14
+ cache[key] = RemoteAssetFile.new(src) unless cache.key?(key)
15
+ cache[key]
16
+ end
17
+
18
+ def uri(src)
19
+ URI.parse(src.sub(%r{^//}, "https://"))
20
+ end
21
+ end
22
+
23
+ def initialize(src)
24
+ @uri = RemoteAssetFile.uri(src)
25
+ @content = nil
26
+ end
27
+
28
+ def content
29
+ return @content unless @content.nil?
30
+
31
+ res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == 'https') do |http|
32
+ req = Net::HTTP::Get.new(@uri)
33
+ req['Accept-Encoding'] = 'gzip, deflate, br'
34
+ http.request(req)
35
+ end
36
+
37
+ @content = res.body
38
+ end
39
+
40
+ def gzipped_size
41
+ @gzipped_size ||= content.bytesize
42
+ end
43
+ end
44
+ end
@@ -2,3 +2,4 @@
2
2
  require_relative 'shopify_liquid/deprecated_filter'
3
3
  require_relative 'shopify_liquid/filter'
4
4
  require_relative 'shopify_liquid/object'
5
+ require_relative 'shopify_liquid/tag'
@@ -10,17 +10,19 @@ module ThemeCheck
10
10
  all.fetch(filter, nil)
11
11
  end
12
12
 
13
+ def labels
14
+ @labels ||= all.keys
15
+ end
16
+
13
17
  private
14
18
 
15
19
  def all
16
- @all ||= begin
17
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/deprecated_filters.yml"))
18
- .values
19
- .each_with_object({}) do |filters, acc|
20
- filters.each do |(filter, alternatives)|
21
- acc[filter] = alternatives
22
- end
23
- end
20
+ @all ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/deprecated_filters.yml"))
21
+ .values
22
+ .each_with_object({}) do |filters, acc|
23
+ filters.each do |(filter, alternatives)|
24
+ acc[filter] = alternatives
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -7,11 +7,9 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @labels ||= begin
11
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/filters.yml"))
12
- .values
13
- .flatten
14
- end
10
+ @labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/filters.yml"))
11
+ .values
12
+ .flatten
15
13
  end
16
14
  end
17
15
  end
@@ -7,15 +7,11 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @labels ||= begin
11
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/objects.yml"))
12
- end
10
+ @labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/objects.yml"))
13
11
  end
14
12
 
15
13
  def plus_labels
16
- @plus_labels ||= begin
17
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/plus_objects.yml"))
18
- end
14
+ @plus_labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/plus_objects.yml"))
19
15
  end
20
16
  end
21
17
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'yaml'
3
+
4
+ module ThemeCheck
5
+ module ShopifyLiquid
6
+ module Tag
7
+ extend self
8
+
9
+ def labels
10
+ @tags ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
11
+ end
12
+ end
13
+ end
14
+ end