theme-check 0.2.2 → 0.3.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/CONTRIBUTING.md +2 -0
- data/README.md +45 -2
- data/RELEASING.md +41 -0
- data/Rakefile +24 -4
- data/config/default.yml +15 -0
- data/data/shopify_liquid/plus_objects.yml +15 -0
- data/dev.yml +2 -0
- data/lib/theme_check.rb +4 -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 +8 -0
- 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 +23 -10
- data/lib/theme_check/language_server/server.rb +2 -2
- 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 +10 -2
@@ -20,7 +20,7 @@ module ThemeCheck
|
|
20
20
|
keys[-1].end_with?('_html') || pluralized_key.end_with?('_html')
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def parse_and_add_offense(key, value)
|
24
24
|
return unless value.is_a?(String)
|
25
25
|
|
26
26
|
html = Nokogiri::HTML5.fragment(value, max_errors: -1)
|
@@ -36,7 +36,7 @@ module ThemeCheck
|
|
36
36
|
visit_nested(v, keys + [k])
|
37
37
|
end
|
38
38
|
elsif html_key?(keys)
|
39
|
-
|
39
|
+
parse_and_add_offense(keys.join('.'), value)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
data/lib/theme_check/cli.rb
CHANGED
@@ -12,6 +12,7 @@ module ThemeCheck
|
|
12
12
|
-l, [--list] # List enabled checks
|
13
13
|
-a, [--auto-correct] # Automatically fix offenses
|
14
14
|
-h, [--help] # Show this. Hi!
|
15
|
+
-v, [--version] # Print Theme Check version
|
15
16
|
|
16
17
|
Description:
|
17
18
|
Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
|
@@ -33,6 +34,8 @@ module ThemeCheck
|
|
33
34
|
case arg
|
34
35
|
when "--help", "-h"
|
35
36
|
raise Abort, USAGE
|
37
|
+
when "--version", "-v"
|
38
|
+
command = :version
|
36
39
|
when "--category", "-c"
|
37
40
|
only_categories << args.shift.to_sym
|
38
41
|
when "--exclude-category", "-x"
|
@@ -68,9 +71,14 @@ module ThemeCheck
|
|
68
71
|
puts @config.enabled_checks
|
69
72
|
end
|
70
73
|
|
74
|
+
def version
|
75
|
+
puts ThemeCheck::VERSION
|
76
|
+
end
|
77
|
+
|
71
78
|
def check
|
72
79
|
puts "Checking #{@config.root} ..."
|
73
|
-
|
80
|
+
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
81
|
+
theme = ThemeCheck::Theme.new(storage)
|
74
82
|
if theme.all.empty?
|
75
83
|
raise Abort, "No templates found.\n#{USAGE}"
|
76
84
|
end
|
data/lib/theme_check/config.rb
CHANGED
@@ -4,6 +4,7 @@ module ThemeCheck
|
|
4
4
|
class Config
|
5
5
|
DOTFILE = '.theme-check.yml'
|
6
6
|
DEFAULT_CONFIG = "#{__dir__}/../../config/default.yml"
|
7
|
+
BOOLEAN = [true, false]
|
7
8
|
|
8
9
|
attr_reader :root
|
9
10
|
attr_accessor :only_categories, :exclude_categories, :auto_correct
|
@@ -11,13 +12,21 @@ module ThemeCheck
|
|
11
12
|
class << self
|
12
13
|
def from_path(path)
|
13
14
|
if (filename = find(path))
|
14
|
-
new(filename.dirname, load_file(filename))
|
15
|
+
new(root: filename.dirname, configuration: load_file(filename))
|
15
16
|
else
|
16
17
|
# No configuration file
|
17
|
-
new(path)
|
18
|
+
new(root: path)
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
22
|
+
def from_string(config)
|
23
|
+
new(configuration: YAML.load(config), should_resolve_requires: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def from_hash(config)
|
27
|
+
new(configuration: config, should_resolve_requires: false)
|
28
|
+
end
|
29
|
+
|
21
30
|
def find(root, needle = DOTFILE)
|
22
31
|
Pathname.new(root).descend.reverse_each do |path|
|
23
32
|
pathname = path.join(needle)
|
@@ -29,81 +38,124 @@ module ThemeCheck
|
|
29
38
|
def load_file(absolute_path)
|
30
39
|
YAML.load_file(absolute_path)
|
31
40
|
end
|
41
|
+
|
42
|
+
def default
|
43
|
+
@default ||= load_file(DEFAULT_CONFIG)
|
44
|
+
end
|
32
45
|
end
|
33
46
|
|
34
|
-
def initialize(root, configuration
|
35
|
-
@configuration = configuration
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
47
|
+
def initialize(root: nil, configuration: nil, should_resolve_requires: true)
|
48
|
+
@configuration = if configuration
|
49
|
+
validate_configuration(configuration)
|
50
|
+
else
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
merge_with_default_configuration!(@configuration)
|
54
|
+
|
55
|
+
@root = if root && @configuration.key?("root")
|
56
|
+
Pathname.new(root).join(@configuration["root"])
|
57
|
+
elsif root
|
58
|
+
Pathname.new(root)
|
40
59
|
end
|
60
|
+
|
41
61
|
@only_categories = []
|
42
62
|
@exclude_categories = []
|
43
63
|
@auto_correct = false
|
44
|
-
|
64
|
+
|
65
|
+
resolve_requires if @root && should_resolve_requires
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](name)
|
69
|
+
@configuration[name]
|
45
70
|
end
|
46
71
|
|
47
72
|
def to_h
|
48
73
|
@configuration
|
49
74
|
end
|
50
75
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
default_configuration.merge(@checks).each do |check_name, properties|
|
55
|
-
if @checks[check_name] && !default_configuration[check_name].nil?
|
56
|
-
valid_properties = valid_check_configuration(check_name)
|
57
|
-
properties = properties.merge(valid_properties)
|
58
|
-
end
|
76
|
+
def check_configurations
|
77
|
+
@check_configurations ||= @configuration.select { |name, _| check_name?(name) }
|
78
|
+
end
|
59
79
|
|
60
|
-
|
80
|
+
def enabled_checks
|
81
|
+
@enabled_checks ||= check_configurations.map do |check_name, options|
|
82
|
+
next unless options["enabled"]
|
61
83
|
|
62
|
-
options = properties.transform_keys(&:to_sym)
|
63
84
|
check_class = ThemeCheck.const_get(check_name)
|
85
|
+
|
64
86
|
next if exclude_categories.include?(check_class.category)
|
65
87
|
next if only_categories.any? && !only_categories.include?(check_class.category)
|
66
88
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
89
|
+
options_for_check = options.transform_keys(&:to_sym)
|
90
|
+
options_for_check.delete(:enabled)
|
91
|
+
check = check_class.new(**options_for_check)
|
92
|
+
check.options = options_for_check
|
93
|
+
check
|
94
|
+
end.compact
|
95
|
+
end
|
71
96
|
|
72
|
-
|
97
|
+
def ignored_patterns
|
98
|
+
self["ignore"] || []
|
73
99
|
end
|
74
100
|
|
75
101
|
private
|
76
102
|
|
77
|
-
def
|
78
|
-
|
103
|
+
def check_name?(name)
|
104
|
+
name.start_with?(/[A-Z]/)
|
79
105
|
end
|
80
106
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
107
|
+
def validate_configuration(configuration, default_configuration = self.class.default, parent_keys = [])
|
108
|
+
valid_configuration = {}
|
109
|
+
|
110
|
+
configuration.each do |key, value|
|
111
|
+
# No validation possible unless we have a default to compare to
|
112
|
+
unless default_configuration
|
113
|
+
valid_configuration[key] = value
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
default = default_configuration[key]
|
118
|
+
keys = parent_keys + [key]
|
119
|
+
name = keys.join(".")
|
120
|
+
|
121
|
+
if check_name?(key)
|
122
|
+
if value.is_a?(Hash)
|
123
|
+
valid_configuration[key] = validate_configuration(value, default, keys)
|
124
|
+
else
|
125
|
+
warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
|
88
126
|
end
|
127
|
+
elsif default.nil?
|
128
|
+
warn("unknown configuration: #{name}")
|
129
|
+
elsif BOOLEAN.include?(default) && !BOOLEAN.include?(value)
|
130
|
+
warn("bad configuration type for #{name}: expected true or false, got #{value.inspect}")
|
131
|
+
elsif !BOOLEAN.include?(default) && default.class != value.class
|
132
|
+
warn("bad configuration type for #{name}: expected a #{default.class}, got #{value.inspect}")
|
133
|
+
else
|
134
|
+
valid_configuration[key] = value
|
89
135
|
end
|
90
136
|
end
|
91
|
-
end
|
92
137
|
|
93
|
-
|
94
|
-
|
138
|
+
valid_configuration
|
139
|
+
end
|
95
140
|
|
96
|
-
|
141
|
+
def merge_with_default_configuration!(configuration, default_configuration = self.class.default)
|
142
|
+
default_configuration.each do |key, default|
|
143
|
+
value = configuration[key]
|
97
144
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
145
|
+
case value
|
146
|
+
when Hash
|
147
|
+
merge_with_default_configuration!(value, default)
|
148
|
+
when nil
|
149
|
+
configuration[key] = default
|
103
150
|
end
|
104
151
|
end
|
152
|
+
configuration
|
153
|
+
end
|
105
154
|
|
106
|
-
|
155
|
+
def resolve_requires
|
156
|
+
self["require"]&.each do |path|
|
157
|
+
require(File.join(@root, path))
|
158
|
+
end
|
107
159
|
end
|
108
160
|
end
|
109
161
|
end
|
@@ -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
|