theme-check 0.1.0 → 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/.gitignore +1 -0
- data/CONTRIBUTING.md +2 -0
- data/README.md +62 -1
- data/RELEASING.md +41 -0
- data/Rakefile +38 -4
- data/config/default.yml +18 -0
- data/data/shopify_liquid/deprecated_filters.yml +10 -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 +15 -5
- data/lib/theme_check/check.rb +11 -0
- data/lib/theme_check/checks.rb +10 -0
- data/lib/theme_check/checks/deprecated_filter.rb +22 -0
- data/lib/theme_check/checks/missing_enable_comment.rb +31 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +12 -12
- data/lib/theme_check/checks/parser_blocking_javascript.rb +55 -0
- data/lib/theme_check/checks/space_inside_braces.rb +19 -7
- data/lib/theme_check/checks/template_length.rb +11 -3
- data/lib/theme_check/checks/undefined_object.rb +107 -34
- data/lib/theme_check/checks/valid_html_translation.rb +2 -2
- data/lib/theme_check/cli.rb +18 -4
- data/lib/theme_check/config.rb +97 -44
- data/lib/theme_check/corrector.rb +31 -0
- 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/liquid_check.rb +2 -2
- data/lib/theme_check/node.rb +13 -0
- data/lib/theme_check/offense.rb +25 -9
- data/lib/theme_check/packager.rb +51 -0
- data/lib/theme_check/printer.rb +13 -4
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +28 -0
- data/lib/theme_check/shopify_liquid/object.rb +6 -0
- data/lib/theme_check/storage.rb +25 -0
- data/lib/theme_check/template.rb +32 -10
- 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 +98 -0
- metadata +21 -7
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
class Corrector
|
5
|
+
def initialize(template:)
|
6
|
+
@template = template
|
7
|
+
end
|
8
|
+
|
9
|
+
def insert_after(node, content)
|
10
|
+
line = @template.full_line(node.line_number)
|
11
|
+
line.insert(node.range[1] + 1, content)
|
12
|
+
end
|
13
|
+
|
14
|
+
def insert_before(node, content)
|
15
|
+
line = @template.full_line(node.line_number)
|
16
|
+
line.insert(node.range[0], content)
|
17
|
+
end
|
18
|
+
|
19
|
+
def replace(node, content)
|
20
|
+
line = @template.full_line(node.line_number)
|
21
|
+
line[node.range[0]..node.range[1]] = content
|
22
|
+
node.markup = content
|
23
|
+
end
|
24
|
+
|
25
|
+
def wrap(node, insert_before, insert_after)
|
26
|
+
line = @template.full_line(node.line_number)
|
27
|
+
line.insert(node.range[0], insert_before)
|
28
|
+
line.insert(node.range[1] + 1 + insert_before.length, insert_after)
|
29
|
+
end
|
30
|
+
end
|
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
|
@@ -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
|
|
@@ -6,8 +6,8 @@ module ThemeCheck
|
|
6
6
|
extend ChecksTracking
|
7
7
|
include ParsingHelpers
|
8
8
|
|
9
|
-
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil)
|
10
|
-
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number)
|
9
|
+
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
|
10
|
+
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/lib/theme_check/node.rb
CHANGED
@@ -22,6 +22,14 @@ module ThemeCheck
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
def markup=(markup)
|
26
|
+
if tag?
|
27
|
+
@value.raw = markup
|
28
|
+
elsif @value.instance_variable_defined?(:@markup)
|
29
|
+
@value.instance_variable_set(:@markup, markup)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
25
33
|
# Array of children nodes.
|
26
34
|
def children
|
27
35
|
@children ||= begin
|
@@ -113,5 +121,10 @@ module ThemeCheck
|
|
113
121
|
false
|
114
122
|
end
|
115
123
|
end
|
124
|
+
|
125
|
+
def range
|
126
|
+
start = template.full_line(line_number).index(markup)
|
127
|
+
[start, start + markup.length - 1]
|
128
|
+
end
|
116
129
|
end
|
117
130
|
end
|
data/lib/theme_check/offense.rb
CHANGED
@@ -3,10 +3,11 @@ module ThemeCheck
|
|
3
3
|
class Offense
|
4
4
|
MAX_SOURCE_EXCERPT_SIZE = 120
|
5
5
|
|
6
|
-
attr_reader :check, :message, :template, :node, :markup, :line_number
|
6
|
+
attr_reader :check, :message, :template, :node, :markup, :line_number, :correction
|
7
7
|
|
8
|
-
def initialize(check:, message: nil, template: nil, node: nil, markup: nil, line_number: nil)
|
8
|
+
def initialize(check:, message: nil, template: nil, node: nil, markup: nil, line_number: nil, correction: nil)
|
9
9
|
@check = check
|
10
|
+
@correction = correction
|
10
11
|
|
11
12
|
if message
|
12
13
|
@message = message
|
@@ -39,7 +40,7 @@ module ThemeCheck
|
|
39
40
|
def source_excerpt
|
40
41
|
return unless line_number
|
41
42
|
@source_excerpt ||= begin
|
42
|
-
excerpt = template.
|
43
|
+
excerpt = template.source_excerpt(line_number)
|
43
44
|
if excerpt.size > MAX_SOURCE_EXCERPT_SIZE
|
44
45
|
excerpt[0, MAX_SOURCE_EXCERPT_SIZE - 3] + '...'
|
45
46
|
else
|
@@ -54,18 +55,22 @@ module ThemeCheck
|
|
54
55
|
end
|
55
56
|
|
56
57
|
def end_line
|
57
|
-
|
58
|
-
|
58
|
+
if markup
|
59
|
+
start_line + markup.count("\n")
|
60
|
+
else
|
61
|
+
start_line
|
62
|
+
end
|
59
63
|
end
|
60
64
|
|
61
65
|
def start_column
|
62
|
-
return 0 unless line_number
|
63
|
-
template.full_line(
|
66
|
+
return 0 unless line_number && markup
|
67
|
+
template.full_line(start_line + 1).index(markup.split("\n", 2).first)
|
64
68
|
end
|
65
69
|
|
66
70
|
def end_column
|
67
|
-
return 0 unless line_number
|
68
|
-
|
71
|
+
return 0 unless line_number && markup
|
72
|
+
markup_end = markup.split("\n").last
|
73
|
+
template.full_line(end_line + 1).index(markup_end) + markup_end.size
|
69
74
|
end
|
70
75
|
|
71
76
|
def code_name
|
@@ -93,6 +98,17 @@ module ThemeCheck
|
|
93
98
|
tokens.join(":") if tokens.any?
|
94
99
|
end
|
95
100
|
|
101
|
+
def correctable?
|
102
|
+
line_number && correction
|
103
|
+
end
|
104
|
+
|
105
|
+
def correct
|
106
|
+
if correctable?
|
107
|
+
corrector = Corrector.new(template: template)
|
108
|
+
correction.call(corrector)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
96
112
|
def to_s
|
97
113
|
if template
|
98
114
|
"#{message} at #{location}"
|