theme-check 1.3.0 → 1.5.2

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