theme-check 1.11.0 → 1.12.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +82 -0
  5. data/README.md +4 -0
  6. data/Rakefile +7 -0
  7. data/TROUBLESHOOTING.md +65 -0
  8. data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
  9. data/data/shopify_liquid/documentation/filters.json +5528 -0
  10. data/data/shopify_liquid/documentation/latest.json +1 -0
  11. data/data/shopify_liquid/documentation/objects.json +19272 -0
  12. data/data/shopify_liquid/documentation/tags.json +1252 -0
  13. data/lib/theme_check/checks/undefined_object.rb +4 -0
  14. data/lib/theme_check/language_server/completion_context.rb +52 -0
  15. data/lib/theme_check/language_server/completion_engine.rb +15 -21
  16. data/lib/theme_check/language_server/completion_provider.rb +16 -1
  17. data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
  18. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
  19. data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
  20. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
  21. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
  22. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
  23. data/lib/theme_check/language_server/handler.rb +3 -1
  24. data/lib/theme_check/language_server/protocol.rb +9 -0
  25. data/lib/theme_check/language_server/type_helper.rb +22 -0
  26. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
  27. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
  28. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
  29. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  30. data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
  31. data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  32. data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  33. data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
  34. data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
  35. data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
  36. data/lib/theme_check/language_server.rb +12 -0
  37. data/lib/theme_check/remote_asset_file.rb +13 -7
  38. data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
  39. data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
  40. data/lib/theme_check/shopify_liquid/filter.rb +4 -0
  41. data/lib/theme_check/shopify_liquid/object.rb +4 -0
  42. data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
  43. data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
  44. data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
  45. data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
  46. data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
  47. data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
  48. data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
  49. data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
  50. data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
  51. data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
  52. data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
  53. data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
  54. data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
  55. data/lib/theme_check/shopify_liquid/tag.rb +4 -0
  56. data/lib/theme_check/shopify_liquid.rb +17 -1
  57. data/lib/theme_check/version.rb +1 -1
  58. data/shipit.rubygems.yml +3 -0
  59. data/theme-check.gemspec +3 -1
  60. metadata +37 -2
@@ -31,13 +31,7 @@ module ThemeCheck
31
31
  return if @uri.nil?
32
32
  return @content unless @content.nil?
33
33
 
34
- res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: @uri.scheme == 'https') do |http|
35
- req = Net::HTTP::Get.new(@uri)
36
- req['Accept-Encoding'] = 'gzip, deflate, br'
37
- http.request(req)
38
- end
39
-
40
- @content = res.body
34
+ @content = request(@uri)
41
35
 
42
36
  rescue OpenSSL::SSL::SSLError, Zlib::StreamError, *NET_HTTP_EXCEPTIONS
43
37
  @contents = ''
@@ -47,5 +41,17 @@ module ThemeCheck
47
41
  return if @uri.nil?
48
42
  @gzipped_size ||= content.bytesize
49
43
  end
44
+
45
+ private
46
+
47
+ def request(uri)
48
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
49
+ req = Net::HTTP::Get.new(uri)
50
+ req['Accept-Encoding'] = 'gzip, deflate, br'
51
+ http.request(req)
52
+ end
53
+
54
+ res.body
55
+ end
50
56
  end
51
57
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class Documentation
6
+ class MarkdownTemplate
7
+ MARKDOWN_RELATIVE_LINK = %r{(\[([^\[]+)\]\((/[^\)]+)\))*}
8
+
9
+ def render(entry)
10
+ [
11
+ title(entry),
12
+ body(entry),
13
+ ].reject(&:empty?).join("\n")
14
+ end
15
+
16
+ private
17
+
18
+ def title(entry)
19
+ "### #{entry.name}"
20
+ end
21
+
22
+ def body(entry)
23
+ [entry.deprecation_reason, entry.summary, entry.description]
24
+ .reject(&:nil?)
25
+ .reject(&:empty?)
26
+ .join(horizontal_rule)
27
+ .tap { |body| break(patch_urls!(body)) }
28
+ end
29
+
30
+ def horizontal_rule
31
+ "\n\n---\n\n"
32
+ end
33
+
34
+ def patch_urls!(body)
35
+ body.gsub(MARKDOWN_RELATIVE_LINK) do |original_link|
36
+ match = Regexp.last_match
37
+
38
+ text = match[2]
39
+ path = match[3]
40
+
41
+ if text && path
42
+ "[#{text}](https://shopify.dev#{path})"
43
+ else
44
+ original_link
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'documentation/markdown_template'
4
+
5
+ module ThemeCheck
6
+ module ShopifyLiquid
7
+ class Documentation
8
+ class << self
9
+ def filter_doc(filter_name)
10
+ render_doc(SourceIndex.filters.find { |entry| entry.name == filter_name })
11
+ end
12
+
13
+ def object_doc(object_name)
14
+ render_doc(SourceIndex.objects.find { |entry| entry.name == object_name })
15
+ end
16
+
17
+ def tag_doc(tag_name)
18
+ render_doc(SourceIndex.tags.find { |entry| entry.name == tag_name })
19
+ end
20
+
21
+ def object_property_doc(object_name, property_name)
22
+ property_entry = SourceIndex
23
+ .objects
24
+ .find { |entry| entry.name == object_name }
25
+ &.properties
26
+ &.find { |prop| prop.name == property_name }
27
+
28
+ render_doc(property_entry)
29
+ end
30
+
31
+ def render_doc(entry)
32
+ return nil unless entry
33
+ markdown_template.render(entry)
34
+ end
35
+
36
+ private
37
+
38
+ def markdown_template
39
+ @markdown_template ||= MarkdownTemplate.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -3,6 +3,10 @@ require 'yaml'
3
3
 
4
4
  module ThemeCheck
5
5
  module ShopifyLiquid
6
+ # TODO: (6/6) https://github.com/Shopify/theme-check/issues/656
7
+ # -
8
+ # Remove 'filters.yml' in favor of 'SourceIndex.filters'
9
+ # -
6
10
  module Filter
7
11
  extend self
8
12
 
@@ -3,6 +3,10 @@ require 'yaml'
3
3
 
4
4
  module ThemeCheck
5
5
  module ShopifyLiquid
6
+ # TODO: (4/6) https://github.com/Shopify/theme-check/issues/656
7
+ # -
8
+ # Remove 'objects.yml' in favor of 'SourceIndex.objects'
9
+ # -
6
10
  module Object
7
11
  extend self
8
12
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module ThemeCheck
6
+ module ShopifyLiquid
7
+ class SourceIndex
8
+ class BaseEntry
9
+ extend Forwardable
10
+
11
+ attr_reader :hash
12
+
13
+ def_delegators :return_type_instance, :generic_type?, :array_type?, :array_type, :to_s
14
+
15
+ def initialize(hash = {})
16
+ @hash = hash || {}
17
+ @return_type = nil
18
+ end
19
+
20
+ def name
21
+ hash['name']
22
+ end
23
+
24
+ def summary
25
+ hash['summary'] || ''
26
+ end
27
+
28
+ def description
29
+ hash['description'] || ''
30
+ end
31
+
32
+ def deprecated?
33
+ hash['deprecated']
34
+ end
35
+
36
+ def deprecation_reason
37
+ return nil unless deprecated?
38
+
39
+ hash['deprecation_reason'] || nil
40
+ end
41
+
42
+ attr_writer :return_type
43
+
44
+ def return_type
45
+ @return_type || to_s
46
+ end
47
+
48
+ def return_type_instance
49
+ ReturnTypeEntry.new(return_type_hash)
50
+ end
51
+
52
+ private
53
+
54
+ def return_type_hash
55
+ hash['return_type']&.first
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class BaseState
7
+ class << self
8
+ def mark_outdated
9
+ @up_to_date = false
10
+ end
11
+
12
+ def mark_up_to_date
13
+ @up_to_date = true
14
+ end
15
+
16
+ def outdated?
17
+ @up_to_date == false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class FilterEntry < BaseEntry
7
+ def parameters
8
+ (hash['parameters'] || [])
9
+ .map { |hash| ParameterEntry.new(hash) }
10
+ end
11
+
12
+ def input_type
13
+ hash['syntax'].split(' | ')[0]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class FilterState < BaseState
7
+ @up_to_date = false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class ObjectEntry < BaseEntry
7
+ def properties
8
+ (hash['properties'] || [])
9
+ .map { |hash| PropertyEntry.new(hash) }
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class ObjectState < BaseState
7
+ @up_to_date = false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class ParameterEntry < BaseEntry
7
+ def summary
8
+ nil
9
+ end
10
+
11
+ private
12
+
13
+ def return_type_hash
14
+ {
15
+ 'type' => (hash['types'] || ['untyped']).first,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class PropertyEntry < BaseEntry; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class ReturnTypeEntry < BaseEntry
7
+ def summary
8
+ nil
9
+ end
10
+
11
+ def to_s
12
+ hash['type']
13
+ end
14
+
15
+ def generic_type?
16
+ hash['type'] == 'generic'
17
+ end
18
+
19
+ def array_type?
20
+ !array_type.nil? && !array_type.empty?
21
+ end
22
+
23
+ def array_type
24
+ hash['array_value']
25
+ end
26
+
27
+ private
28
+
29
+ def return_type_hash
30
+ {
31
+ 'type' => "type<#{self}>",
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class TagEntry < BaseEntry
7
+ def parameters
8
+ (hash['parameters'] || [])
9
+ .map { |hash| ParameterEntry.new(hash) }
10
+ end
11
+
12
+ def return_type_hash
13
+ {
14
+ 'type' => "tag<#{name}>",
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module ShopifyLiquid
5
+ class SourceIndex
6
+ class TagState < BaseState
7
+ @up_to_date = false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pathname'
5
+
6
+ module ThemeCheck
7
+ module ShopifyLiquid
8
+ class SourceIndex
9
+ class << self
10
+ def filters
11
+ @filters = nil if FilterState.outdated?
12
+
13
+ @filters ||= FilterState.mark_up_to_date &&
14
+ load_file(:filters)
15
+ .map { |hash| FilterEntry.new(hash) }
16
+ end
17
+
18
+ def objects
19
+ @objects = nil if ObjectState.outdated?
20
+
21
+ @objects ||= ObjectState.mark_up_to_date &&
22
+ load_file(:objects)
23
+ .concat(built_in_objects)
24
+ .map { |hash| ObjectEntry.new(hash) }
25
+ end
26
+
27
+ def tags
28
+ @tags = nil if TagState.outdated?
29
+
30
+ @tags ||= TagState.mark_up_to_date &&
31
+ load_file(:tags)
32
+ .map { |hash| TagEntry.new(hash) }
33
+ end
34
+
35
+ private
36
+
37
+ def load_file(file_name)
38
+ read_json(local_path!(file_name))
39
+ end
40
+
41
+ def local_path!(file_name)
42
+ SourceManager.download unless SourceManager.has_required_files?
43
+ SourceManager.local_path(file_name)
44
+ end
45
+
46
+ def read_json(path)
47
+ JSON.parse(path.read)
48
+ end
49
+
50
+ def built_in_objects
51
+ load_file('../built_in_liquid_objects')
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'pathname'
5
+ require 'tmpdir'
6
+
7
+ module ThemeCheck
8
+ module ShopifyLiquid
9
+ class SourceManager
10
+ REQUIRED_FILE_NAMES = [:filters, :objects, :tags, :latest].freeze
11
+
12
+ class << self
13
+ def download_or_refresh_files(destination = default_destination)
14
+ if has_required_files?(destination)
15
+ refresh(destination)
16
+ else
17
+ download(destination)
18
+ end
19
+ end
20
+
21
+ def download(destination = default_destination)
22
+ Dir.mkdir(destination) unless destination.exist?
23
+
24
+ REQUIRED_FILE_NAMES.each do |file_name|
25
+ download_file(local_path(file_name, destination), remote_path(file_name))
26
+ end
27
+ end
28
+
29
+ def refresh(destination = default_destination)
30
+ refresh_threads << Thread.new { refresh_thread(destination) }
31
+ end
32
+
33
+ def local_path(file_name, destination = default_destination)
34
+ destination + "#{file_name}.json"
35
+ end
36
+
37
+ def has_required_files?(destination = default_destination)
38
+ REQUIRED_FILE_NAMES.all? { |file_name| local_path(file_name, destination).exist? }
39
+ end
40
+
41
+ def wait_downloads
42
+ refresh_threads.each(&:join)
43
+ end
44
+
45
+ private
46
+
47
+ def refresh_thread(destination)
48
+ return unless refresh_needed?(destination)
49
+
50
+ Dir.mktmpdir do |tmp_dir|
51
+ download(Pathname.new(tmp_dir))
52
+
53
+ FileUtils.cp_r("#{tmp_dir}/.", destination)
54
+
55
+ mark_all_indexes_outdated
56
+ end
57
+ end
58
+
59
+ def refresh_needed?(destination)
60
+ local_latest_content = local_path(:latest, destination).read
61
+ remote_latest_content = open_uri(remote_path(:latest))
62
+
63
+ revision(local_latest_content) != revision(remote_latest_content)
64
+ end
65
+
66
+ def revision(json_content)
67
+ # Raise an error if revision isn't found to avoid returning nil
68
+ JSON.parse(json_content).fetch('revision')
69
+ end
70
+
71
+ def remote_path(file_name)
72
+ "https://raw.githubusercontent.com/Shopify/theme-liquid-docs/main/data/#{file_name}.json"
73
+ end
74
+
75
+ def download_file(local_path, remote_uri)
76
+ File.open(local_path, "wb") do |file|
77
+ content = open_uri(remote_uri)
78
+ file.write(content)
79
+ end
80
+ end
81
+
82
+ def mark_all_indexes_outdated
83
+ SourceIndex::FilterState.mark_outdated
84
+ SourceIndex::ObjectState.mark_outdated
85
+ SourceIndex::TagState.mark_outdated
86
+ end
87
+
88
+ # State
89
+
90
+ def default_destination
91
+ @default_destination ||= Pathname.new("#{__dir__}/../../../data/shopify_liquid/documentation")
92
+ end
93
+
94
+ def refresh_threads
95
+ @refresh_threads ||= []
96
+ end
97
+
98
+ def open_uri(uri_str)
99
+ uri = URI.parse(uri_str)
100
+
101
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
102
+ req = Net::HTTP::Get.new(uri)
103
+ http.request(req)
104
+ end
105
+
106
+ res.body
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -37,6 +37,10 @@ module ThemeCheck
37
37
  label.keys[0]
38
38
  end
39
39
 
40
+ # TODO: (5/6) https://github.com/Shopify/theme-check/issues/656
41
+ # -
42
+ # Remove 'tags.yml' in favor of 'SourceIndex.tags'
43
+ # -
40
44
  def tags_file_contents
41
45
  @tags_file_contents ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
42
46
  end
@@ -1,6 +1,22 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'shopify_liquid/deprecated_filter'
4
+ require_relative 'shopify_liquid/documentation'
3
5
  require_relative 'shopify_liquid/filter'
4
6
  require_relative 'shopify_liquid/object'
5
- require_relative 'shopify_liquid/tag'
7
+ require_relative 'shopify_liquid/source_manager'
8
+ require_relative 'shopify_liquid/source_index'
6
9
  require_relative 'shopify_liquid/system_translations'
10
+ require_relative 'shopify_liquid/tag'
11
+
12
+ require_relative 'shopify_liquid/source_index/base_entry'
13
+ require_relative 'shopify_liquid/source_index/filter_entry'
14
+ require_relative 'shopify_liquid/source_index/object_entry'
15
+ require_relative 'shopify_liquid/source_index/parameter_entry'
16
+ require_relative 'shopify_liquid/source_index/property_entry'
17
+ require_relative 'shopify_liquid/source_index/return_type_entry'
18
+ require_relative 'shopify_liquid/source_index/tag_entry'
19
+ require_relative 'shopify_liquid/source_index/base_state'
20
+ require_relative 'shopify_liquid/source_index/filter_state'
21
+ require_relative 'shopify_liquid/source_index/object_state'
22
+ require_relative 'shopify_liquid/source_index/tag_state'
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.11.0"
3
+ VERSION = "1.12.0"
4
4
  end
@@ -0,0 +1,3 @@
1
+ deploy:
2
+ pre:
3
+ - bundle exec rake download_theme_liquid_docs
data/theme-check.gemspec CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |spec|
18
18
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
19
19
 
20
20
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
- %x{git ls-files -z}.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ # Load all files tracked in git except files in test directory
22
+ # Include untracked files in liquid documentation folder
23
+ %x{git ls-files -z}.split("\x0").reject { |f| f.match(%r{^test/}) } + Dir['data/shopify_liquid/documentation/**']
22
24
  end
23
25
  spec.bindir = "exe"
24
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }