theme-check 1.5.2 → 1.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 960f944628496ea317205e34678a78ec1e560dbfb48f40cfa33be2d82ebe4770
4
- data.tar.gz: 66e2abe15609c6e5a40d13d05aa0daedf9fbd783e31226bbfb0c6961c1d89ad6
3
+ metadata.gz: 713459ed0c35e11e175e51d42237fceb583184af08a00f8f56cdecbbb1c494b8
4
+ data.tar.gz: aa2059da0b820755eee1a8778748d7c7758fef705e29f5def5c74905743553d8
5
5
  SHA512:
6
- metadata.gz: d920362e7729fc739c0a0202e00a605025db3d80c2fdfac5be9d6d9ea6c74bc4922baa82e093851432417863106c14964f6fdc76f66e55fd7947f33887b989c1
7
- data.tar.gz: 26eb4ba427e8380ece15160c2c27cd052802ec233bb61a4a17e36dcf7f008cc7bc569b71649c525c6d57a7d79654469af65ec664726665584806928cf7258483
6
+ metadata.gz: fa6216908fb067365d11ef7c0ea1c2e0a847de124f81ad43824eb49f9373093b6fc54dabbc1062dd9068af11724abc9dad020c6bebc2c71702f4750a8829aa78
7
+ data.tar.gz: f0412fbb0407d75e33ac96f66c760dd39ddb5cd18c751b8f6cb8a6edb4da1d0867147d929b7bae1401eb5837ee1c4aa0f40f60340d70c4a75c5f47a0753ebf58
@@ -8,15 +8,23 @@ jobs:
8
8
 
9
9
  strategy:
10
10
  matrix:
11
- platform: [ubuntu-latest, windows-latest]
11
+ platform:
12
+ - ubuntu-latest
13
+ - windows-latest
12
14
  version:
13
15
  - 3.0.0
14
16
  - 2.6.6
17
+ theme:
18
+ - Shopify/dawn
15
19
 
16
20
  name: Ruby ${{ matrix.platform }} ${{ matrix.version }}
17
21
 
18
22
  steps:
19
23
  - uses: actions/checkout@v2
24
+ - uses: actions/checkout@v2
25
+ with:
26
+ repository: ${{ matrix.theme }}
27
+ path: ./crash-test-theme
20
28
  - name: Set up Ruby ${{ matrix.version }}
21
29
  uses: ruby/setup-ruby@v1
22
30
  with:
@@ -30,6 +38,6 @@ jobs:
30
38
  run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
31
39
  - name: Run tests
32
40
  run: bundle exec rake
33
- - name: Test runtime
34
- # Testing that runtime can execute, not testing the results themselves
35
- run: bundle exec theme-check ./test/theme | grep -q "files inspected"
41
+ - name: Crash test
42
+ run: |
43
+ bundle exec theme-check --fail-level crash ./crash-test-theme
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
1
 
2
+ v1.6.0 / 2021-09-14
3
+ ===================
4
+
5
+ ### Features
6
+
7
+ * Add `--auto-correct` support to `TranslationKeyExists` (add missing translation as TODO to default locale) ([#422](https://github.com/shopify/theme-check/issues/422))
8
+ * Add `--auto-correct` support to `UnusedSnippet` (delete unused file) ([#416](https://github.com/shopify/theme-check/issues/416))
9
+ * Add `--auto-correct` support to `MissingRequiredTemplateFiles` (create missing files) ([#385](https://github.com/shopify/theme-check/issues/385))
10
+
11
+ ### Fixes
12
+
13
+ * Fix `undefined method [] of nil` in `replace_placeholders` ([#441](https://github.com/shopify/theme-check/issues/441), [#444](https://github.com/shopify/theme-check/issues/444))
14
+ * Disable ConvertIncludeToRender corrector until we fix [#445](https://github.com/shopify/theme-check/issues/445) ([#446](https://github.com/shopify/theme-check/issues/446))
15
+ * Fix a couple of correction bugs ([#442](https://github.com/shopify/theme-check/issues/442), [#439](https://github.com/shopify/theme-check/issues/439))
16
+ * Fix `AssetSizeCSS` error when size is nil ([#419](https://github.com/shopify/theme-check/issues/419))
17
+ * Write JSON to file, not a Ruby Hash. ([#434](https://github.com/shopify/theme-check/issues/434), [#432](https://github.com/shopify/theme-check/issues/432))
18
+
2
19
  v1.5.2 / 2021-09-09
3
20
  ===================
4
21
 
@@ -86,6 +86,11 @@ module ThemeCheck
86
86
  def correct_offenses
87
87
  if @auto_correct
88
88
  offenses.each(&:correct)
89
+ end
90
+ end
91
+
92
+ def write_corrections
93
+ if @auto_correct
89
94
  @theme.liquid.each(&:write)
90
95
  @theme.json.each(&:write)
91
96
  end
@@ -9,10 +9,21 @@ module ThemeCheck
9
9
  @content = nil
10
10
  end
11
11
 
12
- alias_method :content, :source
12
+ def rewriter
13
+ @rewriter ||= TemplateRewriter.new(@relative_path, source)
14
+ end
15
+
16
+ def write
17
+ content = rewriter.to_s
18
+ if source != content
19
+ @storage.write(@relative_path, content.gsub("\n", @eol))
20
+ @source = content
21
+ @rewriter = nil
22
+ end
23
+ end
13
24
 
14
25
  def gzipped_size
15
- @gzipped_size ||= Zlib.gzip(content).bytesize
26
+ @gzipped_size ||= Zlib.gzip(source).bytesize
16
27
  end
17
28
 
18
29
  def name
@@ -46,7 +46,7 @@ module ThemeCheck
46
46
  end
47
47
 
48
48
  def severity_value(severity)
49
- SEVERITY_VALUES[severity]
49
+ SEVERITY_VALUES[severity] || -1
50
50
  end
51
51
 
52
52
  def categories(*categories)
@@ -22,5 +22,20 @@ module ThemeCheck
22
22
  node: node
23
23
  )
24
24
  end
25
+
26
+ def href_to_file_size(href)
27
+ # asset_url (+ optional stylesheet_tag) variables
28
+ if href =~ /^#{LIQUID_VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
29
+ asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
30
+ asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
31
+ return if asset.nil?
32
+ asset.gzipped_size
33
+
34
+ # remote URLs
35
+ elsif href =~ %r{^(https?:)?//}
36
+ asset = RemoteAssetFile.from_src(href)
37
+ asset.gzipped_size
38
+ end
39
+ end
25
40
  end
26
41
  end
@@ -13,12 +13,29 @@ module ThemeCheck
13
13
  def on_variable(node)
14
14
  used_filters = node.value.filters.map { |name, *_rest| name }
15
15
  return unless used_filters.include?("stylesheet_tag")
16
- file_size = href_to_file_size('{{' + node.markup + '}}')
16
+ file_size = stylesheet_tag_pipeline_to_file_size(node.markup)
17
+ return if file_size.nil?
17
18
  return if file_size <= @threshold_in_bytes
18
19
  add_offense(
19
20
  "CSS on every page load exceeding compressed size threshold (#{@threshold_in_bytes} Bytes).",
20
21
  node: node
21
22
  )
22
23
  end
24
+
25
+ def stylesheet_tag_pipeline_to_file_size(href)
26
+ # asset_url
27
+ if href =~ /asset_url/ && href =~ Liquid::QuotedString
28
+ asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
29
+ asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
30
+ return if asset.nil?
31
+ asset.gzipped_size
32
+
33
+ # remote URLs
34
+ elsif href =~ %r{(https?:)?//[^'"]+}
35
+ url = Regexp.last_match(0)
36
+ asset = RemoteAssetFile.from_src(url)
37
+ asset.gzipped_size
38
+ end
39
+ end
23
40
  end
24
41
  end
@@ -8,7 +8,8 @@ module ThemeCheck
8
8
 
9
9
  def on_include(node)
10
10
  add_offense("`include` is deprecated - convert it to `render`", node: node) do |corrector|
11
- corrector.replace(node, "render \'#{node.value.template_name_expr}\' ")
11
+ # We need to fix #445 and pass the variables from the context or don't replace at all.
12
+ # corrector.replace(node, "render \'#{node.value.template_name_expr}\' ")
12
13
  end
13
14
  end
14
15
  end
@@ -9,19 +9,33 @@ module ThemeCheck
9
9
  doc docs_url(__FILE__)
10
10
 
11
11
  REQUIRED_LIQUID_FILES = %w(layout/theme)
12
- REQUIRED_TEMPLATE_FILES = %w(
13
- index product collection cart blog article page list-collections search 404
12
+
13
+ REQUIRED_LIQUID_TEMPLATE_FILES = %w(
14
14
  gift_card customers/account customers/activate_account customers/addresses
15
- customers/login customers/order customers/register customers/reset_password password
16
- )
17
- .map { |file| "templates/#{file}" }
15
+ customers/login customers/order customers/register customers/reset_password
16
+ ).map { |file| "templates/#{file}" }
17
+
18
+ REQUIRED_JSON_TEMPLATE_FILES = %w(
19
+ index product collection cart blog article page list-collections search 404
20
+ password
21
+ ).map { |file| "templates/#{file}" }
22
+
23
+ REQUIRED_TEMPLATE_FILES = (REQUIRED_LIQUID_TEMPLATE_FILES + REQUIRED_JSON_TEMPLATE_FILES)
18
24
 
19
25
  def on_end
20
26
  (REQUIRED_LIQUID_FILES - theme.liquid.map(&:name)).each do |file|
21
- add_offense("'#{file}.liquid' is missing")
27
+ add_offense("'#{file}.liquid' is missing") do |corrector|
28
+ corrector.create(@theme, "#{file}.liquid", "")
29
+ end
22
30
  end
23
31
  (REQUIRED_TEMPLATE_FILES - (theme.liquid + theme.json).map(&:name)).each do |file|
24
- add_offense("'#{file}.liquid' or '#{file}.json' is missing")
32
+ add_offense("'#{file}.liquid' or '#{file}.json' is missing") do |corrector|
33
+ if REQUIRED_LIQUID_TEMPLATE_FILES.include?(file)
34
+ corrector.create(@theme, "#{file}.liquid", "")
35
+ else
36
+ corrector.create(@theme, "#{file}.json", "")
37
+ end
38
+ end
25
39
  end
26
40
  end
27
41
  end
@@ -29,7 +29,9 @@ module ThemeCheck
29
29
  "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
30
30
  node: node,
31
31
  markup: key_node.value,
32
- )
32
+ ) do |corrector|
33
+ corrector.add_default_translation_key(@theme.default_locale_json, key_node.value.split("."), "TODO")
34
+ end
33
35
  end
34
36
  end
35
37
 
@@ -24,7 +24,9 @@ module ThemeCheck
24
24
 
25
25
  def on_end
26
26
  missing_snippets.each do |template|
27
- add_offense("This template is not used", template: template)
27
+ add_offense("This template is not used", template: template) do |corrector|
28
+ corrector.remove(@theme, template.relative_path.to_s)
29
+ end
28
30
  end
29
31
  end
30
32
 
@@ -49,7 +49,7 @@ module ThemeCheck
49
49
  "Automatically fix offenses"
50
50
  ) { @auto_correct = true }
51
51
  @option_parser.on(
52
- "--fail-level SEVERITY", Check::SEVERITIES,
52
+ "--fail-level SEVERITY", [:crash] + Check::SEVERITIES,
53
53
  "Minimum severity (error|suggestion|style) for exit with error code"
54
54
  ) do |severity|
55
55
  @fail_level = severity.to_sym
@@ -191,7 +191,10 @@ module ThemeCheck
191
191
  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
192
192
  analyzer.analyze_theme
193
193
  analyzer.correct_offenses
194
- output_with_format(theme, analyzer, out_stream)
194
+ print_with_format(theme, analyzer, out_stream)
195
+ # corrections are committed after printing so that the
196
+ # source_excerpts are still pointing to the uncorrected source.
197
+ analyzer.write_corrections
195
198
  raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
196
199
  offense.check.severity_value <= Check.severity_value(@fail_level)
197
200
  end
@@ -211,7 +214,7 @@ module ThemeCheck
211
214
  STDERR.puts "Profiling is only available in development"
212
215
  end
213
216
 
214
- def output_with_format(theme, analyzer, out_stream)
217
+ def print_with_format(theme, analyzer, out_stream)
215
218
  case @format
216
219
  when :text
217
220
  ThemeCheck::Printer.new(out_stream).print(theme, analyzer.offenses, @config.auto_correct)
@@ -7,25 +7,20 @@ module ThemeCheck
7
7
  end
8
8
 
9
9
  def insert_after(node, content)
10
- line = @template.full_line(node.line_number)
11
- line.insert(node.range[1] + 1, content)
10
+ @template.rewriter.insert_after(node, content)
12
11
  end
13
12
 
14
13
  def insert_before(node, content)
15
- line = @template.full_line(node.line_number)
16
- line.insert(node.range[0], content)
14
+ @template.rewriter.insert_before(node, content)
17
15
  end
18
16
 
19
17
  def replace(node, content)
20
- line = @template.full_line(node.line_number)
21
- line[node.range[0]..node.range[1]] = content
18
+ @template.rewriter.replace(node, content)
22
19
  node.markup = content
23
20
  end
24
21
 
25
22
  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)
23
+ @template.rewriter.wrap(node, insert_before, insert_after)
29
24
  end
30
25
 
31
26
  def create(theme, relative_path, content)
@@ -34,11 +29,25 @@ module ThemeCheck
34
29
 
35
30
  def create_default_locale_json(theme)
36
31
  theme.default_locale_json = JsonFile.new("locales/#{theme.default_locale}.default.json", theme.storage)
37
- theme.default_locale_json.update_contents('{}')
32
+ theme.default_locale_json.update_contents({})
33
+ end
34
+
35
+ def remove(theme, relative_path)
36
+ theme.storage.remove(relative_path)
38
37
  end
39
38
 
40
39
  def mkdir(theme, relative_path)
41
40
  theme.storage.mkdir(relative_path)
42
41
  end
42
+
43
+ def add_default_translation_key(file, key, value)
44
+ hash = file.content
45
+ key.reduce(hash) do |pointer, token|
46
+ return pointer[token] = value if token == key.last
47
+ pointer[token] = {} unless pointer.key?(token)
48
+ pointer[token]
49
+ end
50
+ file.update_contents(hash)
51
+ end
43
52
  end
44
53
  end
@@ -16,14 +16,19 @@ module ThemeCheck
16
16
  end
17
17
 
18
18
  def read(relative_path)
19
- file(relative_path).read
19
+ file(relative_path).read(mode: 'rb', encoding: 'UTF-8')
20
20
  end
21
21
 
22
22
  def write(relative_path, content)
23
23
  reset_memoizers unless file_exists?(relative_path)
24
24
 
25
25
  file(relative_path).dirname.mkpath unless file(relative_path).dirname.directory?
26
- file(relative_path).write(content)
26
+ file(relative_path).write(content, mode: 'w+b', encoding: 'UTF-8')
27
+ end
28
+
29
+ def remove(relative_path)
30
+ file(relative_path).delete
31
+ reset_memoizers
27
32
  end
28
33
 
29
34
  def mkdir(relative_path)
@@ -67,10 +67,10 @@ module ThemeCheck
67
67
  private
68
68
 
69
69
  def replace_placeholders(string)
70
- # Replace all {%#{i}####%} with the actual content.
71
- string.gsub(LIQUID_TAG) do |match|
72
- key = /\d+/.match(match)[0]
73
- @placeholder_values[key.to_i]
70
+ # Replace all {i}####≬ with the actual content.
71
+ string.gsub(HTML_LIQUID_PLACEHOLDER) do |match|
72
+ key = /[0-9a-z]+/.match(match)[0]
73
+ @placeholder_values[key.to_i(36)]
74
74
  end
75
75
  end
76
76
  end
@@ -9,12 +9,11 @@ module ThemeCheck
9
9
 
10
10
  def initialize(checks)
11
11
  @checks = checks
12
- @placeholder_values = []
13
12
  end
14
13
 
15
14
  def visit_template(template)
16
- doc = parse(template)
17
- visit(HtmlNode.new(doc, template, @placeholder_values))
15
+ doc, placeholder_values = parse(template)
16
+ visit(HtmlNode.new(doc, template, placeholder_values))
18
17
  rescue ArgumentError => e
19
18
  call_checks(:on_parse_error, e, template)
20
19
  end
@@ -22,19 +21,32 @@ module ThemeCheck
22
21
  private
23
22
 
24
23
  def parse(template)
24
+ placeholder_values = []
25
25
  parseable_source = +template.source.clone
26
26
 
27
- # Replace all liquid tags with {%#{i}######%} to prevent the HTML
27
+ # Replace all non-empty liquid tags with {i}######≬ to prevent the HTML
28
28
  # parser from freaking out. We transparently replace those placeholders in
29
29
  # HtmlNode.
30
+ #
31
+ # We're using base36 to prevent index bleeding on 36^3 tags.
32
+ # `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
33
+ # Should be enough.
34
+ #
35
+ # The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
36
+ #
37
+ # Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
30
38
  matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
31
39
  value = m[0]
32
- @placeholder_values.push(value)
33
- key = (@placeholder_values.size - 1).to_s
34
- parseable_source[m.begin(0)...m.end(0)] = "{%#{key.ljust(m.end(0) - m.begin(0) - 4, '#')}%}"
40
+ next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
41
+ placeholder_values.push(value)
42
+ key = (placeholder_values.size - 1).to_s(36)
43
+ parseable_source[m.begin(0)...m.end(0)] = "≬#{key.ljust(m.end(0) - m.begin(0) - 2, '#')}≬"
35
44
  end
36
45
 
37
- Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400)
46
+ [
47
+ Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
48
+ placeholder_values,
49
+ ]
38
50
  end
39
51
 
40
52
  def visit(node)
@@ -23,6 +23,10 @@ module ThemeCheck
23
23
  @files[relative_path] = content
24
24
  end
25
25
 
26
+ def remove(relative_path)
27
+ @files.delete(relative_path)
28
+ end
29
+
26
30
  def mkdir(relative_path)
27
31
  @files[relative_path] = nil
28
32
  end
@@ -20,14 +20,19 @@ module ThemeCheck
20
20
  @parser_error
21
21
  end
22
22
 
23
- def update_contents(new_content = '{}')
23
+ def update_contents(new_content = {})
24
+ raise ArgumentError if new_content.is_a?(String)
24
25
  @content = new_content
25
26
  end
26
27
 
27
28
  def write
28
- if source != @content
29
- @storage.write(@relative_path, content)
30
- @source = content
29
+ pretty = JSON.pretty_generate(@content)
30
+ if source.rstrip != pretty.rstrip
31
+ # Most editors add a trailing \n at the end of files. Here we
32
+ # try to maintain the convention.
33
+ eof = source.end_with?("\n") ? "\n" : ""
34
+ @storage.write(@relative_path, pretty.gsub("\n", @eol) + eof)
35
+ @source = pretty
31
36
  end
32
37
  end
33
38
 
@@ -156,11 +156,6 @@ module ThemeCheck
156
156
  end
157
157
  end
158
158
 
159
- def range
160
- start = template.full_line(line_number).index(markup)
161
- [start, start + markup.length - 1]
162
- end
163
-
164
159
  def position
165
160
  @position ||= Position.new(
166
161
  markup,
@@ -169,6 +164,14 @@ module ThemeCheck
169
164
  )
170
165
  end
171
166
 
167
+ def start_index
168
+ position.start_index
169
+ end
170
+
171
+ def end_index
172
+ position.end_index
173
+ end
174
+
172
175
  def start_token
173
176
  return "" if inside_liquid_tag?
174
177
  output = ""
@@ -187,14 +190,6 @@ module ThemeCheck
187
190
  output
188
191
  end
189
192
 
190
- def start_index
191
- position.start_index
192
- end
193
-
194
- def end_index
195
- position.end_index
196
- end
197
-
198
193
  private
199
194
 
200
195
  # Here we're hacking around a glorious bug in Liquid that makes it so the
@@ -144,6 +144,32 @@ module ThemeCheck
144
144
  corrector = Corrector.new(template: template)
145
145
  correction.call(corrector)
146
146
  end
147
+ rescue => e
148
+ ThemeCheck.bug(<<~EOS)
149
+ Exception while running `Offense#correct`:
150
+ ```
151
+ #{e.class}: #{e.message}
152
+ #{e.backtrace.join("\n ")}
153
+ ```
154
+
155
+ Offense:
156
+ ```
157
+ #{JSON.pretty_generate(to_h)}
158
+ ```
159
+ Check options:
160
+ ```
161
+ #{check.options.pretty_inspect}
162
+ ```
163
+ Markup:
164
+ ```
165
+ #{markup}
166
+ ```
167
+ Node.Markup:
168
+ ```
169
+ #{node&.markup}
170
+ ```
171
+ EOS
172
+ exit(2)
147
173
  end
148
174
 
149
175
  def whole_theme?
@@ -5,6 +5,7 @@ module ThemeCheck
5
5
  LIQUID_TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
6
6
  LIQUID_VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
7
7
  LIQUID_TAG_OR_VARIABLE = /#{LIQUID_TAG}|#{LIQUID_VARIABLE}/om
8
+ HTML_LIQUID_PLACEHOLDER = /≬[0-9a-z]+#*≬/m
8
9
  START_OR_END_QUOTE = /(^['"])|(['"]$)/
9
10
 
10
11
  def matches(s, re)
@@ -16,20 +17,5 @@ module ThemeCheck
16
17
  end
17
18
  matches
18
19
  end
19
-
20
- def href_to_file_size(href)
21
- # asset_url (+ optional stylesheet_tag) variables
22
- if href =~ /^#{LIQUID_VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
23
- asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
24
- asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
25
- return if asset.nil?
26
- asset.gzipped_size
27
-
28
- # remote URLs
29
- elsif href =~ %r{^(https?:)?//}
30
- asset = RemoteAssetFile.from_src(href)
31
- asset.gzipped_size
32
- end
33
- end
34
20
  end
35
21
  end
@@ -3,10 +3,11 @@
3
3
  module ThemeCheck
4
4
  class Template < ThemeFile
5
5
  def write
6
- content = updated_content
6
+ content = rewriter.to_s
7
7
  if source != content
8
- @storage.write(@relative_path, content)
8
+ @storage.write(@relative_path, content.gsub("\n", @eol))
9
9
  @source = content
10
+ @rewriter = nil
10
11
  end
11
12
  end
12
13
 
@@ -26,19 +27,8 @@ module ThemeCheck
26
27
  name.start_with?('snippets')
27
28
  end
28
29
 
29
- def lines
30
- # Retain trailing newline character
31
- @lines ||= source.split("\n", -1)
32
- end
33
-
34
- # Not entirely obvious but lines is mutable, corrections are to be
35
- # applied on @lines.
36
- def updated_content
37
- lines.join("\n")
38
- end
39
-
40
- def excerpt(line)
41
- lines[line - 1].strip
30
+ def rewriter
31
+ @rewriter ||= TemplateRewriter.new(@relative_path, source)
42
32
  end
43
33
 
44
34
  def source_excerpt(line)
@@ -46,10 +36,6 @@ module ThemeCheck
46
36
  original_lines[line - 1].strip
47
37
  end
48
38
 
49
- def full_line(line)
50
- lines[line - 1]
51
- end
52
-
53
39
  def parse
54
40
  @ast ||= self.class.parse(source)
55
41
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser'
4
+
5
+ module ThemeCheck
6
+ class TemplateRewriter
7
+ def initialize(name, source)
8
+ @buffer = Parser::Source::Buffer.new(name, source: source)
9
+ @rewriter = Parser::Source::TreeRewriter.new(
10
+ @buffer
11
+ )
12
+ end
13
+
14
+ def insert_before(node, content)
15
+ @rewriter.insert_before(
16
+ range(node.start_index, node.end_index),
17
+ content
18
+ )
19
+ end
20
+
21
+ def insert_after(node, content)
22
+ @rewriter.insert_after(
23
+ range(node.start_index, node.end_index),
24
+ content
25
+ )
26
+ end
27
+
28
+ def replace(node, content)
29
+ @rewriter.replace(
30
+ range(node.start_index, node.end_index),
31
+ content
32
+ )
33
+ end
34
+
35
+ def wrap(node, insert_before, insert_after)
36
+ @rewriter.wrap(
37
+ range(node.start_index, node.end_index),
38
+ insert_before,
39
+ insert_after,
40
+ )
41
+ end
42
+
43
+ def to_s
44
+ @rewriter.process
45
+ end
46
+
47
+ private
48
+
49
+ def range(start_index, end_index)
50
+ Parser::Source::Range.new(
51
+ @buffer,
52
+ start_index,
53
+ end_index,
54
+ )
55
+ end
56
+ end
57
+ end
@@ -6,6 +6,8 @@ module ThemeCheck
6
6
  def initialize(relative_path, storage)
7
7
  @relative_path = relative_path
8
8
  @storage = storage
9
+ @source = nil
10
+ @eol = "\n"
9
11
  end
10
12
 
11
13
  def path
@@ -20,8 +22,23 @@ module ThemeCheck
20
22
  relative_path.sub_ext('').to_s
21
23
  end
22
24
 
25
+ # For the corrector to work properly, we should have a
26
+ # simple mental model of the internal representation of eol
27
+ # characters (Windows uses \r\n, Linux uses \n).
28
+ #
29
+ # Parser::Source::Buffer strips the \r from the source file, so if
30
+ # you are autocorrecting the file you might lose that info and
31
+ # cause a git diff. It also makes the node.start_index/end_index
32
+ # calculation break. That's not cool.
33
+ #
34
+ # So in here we track whether the source file has \r\n in it and
35
+ # we'll make sure that the file we write has the same eol as the
36
+ # source file.
23
37
  def source
24
- @source ||= @storage.read(@relative_path)
38
+ return @source if @source
39
+ @source = @storage.read(@relative_path)
40
+ @eol = @source.include?("\r\n") ? "\r\n" : "\n"
41
+ @source = @source.gsub("\r\n", "\n")
25
42
  end
26
43
 
27
44
  def json?
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.5.2"
3
+ VERSION = "1.6.0"
4
4
  end
data/lib/theme_check.rb CHANGED
@@ -34,6 +34,7 @@ require_relative "theme_check/string_helpers"
34
34
  require_relative "theme_check/file_system_storage"
35
35
  require_relative "theme_check/in_memory_storage"
36
36
  require_relative "theme_check/tags"
37
+ require_relative "theme_check/template_rewriter"
37
38
  require_relative "theme_check/template"
38
39
  require_relative "theme_check/theme"
39
40
  require_relative "theme_check/visitor"
data/theme-check.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  spec.add_dependency('liquid', '>= 5.0.1')
28
28
  spec.add_dependency('nokogiri', '>= 1.12')
29
+ spec.add_dependency('parser', '~> 3')
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-09 00:00:00.000000000 Z
11
+ date: 2021-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: parser
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
41
55
  description:
42
56
  email:
43
57
  - marcandre.cournoyer@shopify.com
@@ -229,6 +243,7 @@ files:
229
243
  - lib/theme_check/string_helpers.rb
230
244
  - lib/theme_check/tags.rb
231
245
  - lib/theme_check/template.rb
246
+ - lib/theme_check/template_rewriter.rb
232
247
  - lib/theme_check/theme.rb
233
248
  - lib/theme_check/theme_file.rb
234
249
  - lib/theme_check/version.rb