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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +7 -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/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/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 +16 -1
- data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -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 +47 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
- 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 +57 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -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 +43 -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 +94 -0
- data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -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/documentation/markdown_template.rb +51 -0
- data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
- data/lib/theme_check/shopify_liquid/filter.rb +4 -0
- data/lib/theme_check/shopify_liquid/object.rb +4 -0
- 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 +56 -0
- data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
- data/lib/theme_check/shopify_liquid/tag.rb +4 -0
- 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 +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,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
|
-
|
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 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]
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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"
|