theme-check 0.4.0 → 0.7.2

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 (108) 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 +40 -0
  5. data/CONTRIBUTING.md +5 -2
  6. data/Gemfile +5 -3
  7. data/LICENSE.md +2 -0
  8. data/README.md +12 -2
  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 +1 -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 +99 -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 +29 -9
  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 +2 -0
  79. data/lib/theme_check/language_server/completion_engine.rb +3 -3
  80. data/lib/theme_check/language_server/completion_helper.rb +0 -10
  81. data/lib/theme_check/language_server/completion_provider.rb +5 -0
  82. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +6 -2
  83. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
  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 +2 -2
  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 +67 -20
  89. data/lib/theme_check/language_server/server.rb +9 -4
  90. data/lib/theme_check/liquid_check.rb +11 -0
  91. data/lib/theme_check/node.rb +1 -2
  92. data/lib/theme_check/offense.rb +3 -1
  93. data/lib/theme_check/packager.rb +1 -1
  94. data/lib/theme_check/regex_helpers.rb +15 -0
  95. data/lib/theme_check/releaser.rb +39 -0
  96. data/lib/theme_check/remote_asset_file.rb +44 -0
  97. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +10 -8
  98. data/lib/theme_check/shopify_liquid/filter.rb +3 -5
  99. data/lib/theme_check/shopify_liquid/object.rb +2 -6
  100. data/lib/theme_check/shopify_liquid/tag.rb +1 -3
  101. data/lib/theme_check/storage.rb +3 -3
  102. data/lib/theme_check/string_helpers.rb +47 -0
  103. data/lib/theme_check/tags.rb +1 -2
  104. data/lib/theme_check/theme.rb +7 -1
  105. data/lib/theme_check/version.rb +1 -1
  106. data/packaging/homebrew/theme_check.base.rb +1 -1
  107. data/theme-check.gemspec +1 -2
  108. metadata +46 -18
@@ -7,12 +7,12 @@ module ThemeCheck
7
7
  return [] unless can_complete?(content, cursor)
8
8
  partial = first_word(content) || ''
9
9
  ShopifyLiquid::Tag.labels
10
- .select { |w| w.starts_with?(partial) }
10
+ .select { |w| w.start_with?(partial) }
11
11
  .map { |tag| tag_to_completion(tag) }
12
12
  end
13
13
 
14
14
  def can_complete?(content, cursor)
15
- content.starts_with?(Liquid::TagStart) && (
15
+ content.start_with?(Liquid::TagStart) && (
16
16
  cursor_on_first_word?(content, cursor) ||
17
17
  cursor_on_start_content?(content, cursor, Liquid::TagStart)
18
18
  )
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ PARTIAL_RENDER = %r{
6
+ \{\%-?\s*render\s+'(?<partial>[^']*)'|
7
+ \{\%-?\s*render\s+"(?<partial>[^"]*)"
8
+ }mix
9
+ end
10
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentLinkEngine
6
+ include PositionHelper
7
+ include RegexHelpers
8
+
9
+ def initialize(storage)
10
+ @storage = storage
11
+ end
12
+
13
+ def document_links(relative_path)
14
+ buffer = @storage.read(relative_path)
15
+ return [] unless buffer
16
+ matches(buffer, PARTIAL_RENDER).map do |match|
17
+ start_line, start_character = from_index_to_line_column(
18
+ buffer,
19
+ match.begin(:partial),
20
+ )
21
+
22
+ end_line, end_character = from_index_to_line_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
+ }
40
+ end
41
+ end
42
+
43
+ def link(partial)
44
+ "file://#{@storage.path('snippets/' + partial + '.liquid')}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -8,6 +8,7 @@ module ThemeCheck
8
8
  triggerCharacters: ['.', '{{ ', '{% '],
9
9
  context: true,
10
10
  },
11
+ documentLinkProvider: true,
11
12
  textDocumentSync: {
12
13
  openClose: true,
13
14
  change: TextDocumentSyncKind::FULL,
@@ -19,12 +20,13 @@ module ThemeCheck
19
20
  def initialize(server)
20
21
  @server = server
21
22
  @previously_reported_files = Set.new
22
- @storage = InMemoryStorage.new
23
- @completion_engine = CompletionEngine.new(@storage)
24
23
  end
25
24
 
26
25
  def on_initialize(id, params)
27
26
  @root_path = params["rootPath"]
27
+ @storage = in_memory_storage(@root_path)
28
+ @completion_engine = CompletionEngine.new(@storage)
29
+ @document_link_engine = DocumentLinkEngine.new(@storage)
28
30
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
29
31
  send_response(
30
32
  id: id,
@@ -40,41 +42,70 @@ module ThemeCheck
40
42
  alias_method :on_shutdown, :on_exit
41
43
 
42
44
  def on_text_document_did_change(_id, params)
43
- uri = text_document_uri(params)
44
- @storage.write(uri, content_changes_text(params))
45
+ relative_path = relative_path_from_text_document_uri(params)
46
+ @storage.write(relative_path, content_changes_text(params))
45
47
  end
46
48
 
47
49
  def on_text_document_did_close(_id, params)
48
- uri = text_document_uri(params)
49
- @storage.write(uri, nil)
50
+ relative_path = relative_path_from_text_document_uri(params)
51
+ @storage.write(relative_path, "")
50
52
  end
51
53
 
52
54
  def on_text_document_did_open(_id, params)
53
- uri = text_document_uri(params)
54
- @storage.write(uri, text_document_text(params))
55
- analyze_and_send_offenses(uri)
55
+ relative_path = relative_path_from_text_document_uri(params)
56
+ @storage.write(relative_path, text_document_text(params))
57
+ analyze_and_send_offenses(text_document_uri(params))
56
58
  end
57
59
 
58
60
  def on_text_document_did_save(_id, params)
59
61
  analyze_and_send_offenses(text_document_uri(params))
60
62
  end
61
63
 
64
+ def on_text_document_document_link(id, params)
65
+ relative_path = relative_path_from_text_document_uri(params)
66
+ send_response(
67
+ id: id,
68
+ result: document_links(relative_path)
69
+ )
70
+ end
71
+
62
72
  def on_text_document_completion(id, params)
63
- uri = text_document_uri(params)
73
+ relative_path = relative_path_from_text_document_uri(params)
64
74
  line = params.dig('position', 'line')
65
75
  col = params.dig('position', 'character')
66
76
  send_response(
67
77
  id: id,
68
- result: completions(uri, line, col)
78
+ result: completions(relative_path, line, col)
69
79
  )
70
80
  end
71
81
 
72
82
  private
73
83
 
84
+ def in_memory_storage(root)
85
+ config = config_for_path(root)
86
+
87
+ # Make a real FS to get the files from the snippets folder
88
+ fs = ThemeCheck::FileSystemStorage.new(
89
+ config.root,
90
+ ignored_patterns: config.ignored_patterns
91
+ )
92
+
93
+ # Turn that into a hash of empty buffers
94
+ files = fs.files
95
+ .map { |fn| [fn, ""] }
96
+ .to_h
97
+
98
+ InMemoryStorage.new(files, config.root)
99
+ end
100
+
74
101
  def text_document_uri(params)
75
102
  params.dig('textDocument', 'uri').sub('file://', '')
76
103
  end
77
104
 
105
+ def relative_path_from_text_document_uri(params)
106
+ @storage.relative_path(text_document_uri(params))
107
+ end
108
+
78
109
  def text_document_text(params)
79
110
  params.dig('textDocument', 'text')
80
111
  end
@@ -83,9 +114,13 @@ module ThemeCheck
83
114
  params.dig('contentChanges', 0, 'text')
84
115
  end
85
116
 
86
- def analyze_and_send_offenses(file_path)
87
- root = ThemeCheck::Config.find(file_path) || @root_path
88
- config = ThemeCheck::Config.from_path(root)
117
+ def config_for_path(path)
118
+ root = ThemeCheck::Config.find(path) || @root_path
119
+ ThemeCheck::Config.from_path(root)
120
+ end
121
+
122
+ def analyze_and_send_offenses(absolute_path)
123
+ config = config_for_path(absolute_path)
89
124
  storage = ThemeCheck::FileSystemStorage.new(
90
125
  config.root,
91
126
  ignored_patterns: config.ignored_patterns
@@ -104,8 +139,12 @@ module ThemeCheck
104
139
  analyzer.offenses
105
140
  end
106
141
 
107
- def completions(uri, line, col)
108
- @completion_engine.completions(uri, line, col)
142
+ def completions(relative_path, line, col)
143
+ @completion_engine.completions(relative_path, line, col)
144
+ end
145
+
146
+ def document_links(relative_path)
147
+ @document_link_engine.document_links(relative_path)
109
148
  end
110
149
 
111
150
  def send_diagnostics(offenses)
@@ -131,19 +170,27 @@ module ThemeCheck
131
170
  send_response(
132
171
  method: 'textDocument/publishDiagnostics',
133
172
  params: {
134
- uri: "file:#{path}",
173
+ uri: "file://#{path}",
135
174
  diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
136
175
  },
137
176
  )
138
177
  end
139
178
 
140
179
  def offense_to_diagnostic(offense)
141
- {
180
+ diagnostic = {
181
+ code: offense.code_name,
182
+ message: offense.message,
142
183
  range: range(offense),
143
184
  severity: severity(offense),
144
- code: offense.code_name,
145
185
  source: "theme-check",
146
- message: offense.message,
186
+ }
187
+ diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
188
+ diagnostic
189
+ end
190
+
191
+ def code_description(offense)
192
+ {
193
+ href: offense.doc,
147
194
  }
148
195
  end
149
196
 
@@ -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
@@ -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?
@@ -32,6 +32,8 @@ module ThemeCheck
32
32
  node&.markup
33
33
  end
34
34
 
35
+ raise ArgumentError, "Offense markup cannot be an empty string" if @markup.is_a?(String) && @markup.empty?
36
+
35
37
  @line_number = if line_number
36
38
  line_number
37
39
  elsif @node
@@ -83,7 +85,7 @@ module ThemeCheck
83
85
  end
84
86
 
85
87
  def check_name
86
- check.class.name.demodulize
88
+ StringHelpers.demodulize(check.class.name)
87
89
  end
88
90
 
89
91
  def doc
@@ -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