theme-check 0.2.2 → 0.4.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +40 -0
- data/CONTRIBUTING.md +2 -0
- data/README.md +45 -4
- data/RELEASING.md +41 -0
- data/Rakefile +24 -4
- data/config/default.yml +16 -0
- data/data/shopify_liquid/plus_objects.yml +15 -0
- data/data/shopify_liquid/tags.yml +26 -0
- data/dev.yml +2 -0
- data/lib/theme_check.rb +5 -0
- data/lib/theme_check/analyzer.rb +0 -6
- data/lib/theme_check/check.rb +11 -0
- data/lib/theme_check/checks.rb +10 -0
- data/lib/theme_check/checks/missing_enable_comment.rb +31 -0
- data/lib/theme_check/checks/parser_blocking_javascript.rb +55 -0
- data/lib/theme_check/checks/space_inside_braces.rb +1 -0
- data/lib/theme_check/checks/template_length.rb +11 -3
- data/lib/theme_check/checks/undefined_object.rb +27 -6
- data/lib/theme_check/checks/unused_assign.rb +4 -3
- data/lib/theme_check/checks/valid_html_translation.rb +2 -2
- data/lib/theme_check/cli.rb +31 -7
- data/lib/theme_check/config.rb +95 -43
- data/lib/theme_check/corrector.rb +0 -4
- data/lib/theme_check/disabled_checks.rb +77 -0
- data/lib/theme_check/file_system_storage.rb +51 -0
- data/lib/theme_check/in_memory_storage.rb +37 -0
- data/lib/theme_check/json_file.rb +12 -10
- data/lib/theme_check/language_server.rb +10 -0
- data/lib/theme_check/language_server/completion_engine.rb +38 -0
- data/lib/theme_check/language_server/completion_helper.rb +35 -0
- data/lib/theme_check/language_server/completion_provider.rb +23 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +47 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
- data/lib/theme_check/language_server/handler.rb +74 -13
- data/lib/theme_check/language_server/position_helper.rb +27 -0
- data/lib/theme_check/language_server/protocol.rb +41 -0
- data/lib/theme_check/language_server/server.rb +2 -2
- data/lib/theme_check/language_server/tokens.rb +55 -0
- data/lib/theme_check/offense.rb +51 -14
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/shopify_liquid/object.rb +6 -0
- data/lib/theme_check/shopify_liquid/tag.rb +16 -0
- data/lib/theme_check/storage.rb +25 -0
- data/lib/theme_check/template.rb +26 -21
- data/lib/theme_check/theme.rb +14 -9
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +14 -3
- data/packaging/homebrew/theme_check.base.rb +10 -6
- metadata +22 -2
@@ -9,27 +9,23 @@ module ThemeCheck
|
|
9
9
|
def insert_after(node, content)
|
10
10
|
line = @template.full_line(node.line_number)
|
11
11
|
line.insert(node.range[1] + 1, content)
|
12
|
-
@template.update!
|
13
12
|
end
|
14
13
|
|
15
14
|
def insert_before(node, content)
|
16
15
|
line = @template.full_line(node.line_number)
|
17
16
|
line.insert(node.range[0], content)
|
18
|
-
@template.update!
|
19
17
|
end
|
20
18
|
|
21
19
|
def replace(node, content)
|
22
20
|
line = @template.full_line(node.line_number)
|
23
21
|
line[node.range[0]..node.range[1]] = content
|
24
22
|
node.markup = content
|
25
|
-
@template.update!
|
26
23
|
end
|
27
24
|
|
28
25
|
def wrap(node, insert_before, insert_after)
|
29
26
|
line = @template.full_line(node.line_number)
|
30
27
|
line.insert(node.range[0], insert_before)
|
31
28
|
line.insert(node.range[1] + 1 + insert_before.length, insert_after)
|
32
|
-
@template.update!
|
33
29
|
end
|
34
30
|
end
|
35
31
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
class DisabledChecks
|
5
|
+
DISABLE_START = 'theme-check-disable'
|
6
|
+
DISABLE_END = 'theme-check-enable'
|
7
|
+
DISABLE_PREFIX_PATTERN = /#{DISABLE_START}|#{DISABLE_END}/
|
8
|
+
|
9
|
+
ACTION_DISABLE_CHECKS = :disable
|
10
|
+
ACTION_ENABLE_CHECKS = :enable
|
11
|
+
ACTION_UNRELATED_COMMENT = :unrelated
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@disabled = []
|
15
|
+
@all_disabled = false
|
16
|
+
@full_document_disabled = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def update(node)
|
20
|
+
text = comment_text(node)
|
21
|
+
|
22
|
+
if start_disabling?(text)
|
23
|
+
@disabled = checks_from_text(text)
|
24
|
+
@all_disabled = @disabled.empty?
|
25
|
+
|
26
|
+
if node&.line_number == 1
|
27
|
+
@full_document_disabled = true
|
28
|
+
end
|
29
|
+
elsif stop_disabling?(text)
|
30
|
+
checks = checks_from_text(text)
|
31
|
+
@disabled = checks.empty? ? [] : @disabled - checks
|
32
|
+
|
33
|
+
@all_disabled = false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether any checks are currently disabled
|
38
|
+
def any?
|
39
|
+
!@disabled.empty? || @all_disabled
|
40
|
+
end
|
41
|
+
|
42
|
+
# Whether all checks should be disabled
|
43
|
+
def all_disabled?
|
44
|
+
@all_disabled
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get a list of all the individual disabled checks
|
48
|
+
def all
|
49
|
+
@disabled
|
50
|
+
end
|
51
|
+
|
52
|
+
# If the first line of the document is a theme-check-disable comment
|
53
|
+
def full_document_disabled?
|
54
|
+
@full_document_disabled
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def comment_text(node)
|
60
|
+
node.value.nodelist.join
|
61
|
+
end
|
62
|
+
|
63
|
+
def start_disabling?(text)
|
64
|
+
text.strip.starts_with?(DISABLE_START)
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop_disabling?(text)
|
68
|
+
text.strip.starts_with?(DISABLE_END)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return a list of checks from a theme-check-disable comment
|
72
|
+
# Returns [] if all checks are meant to be disabled
|
73
|
+
def checks_from_text(text)
|
74
|
+
text.gsub(DISABLE_PREFIX_PATTERN, '').strip.split(',').map(&:strip)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "pathname"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
class FileSystemStorage < Storage
|
6
|
+
attr_reader :root
|
7
|
+
|
8
|
+
def initialize(root, ignored_patterns: [])
|
9
|
+
@root = Pathname.new(root)
|
10
|
+
@ignored_patterns = ignored_patterns
|
11
|
+
@files = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def path(relative_path)
|
15
|
+
@root.join(relative_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(relative_path)
|
19
|
+
file(relative_path).read
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(relative_path, content)
|
23
|
+
file(relative_path).write(content)
|
24
|
+
end
|
25
|
+
|
26
|
+
def files
|
27
|
+
@file_array ||= glob("**/*")
|
28
|
+
.map { |path| path.relative_path_from(@root).to_s }
|
29
|
+
end
|
30
|
+
|
31
|
+
def directories
|
32
|
+
@directories ||= glob('*')
|
33
|
+
.select { |f| File.directory?(f) }
|
34
|
+
.map { |f| f.relative_path_from(@root).to_s }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def glob(pattern)
|
40
|
+
@root.glob(pattern).reject do |path|
|
41
|
+
relative_path = path.relative_path_from(@root)
|
42
|
+
@ignored_patterns.any? { |ignored| relative_path.fnmatch?(ignored) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def file(name)
|
47
|
+
return @files[name] if @files[name]
|
48
|
+
@files[name] = root.join(name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# An in-memory storage is not written to disk. The reasons why you'd
|
4
|
+
# want to do that are your own. The idea is to not write to disk
|
5
|
+
# something that doesn't need to be there. If you have your template
|
6
|
+
# as a big hash already, leave it like that and save yourself some IO.
|
7
|
+
module ThemeCheck
|
8
|
+
class InMemoryStorage < Storage
|
9
|
+
def initialize(files = {})
|
10
|
+
@files = files
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(name)
|
14
|
+
name
|
15
|
+
end
|
16
|
+
|
17
|
+
def read(name)
|
18
|
+
@files[name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(name, content)
|
22
|
+
@files[name] = content
|
23
|
+
end
|
24
|
+
|
25
|
+
def files
|
26
|
+
@values ||= @files.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def directories
|
30
|
+
@directories ||= @files
|
31
|
+
.keys
|
32
|
+
.flat_map { |relative_path| Pathname.new(relative_path).ascend.to_a }
|
33
|
+
.map(&:to_s)
|
34
|
+
.uniq
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -4,22 +4,20 @@ require "pathname"
|
|
4
4
|
|
5
5
|
module ThemeCheck
|
6
6
|
class JsonFile
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@path = Pathname(path)
|
11
|
-
@root = Pathname(root)
|
7
|
+
def initialize(relative_path, storage)
|
8
|
+
@relative_path = relative_path
|
9
|
+
@storage = storage
|
12
10
|
@loaded = false
|
13
11
|
@content = nil
|
14
12
|
@parser_error = nil
|
15
13
|
end
|
16
14
|
|
17
|
-
def
|
18
|
-
@path
|
15
|
+
def path
|
16
|
+
@storage.path(@relative_path)
|
19
17
|
end
|
20
18
|
|
21
|
-
def
|
22
|
-
|
19
|
+
def relative_path
|
20
|
+
@relative_pathname ||= Pathname.new(@relative_path)
|
23
21
|
end
|
24
22
|
|
25
23
|
def content
|
@@ -32,12 +30,16 @@ module ThemeCheck
|
|
32
30
|
@parser_error
|
33
31
|
end
|
34
32
|
|
33
|
+
def name
|
34
|
+
relative_path.sub_ext('').to_s
|
35
|
+
end
|
36
|
+
|
35
37
|
private
|
36
38
|
|
37
39
|
def load!
|
38
40
|
return if @loaded
|
39
41
|
|
40
|
-
@content = JSON.parse(
|
42
|
+
@content = JSON.parse(@storage.read(@relative_path))
|
41
43
|
rescue JSON::ParserError => e
|
42
44
|
@parser_error = e
|
43
45
|
ensure
|
@@ -1,6 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require_relative "language_server/protocol"
|
2
3
|
require_relative "language_server/handler"
|
3
4
|
require_relative "language_server/server"
|
5
|
+
require_relative "language_server/tokens"
|
6
|
+
require_relative "language_server/position_helper"
|
7
|
+
require_relative "language_server/completion_helper"
|
8
|
+
require_relative "language_server/completion_provider"
|
9
|
+
require_relative "language_server/completion_engine"
|
10
|
+
|
11
|
+
Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
|
12
|
+
require file
|
13
|
+
end
|
4
14
|
|
5
15
|
module ThemeCheck
|
6
16
|
module LanguageServer
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CompletionEngine
|
6
|
+
include PositionHelper
|
7
|
+
|
8
|
+
def initialize(storage)
|
9
|
+
@storage = storage
|
10
|
+
@providers = CompletionProvider.all.map(&:new)
|
11
|
+
end
|
12
|
+
|
13
|
+
def completions(name, line, col)
|
14
|
+
buffer = @storage.read(name)
|
15
|
+
cursor = from_line_column_to_index(buffer, line, col)
|
16
|
+
token = find_token(buffer, cursor)
|
17
|
+
return [] if token.nil?
|
18
|
+
|
19
|
+
@providers.flat_map do |p|
|
20
|
+
p.completions(
|
21
|
+
token.content,
|
22
|
+
cursor - token.start
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_token(buffer, cursor)
|
28
|
+
Tokens.new(buffer).find do |token|
|
29
|
+
# Here we include the next character and exclude the first
|
30
|
+
# one becase when we want to autocomplete inside a token
|
31
|
+
# and at most 1 outside it since the cursor could be placed
|
32
|
+
# at the end of the token.
|
33
|
+
token.start < cursor && cursor <= token.end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
module CompletionHelper
|
6
|
+
WORD = /\w+/
|
7
|
+
|
8
|
+
def cursor_on_start_content?(content, cursor, regex)
|
9
|
+
content.slice(0, cursor).match?(/#{regex}(?:\s|\n)*$/m)
|
10
|
+
end
|
11
|
+
|
12
|
+
def cursor_on_first_word?(content, cursor)
|
13
|
+
word = content.match(WORD)
|
14
|
+
return false if word.nil?
|
15
|
+
word_start = word.begin(0)
|
16
|
+
word_end = word.end(0)
|
17
|
+
word_start <= cursor && cursor <= word_end
|
18
|
+
end
|
19
|
+
|
20
|
+
def first_word(content)
|
21
|
+
return content.match(WORD)[0] if content.match?(WORD)
|
22
|
+
end
|
23
|
+
|
24
|
+
def matches(s, re)
|
25
|
+
start_at = 0
|
26
|
+
matches = []
|
27
|
+
while (m = s.match(re, start_at))
|
28
|
+
matches.push(m)
|
29
|
+
start_at = m.end(0)
|
30
|
+
end
|
31
|
+
matches
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CompletionProvider
|
6
|
+
include CompletionHelper
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def all
|
10
|
+
@all ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def inherited(subclass)
|
14
|
+
all << subclass
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def completions(content, cursor)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class FilterCompletionProvider < CompletionProvider
|
6
|
+
NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o
|
7
|
+
|
8
|
+
def completions(content, cursor)
|
9
|
+
return [] unless can_complete?(content, cursor)
|
10
|
+
ShopifyLiquid::Filter.labels
|
11
|
+
.select { |w| w.starts_with?(partial(content, cursor)) }
|
12
|
+
.map { |filter| filter_to_completion(filter) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def can_complete?(content, cursor)
|
16
|
+
content.match?(Liquid::FilterSeparator) && (
|
17
|
+
cursor_on_start_content?(content, cursor, Liquid::FilterSeparator) ||
|
18
|
+
cursor_on_filter?(content, cursor)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def cursor_on_filter?(content, cursor)
|
25
|
+
return false unless content.match?(NAMED_FILTER)
|
26
|
+
matches(content, NAMED_FILTER).any? do |match|
|
27
|
+
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def partial(content, cursor)
|
32
|
+
return '' unless content.match?(NAMED_FILTER)
|
33
|
+
partial_match = matches(content, NAMED_FILTER).find do |match|
|
34
|
+
match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
|
35
|
+
end
|
36
|
+
partial_match[1]
|
37
|
+
end
|
38
|
+
|
39
|
+
def filter_to_completion(filter)
|
40
|
+
{
|
41
|
+
label: filter,
|
42
|
+
kind: CompletionItemKinds::FUNCTION,
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class ObjectCompletionProvider < CompletionProvider
|
6
|
+
def completions(content, cursor)
|
7
|
+
return [] unless can_complete?(content, cursor)
|
8
|
+
partial = first_word(content) || ''
|
9
|
+
ShopifyLiquid::Object.labels
|
10
|
+
.select { |w| w.starts_with?(partial) }
|
11
|
+
.map { |object| object_to_completion(object) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_complete?(content, cursor)
|
15
|
+
content.match?(Liquid::VariableStart) && (
|
16
|
+
cursor_on_first_word?(content, cursor) ||
|
17
|
+
cursor_on_start_content?(content, cursor, Liquid::VariableStart)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def object_to_completion(object)
|
24
|
+
{
|
25
|
+
label: object,
|
26
|
+
kind: CompletionItemKinds::VARIABLE,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|