Module: Metz::ErbRubyExtractor
- Defined in:
- lib/metz/erb_ruby_extractor.rb
Overview
Ruby extractor for ERB / HAML / SLIM view templates. Registered with
RuboCop::Runner.ruby_extractors so that view-aware Metz cops (currently
Metz/ViewsDeepNavigation) can run against ERB content. RuboCop's parser
would otherwise emit Lint/Syntax for any .erb file because the raw
template is not a valid Ruby program.
The extractor scans for ERB tags, hands each <% ... %> / <%= ... %>
body to RuboCop as its own ProcessedSource, and records the byte offset
of the body inside the original template. RuboCop adds that offset back
to every offense location, so reported line / column numbers point at
the original .erb file -- not the synthetic snippet.
Control-flow openers such as <% if ... %> are reduced to the condition
expression. Other invalid fragments are skipped to avoid Lint/Syntax
noise. ERB comments (<%# ... %>) are skipped as well.
Constant Summary collapse
- ERB_TAG =
/<%(?<kind>[-=#]?)(?<body>.*?)-?%>/m- CONTROL_CONDITION =
/\A(?<prefix>\s*(?:if|unless|while|until|elsif|case)\b\s*)(?<expr>.*?)(?:\s+then)?\s*\z/m- TRAILING_DO_BLOCK =
/\A(?<expr>.*?)(?<suffix>\s+do\b(?:\s*\|[^|]*\|)?\s*)\z/m- MAGIC_COMMENT =
"# frozen_string_literal: true\n\n"
Class Method Summary collapse
- .call(processed_source) ⇒ Object
- .code_tag?(kind) ⇒ Boolean
- .control_expression(expr) ⇒ Object
- .empty_snippet(processed_source) ⇒ Object
- .enumerate_matches(raw, pos: 0, matches: []) ⇒ Object
- .expression_offset(body_offset, match) ⇒ Object
- .expression_source(body, ruby_version, path, body_offset) ⇒ Object
- .extract_snippets(processed_source) ⇒ Object
- .handles?(path) ⇒ Boolean
- .install! ⇒ Object
- .snippet_for(match, processed_source) ⇒ Object
- .source_and_offset(match, ruby_version, path) ⇒ Object
Class Method Details
.call(processed_source) ⇒ Object
29 30 31 32 33 34 35 36 |
# File 'lib/metz/erb_ruby_extractor.rb', line 29 def call(processed_source) path = processed_source.path return nil unless path && handles?(path) snippets = extract_snippets(processed_source) snippets << empty_snippet(processed_source) if snippets.empty? snippets end |
.code_tag?(kind) ⇒ Boolean
81 82 83 |
# File 'lib/metz/erb_ruby_extractor.rb', line 81 def code_tag?(kind) kind.empty? || kind == "-" end |
.control_expression(expr) ⇒ Object
93 94 95 |
# File 'lib/metz/erb_ruby_extractor.rb', line 93 def control_expression(expr) "#{MAGIC_COMMENT}#{expr}\n" end |
.empty_snippet(processed_source) ⇒ Object
101 102 103 104 105 |
# File 'lib/metz/erb_ruby_extractor.rb', line 101 def empty_snippet(processed_source) ruby_version = processed_source.ruby_version || RUBY_VERSION.to_f ps = RuboCop::ProcessedSource.new("", ruby_version, processed_source.path) { offset: 0, processed_source: ps } end |
.enumerate_matches(raw, pos: 0, matches: []) ⇒ Object
54 55 56 57 58 59 60 |
# File 'lib/metz/erb_ruby_extractor.rb', line 54 def enumerate_matches(raw, pos: 0, matches: []) while (match = raw.match(ERB_TAG, pos)) matches << match pos = match.end(0) end matches end |
.expression_offset(body_offset, match) ⇒ Object
97 98 99 |
# File 'lib/metz/erb_ruby_extractor.rb', line 97 def expression_offset(body_offset, match) body_offset + match.begin(:expr) - MAGIC_COMMENT.bytesize end |
.expression_source(body, ruby_version, path, body_offset) ⇒ Object
85 86 87 88 89 90 91 |
# File 'lib/metz/erb_ruby_extractor.rb', line 85 def expression_source(body, ruby_version, path, body_offset) match = body.match(CONTROL_CONDITION) || body.match(TRAILING_DO_BLOCK) return unless match ps = RuboCop::ProcessedSource.new(control_expression(match[:expr]), ruby_version, path) [ps, expression_offset(body_offset, match)] if ps.valid_syntax? end |
.extract_snippets(processed_source) ⇒ Object
48 49 50 51 52 |
# File 'lib/metz/erb_ruby_extractor.rb', line 48 def extract_snippets(processed_source) enumerate_matches(processed_source.raw_source).filter_map do |match| snippet_for(match, processed_source) end end |
.handles?(path) ⇒ Boolean
38 39 40 |
# File 'lib/metz/erb_ruby_extractor.rb', line 38 def handles?(path) File.extname(path.to_s) == ".erb" && ::Metz::FileClassifier.view?(path) end |
.install! ⇒ Object
42 43 44 45 46 |
# File 'lib/metz/erb_ruby_extractor.rb', line 42 def install! extractors = RuboCop::Runner.ruby_extractors extractor = method(:call) extractors.unshift(extractor) unless extractors.include?(extractor) end |
.snippet_for(match, processed_source) ⇒ Object
62 63 64 65 66 67 68 69 70 |
# File 'lib/metz/erb_ruby_extractor.rb', line 62 def snippet_for(match, processed_source) return if match[:kind] == "#" ruby_version = processed_source.ruby_version || RUBY_VERSION.to_f ps, offset = source_and_offset(match, ruby_version, processed_source.path) return unless ps { offset: offset, processed_source: ps } end |
.source_and_offset(match, ruby_version, path) ⇒ Object
72 73 74 75 76 77 78 79 |
# File 'lib/metz/erb_ruby_extractor.rb', line 72 def source_and_offset(match, ruby_version, path) body = match[:body] ps = RuboCop::ProcessedSource.new(body, ruby_version, path) return [ps, match.begin(:body)] if ps.valid_syntax? return unless code_tag?(match[:kind]) expression_source(body, ruby_version, path, match.begin(:body)) end |