theme-check 1.11.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ class AssignmentsFinder
7
+ include RegexHelpers
8
+
9
+ attr_reader :content, :scope_visitor
10
+
11
+ def initialize(content)
12
+ @content = close_tag(content)
13
+ @scope_visitor = ScopeVisitor.new
14
+ end
15
+
16
+ def find!
17
+ template = parse(content)
18
+
19
+ if template
20
+ visit_template(template)
21
+ return
22
+ end
23
+
24
+ liquid_tags.each do |tag|
25
+ visit_template(last_line_parse(tag))
26
+ end
27
+ end
28
+
29
+ def assignments
30
+ current_scope = scope_visitor.current_scope
31
+ current_scope.variables
32
+ end
33
+
34
+ private
35
+
36
+ def visit_template(template)
37
+ scope_visitor.visit_template(template)
38
+ end
39
+
40
+ def liquid_tags
41
+ matches(content, LIQUID_TAG_OR_VARIABLE)
42
+ .flat_map { |match| match[0] }
43
+ end
44
+
45
+ def parse(content)
46
+ regular_parse(content) || tolerant_parse(content)
47
+ end
48
+
49
+ def regular_parse(content)
50
+ Liquid::Template.parse(content)
51
+ rescue Liquid::SyntaxError
52
+ # Ignore syntax errors at the regular parse phase
53
+ end
54
+
55
+ def tolerant_parse(content)
56
+ TolerantParser::Template.parse(content)
57
+ rescue StandardError
58
+ # Ignore any error at the tolerant parse phase
59
+ end
60
+
61
+ def last_line_parse(content)
62
+ parsable_content = LiquidFixer.new(content).parsable
63
+
64
+ regular_parse(parsable_content)
65
+ end
66
+
67
+ def close_tag(content)
68
+ lines = content.lines
69
+ end_tag = lines.last =~ VARIABLE_START ? ' }}' : ' %}'
70
+
71
+ content + end_tag
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ module Constants
7
+ ANY_STARTING_TAG = /\s*#{Liquid::AnyStartingTag}/
8
+ ANY_ENDING_TAG = /#{Liquid::TagEnd}|#{Liquid::VariableEnd}\s*^/om
9
+
10
+ UNCLOSED_SQUARE_BRACKET = /\[[^\]]*\Z/
11
+ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED = %r{
12
+ (
13
+ # quotes not preceded by a [
14
+ (?<!\[)['"]|
15
+ # closing ]
16
+ \]|
17
+ # opening [
18
+ \[
19
+ )$
20
+ }x
21
+
22
+ VARIABLE_START = /\s*#{Liquid::VariableStart}/
23
+ VARIABLE_LOOKUP_CHARACTERS = /[a-z0-9_.'"\]\[]/i
24
+ VARIABLE_LOOKUP = /#{VARIABLE_LOOKUP_CHARACTERS}+/o
25
+ SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS = %r{
26
+ (?:
27
+ \s(?:
28
+ if|elsif|unless|and|or|#{Liquid::Condition.operators.keys.join("|")}
29
+ |echo
30
+ |case|when
31
+ |cycle
32
+ |in
33
+ )
34
+ |[:,=]
35
+ )
36
+ \s+
37
+ }omix
38
+ ENDS_WITH_BLANK_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}$/oimx
39
+ ENDS_WITH_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}#{VARIABLE_LOOKUP}$/oimx
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ ##
7
+ # Attempt to turn the code of the token until the cursor position into
8
+ # valid liquid code.
9
+ #
10
+ class LiquidFixer
11
+ include Constants
12
+
13
+ attr_reader :content, :cursor
14
+
15
+ def initialize(content, cursor = nil)
16
+ @content = content
17
+ @cursor = cursor || content.size
18
+ end
19
+
20
+ def parsable
21
+ # Welcome to Hackcity
22
+ @markup = content[0...cursor]
23
+
24
+ catch(:empty_lookup_markup) do
25
+ # close open delimiters
26
+ @markup += "'" if @markup.count("'").odd?
27
+ @markup += '"' if @markup.count('"').odd?
28
+ @markup += "]" if @markup =~ UNCLOSED_SQUARE_BRACKET
29
+
30
+ @ends_with_blank_potential_lookup = @markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
31
+ @markup = last_line if liquid_tag?
32
+
33
+ @markup = "{% #{@markup}" unless has_start_tag?
34
+
35
+ # close the tag
36
+ @markup += tag_end unless has_end_tag?
37
+
38
+ # close if statements
39
+ @markup += '{% endif %}' if tag?('if')
40
+
41
+ # close unless statements
42
+ @markup += '{% endunless %}' if tag?('unless')
43
+
44
+ # close elsif statements
45
+ @markup = "{% if x %}#{@markup}{% endif %}" if tag?('elsif')
46
+
47
+ # close case statements
48
+ @markup += '{% endcase %}' if tag?('case')
49
+
50
+ # close when statements
51
+ @markup = "{% case x %}#{@markup}{% endcase %}" if tag?('when')
52
+
53
+ # close for statements
54
+ @markup += '{% endfor %}' if tag?('for')
55
+
56
+ # close tablerow statements
57
+ @markup += '{% endtablerow %}' if tag?('tablerow')
58
+
59
+ @markup
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def tag?(tag_name)
66
+ if @markup =~ tag_regex(tag_name)
67
+ throw(:empty_lookup_markup, '') if @ends_with_blank_potential_lookup
68
+ true
69
+ else
70
+ false
71
+ end
72
+ end
73
+
74
+ def last_line
75
+ lines = @markup.rstrip.lines
76
+
77
+ last_line = lines.pop.lstrip while last_line.nil? || last_line =~ ANY_ENDING_TAG
78
+ last_line
79
+ end
80
+
81
+ def liquid_tag?
82
+ @markup =~ tag_regex('liquid')
83
+ end
84
+
85
+ def has_start_tag?
86
+ @markup =~ ANY_STARTING_TAG
87
+ end
88
+
89
+ def has_end_tag?
90
+ @markup =~ ANY_ENDING_TAG
91
+ end
92
+
93
+ def tag_end
94
+ @markup =~ VARIABLE_START ? ' }}' : ' %}'
95
+ end
96
+
97
+ def tag_regex(tag_name)
98
+ ShopifyLiquid::Tag.tag_regex(tag_name)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ class PotentialLookup < Struct.new(:name, :lookups, :scope)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ module TolerantParser
7
+ class Template
8
+ class << self
9
+ def parse(content)
10
+ ##
11
+ # The tolerant parser relies on a tolerant custom parse
12
+ # context to creates a new 'Template' object, even when
13
+ # a block is not closed.
14
+ Liquid::Template.parse(content, custom_parse_context)
15
+ end
16
+
17
+ private
18
+
19
+ def custom_parse_context
20
+ ParseContext.new
21
+ end
22
+ end
23
+ end
24
+
25
+ class ParseContext < Liquid::ParseContext
26
+ def new_block_body
27
+ BlockBody.new
28
+ end
29
+ end
30
+
31
+ class BlockBody < Liquid::BlockBody
32
+ ##
33
+ # The tags are statically defined and referenced at the
34
+ # 'Liquid::Template', so the TolerantParser just uses the
35
+ # redefined tags at this custom block body. Thus, there's
36
+ # no side-effects between the regular and the tolerant parsers.
37
+ def registered_tags
38
+ Tags.new(super)
39
+ end
40
+ end
41
+
42
+ class Tags
43
+ module TolerantBlockBody
44
+ ##
45
+ # This module defines the tolerant parse body that doesn't
46
+ # raise syntax errors when a block is not closed. Thus, the
47
+ # tolerant parser can build the AST for templates with this
48
+ # kind of error, which is quite common in language servers.
49
+ def parse_body(body, tokens)
50
+ super
51
+ rescue StandardError
52
+ false
53
+ end
54
+ end
55
+
56
+ class Case < Liquid::Case
57
+ include TolerantBlockBody
58
+ end
59
+
60
+ class For < Liquid::For
61
+ include TolerantBlockBody
62
+ end
63
+
64
+ class If < Liquid::If
65
+ include TolerantBlockBody
66
+ end
67
+
68
+ class TableRow < Liquid::TableRow
69
+ include TolerantBlockBody
70
+ end
71
+
72
+ class Unless < Liquid::Unless
73
+ include TolerantBlockBody
74
+ end
75
+
76
+ def initialize(standard_tags)
77
+ @standard_tags = standard_tags
78
+ @tolerant_tags = {
79
+ 'case' => Case,
80
+ 'for' => For,
81
+ 'if' => If,
82
+ 'tablerow' => TableRow,
83
+ 'unless' => Unless,
84
+ }
85
+ end
86
+
87
+ def [](key)
88
+ @tolerant_tags[key] || @standard_tags[key]
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,53 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ostruct'
4
+
3
5
  module ThemeCheck
4
6
  module LanguageServer
5
7
  module VariableLookupFinder
8
+ include Constants
9
+ include TypeHelper
6
10
  extend self
7
11
 
8
- UNCLOSED_SQUARE_BRACKET = /\[[^\]]*\Z/
9
- ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED = %r{
10
- (
11
- # quotes not preceded by a [
12
- (?<!\[)['"]|
13
- # closing ]
14
- \]|
15
- # opening [
16
- \[
17
- )$
18
- }x
19
-
20
- VARIABLE_LOOKUP_CHARACTERS = /[a-z0-9_.'"\]\[]/i
21
- VARIABLE_LOOKUP = /#{VARIABLE_LOOKUP_CHARACTERS}+/o
22
- SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS = %r{
23
- (?:
24
- \s(?:
25
- if|elsif|unless|and|or|#{Liquid::Condition.operators.keys.join("|")}
26
- |echo
27
- |case|when
28
- |cycle
29
- |in
30
- )
31
- |[:,=]
32
- )
33
- \s+
34
- }omix
35
- ENDS_WITH_BLANK_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}$/oimx
36
- ENDS_WITH_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}#{VARIABLE_LOOKUP}$/oimx
12
+ def lookup(context)
13
+ content = context.content
14
+ cursor = context.cursor
37
15
 
38
- def lookup(content, cursor)
39
16
  return if cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
40
- potential_lookup = lookup_liquid_variable(content, cursor) || lookup_liquid_tag(content, cursor)
41
17
 
42
- # And we only return it if it's parsed by Liquid as VariableLookup
43
- return unless potential_lookup.is_a?(Liquid::VariableLookup)
44
- potential_lookup
18
+ variable_lookup = lookup_liquid_variable(content, cursor) || lookup_liquid_tag(content, cursor)
19
+
20
+ return variable_lookup if variable_lookup.is_a?(PotentialLookup)
21
+ return unless variable_lookup.is_a?(Liquid::VariableLookup)
22
+
23
+ potential_lookup(variable_lookup, context)
24
+ end
25
+
26
+ def lookup_literal(context)
27
+ lookup_liquid_variable(context.content, context.cursor)
45
28
  end
46
29
 
47
30
  private
48
31
 
32
+ def potential_lookup(variable, context)
33
+ return variable if context.buffer.nil? || context.buffer.empty?
34
+
35
+ buffer = context.buffer[0...context.absolute_cursor]
36
+ lookups = variable.lookups
37
+ assignments = find_assignments(buffer)
38
+
39
+ while assignments[variable.name]
40
+ variable = assignments[variable.name]
41
+ lookups = variable.lookups + lookups
42
+ end
43
+
44
+ PotentialLookup.new(variable.name, lookups)
45
+ end
46
+
47
+ def find_assignments(buffer)
48
+ finder = AssignmentsFinder.new(buffer)
49
+ finder.find!
50
+ finder.assignments
51
+ end
52
+
49
53
  def cursor_is_on_bracket_position_that_cant_be_completed(content, cursor)
50
- content[0..cursor - 1] =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED
54
+ content_before_cursor = content[0..cursor - 1]
55
+ return false unless /[\[\]]/.match?(content_before_cursor)
56
+
57
+ content_before_cursor =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED
51
58
  end
52
59
 
53
60
  def cursor_is_on_liquid_variable_lookup_position(content, cursor)
@@ -65,6 +72,7 @@ module ThemeCheck
65
72
 
66
73
  def lookup_liquid_variable(content, cursor)
67
74
  return unless cursor_is_on_liquid_variable_lookup_position(content, cursor)
75
+
68
76
  start_index = content.match(/#{Liquid::VariableStart}-?/o).end(0) + 1
69
77
  end_index = cursor - 1
70
78
 
@@ -78,14 +86,14 @@ module ThemeCheck
78
86
  markup = content[start_index..end_index]
79
87
 
80
88
  # Early return for incomplete variables
81
- return empty_lookup if markup =~ /\s+$/
89
+ return empty_lookup if /\s+$/.match?(markup)
82
90
 
83
91
  # Now we go to hack city... The cursor might be in the middle
84
92
  # of a string/square bracket lookup. We need to close those
85
93
  # otherwise the variable parse won't work.
86
94
  markup += "'" if markup.count("'").odd?
87
95
  markup += '"' if markup.count('"').odd?
88
- markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
96
+ markup += "]" if UNCLOSED_SQUARE_BRACKET.match?(markup)
89
97
 
90
98
  variable = variable_from_markup(markup)
91
99
 
@@ -122,12 +130,12 @@ module ThemeCheck
122
130
  return unless cursor_is_on_liquid_tag_lookup_position(content, cursor)
123
131
 
124
132
  markup = parseable_markup(content, cursor)
125
- return empty_lookup if markup == :empty_lookup_markup
133
+ return empty_lookup if markup.empty?
126
134
 
127
135
  template = Liquid::Template.parse(markup)
128
136
  current_tag = template.root.nodelist[0]
129
137
 
130
- case current_tag.tag_name
138
+ case current_tag&.tag_name
131
139
  when "if", "unless"
132
140
  variable_lookup_for_if_tag(current_tag)
133
141
  when "case"
@@ -144,70 +152,16 @@ module ThemeCheck
144
152
  variable_lookup_for_assign_tag(current_tag)
145
153
  when "echo"
146
154
  variable_lookup_for_echo_tag(current_tag)
155
+ else
156
+ empty_lookup
147
157
  end
148
-
149
- # rubocop:disable Style/RedundantReturn
150
158
  rescue Liquid::SyntaxError
151
159
  # We don't complete variable for liquid syntax errors
152
- return
160
+ empty_lookup
153
161
  end
154
- # rubocop:enable Style/RedundantReturn
155
-
156
- def parseable_markup(content, cursor)
157
- start_index = 0
158
- end_index = cursor - 1
159
- markup = content[start_index..end_index]
160
162
 
161
- # Welcome to Hackcity
162
- markup += "'" if markup.count("'").odd?
163
- markup += '"' if markup.count('"').odd?
164
- markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
165
-
166
- # Now check if it's a liquid tag
167
- is_liquid_tag = markup =~ tag_regex('liquid')
168
- ends_with_blank_potential_lookup = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP
169
- last_line = markup.rstrip.lines.last
170
- markup = "{% #{last_line}" if is_liquid_tag
171
-
172
- # Close the tag
173
- markup += ' %}'
174
-
175
- # if statements
176
- is_if_tag = markup =~ tag_regex('if')
177
- return :empty_lookup_markup if is_if_tag && ends_with_blank_potential_lookup
178
- markup += '{% endif %}' if is_if_tag
179
-
180
- # unless statements
181
- is_unless_tag = markup =~ tag_regex('unless')
182
- return :empty_lookup_markup if is_unless_tag && ends_with_blank_potential_lookup
183
- markup += '{% endunless %}' if is_unless_tag
184
-
185
- # elsif statements
186
- is_elsif_tag = markup =~ tag_regex('elsif')
187
- return :empty_lookup_markup if is_elsif_tag && ends_with_blank_potential_lookup
188
- markup = '{% if x %}' + markup + '{% endif %}' if is_elsif_tag
189
-
190
- # case statements
191
- is_case_tag = markup =~ tag_regex('case')
192
- return :empty_lookup_markup if is_case_tag && ends_with_blank_potential_lookup
193
- markup += "{% endcase %}" if is_case_tag
194
-
195
- # when
196
- is_when_tag = markup =~ tag_regex('when')
197
- return :empty_lookup_markup if is_when_tag && ends_with_blank_potential_lookup
198
- markup = "{% case x %}" + markup + "{% endcase %}" if is_when_tag
199
-
200
- # for statements
201
- is_for_tag = markup =~ tag_regex('for')
202
- return :empty_lookup_markup if is_for_tag && ends_with_blank_potential_lookup
203
- markup += "{% endfor %}" if is_for_tag
204
-
205
- # tablerow statements
206
- is_tablerow_tag = markup =~ tag_regex('tablerow')
207
- return :empty_lookup_markup if is_tablerow_tag && ends_with_blank_potential_lookup
208
- markup += "{% endtablerow %}" if is_tablerow_tag
209
-
210
- markup
163
+ def parseable_markup(content, cursor = nil)
164
+ LiquidFixer.new(content, cursor).parsable
211
165
  end
212
166
 
213
167
  def variable_lookup_for_if_tag(if_tag)
@@ -218,11 +172,13 @@ module ThemeCheck
218
172
  def variable_lookup_for_condition(condition)
219
173
  return variable_lookup_for_condition(condition.child_condition) if condition.child_condition
220
174
  return condition.right if condition.right
175
+
221
176
  condition.left
222
177
  end
223
178
 
224
179
  def variable_lookup_for_case_tag(case_tag)
225
180
  return variable_lookup_for_case_block(case_tag.blocks.last) unless case_tag.blocks.empty?
181
+
226
182
  case_tag.left
227
183
  end
228
184
 
@@ -243,7 +199,8 @@ module ThemeCheck
243
199
  end
244
200
 
245
201
  def variable_lookup_for_render_tag(render_tag)
246
- return empty_lookup if render_tag.raw =~ /:\s*$/
202
+ return empty_lookup if /:\s*$/.match?(render_tag.raw)
203
+
247
204
  render_tag.attributes.values.last
248
205
  end
249
206
 
@@ -265,8 +222,10 @@ module ThemeCheck
265
222
  last_filter_argument(variable.filters)
266
223
  elsif variable.name.nil?
267
224
  empty_lookup
268
- else
225
+ elsif variable.name.is_a?(Liquid::VariableLookup)
269
226
  variable.name
227
+ else
228
+ PotentialLookup.new(input_type_of(variable.name), [])
270
229
  end
271
230
  end
272
231
 
@@ -280,6 +239,7 @@ module ThemeCheck
280
239
  filter = filters.last
281
240
  return filter[2].values.last if filter.size == 3
282
241
  return filter[1].last if filter.size == 2
242
+
283
243
  nil
284
244
  end
285
245
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupTraverser
6
+ extend self
7
+
8
+ def lookup_object_and_property(potential_lookup)
9
+ object, generic_type = find_object_and_generic_type(potential_lookup)
10
+ property = nil
11
+
12
+ potential_lookup.lookups.each do |name|
13
+ prop = find_property(object, name)
14
+
15
+ next unless prop
16
+
17
+ generic_type = generic_type(prop) if generic_type?(prop)
18
+
19
+ property = prop
20
+ property.return_type = generic_type if prop.generic_type?
21
+ object = find_object(prop.return_type)
22
+ end
23
+
24
+ [object, property]
25
+ end
26
+
27
+ def find_object_and_generic_type(potential_lookup)
28
+ generic_type = nil
29
+ object = find_object(potential_lookup.name)
30
+
31
+ # Objects like 'product' are a complex structure with fields
32
+ # and their return type is not present.
33
+ #
34
+ # However, we also handle objects that have simple built-in types,
35
+ # like 'current_tags', which is an 'array'. So, we follow them until
36
+ # the source type:
37
+ while object&.return_type
38
+ generic_type = generic_type(object) if generic_type?(object)
39
+ object = find_object(object.return_type)
40
+ end
41
+
42
+ [object, generic_type]
43
+ end
44
+
45
+ # Currently, we're handling generic types only for arrays,
46
+ # so we get the array type
47
+ def generic_type(object)
48
+ object.array_type
49
+ end
50
+
51
+ # Currently, we're handling generic types only for arrays,
52
+ # so we check if it's an array type
53
+ def generic_type?(object)
54
+ object.array_type?
55
+ end
56
+
57
+ def find_property(object, property_name)
58
+ object
59
+ &.properties
60
+ &.find { |property| property.name == property_name }
61
+ end
62
+
63
+ def find_object(object_name)
64
+ ShopifyLiquid::SourceIndex
65
+ .objects
66
+ .find { |entry| entry.name == object_name }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative "language_server/protocol"
3
4
  require_relative "language_server/constants"
4
5
  require_relative "language_server/configuration"
@@ -7,9 +8,19 @@ require_relative "language_server/messenger"
7
8
  require_relative "language_server/io_messenger"
8
9
  require_relative "language_server/bridge"
9
10
  require_relative "language_server/uri_helper"
11
+ require_relative "language_server/type_helper"
10
12
  require_relative "language_server/server"
11
13
  require_relative "language_server/tokens"
14
+ require_relative "language_server/variable_lookup_finder/potential_lookup"
15
+ require_relative "language_server/variable_lookup_finder/tolerant_parser"
16
+ require_relative "language_server/variable_lookup_finder/assignments_finder/node_handler"
17
+ require_relative "language_server/variable_lookup_finder/assignments_finder/scope_visitor"
18
+ require_relative "language_server/variable_lookup_finder/assignments_finder/scope"
19
+ require_relative "language_server/variable_lookup_finder/assignments_finder"
20
+ require_relative "language_server/variable_lookup_finder/constants"
21
+ require_relative "language_server/variable_lookup_finder/liquid_fixer"
12
22
  require_relative "language_server/variable_lookup_finder"
23
+ require_relative "language_server/variable_lookup_traverser"
13
24
  require_relative "language_server/diagnostic"
14
25
  require_relative "language_server/diagnostics_manager"
15
26
  require_relative "language_server/diagnostics_engine"
@@ -17,6 +28,7 @@ require_relative "language_server/document_change_corrector"
17
28
  require_relative "language_server/versioned_in_memory_storage"
18
29
  require_relative "language_server/client_capabilities"
19
30
 
31
+ require_relative "language_server/completion_context"
20
32
  require_relative "language_server/completion_helper"
21
33
  require_relative "language_server/completion_provider"
22
34
  require_relative "language_server/completion_engine"