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
@@ -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"