theme-check 1.11.0 → 1.12.1

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +18 -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/deprecated_filters.json +22 -0
  10. data/data/shopify_liquid/documentation/filters.json +5528 -0
  11. data/data/shopify_liquid/documentation/latest.json +1 -0
  12. data/data/shopify_liquid/documentation/objects.json +19272 -0
  13. data/data/shopify_liquid/documentation/tags.json +1252 -0
  14. data/data/shopify_liquid/plus_labels.json +15 -0
  15. data/data/shopify_liquid/theme_app_extension_labels.json +3 -0
  16. data/lib/theme_check/checks/undefined_object.rb +4 -0
  17. data/lib/theme_check/language_server/completion_context.rb +52 -0
  18. data/lib/theme_check/language_server/completion_engine.rb +15 -21
  19. data/lib/theme_check/language_server/completion_provider.rb +26 -1
  20. data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +40 -0
  21. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
  22. data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +48 -0
  23. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +15 -10
  24. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
  25. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
  26. data/lib/theme_check/language_server/handler.rb +3 -1
  27. data/lib/theme_check/language_server/protocol.rb +9 -0
  28. data/lib/theme_check/language_server/type_helper.rb +22 -0
  29. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
  30. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +60 -0
  31. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +44 -0
  32. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  33. data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +44 -0
  34. data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  35. data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  36. data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +114 -0
  37. data/lib/theme_check/language_server/variable_lookup_finder.rb +67 -100
  38. data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
  39. data/lib/theme_check/language_server.rb +12 -0
  40. data/lib/theme_check/remote_asset_file.rb +13 -7
  41. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +1 -1
  42. data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
  43. data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
  44. data/lib/theme_check/shopify_liquid/filter.rb +35 -3
  45. data/lib/theme_check/shopify_liquid/object.rb +8 -3
  46. data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
  47. data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
  48. data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
  49. data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
  50. data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
  51. data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
  52. data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
  53. data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
  54. data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
  55. data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
  56. data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
  57. data/lib/theme_check/shopify_liquid/source_index.rb +76 -0
  58. data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
  59. data/lib/theme_check/shopify_liquid/tag.rb +11 -1
  60. data/lib/theme_check/shopify_liquid.rb +17 -1
  61. data/lib/theme_check/version.rb +1 -1
  62. data/shipit.rubygems.yml +3 -0
  63. data/theme-check.gemspec +3 -1
  64. metadata +40 -8
  65. data/data/shopify_liquid/deprecated_filters.yml +0 -14
  66. data/data/shopify_liquid/filters.yml +0 -211
  67. data/data/shopify_liquid/objects.yml +0 -84
  68. data/data/shopify_liquid/plus_objects.yml +0 -15
  69. data/data/shopify_liquid/tags.yml +0 -30
  70. data/data/shopify_liquid/theme_app_extension_objects.yml +0 -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,44 @@
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
+ |paginate
31
+ |case|when
32
+ |cycle
33
+ |in
34
+ )
35
+ |[:,=]
36
+ )
37
+ \s+
38
+ }omix
39
+ ENDS_WITH_BLANK_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}$/oimx
40
+ ENDS_WITH_POTENTIAL_LOOKUP = /#{SYMBOLS_PRECEDING_POTENTIAL_LOOKUPS}#{VARIABLE_LOOKUP}$/oimx
41
+ end
42
+ end
43
+ end
44
+ 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,114 @@
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
+ class Paginate < Liquid::Tag
77
+ include TolerantBlockBody
78
+ end
79
+
80
+ class Form < Liquid::Tag
81
+ include TolerantBlockBody
82
+ end
83
+
84
+ class Style < Liquid::Tag
85
+ include TolerantBlockBody
86
+ end
87
+
88
+ class Stylesheet < Liquid::Tag
89
+ include TolerantBlockBody
90
+ end
91
+
92
+ def initialize(standard_tags)
93
+ @standard_tags = standard_tags
94
+ @tolerant_tags = {
95
+ 'case' => Case,
96
+ 'for' => For,
97
+ 'form' => Form,
98
+ 'if' => If,
99
+ 'paginate' => Paginate,
100
+ 'style' => Style,
101
+ 'stylesheet' => Stylesheet,
102
+ 'tablerow' => TableRow,
103
+ 'unless' => Unless,
104
+ }
105
+ end
106
+
107
+ def [](key)
108
+ @tolerant_tags[key] || @standard_tags[key]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,53 +1,67 @@
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 as_potential_lookup(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
+ assignments_path = []
39
+
40
+ while assignments[variable.name] && !assignments_path.include?(assignments[variable.name])
41
+ variable = assignments[variable.name]
42
+ lookups = variable.lookups + lookups
43
+
44
+ assignments_path << variable
45
+ end
46
+
47
+ as_potential_lookup(variable, lookups: lookups)
48
+ end
49
+
50
+ def find_assignments(buffer)
51
+ finder = AssignmentsFinder.new(buffer)
52
+ finder.find!
53
+ finder.assignments
54
+ end
55
+
56
+ def as_potential_lookup(variable, lookups: nil)
57
+ PotentialLookup.new(variable.name, lookups || variable.lookups)
58
+ end
59
+
49
60
  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
61
+ content_before_cursor = content[0..cursor - 1]
62
+ return false unless /[\[\]]/.match?(content_before_cursor)
63
+
64
+ content_before_cursor =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED
51
65
  end
52
66
 
53
67
  def cursor_is_on_liquid_variable_lookup_position(content, cursor)
@@ -65,6 +79,7 @@ module ThemeCheck
65
79
 
66
80
  def lookup_liquid_variable(content, cursor)
67
81
  return unless cursor_is_on_liquid_variable_lookup_position(content, cursor)
82
+
68
83
  start_index = content.match(/#{Liquid::VariableStart}-?/o).end(0) + 1
69
84
  end_index = cursor - 1
70
85
 
@@ -78,14 +93,14 @@ module ThemeCheck
78
93
  markup = content[start_index..end_index]
79
94
 
80
95
  # Early return for incomplete variables
81
- return empty_lookup if markup =~ /\s+$/
96
+ return empty_lookup if /\s+$/.match?(markup)
82
97
 
83
98
  # Now we go to hack city... The cursor might be in the middle
84
99
  # of a string/square bracket lookup. We need to close those
85
100
  # otherwise the variable parse won't work.
86
101
  markup += "'" if markup.count("'").odd?
87
102
  markup += '"' if markup.count('"').odd?
88
- markup += "]" if markup =~ UNCLOSED_SQUARE_BRACKET
103
+ markup += "]" if UNCLOSED_SQUARE_BRACKET.match?(markup)
89
104
 
90
105
  variable = variable_from_markup(markup)
91
106
 
@@ -122,12 +137,12 @@ module ThemeCheck
122
137
  return unless cursor_is_on_liquid_tag_lookup_position(content, cursor)
123
138
 
124
139
  markup = parseable_markup(content, cursor)
125
- return empty_lookup if markup == :empty_lookup_markup
140
+ return empty_lookup if markup.empty?
126
141
 
127
142
  template = Liquid::Template.parse(markup)
128
143
  current_tag = template.root.nodelist[0]
129
144
 
130
- case current_tag.tag_name
145
+ case current_tag&.tag_name
131
146
  when "if", "unless"
132
147
  variable_lookup_for_if_tag(current_tag)
133
148
  when "case"
@@ -144,70 +159,16 @@ module ThemeCheck
144
159
  variable_lookup_for_assign_tag(current_tag)
145
160
  when "echo"
146
161
  variable_lookup_for_echo_tag(current_tag)
162
+ else
163
+ empty_lookup
147
164
  end
148
-
149
- # rubocop:disable Style/RedundantReturn
150
165
  rescue Liquid::SyntaxError
151
166
  # We don't complete variable for liquid syntax errors
152
- return
167
+ empty_lookup
153
168
  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
169
 
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
170
+ def parseable_markup(content, cursor = nil)
171
+ LiquidFixer.new(content, cursor).parsable
211
172
  end
212
173
 
213
174
  def variable_lookup_for_if_tag(if_tag)
@@ -218,11 +179,13 @@ module ThemeCheck
218
179
  def variable_lookup_for_condition(condition)
219
180
  return variable_lookup_for_condition(condition.child_condition) if condition.child_condition
220
181
  return condition.right if condition.right
182
+
221
183
  condition.left
222
184
  end
223
185
 
224
186
  def variable_lookup_for_case_tag(case_tag)
225
187
  return variable_lookup_for_case_block(case_tag.blocks.last) unless case_tag.blocks.empty?
188
+
226
189
  case_tag.left
227
190
  end
228
191
 
@@ -243,7 +206,8 @@ module ThemeCheck
243
206
  end
244
207
 
245
208
  def variable_lookup_for_render_tag(render_tag)
246
- return empty_lookup if render_tag.raw =~ /:\s*$/
209
+ return empty_lookup if /:\s*$/.match?(render_tag.raw)
210
+
247
211
  render_tag.attributes.values.last
248
212
  end
249
213
 
@@ -265,8 +229,10 @@ module ThemeCheck
265
229
  last_filter_argument(variable.filters)
266
230
  elsif variable.name.nil?
267
231
  empty_lookup
268
- else
232
+ elsif variable.name.is_a?(Liquid::VariableLookup)
269
233
  variable.name
234
+ else
235
+ PotentialLookup.new(input_type_of(variable.name), [])
270
236
  end
271
237
  end
272
238
 
@@ -280,6 +246,7 @@ module ThemeCheck
280
246
  filter = filters.last
281
247
  return filter[2].values.last if filter.size == 3
282
248
  return filter[1].last if filter.size == 2
249
+
283
250
  nil
284
251
  end
285
252
 
@@ -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