theme-check 0.2.0 → 0.3.3
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 +33 -0
- data/CONTRIBUTING.md +2 -0
- data/README.md +45 -2
- 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/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 +9 -1
- 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/handler.rb +38 -13
- data/lib/theme_check/language_server/server.rb +2 -2
- data/lib/theme_check/offense.rb +3 -1
- data/lib/theme_check/shopify_liquid/object.rb +6 -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 +11 -2
@@ -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
|
@@ -14,6 +14,7 @@ module ThemeCheck
|
|
14
14
|
|
15
15
|
def initialize(server)
|
16
16
|
@server = server
|
17
|
+
@previously_reported_files = Set.new
|
17
18
|
end
|
18
19
|
|
19
20
|
def on_initialize(id, params)
|
@@ -41,27 +42,51 @@ module ThemeCheck
|
|
41
42
|
def analyze_and_send_offenses(file_path)
|
42
43
|
root = ThemeCheck::Config.find(file_path) || @root_path
|
43
44
|
config = ThemeCheck::Config.from_path(root)
|
44
|
-
|
45
|
-
|
45
|
+
storage = ThemeCheck::FileSystemStorage.new(
|
46
|
+
config.root,
|
47
|
+
ignored_patterns: config.ignored_patterns
|
48
|
+
)
|
49
|
+
theme = ThemeCheck::Theme.new(storage)
|
50
|
+
|
51
|
+
offenses = analyze(theme, config)
|
52
|
+
log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
|
53
|
+
send_diagnostics(offenses)
|
54
|
+
end
|
46
55
|
|
56
|
+
def analyze(theme, config)
|
57
|
+
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
47
58
|
log("Checking #{config.root}")
|
48
59
|
analyzer.analyze_theme
|
49
|
-
|
50
|
-
send_offenses(analyzer.offenses)
|
60
|
+
analyzer.offenses
|
51
61
|
end
|
52
62
|
|
53
|
-
def
|
63
|
+
def send_diagnostics(offenses)
|
64
|
+
reported_files = Set.new
|
65
|
+
|
54
66
|
offenses.group_by(&:template).each do |template, template_offenses|
|
55
67
|
next unless template
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
)
|
68
|
+
send_diagnostic(template.path, template_offenses)
|
69
|
+
reported_files << template.path
|
70
|
+
end
|
71
|
+
|
72
|
+
# Publish diagnostics with empty array if all issues on a previously reported template
|
73
|
+
# have been solved.
|
74
|
+
(@previously_reported_files - reported_files).each do |path|
|
75
|
+
send_diagnostic(path, [])
|
64
76
|
end
|
77
|
+
|
78
|
+
@previously_reported_files = reported_files
|
79
|
+
end
|
80
|
+
|
81
|
+
def send_diagnostic(path, offenses)
|
82
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
83
|
+
send_response(
|
84
|
+
method: 'textDocument/publishDiagnostics',
|
85
|
+
params: {
|
86
|
+
uri: "file:#{path}",
|
87
|
+
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
88
|
+
},
|
89
|
+
)
|
65
90
|
end
|
66
91
|
|
67
92
|
def offense_to_diagnostic(offense)
|
@@ -9,6 +9,8 @@ module ThemeCheck
|
|
9
9
|
class IncompatibleStream < StandardError; end
|
10
10
|
|
11
11
|
class Server
|
12
|
+
attr_reader :handler
|
13
|
+
|
12
14
|
def initialize(
|
13
15
|
in_stream: STDIN,
|
14
16
|
out_stream: STDOUT,
|
@@ -87,8 +89,6 @@ module ThemeCheck
|
|
87
89
|
|
88
90
|
if @handler.respond_to?(method_name)
|
89
91
|
@handler.send(method_name, id, params)
|
90
|
-
else
|
91
|
-
log("Handler does not respond to #{method_name}")
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
data/lib/theme_check/offense.rb
CHANGED
@@ -11,6 +11,12 @@ module ThemeCheck
|
|
11
11
|
YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/objects.yml"))
|
12
12
|
end
|
13
13
|
end
|
14
|
+
|
15
|
+
def plus_labels
|
16
|
+
@plus_labels ||= begin
|
17
|
+
YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/plus_objects.yml"))
|
18
|
+
end
|
19
|
+
end
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
class Storage
|
5
|
+
def read(relative_path)
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(relative_path, content)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(relative_path)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def files
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def directories
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/theme_check/template.rb
CHANGED
@@ -3,16 +3,29 @@ require "pathname"
|
|
3
3
|
|
4
4
|
module ThemeCheck
|
5
5
|
class Template
|
6
|
-
|
6
|
+
def initialize(relative_path, storage)
|
7
|
+
@storage = storage
|
8
|
+
@relative_path = relative_path
|
9
|
+
end
|
7
10
|
|
8
|
-
def
|
9
|
-
@path
|
10
|
-
@root = Pathname(root)
|
11
|
-
@updated = false
|
11
|
+
def path
|
12
|
+
@storage.path(@relative_path)
|
12
13
|
end
|
13
14
|
|
14
15
|
def relative_path
|
15
|
-
@
|
16
|
+
@relative_pathname ||= Pathname.new(@relative_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def source
|
20
|
+
@source ||= @storage.read(@relative_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def write
|
24
|
+
content = updated_content
|
25
|
+
if source != content
|
26
|
+
@storage.write(@relative_path, content)
|
27
|
+
@source = content
|
28
|
+
end
|
16
29
|
end
|
17
30
|
|
18
31
|
def name
|
@@ -31,15 +44,17 @@ module ThemeCheck
|
|
31
44
|
name.start_with?('snippets')
|
32
45
|
end
|
33
46
|
|
34
|
-
def source
|
35
|
-
@source ||= @path.read
|
36
|
-
end
|
37
|
-
|
38
47
|
def lines
|
39
48
|
# Retain trailing newline character
|
40
49
|
@lines ||= source.split("\n", -1)
|
41
50
|
end
|
42
51
|
|
52
|
+
# Not entirely obvious but lines is mutable, corrections are to be
|
53
|
+
# applied on @lines.
|
54
|
+
def updated_content
|
55
|
+
lines.join("\n")
|
56
|
+
end
|
57
|
+
|
43
58
|
def excerpt(line)
|
44
59
|
lines[line - 1].strip
|
45
60
|
end
|
@@ -65,18 +80,8 @@ module ThemeCheck
|
|
65
80
|
parse.root
|
66
81
|
end
|
67
82
|
|
68
|
-
def update!
|
69
|
-
@updated = true
|
70
|
-
end
|
71
|
-
|
72
|
-
def write
|
73
|
-
if @updated
|
74
|
-
@path.write(lines.join("\n"))
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
83
|
def ==(other)
|
79
|
-
other.is_a?(Template) &&
|
84
|
+
other.is_a?(Template) && relative_path == other.relative_path
|
80
85
|
end
|
81
86
|
|
82
87
|
def self.parse(source)
|
data/lib/theme_check/theme.rb
CHANGED
@@ -4,18 +4,27 @@ require "pathname"
|
|
4
4
|
module ThemeCheck
|
5
5
|
class Theme
|
6
6
|
DEFAULT_LOCALE_REGEXP = %r{^locales/(.*)\.default$}
|
7
|
-
|
7
|
+
LIQUID_REGEX = /\.liquid$/i
|
8
|
+
JSON_REGEX = /\.json$/i
|
8
9
|
|
9
|
-
def initialize(
|
10
|
-
@
|
10
|
+
def initialize(storage)
|
11
|
+
@storage = storage
|
11
12
|
end
|
12
13
|
|
13
14
|
def liquid
|
14
|
-
@liquid ||= @
|
15
|
+
@liquid ||= @storage.files
|
16
|
+
.select { |path| LIQUID_REGEX.match?(path) }
|
17
|
+
.map { |path| Template.new(path, @storage) }
|
15
18
|
end
|
16
19
|
|
17
20
|
def json
|
18
|
-
@json ||= @
|
21
|
+
@json ||= @storage.files
|
22
|
+
.select { |path| JSON_REGEX.match?(path) }
|
23
|
+
.map { |path| JsonFile.new(path, @storage) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def directories
|
27
|
+
@storage.directories
|
19
28
|
end
|
20
29
|
|
21
30
|
def default_locale_json
|
@@ -52,9 +61,5 @@ module ThemeCheck
|
|
52
61
|
def snippets
|
53
62
|
liquid.select(&:snippet?)
|
54
63
|
end
|
55
|
-
|
56
|
-
def directories
|
57
|
-
@directories ||= @root.glob('*').select { |f| File.directory?(f) }.map { |f| f.relative_path_from(@root) }
|
58
|
-
end
|
59
64
|
end
|
60
65
|
end
|
data/lib/theme_check/version.rb
CHANGED
data/lib/theme_check/visitor.rb
CHANGED
@@ -6,12 +6,15 @@ module ThemeCheck
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def visit_template(template)
|
9
|
+
@disabled_checks = DisabledChecks.new
|
9
10
|
visit(Node.new(template.root, nil, template))
|
10
11
|
rescue Liquid::Error => exception
|
11
12
|
exception.template_name = template.name
|
12
13
|
call_checks(:on_error, exception)
|
13
14
|
end
|
14
15
|
|
16
|
+
private
|
17
|
+
|
15
18
|
def visit(node)
|
16
19
|
call_checks(:on_node, node)
|
17
20
|
call_checks(:on_tag, node) if node.tag?
|
@@ -22,16 +25,24 @@ module ThemeCheck
|
|
22
25
|
call_checks(:after_tag, node) if node.tag?
|
23
26
|
call_checks(:after_node, node)
|
24
27
|
end
|
25
|
-
end
|
26
28
|
|
27
|
-
|
29
|
+
@disabled_checks.update(node) if node.comment?
|
30
|
+
end
|
28
31
|
|
29
32
|
def visit_children(node)
|
30
33
|
node.children.each { |child| visit(child) }
|
31
34
|
end
|
32
35
|
|
33
36
|
def call_checks(method, *args)
|
34
|
-
|
37
|
+
checks.call(method, *args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def checks
|
41
|
+
return @checks unless @disabled_checks.any?
|
42
|
+
|
43
|
+
return @checks.always_enabled if @disabled_checks.all_disabled?
|
44
|
+
|
45
|
+
@checks.except_for(@disabled_checks)
|
35
46
|
end
|
36
47
|
end
|
37
48
|
end
|