theme-check 0.4.0 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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