theme-check 1.3.0 → 1.5.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +38 -0
  5. data/CONTRIBUTING.md +58 -0
  6. data/Gemfile +3 -0
  7. data/config/default.yml +4 -1
  8. data/data/shopify_liquid/objects.yml +1 -0
  9. data/docs/checks/deprecate_lazysizes.md +0 -3
  10. data/docs/checks/deprecated_global_app_block_type.md +65 -0
  11. data/docs/checks/template_length.md +1 -1
  12. data/docs/flamegraph.svg +18488 -0
  13. data/lib/theme_check/analyzer.rb +1 -0
  14. data/lib/theme_check/checks/default_locale.rb +3 -1
  15. data/lib/theme_check/checks/deprecate_lazysizes.rb +6 -3
  16. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +57 -0
  17. data/lib/theme_check/checks/liquid_tag.rb +1 -1
  18. data/lib/theme_check/checks/pagination_size.rb +33 -14
  19. data/lib/theme_check/checks/remote_asset.rb +2 -2
  20. data/lib/theme_check/checks/required_directories.rb +3 -1
  21. data/lib/theme_check/checks/space_inside_braces.rb +47 -24
  22. data/lib/theme_check/checks/template_length.rb +1 -1
  23. data/lib/theme_check/cli.rb +28 -5
  24. data/lib/theme_check/corrector.rb +9 -0
  25. data/lib/theme_check/file_system_storage.rb +6 -0
  26. data/lib/theme_check/in_memory_storage.rb +4 -0
  27. data/lib/theme_check/json_file.rb +11 -0
  28. data/lib/theme_check/json_printer.rb +6 -1
  29. data/lib/theme_check/language_server/constants.rb +18 -11
  30. data/lib/theme_check/language_server/document_link_engine.rb +3 -67
  31. data/lib/theme_check/language_server/document_link_provider.rb +71 -0
  32. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  33. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  34. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  35. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  36. data/lib/theme_check/language_server/handler.rb +17 -13
  37. data/lib/theme_check/language_server/server.rb +11 -13
  38. data/lib/theme_check/language_server/uri_helper.rb +37 -0
  39. data/lib/theme_check/language_server.rb +6 -0
  40. data/lib/theme_check/node.rb +120 -8
  41. data/lib/theme_check/position.rb +27 -16
  42. data/lib/theme_check/position_helper.rb +13 -15
  43. data/lib/theme_check/printer.rb +9 -5
  44. data/lib/theme_check/remote_asset_file.rb +4 -0
  45. data/lib/theme_check/theme.rb +2 -1
  46. data/lib/theme_check/version.rb +1 -1
  47. metadata +11 -2
@@ -3,9 +3,13 @@ require 'json'
3
3
 
4
4
  module ThemeCheck
5
5
  class JsonPrinter
6
+ def initialize(out_stream = STDOUT)
7
+ @out = out_stream
8
+ end
9
+
6
10
  def print(offenses)
7
11
  json = offenses_by_path(offenses)
8
- puts JSON.dump(json)
12
+ @out.puts JSON.dump(json)
9
13
  end
10
14
 
11
15
  def offenses_by_path(offenses)
@@ -21,6 +25,7 @@ module ThemeCheck
21
25
  styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] },
22
26
  }
23
27
  end
28
+ .sort_by { |o| o[:path] }
24
29
  end
25
30
  end
26
31
  end
@@ -2,21 +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>[^"]*)"|
9
+
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')
8
19
 
9
- # in liquid tags the whole line is white space until render
10
- ^\s*render\s+'(?<partial>[^']*)'|
11
- ^\s*render\s+"(?<partial>[^"]*)"
12
- }mix
13
20
  ASSET_INCLUDE = %r{
14
- \{\%-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
15
- \{\%-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
21
+ \{\{-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
22
+ \{\{-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
16
23
 
17
24
  # in liquid tags the whole line is white space until the asset partial
18
- ^\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
19
- ^\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
25
+ ^\s*(?:echo|assign[^=]*\=)\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
26
+ ^\s*(?:echo|assign[^=]*\=)\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
20
27
  }mix
21
28
  end
22
29
  end
@@ -3,81 +3,17 @@
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
- snippet_matches = 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: snippet_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
- asset_matches = matches(buffer, ASSET_INCLUDE).map do |match|
42
- start_line, start_character = from_index_to_row_column(
43
- buffer,
44
- match.begin(:partial),
45
- )
46
-
47
- end_line, end_character = from_index_to_row_column(
48
- buffer,
49
- match.end(:partial)
50
- )
51
-
52
- {
53
- target: asset_link(match[:partial]),
54
- range: {
55
- start: {
56
- line: start_line,
57
- character: start_character,
58
- },
59
- end: {
60
- line: end_line,
61
- character: end_character,
62
- },
63
- },
64
- }
65
- end
66
- snippet_matches + asset_matches
67
- end
68
-
69
- def snippet_link(partial)
70
- file_link('snippets', partial, '.liquid')
71
- end
72
-
73
- def asset_link(partial)
74
- file_link('assets', partial, '')
75
- end
76
-
77
- private
78
-
79
- def file_link(directory, partial, extension)
80
- "file://#{@storage.path(directory + '/' + partial + extension)}"
81
17
  end
82
18
  end
83
19
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentLinkProvider
6
+ include RegexHelpers
7
+ include PositionHelper
8
+ include URIHelper
9
+
10
+ class << self
11
+ attr_accessor :partial_regexp, :destination_directory, :destination_postfix
12
+
13
+ def all
14
+ @all ||= []
15
+ end
16
+
17
+ def inherited(subclass)
18
+ all << subclass
19
+ end
20
+ end
21
+
22
+ def initialize(storage = InMemoryStorage.new)
23
+ @storage = storage
24
+ end
25
+
26
+ def partial_regexp
27
+ self.class.partial_regexp
28
+ end
29
+
30
+ def destination_directory
31
+ self.class.destination_directory
32
+ end
33
+
34
+ def destination_postfix
35
+ self.class.destination_postfix
36
+ end
37
+
38
+ def document_links(buffer)
39
+ matches(buffer, partial_regexp).map do |match|
40
+ start_line, start_character = from_index_to_row_column(
41
+ buffer,
42
+ match.begin(:partial),
43
+ )
44
+
45
+ end_line, end_character = from_index_to_row_column(
46
+ buffer,
47
+ match.end(:partial)
48
+ )
49
+
50
+ {
51
+ target: file_link(match[:partial]),
52
+ range: {
53
+ start: {
54
+ line: start_line,
55
+ character: start_character,
56
+ },
57
+ end: {
58
+ line: end_line,
59
+ character: end_character,
60
+ },
61
+ },
62
+ }
63
+ end
64
+ end
65
+
66
+ def file_link(partial)
67
+ file_uri(@storage.path(destination_directory + '/' + partial + destination_postfix))
68
+ end
69
+ end
70
+ end
71
+ 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,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "benchmark"
3
- require "uri"
4
- require "cgi"
5
4
 
6
5
  module ThemeCheck
7
6
  module LanguageServer
8
7
  class Handler
8
+ include URIHelper
9
+
9
10
  CAPABILITIES = {
10
11
  completionProvider: {
11
12
  triggerCharacters: ['.', '{{ ', '{% '],
@@ -26,7 +27,7 @@ module ThemeCheck
26
27
  end
27
28
 
28
29
  def on_initialize(id, params)
29
- @root_path = path_from_uri(params["rootUri"]) || params["rootPath"]
30
+ @root_path = root_path_from_params(params)
30
31
 
31
32
  # Tell the client we don't support anything if there's no rootPath
32
33
  return send_response(id, { capabilities: {} }) if @root_path.nil?
@@ -55,10 +56,9 @@ module ThemeCheck
55
56
  end
56
57
 
57
58
  def on_text_document_did_open(_id, params)
58
- return unless @diagnostics_tracker.first_run?
59
59
  relative_path = relative_path_from_text_document_uri(params)
60
60
  @storage.write(relative_path, text_document_text(params))
61
- analyze_and_send_offenses(text_document_uri(params))
61
+ analyze_and_send_offenses(text_document_uri(params)) if @diagnostics_tracker.first_run?
62
62
  end
63
63
 
64
64
  def on_text_document_did_save(_id, params)
@@ -97,19 +97,23 @@ module ThemeCheck
97
97
  end
98
98
 
99
99
  def text_document_uri(params)
100
- path_from_uri(params.dig('textDocument', 'uri'))
101
- end
102
-
103
- def path_from_uri(uri_string)
104
- return if uri_string.nil?
105
- uri = URI(uri_string)
106
- CGI.unescape(uri.path)
100
+ file_path(params.dig('textDocument', 'uri'))
107
101
  end
108
102
 
109
103
  def relative_path_from_text_document_uri(params)
110
104
  @storage.relative_path(text_document_uri(params))
111
105
  end
112
106
 
107
+ def root_path_from_params(params)
108
+ root_uri = params["rootUri"]
109
+ root_path = params["rootPath"]
110
+ if root_uri
111
+ file_path(root_uri)
112
+ elsif root_path
113
+ root_path
114
+ end
115
+ end
116
+
113
117
  def text_document_text(params)
114
118
  params.dig('textDocument', 'text')
115
119
  end
@@ -175,7 +179,7 @@ module ThemeCheck
175
179
  def send_diagnostic(path, offenses)
176
180
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
177
181
  send_notification('textDocument/publishDiagnostics', {
178
- uri: "file://#{path}",
182
+ uri: file_uri(path),
179
183
  diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
180
184
  })
181
185
  end
@@ -25,6 +25,15 @@ module ThemeCheck
25
25
  @out = out_stream
26
26
  @err = err_stream
27
27
 
28
+ # Because programming is fun,
29
+ #
30
+ # Ruby on Windows turns \n into \r\n. Which means that \r\n
31
+ # gets turned into \r\r\n. Which means that the protocol
32
+ # breaks on windows unless we turn STDOUT into binary mode.
33
+ #
34
+ # Hours wasted: 9.
35
+ @out.binmode
36
+
28
37
  @out.sync = true # do not buffer
29
38
  @err.sync = true # do not buffer
30
39
 
@@ -52,19 +61,8 @@ module ThemeCheck
52
61
  response_body = JSON.dump(response)
53
62
  log(JSON.pretty_generate(response)) if $DEBUG
54
63
 
55
- # Because programming is fun,
56
- #
57
- # Ruby on Windows turns \n into \r\n. Which means that \r\n
58
- # gets turned into \r\r\n. Which means that the protocol
59
- # breaks on windows unless we turn STDOUT into binary mode and
60
- # set the encoding manually (yuk!) or we do this little hack
61
- # here and put \n which gets transformed into \r\n on windows
62
- # only...
63
- #
64
- # Hours wasted: 8.
65
- eol = Gem.win_platform? ? "\n" : "\r\n"
66
- @out.write("Content-Length: #{response_body.bytesize}#{eol}")
67
- @out.write(eol)
64
+ @out.write("Content-Length: #{response_body.bytesize}\r\n")
65
+ @out.write("\r\n")
68
66
  @out.write(response_body)
69
67
  @out.flush
70
68
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+ require "uri"
5
+ require "cgi"
6
+
7
+ module ThemeCheck
8
+ module LanguageServer
9
+ module URIHelper
10
+ # Will URI.encode a string the same way VS Code would. There are two things
11
+ # to watch out for:
12
+ #
13
+ # 1. VS Code still uses the outdated '%20' for spaces
14
+ # 2. VS Code prefixes Windows paths with / (so /C:/Users/... is expected)
15
+ #
16
+ # Exists because of https://github.com/Shopify/theme-check/issues/360
17
+ def file_uri(absolute_path)
18
+ "file://" + absolute_path
19
+ .to_s
20
+ .split('/')
21
+ .map { |x| CGI.escape(x).gsub('+', '%20') }
22
+ .join('/')
23
+ .sub(%r{^/?}, '/') # Windows paths should be prefixed by /c:
24
+ end
25
+
26
+ def file_path(uri_string)
27
+ return if uri_string.nil?
28
+ uri = URI(uri_string)
29
+ path = CGI.unescape(uri.path)
30
+ # On Windows, VS Code sends the URLs as file:///C:/...
31
+ # /C:/1234 is not a valid path in ruby. So we strip the slash.
32
+ path = path.sub(%r{^/([a-z]:/)}i, '\1')
33
+ path
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "language_server/protocol"
3
3
  require_relative "language_server/constants"
4
+ require_relative "language_server/uri_helper"
4
5
  require_relative "language_server/handler"
5
6
  require_relative "language_server/server"
6
7
  require_relative "language_server/tokens"
@@ -8,6 +9,7 @@ require_relative "language_server/variable_lookup_finder"
8
9
  require_relative "language_server/completion_helper"
9
10
  require_relative "language_server/completion_provider"
10
11
  require_relative "language_server/completion_engine"
12
+ require_relative "language_server/document_link_provider"
11
13
  require_relative "language_server/document_link_engine"
12
14
  require_relative "language_server/diagnostics_tracker"
13
15
 
@@ -15,6 +17,10 @@ Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
15
17
  require file
16
18
  end
17
19
 
20
+ Dir[__dir__ + "/language_server/document_link_providers/*.rb"].each do |file|
21
+ require file
22
+ end
23
+
18
24
  module ThemeCheck
19
25
  module LanguageServer
20
26
  def self.start