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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +82 -0
- data/README.md +4 -0
- data/Rakefile +7 -0
- data/TROUBLESHOOTING.md +65 -0
- data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
- data/data/shopify_liquid/deprecated_filters.json +22 -0
- data/data/shopify_liquid/documentation/filters.json +5528 -0
- data/data/shopify_liquid/documentation/latest.json +1 -0
- data/data/shopify_liquid/documentation/objects.json +19272 -0
- data/data/shopify_liquid/documentation/tags.json +1252 -0
- data/data/shopify_liquid/plus_labels.json +15 -0
- data/data/shopify_liquid/theme_app_extension_labels.json +3 -0
- data/lib/theme_check/checks/undefined_object.rb +4 -0
- data/lib/theme_check/language_server/completion_context.rb +52 -0
- data/lib/theme_check/language_server/completion_engine.rb +15 -21
- data/lib/theme_check/language_server/completion_provider.rb +26 -1
- data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +40 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
- data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +48 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +15 -10
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
- data/lib/theme_check/language_server/handler.rb +3 -1
- data/lib/theme_check/language_server/protocol.rb +9 -0
- data/lib/theme_check/language_server/type_helper.rb +22 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +60 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +44 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
- data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +44 -0
- data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
- data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
- data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +114 -0
- data/lib/theme_check/language_server/variable_lookup_finder.rb +67 -100
- data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
- data/lib/theme_check/language_server.rb +12 -0
- data/lib/theme_check/remote_asset_file.rb +13 -7
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +1 -1
- data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
- data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
- data/lib/theme_check/shopify_liquid/filter.rb +35 -3
- data/lib/theme_check/shopify_liquid/object.rb +8 -3
- data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
- data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
- data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
- data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
- data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index.rb +76 -0
- data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
- data/lib/theme_check/shopify_liquid/tag.rb +11 -1
- data/lib/theme_check/shopify_liquid.rb +17 -1
- data/lib/theme_check/version.rb +1 -1
- data/shipit.rubygems.yml +3 -0
- data/theme-check.gemspec +3 -1
- metadata +40 -8
- data/data/shopify_liquid/deprecated_filters.yml +0 -14
- data/data/shopify_liquid/filters.yml +0 -211
- data/data/shopify_liquid/objects.yml +0 -84
- data/data/shopify_liquid/plus_objects.yml +0 -15
- data/data/shopify_liquid/tags.yml +0 -30
- 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,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
|
-
|
9
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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]
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
162
|
-
|
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
|
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
|
-
|
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
|