From a9761b2a00f79ab1dd53029fe4e0e54947d36fa3 Mon Sep 17 00:00:00 2001 From: Maxime Lapointe Date: Thu, 6 Jul 2023 16:57:23 -0400 Subject: [PATCH] Improve rubocop handling of multiline ruby (#415) * Remove RubyExtractor It's not used since the new RubyExtraction system * More uniform handling of comments with a space Ex: - #hi and = #world They both get turned to - comments * Rubocop: Improved handling for multiline scripts and tag attributes * Normalize handling of indentation for tag attributes and tag script Before, if a tag had children, the attributes and script of the tag would be nested in the begin, adding to their indentation. If the tag did not have childrne, the attributes and script of the tag would not be nested, leading to occasional inconsistencies in what would happen to them based on the presence of a child... Now they are always nested. --------- Co-authored-by: Shane da Silva --- CHANGELOG.md | 5 +- lib/haml_lint/linter/rubocop.rb | 1 - .../ruby_extraction/chunk_extractor.rb | 232 +++++-- lib/haml_lint/ruby_extraction/script_chunk.rb | 150 ++++- .../ruby_extraction/tag_attributes_chunk.rb | 28 +- lib/haml_lint/ruby_extractor.rb | 224 ------- lib/haml_lint/utils.rb | 5 + .../rubocop_autocorrect_edge_cases_spec.rb | 6 +- .../changing_indentation_examples.txt | 120 ++-- .../interpolation_examples.txt | 24 +- .../multibyte_chars_examples.txt | 60 +- .../multiline_examples.txt | 158 ++++- .../script_examples.txt | 58 +- .../tag_attributes_examples.txt | 360 ++++++---- .../tag_complex_examples.txt | 216 +++--- .../tag_script_examples.txt | 72 +- spec/haml_lint/linter/rubocop_spec.rb | 2 +- .../ruby_extraction/chunk_extractor_spec.rb | 216 ++++++ .../ruby_extraction/script_chunk_spec.rb | 169 +++++ spec/haml_lint/ruby_extractor_spec.rb | 621 ------------------ 20 files changed, 1471 insertions(+), 1256 deletions(-) delete mode 100644 lib/haml_lint/ruby_extractor.rb create mode 100644 spec/haml_lint/ruby_extraction/chunk_extractor_spec.rb create mode 100644 spec/haml_lint/ruby_extraction/script_chunk_spec.rb delete mode 100644 spec/haml_lint/ruby_extractor_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index abfdc0ff..6a81b888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # HAML-Lint Changelog +# master (unreleased) + * Fix `Marshal.dump` error when using `--parallel` option and `RepeatedId` Linter +* Improved support for multiline code with RuboCop linting/auto-correction # 0.47.0 -* Bugfixes related to experimental auto-correct +* Bugfixes related to experimental auto-correct with RuboCop * Fix `Marshal.dump` errors when using `--parallel` option ## 0.46.0 diff --git a/lib/haml_lint/linter/rubocop.rb b/lib/haml_lint/linter/rubocop.rb index c764416c..9dc2594e 100644 --- a/lib/haml_lint/linter/rubocop.rb +++ b/lib/haml_lint/linter/rubocop.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'haml_lint/ruby_extractor' require 'rubocop' require 'tempfile' diff --git a/lib/haml_lint/ruby_extraction/chunk_extractor.rb b/lib/haml_lint/ruby_extraction/chunk_extractor.rb index 3d684526..911f87a8 100644 --- a/lib/haml_lint/ruby_extraction/chunk_extractor.rb +++ b/lib/haml_lint/ruby_extraction/chunk_extractor.rb @@ -11,6 +11,12 @@ class ChunkExtractor attr_reader :script_output_prefix + HAML_PARSER_INSTANCE = if Haml::VERSION >= '5.0.0' + ::Haml::Parser.new({}) + else + ::Haml::Parser.new('', {}) + end + def initialize(document, script_output_prefix:) @document = document @script_output_prefix = script_output_prefix @@ -19,13 +25,18 @@ def initialize(document, script_output_prefix:) def extract raise 'Already extracted' if @ruby_chunks - @ruby_chunks = [] - @original_haml_lines = @document.source_lines + prepare_extract visit(@document.tree) @ruby_chunks end + # Useful for tests + def prepare_extract + @ruby_chunks = [] + @original_haml_lines = @document.source_lines + end + def visit_root(_node) yield # Collect lines of code from children end @@ -71,33 +82,37 @@ def visit_haml_comment(node) # Visiting comments which are output to HTML. Lines looking like # ` / This will be in the HTML source!` def visit_comment(node) - lines = raw_lines_of_interest(node.line - 1) - indent = lines.first.index(/\S/) + line = @original_haml_lines[node.line - 1] + indent = line.index(/\S/) @ruby_chunks << PlaceholderMarkerChunk.new(node, 'comment', indent: indent) end # Visit a script which outputs. Lines looking like ` = foo` def visit_script(node, &block) - lines = raw_lines_of_interest(node.line - 1) + raw_first_line = @original_haml_lines[node.line - 1] - if lines.first !~ /\A\s*[-=]/ + if raw_first_line !~ /\A\s*[-=]/ # The line doesn't start with a - or a =, this is actually a "plain" # that contains interpolation. - indent = lines.first.index(/\S/) + indent = raw_first_line.index(/\S/) @ruby_chunks << PlaceholderMarkerChunk.new(node, 'interpolation', indent: indent) - add_interpolation_chunks(node, lines.first, node.line - 1, indent: indent) + add_interpolation_chunks(node, raw_first_line, node.line - 1, indent: indent) return end + _first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1) + # We want the actual indentation and prefix for the first line + first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip + process_multiline!(first_line) + lines[0] = lines[0].sub(/(=[ \t]?)/, '') line_indentation = Regexp.last_match(1).size raw_code = lines.join("\n") if lines[0][/\S/] == '#' - # a script that only constains a comment... needs special handling - comment_index = lines[0].index(/\S/) - lines[0].insert(comment_index + 1, " #{script_output_prefix.rstrip}") + # a "=" script that only contains a comment... No need for the "HL.out = " prefix, + # just treat it as comment which will turn into a "-" comment else lines[0] = HamlLint::Utils.insert_after_indentation(lines[0], script_output_prefix) end @@ -131,7 +146,11 @@ def visit_script(node, &block) # Visit a script which doesn't output. Lines looking like ` - foo` def visit_silent_script(node, &block) - lines = raw_lines_of_interest(node.line - 1) + _first_line_offset, lines = extract_raw_ruby_lines(node.script, node.line - 1) + # We want the actual indentation and prefix for the first line + first_line = lines[0] = @original_haml_lines[node.line - 1].rstrip + process_multiline!(first_line) + lines[0] = lines[0].sub(/(-[ \t]?)/, '') nb_to_deindent = Regexp.last_match(1).size @@ -184,26 +203,32 @@ def finish_visit_any_script(node, lines, raw_code: nil, must_start_chunk: false) def visit_tag(node) indent = @original_haml_lines[node.line - 1].index(/\S/) - has_children = !node.children.empty? - if has_children - # We don't want to use a block because assignments in a block are local to that block, - # so the semantics of the extracted ruby would be different from the one generated by - # Haml. Those differences can make some cops, such as UselessAssignment, have false - # positives - code = 'begin' - @ruby_chunks << AdHocChunk.new(node, - [' ' * indent + code]) - indent += 2 - end + # We don't want to use a block because assignments in a block are local to that block, + # so the semantics of the extracted ruby would be different from the one generated by + # Haml. Those differences can make some cops, such as UselessAssignment, have false + # positives + code = 'begin' + @ruby_chunks << AdHocChunk.new(node, + [' ' * indent + code]) + indent += 2 - @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent) + tag_chunk = PlaceholderMarkerChunk.new(node, 'tag', indent: indent) + @ruby_chunks << tag_chunk current_line_index = visit_tag_attributes(node, indent: indent) visit_tag_script(node, line_index: current_line_index, indent: indent) - if has_children - yield - indent -= 2 + yield + + indent -= 2 + + if @ruby_chunks.last.equal?(tag_chunk) + # So there is nothing going "in" the tag, remove the wrapping "begin" and replace the PlaceholderMarkerChunk + # by one less indented + @ruby_chunks.pop + @ruby_chunks.pop + @ruby_chunks << PlaceholderMarkerChunk.new(node, 'tag', indent: indent) + else @ruby_chunks << AdHocChunk.new(node, [' ' * indent + 'ensure', ' ' * indent + ' HL.noop', ' ' * indent + 'end'], haml_line_index: @ruby_chunks.last.haml_end_line_index) @@ -222,11 +247,16 @@ def visit_tag_attributes(node, indent:) attributes_code = additional_attributes.first if !attributes_code && node.hash_attributes? && node.dynamic_attributes_sources.empty? # No idea why .foo{:bar => 123} doesn't get here, but .foo{:bar => '123'} does... - # The code we get for the later is {:bar => '123'}. + # The code we get for the latter is {:bar => '123'}. # We normalize it by removing the { } so that it matches wha we normally get attributes_code = node.dynamic_attributes_source[:hash][1...-1] end + if attributes_code&.start_with?('{') + # Looks like the .foo(bar = 123) case. Ignoring. + attributes_code = nil + end + return final_line_index unless attributes_code # Attributes have different ways to be given to us: # .foo{bar: 123} => "bar: 123" @@ -235,14 +265,13 @@ def visit_tag_attributes(node, indent:) # .foo(bar = 123) => '{"bar" => 123,}' # .foo{html_attrs('fr-fr')} => html_attrs('fr-fr') # - # The (bar = 123) case is extra painful to autocorrect (so is ignored). + # The (bar = 123) case is extra painful to autocorrect (so is ignored up there). # #raw_ruby_from_haml will "detect" this case by not finding the code. # # We wrap the result in a method to have a valid syntax for all 3 ways # without having to differentiate them. - first_line_offset, raw_attributes_lines = raw_ruby_lines_from_haml(attributes_code, - node.line - 1) - + first_line_offset, raw_attributes_lines = extract_raw_tag_attributes_ruby_lines(attributes_code, + node.line - 1) return final_line_index unless raw_attributes_lines final_line_index += raw_attributes_lines.size - 1 @@ -276,7 +305,7 @@ def visit_tag_script(node, line_index:, indent:) # We ignore scripts which are just a comment return if node.script[/\S/] == '#' - first_line_offset, script_lines = raw_ruby_lines_from_haml(node.script, line_index) + first_line_offset, script_lines = extract_raw_ruby_lines(node.script, line_index) if script_lines.nil? # This is a string with interpolation after a tag @@ -380,52 +409,87 @@ def add_interpolation_chunks(node, code, haml_line_index, indent:, line_start_in end end + def process_multiline!(line) + if HAML_PARSER_INSTANCE.send(:is_multiline?, line) + line.chop!.rstrip! + true + else + false + end + end + # Returns the raw lines from the haml for the given index. # Multiple lines are returned when a line ends with a comma as that is the only # time HAMLs allows Ruby lines to be split. - def raw_lines_of_interest(first_line_index) - line_index = first_line_index - lines_of_interest = [@original_haml_lines[line_index]] - - while @original_haml_lines[line_index].rstrip.end_with?(',') - line_index += 1 - lines_of_interest << @original_haml_lines[line_index] - end - - lines_of_interest - end # Haml's line-splitting rules (allowed after comma in scripts and attributes) are handled # at the parser level, so Haml doesn't provide the code as it is actually formatted in the Haml # file. #raw_ruby_from_haml extracts the ruby code as it is exactly in the Haml file. # The first and last lines may not be the complete lines from the Haml, only the Ruby parts # and the indentation between the first and last list. - def raw_ruby_lines_from_haml(code, first_line_index) - stripped_code = code.strip - return if stripped_code.empty? - lines_of_interest = raw_lines_of_interest(first_line_index) + # HAML transforms the ruby code in many ways as it parses a document. Often removing lines and/or + # indentation. This is quite annoying for us since we want the exact layout of the code to analyze it. + # + # This function receives the code as haml provides it and the line where it starts. It returns + # the actual code as it is in the haml file, keeping breaks and indentation for the following lines. + # In addition, the start position of the code in the first line. + # + # The rules for handling multiline code in HAML are as follow: + # * if the line being processed ends with a space and a pipe, then append to the line (without + # newlines) every following lines that also end with a space and a pipe. This means the last line of + # the "block" also needs a pipe at the end. + # * after processing the pipes, when dealing with ruby code (and not in tag attributes' hash), if the line + # (which maybe span across multiple lines) ends with a comma, add the next line to the current piece of code. + # + # @return [first_line_offset, ruby_lines] + def extract_raw_ruby_lines(haml_processed_ruby_code, first_line_index) + haml_processed_ruby_code = haml_processed_ruby_code.strip + first_line = @original_haml_lines[first_line_index] - if lines_of_interest.size == 1 - index = lines_of_interest.first.index(stripped_code) - if lines_of_interest.first.include?(stripped_code) - return [index, [stripped_code]] - else - # Sometimes, the code just isn't in the Haml when Haml does transformations to it - return - end + char_index = first_line.index(haml_processed_ruby_code) + + if char_index + return [char_index, [haml_processed_ruby_code]] + end + + cur_line_index = first_line_index + cur_line = first_line.rstrip + lines = [] + + # The pipes must also be on the last line of the multi-line section + while cur_line && process_multiline!(cur_line) + lines << cur_line + cur_line_index += 1 + cur_line = @original_haml_lines[cur_line_index].rstrip + end + + if lines.empty? + lines << cur_line + else + # The pipes must also be on the last line of the multi-line section. So cur_line is not the next line. + # We want to go back to check for commas + cur_line_index -= 1 + cur_line = lines.last + end + + while HAML_PARSER_INSTANCE.send(:is_ruby_multiline?, cur_line) + cur_line_index += 1 + cur_line = @original_haml_lines[cur_line_index].rstrip + lines << cur_line end - raw_haml = lines_of_interest.join("\n") + joined_lines = lines.join("\n") + + if haml_processed_ruby_code.include?("\n") + haml_processed_ruby_code = haml_processed_ruby_code.gsub("\n", ' ') + end - # Need the gsub because while multiline scripts are turned into a single line, - # by haml, multiline tag attributes are not. - code_parts = stripped_code.gsub("\n", ' ').split(/,\s*/) + haml_processed_ruby_code.split(/[, ]/) - regexp_code = code_parts.map { |c| Regexp.quote(c) }.join(',\\s*') - regexp = Regexp.new(regexp_code) + regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/,\s*|\s+/), '(?:,\\s*|\\s+)') - match = raw_haml.match(regexp) + match = joined_lines.match(regexp) # This can happen when pipes are used as marker for multiline parts, and when tag attributes change lines # without ending by a comma. This is quite a can of worm and is probably not too frequent, so for now, # these cases are not supported. @@ -438,6 +502,48 @@ def raw_ruby_lines_from_haml(code, first_line_index) [first_line_offset, ruby_lines] end + # Tag attributes actually handle multiline differently than scripts. + # The basic system basically keeps considering more lines until it meets the closing braces, but still + # processes pipes too (same as extract_raw_ruby_lines). + def extract_raw_tag_attributes_ruby_lines(haml_processed_ruby_code, first_line_index) + haml_processed_ruby_code = haml_processed_ruby_code.strip + first_line = @original_haml_lines[first_line_index] + + char_index = first_line.index(haml_processed_ruby_code) + + if char_index + return [char_index, [haml_processed_ruby_code]] + end + + min_non_white_chars_to_add = haml_processed_ruby_code.scan(/\S/).size + + regexp = HamlLint::Utils.regexp_for_parts(haml_processed_ruby_code.split(/\s+/), '\\s+') + + joined_lines = first_line.rstrip + process_multiline!(joined_lines) + + cur_line_index = first_line_index + 1 + while @original_haml_lines[cur_line_index] && min_non_white_chars_to_add > 0 + new_line = @original_haml_lines[cur_line_index].rstrip + process_multiline!(new_line) + + min_non_white_chars_to_add -= new_line.scan(/\S/).size + joined_lines << "\n" + joined_lines << new_line + cur_line_index += 1 + end + + match = joined_lines.match(regexp) + + return if match.nil? + + first_line_offset = match.begin(0) + raw_ruby = match[0] + ruby_lines = raw_ruby.split("\n") + + [first_line_offset, ruby_lines] + end + def wrap_lines(lines, wrap_depth) lines = lines.dup wrapping_prefix = 'W' * (wrap_depth - 1) + '(' diff --git a/lib/haml_lint/ruby_extraction/script_chunk.rb b/lib/haml_lint/ruby_extraction/script_chunk.rb index 328647a5..ec40ad08 100644 --- a/lib/haml_lint/ruby_extraction/script_chunk.rb +++ b/lib/haml_lint/ruby_extraction/script_chunk.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'ripper' + module HamlLint::RubyExtraction # Chunk for handling outputting and silent scripts, so ` = foo` and ` - bar` # Does NOT handle a script beside a tag (ex: `%div= spam`) @@ -79,54 +81,152 @@ def start_marker_indent [default_indent, previous_chunk&.end_marker_indent || previous_chunk&.start_marker_indent].compact.max end - def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics + def transfer_correction_logic(coordinator, to_ruby_lines, haml_lines) + to_haml_lines = self.class.format_ruby_lines_to_haml_lines(to_ruby_lines, + script_output_prefix: coordinator.script_output_prefix) + + haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines + end + + ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH = %w[else begin ensure].freeze + + def self.format_ruby_lines_to_haml_lines(to_ruby_lines, script_output_prefix:) # rubocop:disable Metrics to_ruby_lines.reject! { |l| l.strip == 'end' } + return [] if to_ruby_lines.empty? - output_comment_prefix = ' ' + coordinator.script_output_prefix.rstrip - to_ruby_lines.map! do |line| - if line.lstrip.start_with?('#' + output_comment_prefix) - line = line.dup - comment_index = line.index('#') - removal_start_index = comment_index + 1 - removal_end_index = removal_start_index + output_comment_prefix.size - line[removal_start_index...removal_end_index] = '' - # It will be removed again below, but will know its suposed to be a = - line.insert(comment_index, coordinator.script_output_prefix) - end - line - end + statement_start_line_indexes = find_statement_start_line_indexes(to_ruby_lines) continued_line_indent_delta = 2 + cur_line_start_index = nil + line_start_indexes_that_need_pipes = [] + to_haml_lines = to_ruby_lines.map.with_index do |line, i| if line !~ /\S/ # whitespace or empty lines, we don't want any indentation '' - elsif line_starts_script?(to_ruby_lines, i) + elsif statement_start_line_indexes.include?(i) + cur_line_start_index = i code_start = line.index(/\S/) - if line[code_start..].start_with?(coordinator.script_output_prefix) - line = line.sub(coordinator.script_output_prefix, '') - continued_line_indent_delta = 2 - coordinator.script_output_prefix.size + if line[code_start..].start_with?(script_output_prefix) + line = line.sub(script_output_prefix, '') + # The line may have been too indented because of the "HL.out = " prefix + continued_line_indent_delta = 2 - script_output_prefix.size "#{line[0...code_start]}= #{line[code_start..]}" else continued_line_indent_delta = 2 "#{line[0...code_start]}- #{line[code_start..]}" end else + unless to_ruby_lines[i - 1].end_with?(',') + line_start_indexes_that_need_pipes << cur_line_start_index + end + HamlLint::Utils.indent(line, continued_line_indent_delta) end end - haml_lines[@haml_line_index..haml_end_line_index] = to_haml_lines - end + # Starting from the end because we need to add newlines when 2 groups of lines need pipes, so that they are + # separate. + line_start_indexes_that_need_pipes.reverse_each do |cur_line_i| + loop do + cur_line = to_haml_lines[cur_line_i] + break if cur_line.nil? || cur_line.empty? + to_haml_lines[cur_line_i] = cur_line + ' |' + cur_line_i += 1 + + break if statement_start_line_indexes.include?(cur_line_i) + end + + next_line = to_haml_lines[cur_line_i] + if next_line && HamlLint::RubyExtraction::ChunkExtractor::HAML_PARSER_INSTANCE.send(:is_multiline?, next_line) + to_haml_lines.insert(cur_line_i, '') + end + end - def unfinished_script_line?(lines, line_index) - !!lines[line_index][/,[ \t]*\z/] + to_haml_lines end - def line_starts_script?(lines, line_index) - return true if line_index == 0 - !unfinished_script_line?(lines, line_index - 1) + def self.find_statement_start_line_indexes(to_ruby_lines) # rubocop:disable Metrics + if to_ruby_lines.size == 1 + if to_ruby_lines.first[/\S/] + return [0] + else + return [] + end + end + statement_start_line_indexes = [] # 0-indexed + allow_expression_after_line_number = 0 # 1-indexed + last_do_keyword_line_number = nil # 1-indexed, like Ripper.lex + + to_ruby_string = to_ruby_lines.join("\n") + if RUBY_VERSION < '3.1' + # Ruby 2.6's Ripper has issues when it encounters a else, when, elsif without a matching if/case before. + # It literally stop lexing at that point without any error. + # Ex from 2.7.8: + # require 'ripper' + # Ripper.lex("a\nelse\nb") + # #=> [[[1, 0], :on_ident, "a", CMDARG], [[1, 1], :on_nl, "\n", BEG], [[2, 0], :on_kw, "else", BEG]] + # So we add enough ifs to last quite a few layer. Hopefully enough for all needs. To clarify, there would need + # as many "end" keyword in a single ScriptChunk followed by one of the problematic keyword for the problem + # to show up. + # Considering that a `end` without anything else on the line is removed from to_ruby_lines before getting here + # (in format_ruby_lines_to_haml_lines), 10 ifs should be plenty. + to_ruby_string = ('if a;' * 10) + to_ruby_string + end + + last_line_number_seen = nil + Ripper.lex(to_ruby_string).each do |start_loc, token, str| + last_line_number_seen = start_loc[0] + if token == :on_nl + # :on_nl happens when we have a meaningful line change. + allow_expression_after_line_number = start_loc[0] + next + elsif token == :on_ignored_nl + # :on_ignored_nl happens for newlines within an expression, or consecutive newlines.. + # and some cases we care about such as a newline after the pipes after arguments of a block + if last_do_keyword_line_number == start_loc[0] + # When starting a block, Ripper.lex gives :on_ignored_nl + allow_expression_after_line_number = start_loc[0] + end + next + end + + if allow_expression_after_line_number && str[/\S/] + if allow_expression_after_line_number < start_loc[0] + # Ripper.lex returns line numbers 1-indexed, we want 0-indexed + statement_start_line_indexes << start_loc[0] - 1 + end + allow_expression_after_line_number = nil + end + + if token == :on_comment + # :on_comment contain its own newline at the end of the content + allow_expression_after_line_number = start_loc[0] + elsif token == :on_kw + if str == 'do' + # Because of the possible arguments for the block, we can't simply set is_between_expressions to true + last_do_keyword_line_number = start_loc[0] + elsif ALLOW_EXPRESSION_AFTER_LINE_ENDING_WITH.include?(str) + allow_expression_after_line_number = start_loc[0] + end + end + end + + # number is 1-indexed, and we want the line after it, so that's great + if last_line_number_seen < to_ruby_lines.size && to_ruby_lines[last_line_number_seen..].any? { |l| l[/\S/] } + # There are non-empty lines after the last line Ripper showed us, that's a problem! + msg = +'It seems Ripper did not properly process some source code. Please make sure you are on the ' + msg << 'latest Haml-Lint version, then create an issue at ' + msg << "https://github.com/sds/haml-lint/issues and include the following information:\n" + msg << "Ruby version: #{RUBY_VERSION}\n" + msg << "Haml-Lint version: #{HamlLint::VERSION}\n" + msg << "HAML version: #{Haml::VERSION}\n" + msg << "problematic source code:\n```\n#{to_ruby_lines.join("\n")}\n```" + raise msg + end + + statement_start_line_indexes end end end diff --git a/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb b/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb index d0b2706b..7aef9d7a 100644 --- a/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +++ b/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb @@ -8,19 +8,43 @@ def initialize(*args, indent_to_remove:, **kwargs) @indent_to_remove = indent_to_remove end - def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines) + def transfer_correction_logic(_coordinator, to_ruby_lines, haml_lines) # rubocop:disable Metrics + return if @ruby_lines == to_ruby_lines + affected_haml_lines = haml_lines[@haml_line_index..haml_end_line_index] affected_haml = affected_haml_lines.join("\n") from_ruby = unwrap(@ruby_lines).join("\n") + + if to_ruby_lines.size > 1 + min_indent = to_ruby_lines.first[/^\s*/] + to_ruby_lines.each.with_index do |line, i| + next if i == 0 + next if line.start_with?(min_indent) + to_ruby_lines[i] = "#{min_indent}#{line.lstrip}" + end + end + to_ruby = unwrap(to_ruby_lines).join("\n") affected_start_index = affected_haml.index(from_ruby) - affected_end_index = affected_start_index + from_ruby.size + if affected_start_index + affected_end_index = affected_start_index + from_ruby.size + else + regexp = HamlLint::Utils.regexp_for_parts(from_ruby.split("\n"), "(?:\s*\\|?\n)") + mo = affected_haml.match(regexp) + affected_start_index = mo.begin(0) + affected_end_index = mo.end(0) + end + affected_haml[affected_start_index...affected_end_index] = to_ruby haml_lines[@haml_line_index..haml_end_line_index] = affected_haml.split("\n") + + if haml_lines[haml_end_line_index].end_with?(' |') + haml_lines[haml_end_line_index].chop!.rstrip! + end end def unwrap(lines) diff --git a/lib/haml_lint/ruby_extractor.rb b/lib/haml_lint/ruby_extractor.rb deleted file mode 100644 index 87b3d63e..00000000 --- a/lib/haml_lint/ruby_extractor.rb +++ /dev/null @@ -1,224 +0,0 @@ -# frozen_string_literal: true - -module HamlLint - # Utility class for extracting Ruby script from a HAML file that can then be - # linted with a Ruby linter (i.e. is "legal" Ruby). The goal is to turn this: - # - # - if signed_in?(viewer) - # %span Stuff - # = link_to 'Sign Out', sign_out_path - # - else - # .some-class{ class: my_method }= my_method - # = link_to 'Sign In', sign_in_path - # - # into this: - # - # if signed_in?(viewer) - # link_to 'Sign Out', sign_out_path - # else - # { class: my_method } - # my_method - # link_to 'Sign In', sign_in_path - # end - # - # The translation won't be perfect, and won't make any real sense, but the - # relationship between variable declarations/uses and the flow control graph - # will remain intact. - class RubyExtractor - include HamlVisitor - - # Stores the extracted source and a map of lines of generated source to the - # original source that created them. - # - # @attr_reader source [String] generated source code - # @attr_reader source_map [Hash] map of line numbers from generated source - # to original source line number - RubySource = Struct.new(:source, :source_map) - - # Extracts Ruby code from Sexp representing a Slim document. - # - # @param document [HamlLint::Document] - # @return [HamlLint::RubyExtractor::RubySource] - def extract(document) - visit(document.tree) - RubySource.new(@source_lines.join("\n"), @source_map) - end - - def visit_root(_node) - @source_lines = [] - @source_map = {} - @line_count = 0 - @indent_level = 0 - @output_count = 0 - - yield # Collect lines of code from children - end - - def visit_plain(node) - # Don't output the text, as we don't want to have to deal with any RuboCop - # cops regarding StringQuotes or AsciiComments, and it's not important to - # overall document anyway. - add_dummy_puts(node) - end - - def visit_tag(node) - additional_attributes = node.dynamic_attributes_sources - - # Include dummy references to code executed in attributes list - # (this forces a "use" of a variable to prevent "assigned but unused - # variable" lints) - additional_attributes.each do |attributes_code| - # Normalize by removing excess whitespace to avoid format lints - attributes_code = attributes_code.gsub(/\s*\n\s*/, "\n").strip - - # Attributes can either be a method call or a literal hash, so wrap it - # in a method call itself in order to avoid having to differentiate the - # two. Use the tag name for the method to differentiate different tag types - # for RuboCop and prevent erroneous warnings. - add_line("#{node.tag_name}(#{attributes_code})", node) - end - - check_tag_static_hash_source(node) - - # We add a dummy puts statement to represent the tag name being output. - # This prevents some erroneous RuboCop warnings. - add_dummy_puts(node, node.tag_name) - - code = node.script.strip - add_line(code, node) unless code.empty? - end - - def after_visit_tag(node) - # We add a dummy puts statement for closing tag. - add_dummy_puts(node, "#{node.tag_name}/") - end - - def visit_script(node) - code = node.text - - add_line(code.strip, node) - - start_block = anonymous_block?(code) || start_block_keyword?(code) - - if start_block - @indent_level += 1 - end - - yield # Continue extracting code from children - - if start_block - @indent_level -= 1 - add_line('end', node) - end - end - - def visit_haml_comment(node) - # We want to preseve leading whitespace if it exists, but include leading - # whitespace if it doesn't exist so that RuboCop's LeadingCommentSpace - # doesn't complain - comment = node.text - .gsub(/\n(\S)/, "\n# \\1") - .gsub(/\n(\s)/, "\n#\\1") - add_line("##{comment}", node) - end - - def visit_silent_script(node, &block) - visit_script(node, &block) - end - - def visit_filter(node) - if node.filter_type == 'ruby' - node.text.split("\n").each_with_index do |line, index| - add_line(line, node.line + index + 1, discard_blanks: false) - end - else - add_dummy_puts(node, ":#{node.filter_type}") - HamlLint::Utils.extract_interpolated_values(node.text) do |interpolated_code, line| - add_line(interpolated_code, node.line + line) - end - end - end - - private - - def check_tag_static_hash_source(node) - # Haml::Parser converts hashrocket-style hash attributes of strings and symbols - # to static attributes, and excludes them from the dynamic attribute sources: - # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L400-L404 - # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L540-L554 - # Here, we add the hash source back in so it can be inspected by rubocop. - if node.hash_attributes? && node.dynamic_attributes_sources.empty? - normalized_attr_source = node.dynamic_attributes_source[:hash].gsub(/\s*\n\s*/, ' ') - - add_line(normalized_attr_source, node) - end - end - - # Adds a dummy method call with a unique name so we don't get - # Style/IdenticalConditionalBranches RuboCop warnings - def add_dummy_puts(node, annotation = nil) - annotation = " # #{annotation}" if annotation - add_line("_haml_lint_puts_#{@output_count}#{annotation}", node) - @output_count += 1 - end - - def add_line(code, node_or_line, discard_blanks: true) - return if code.empty? && discard_blanks - - indent_level = @indent_level - - if node_or_line.respond_to?(:line) && mid_block_keyword?(code) - # Since mid-block keywords are children of the corresponding start block - # keyword, we need to reduce their indentation level by 1. However, we - # don't do this unless this is an actual tag node (a raw line number - # means this came from a `:ruby` filter). - indent_level -= 1 - end - - indent = (' ' * 2 * indent_level) - - @source_lines << indent_code(code, indent) - - original_line = - node_or_line.respond_to?(:line) ? node_or_line.line : node_or_line - - # For interpolated code in filters that spans multiple lines, the - # resulting code will span multiple lines, so we need to create a - # mapping for each line. - (code.count("\n") + 1).times do - @line_count += 1 - @source_map[@line_count] = original_line - end - end - - def indent_code(code, indent) - codes = code.split("\n") - codes.map { |c| indent + c }.join("\n") - end - - def anonymous_block?(text) - text =~ /\bdo\s*(\|\s*[^|]*\s*\|)?(\s*#.*)?\z/ - end - - START_BLOCK_KEYWORDS = %w[if unless case begin for until while].freeze - def start_block_keyword?(text) - START_BLOCK_KEYWORDS.include?(block_keyword(text)) - end - - MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure].freeze - def mid_block_keyword?(text) - MID_BLOCK_KEYWORDS.include?(block_keyword(text)) - end - - LOOP_KEYWORDS = %w[for until while].freeze - def block_keyword(text) - # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't - if (keyword = text[/\A\s*([^\s]+)\s+/, 1]) && LOOP_KEYWORDS.include?(keyword) - return keyword - end - - return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0] - keyword[0] || keyword[1] - end - end -end diff --git a/lib/haml_lint/utils.rb b/lib/haml_lint/utils.rb index d44ee758..0f24971c 100644 --- a/lib/haml_lint/utils.rb +++ b/lib/haml_lint/utils.rb @@ -276,5 +276,10 @@ def with_captured_streams(stdin_str, &_block) ensure $stdin = original_stdin end + + def regexp_for_parts(parts, join_regexp) + regexp_code = parts.map { |c| Regexp.quote(c) }.join(join_regexp) + Regexp.new(regexp_code) + end end end diff --git a/spec/haml_lint/linter/rubocop_autocorrect_edge_cases_spec.rb b/spec/haml_lint/linter/rubocop_autocorrect_edge_cases_spec.rb index ae6af2d3..4794d5c5 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_edge_cases_spec.rb +++ b/spec/haml_lint/linter/rubocop_autocorrect_edge_cases_spec.rb @@ -51,7 +51,7 @@ abc --- haml_lint_marker_1 - if a#{' '} + if a haml_lint_marker_3 haml_lint_plain_4 $$2 end @@ -71,7 +71,7 @@ abc --- haml_lint_marker_1 - [].each do#{' '} + [].each do haml_lint_marker_3 haml_lint_plain_4 $$2 end @@ -92,7 +92,7 @@ = foo --- haml_lint_marker_1 - [].each do |foo|#{' '} + [].each do |foo| HL.out = foo $$2 end haml_lint_marker_5 diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/changing_indentation_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/changing_indentation_examples.txt index fe954fe7..920be02f 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/changing_indentation_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/changing_indentation_examples.txt @@ -259,20 +259,28 @@ haml_lint_marker_1 if a if b $$2 haml_lint_marker_4 - haml_lint_tag_5 $$3 - haml_lint_marker_6 - WWWW(bar: 123, abc: '42') - haml_lint_marker_8 + begin $$3 + haml_lint_tag_6 + haml_lint_marker_7 + WW(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end end --- haml_lint_marker_1 if a && b haml_lint_marker_4 - haml_lint_tag_5 - haml_lint_marker_6 - WWWW(bar: 123, abc: '42') - haml_lint_marker_8 + begin + haml_lint_tag_6 + haml_lint_marker_7 + WW(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end --- - if a && b @@ -288,20 +296,28 @@ haml_lint_marker_1 if a if b $$2 haml_lint_marker_4 - haml_lint_tag_5 $$3 - haml_lint_marker_6 - WWWW(:bar => 123, abc: '42') - haml_lint_marker_8 + begin $$3 + haml_lint_tag_6 + haml_lint_marker_7 + WW(:bar => 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end end --- haml_lint_marker_1 if a && b haml_lint_marker_4 - haml_lint_tag_5 - haml_lint_marker_6 - WWWW(bar: 123, abc: '42') - haml_lint_marker_8 + begin + haml_lint_tag_6 + haml_lint_marker_7 + WW(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end --- - if a && b @@ -317,20 +333,28 @@ haml_lint_marker_1 if a if b $$2 haml_lint_marker_4 - haml_lint_tag_5 $$3 - haml_lint_marker_6 - WWWW(bar: 123, abc: '42') - haml_lint_marker_8 + begin $$3 + haml_lint_tag_6 + haml_lint_marker_7 + WW(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end end --- haml_lint_marker_1 if a && b haml_lint_marker_4 - haml_lint_tag_5 - haml_lint_marker_6 - WWWW(bar: 123, abc: '42') - haml_lint_marker_8 + begin + haml_lint_tag_6 + haml_lint_marker_7 + WW(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end --- - if a && b @@ -369,20 +393,28 @@ haml_lint_marker_1 if a if b $$2 haml_lint_marker_4 - haml_lint_tag_5 $$3 - haml_lint_marker_6 - HL.out = foo(:bar => 123, abc: '42') - haml_lint_marker_8 + begin $$3 + haml_lint_tag_6 + haml_lint_marker_7 + HL.out = foo(:bar => 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end end --- haml_lint_marker_1 if a && b haml_lint_marker_4 - haml_lint_tag_5 - haml_lint_marker_6 - HL.out = foo(bar: 123, abc: '42') - haml_lint_marker_8 + begin + haml_lint_tag_6 + haml_lint_marker_7 + HL.out = foo(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end --- - if a && b @@ -398,20 +430,28 @@ haml_lint_marker_1 if a if b $$2 haml_lint_marker_4 - haml_lint_tag_5 $$3 - haml_lint_marker_6 - HL.out = foo(bar: 123, abc: '42') - haml_lint_marker_8 + begin $$3 + haml_lint_tag_6 + haml_lint_marker_7 + HL.out = foo(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end end --- haml_lint_marker_1 if a && b haml_lint_marker_4 - haml_lint_tag_5 - haml_lint_marker_6 - HL.out = foo(bar: 123, abc: '42') - haml_lint_marker_8 + begin + haml_lint_tag_6 + haml_lint_marker_7 + HL.out = foo(bar: 123, abc: '42') + haml_lint_marker_9 + ensure + HL.noop + end end --- - if a && b diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/interpolation_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/interpolation_examples.txt index f69eeb2a..6faf4d36 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/interpolation_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/interpolation_examples.txt @@ -797,14 +797,22 @@ haml_lint_marker_5 !!! Fixes interpolation in a tag's text %tag #{foo(:bar => 123)} --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(:bar => 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(:bar => 123) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(bar: 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(bar: 123) + haml_lint_marker_5 +ensure + HL.noop +end --- %tag #{foo(bar: 123)} diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/multibyte_chars_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/multibyte_chars_examples.txt index 3a4b5897..f68e1d07 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/multibyte_chars_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/multibyte_chars_examples.txt @@ -44,15 +44,23 @@ haml_lint_marker_4 !# Those plain pass by a different place %tag ©#{foo(:bar => 123)} --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(:bar => 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(:bar => 123) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(bar: 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(bar: 123) + haml_lint_marker_5 +ensure + HL.noop +end --- %tag ©#{foo(bar: 123)} @@ -60,20 +68,28 @@ haml_lint_marker_4 !!! Multibyte char in tag attributes with tag script doesn't mess things up %tag{:abc => '©'}= foo(:bar => 123) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:abc => '©') -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(:bar => 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:abc => '©') + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(:bar => 123) + haml_lint_marker_8 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(abc: '©') -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(bar: 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(abc: '©') + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(bar: 123) + haml_lint_marker_8 +ensure + HL.noop +end --- %tag{abc: '©'}= foo(bar: 123) diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/multiline_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/multiline_examples.txt index 156bff56..c1f48f36 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/multiline_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/multiline_examples.txt @@ -9,18 +9,72 @@ SKIP %span{ foo: 'bar', spam: 42 } -!!! Multiline tag attributes with comma and pipe on same line is ignored (pipe not supported) +!!! Multiline tag attributes using pipes only {% haml_version >= '5.2' %} +!# Rubocop doesn't seem to do much here.. odd +%span{ foo: | + "bar", spam: 42 } | +--- +SKIP +--- +SKIP +--- +%span{ foo: + 'bar', spam: 42 } + +!!! Multiline tag attributes using pipes only {% haml_version < '5.2' %} +!# Rubocop doesn't seem to do much here.. odd +%span{ foo: | + "bar", spam: 42 } | +--- +SKIP +--- +SKIP +--- +%span{ foo: | + "bar", spam: 42 } | + +!!! Multiline tag attributes with comma and pipe on same line removes pipes if there is a change %span{ foo: 'bar', | - spam: 42 } + spam: 42 } | --- SKIP --- SKIP --- +%span{ foo: 'bar', + spam: 42 } + +!!! Multiline tag attributes with comma and pipe on same line leaves pipes if there is no change %span{ foo: 'bar', | - spam: 42 } + spam: 42 } | +--- +SKIP +--- +SKIP +--- +%span{ foo: 'bar', | + spam: 42 } | + +!!! Multiline tag attributes with comma and pipe on same line {% haml_version >= '5.2' %} +!# Tag attributes has different rules and doesn't actually need pipes +!# But for haml-lint, we put the pipes in the generated code for simplicity +!# except when everything can be handled using commas +%span{ foo: + "bar", + spam: + 42 } +--- +SKIP +--- +SKIP +--- +%span{ foo: + 'bar', + spam: + 42 } -!!! Multiline tag attributes with comma then comma and pipe on same line is ignored (pipe not supported) +!!! Multiline tag attributes with commas and a random useless pipe +!# the pipe does basically nothing here... %span{ foo: 'bar', hello: 123, | spam: 42 } @@ -30,10 +84,10 @@ SKIP SKIP --- %span{ foo: 'bar', - hello: 123, | - spam: 42 } + hello: 123, + spam: 42 } -!!! Multiline tag attributes with comma then pipe within string is ignored (pipe not supported) {% haml_version >= '5.2' %} +!!! Multiline tag attributes with comma then pipe within string is ignored (pipe not supported) {% haml_version >= '5.2' %} asdf %span{ foo: "bar", spam: 'Text1 | and more' } @@ -42,8 +96,8 @@ SKIP --- SKIP --- -%span{ foo: "bar", - spam: 'Text1 | +%span{ foo: 'bar', + spam: 'Text1 and more' } !!! Multiline tag attributes with comma then no marker is ignored (multiline without comma not supported) {% haml_version >= '5.2' %} @@ -55,11 +109,11 @@ SKIP --- SKIP --- -%span{ foo: "bar", - spam: +%span{ foo: 'bar', + spam: 42} -!!! Multiline tag attributes without marker is ignored (multiline without comma not supported) {% haml_version >= '5.2' %} +!!! Multiline tag attributes without marker {% haml_version >= '5.2' %} %span{ foo: "bar"} --- @@ -68,4 +122,82 @@ SKIP SKIP --- %span{ foo: - "bar"} + 'bar'} + + +!!! Multiline tag script using comma and pipe +%tag= aa(foo: "bar", | + spam: 42) | +--- +SKIP +--- +SKIP +--- +%tag= aa(foo: 'bar', + spam: 42) + +!!! Multiline tag script using comma +%tag= aa(foo: "bar", + spam: 42) +--- +SKIP +--- +SKIP +--- +%tag= aa(foo: 'bar', + spam: 42) + +!!! Multiline tag attributes with multiline if {% haml_version >= '5.2' %} +%h1{style: (if foo? + "color: red;" + else + (bar ? 'color:#f28e02' : '') + end)} +--- +SKIP +--- +SKIP +--- +%h1{style: (if foo? + 'color: red;' + else + (bar ? 'color:#f28e02' : '') + end)} + +!!! Multiline tag attributes with multiline if and a child {% haml_version >= '5.2' %} +%h1{style: (if foo? + "color: red;" + else + (bar ? 'color:#f28e02' : '') + end)} + abc +--- +SKIP +--- +SKIP +--- +%h1{style: (if foo? + 'color: red;' + else + (bar ? 'color:#f28e02' : '') + end)} + abc + +!!! Nested multiline tag with multiline if {% haml_version >= '5.2' %} +%tag + %h1{style: (if foo? + "color: red;" + else + (bar ? 'color:#f28e02' : '') + end)} +--- +SKIP +--- +SKIP +--- +%tag + %h1{style: (if foo? + 'color: red;' + else + (bar ? 'color:#f28e02' : '') + end)} diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/script_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/script_examples.txt index d202f71c..67572263 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/script_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/script_examples.txt @@ -160,7 +160,7 @@ haml_lint_marker_5 :hello => 42) --- haml_lint_marker_1 -^^foo(:bar => 123,<%= ' ' %> +^^foo(:bar => 123, %% :hello => 42) $$2 haml_lint_marker_4 --- @@ -663,11 +663,61 @@ haml_lint_marker_3 = # --- haml_lint_marker_1 -# HL.out = +# haml_lint_marker_3 --- haml_lint_marker_1 -# HL.out = haml_lint_marker_3 --- -= # + + +!!! silent keeps a comment +- # hello +--- +haml_lint_marker_1 +# hello +haml_lint_marker_3 +--- +haml_lint_marker_1 +# hello +haml_lint_marker_3 +--- +- # hello + +!!! script keeps an empty comment += # hello +--- +haml_lint_marker_1 +# hello +haml_lint_marker_3 +--- +haml_lint_marker_1 +# hello +haml_lint_marker_3 +--- +- # hello + +!!! Multiline script using comma and pipe (pipes are not needed, so removed) +^ aa(foo: "bar", | + spam: 42) | +--- +SKIP +--- +SKIP +--- +^ aa(foo: 'bar', + spam: 42) + +!!! Multiline script using pipes +^ aa(foo: | + "bar", | + spam: 42 | + ) | +--- +SKIP +--- +SKIP +--- +^ aa(foo: | + 'bar', | + spam: 42) | diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_attributes_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_attributes_examples.txt index f8baee0e..111f693e 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_attributes_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_attributes_examples.txt @@ -1,15 +1,23 @@ !!! fixes attribute hash %tag{:bar => 123} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123) + haml_lint_marker_5 +ensure + HL.noop +end --- %tag{bar: 123} @@ -19,15 +27,23 @@ haml_lint_marker_4 !# `node.dynamic_attributes_sources` returns nothing, but `node.dynamic_attributes_source[:hash]` has content. %tag{:bar => '123'} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => '123') -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => '123') + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: '123') -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: '123') + haml_lint_marker_5 +ensure + HL.noop +end --- %tag{bar: '123'} @@ -35,15 +51,23 @@ haml_lint_marker_4 !!! fixes multi-attributes hash %tag{:bar => 123, 'string_key' => code} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, 'string_key' => code) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, 'string_key' => code) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, 'string_key' => code) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, 'string_key' => code) + haml_lint_marker_5 +ensure + HL.noop +end --- !# Only the spacing gets fixed. Rubocop's default doesn't for colon-style when there are string keys %tag{:bar => 123, 'string_key' => code} @@ -53,17 +77,25 @@ haml_lint_marker_4 %tag{'bar' => 123, 'string_key' => code} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW('bar' => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW('bar' => 123, 'string_key' => code) $$2 -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW('bar' => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW('bar' => 123, 'string_key' => code) -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- !# Only the spacing gets fixed. Rubocop's default doesn't for colon-style when there are string keys %tag{'bar' => 123, @@ -73,15 +105,23 @@ haml_lint_marker_5 !!! fixes a tag with colon-style attributes and classes and an id %tag.class_one.class_two#with_an_id{bar: 123, hello:'42'} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW(bar: 123, hello:'42') -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW(bar: 123, hello:'42') + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW(bar: 123, hello: '42') -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW(bar: 123, hello: '42') + haml_lint_marker_5 +ensure + HL.noop +end --- %tag.class_one.class_two#with_an_id{bar: 123, hello: '42'} @@ -89,15 +129,23 @@ haml_lint_marker_4 !!! fixes multi-attributes mixed-style hash %tag{:bar => 123, hello: 42} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, hello: 42) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, hello: 42) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123, hello: 42) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123, hello: 42) + haml_lint_marker_5 +ensure + HL.noop +end --- !# Only the spacing gets fixed. Rubocop's default doesn't for colon-style when there are string keys %tag{bar: 123, hello: 42} @@ -107,17 +155,25 @@ haml_lint_marker_4 %tag{:bar => 123, :hello => 42} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, :hello => 42) $$2 -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123, hello: 42) -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- %tag{bar: 123, hello: 42} @@ -126,17 +182,25 @@ haml_lint_marker_5 %tag{:bar => 123, :hello => 42} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, :hello => 42) $$2 -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123, hello: 42) -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- %tag{bar: 123, hello: 42} @@ -146,17 +210,25 @@ haml_lint_marker_5 %tag-is-long{:bar => 123, :hello => 42} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWWWWWWWWWW(:bar => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WWWWWWWWWW(:bar => 123, :hello => 42) $$2 -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWWWWWWWWWW(bar: 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WWWWWWWWWW(bar: 123, hello: 42) -haml_lint_marker_5 + haml_lint_marker_6 +ensure + HL.noop +end --- %tag-is-long{bar: 123, hello: 42} @@ -169,22 +241,30 @@ haml_lint_marker_5 --- begin haml_lint_tag_2 - haml_lint_tag_3 $$2 - haml_lint_marker_4 - WWWWWWWWWWWW(:bar => 123, + begin $$2 + haml_lint_tag_4 + haml_lint_marker_5 + WWWWWWWWWW(:bar => 123, :hello => 42) $$3 - haml_lint_marker_7 + haml_lint_marker_8 + ensure + HL.noop + end ensure HL.noop end --- begin haml_lint_tag_2 - haml_lint_tag_3 - haml_lint_marker_4 - WWWWWWWWWWWW(bar: 123, + begin + haml_lint_tag_4 + haml_lint_marker_5 + WWWWWWWWWW(bar: 123, hello: 42) - haml_lint_marker_7 + haml_lint_marker_8 + ensure + HL.noop + end ensure HL.noop end @@ -200,20 +280,28 @@ end --- begin haml_lint_tag_2 - haml_lint_tag_3 $$2 - haml_lint_marker_4 - WWWW(:bar => 123, :hello => 42) - haml_lint_marker_6 + begin $$2 + haml_lint_tag_4 + haml_lint_marker_5 + WW(:bar => 123, :hello => 42) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end --- begin haml_lint_tag_2 - haml_lint_tag_3 - haml_lint_marker_4 - WWWW(bar: 123, hello: 42) - haml_lint_marker_6 + begin + haml_lint_tag_4 + haml_lint_marker_5 + WW(bar: 123, hello: 42) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end @@ -229,19 +317,27 @@ end haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 $$2 - haml_lint_marker_5 - WWWW(:bar => 123, :hello => 42) - haml_lint_marker_7 + begin $$2 + haml_lint_tag_5 + haml_lint_marker_6 + WW(:bar => 123, :hello => 42) + haml_lint_marker_8 + ensure + HL.noop + end end --- haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 - haml_lint_marker_5 - WWWW(bar: 123, hello: 42) - haml_lint_marker_7 + begin + haml_lint_tag_5 + haml_lint_marker_6 + WW(bar: 123, hello: 42) + haml_lint_marker_8 + ensure + HL.noop + end end --- - deeper do @@ -261,15 +357,23 @@ haml_lint_tag_1 !!! fixes attribute methods %tag{foo(bar , :hello => 42)} --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(foo(bar , :hello => 42)) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(foo(bar , :hello => 42)) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(foo(bar, hello: 42)) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(foo(bar, hello: 42)) + haml_lint_marker_5 +ensure + HL.noop +end --- %tag{foo(bar, hello: 42)} @@ -280,20 +384,28 @@ haml_lint_marker_4 --- begin haml_lint_tag_2 - haml_lint_tag_3 $$2 - haml_lint_marker_4 - WWWW(foo(bar , :hello => 42)) - haml_lint_marker_6 + begin $$2 + haml_lint_tag_4 + haml_lint_marker_5 + WW(foo(bar , :hello => 42)) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end --- begin haml_lint_tag_2 - haml_lint_tag_3 - haml_lint_marker_4 - WWWW(foo(bar, hello: 42)) - haml_lint_marker_6 + begin + haml_lint_tag_4 + haml_lint_marker_5 + WW(foo(bar, hello: 42)) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end @@ -308,19 +420,27 @@ end haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 $$2 - haml_lint_marker_5 - WWWW(foo(bar , :hello => 42)) - haml_lint_marker_7 + begin $$2 + haml_lint_tag_5 + haml_lint_marker_6 + WW(foo(bar , :hello => 42)) + haml_lint_marker_8 + ensure + HL.noop + end end --- haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 - haml_lint_marker_5 - WWWW(foo(bar, hello: 42)) - haml_lint_marker_7 + begin + haml_lint_tag_5 + haml_lint_marker_6 + WW(foo(bar, hello: 42)) + haml_lint_marker_8 + ensure + HL.noop + end end --- - deeper do diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_complex_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_complex_examples.txt index 8de7c7b8..c26f8f7a 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_complex_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_complex_examples.txt @@ -4,21 +4,29 @@ !!! Attributes and a script, where the script contains a replica of the attributes %tag{:bar => 123}= foo(:bar => 123) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(:bar => 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(:bar => 123) + haml_lint_marker_8 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(bar: 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(bar: 123) + haml_lint_marker_8 +ensure + HL.noop +end --- %tag{bar: 123}= foo(bar: 123) @@ -26,21 +34,29 @@ haml_lint_marker_7 !!! Attributes and a script, where the attributes contains a replica of the script %tag{:bar => spam(:bing => 512)}= spam(:bing => 512) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => spam(:bing => 512)) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = spam(:bing => 512) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => spam(:bing => 512)) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = spam(:bing => 512) + haml_lint_marker_8 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: spam(bing: 512)) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = spam(bing: 512) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: spam(bing: 512)) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = spam(bing: 512) + haml_lint_marker_8 +ensure + HL.noop +end --- %tag{bar: spam(bing: 512)}= spam(bing: 512) @@ -48,21 +64,29 @@ haml_lint_marker_7 !!! Attributes and a script, where the attributes and the script are identical %tag{foo(:bar => 123)}= foo(:bar => 123) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(foo(:bar => 123)) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(:bar => 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(foo(:bar => 123)) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(:bar => 123) + haml_lint_marker_8 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(foo(bar: 123)) -haml_lint_marker_4 -haml_lint_marker_5 -HL.out = foo(bar: 123) -haml_lint_marker_7 +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(foo(bar: 123)) + haml_lint_marker_5 + haml_lint_marker_6 + HL.out = foo(bar: 123) + haml_lint_marker_8 +ensure + HL.noop +end --- %tag{foo(bar: 123)}= foo(bar: 123) @@ -71,23 +95,31 @@ haml_lint_marker_7 %tag{:bar => 123, :hello => 42}= spam(:bing => 512) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, :hello => 42) $$2 -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(:bing => 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(:bing => 512) + haml_lint_marker_9 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123, hello: 42) -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(bing: 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(bing: 512) + haml_lint_marker_9 +ensure + HL.noop +end --- %tag{bar: 123, hello: 42}= spam(bing: 512) @@ -97,23 +129,31 @@ haml_lint_marker_8 %tag{:bar => spam(:bing => 512), :hello => 42}= spam(:bing => 512) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => spam(:bing => 512), +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => spam(:bing => 512), :hello => 42) $$2 -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(:bing => 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(:bing => 512) + haml_lint_marker_9 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: spam(bing: 512), +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: spam(bing: 512), hello: 42) -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(bing: 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(bing: 512) + haml_lint_marker_9 +ensure + HL.noop +end --- %tag{bar: spam(bing: 512), hello: 42}= spam(bing: 512) @@ -123,23 +163,31 @@ haml_lint_marker_8 %tag{:bar => 123, :hello => spam(:bing => 512)}= spam(:bing => 512) --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(:bar => 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(:bar => 123, :hello => spam(:bing => 512)) $$2 -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(:bing => 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(:bing => 512) + haml_lint_marker_9 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -WWWW(bar: 123, +begin + haml_lint_tag_2 + haml_lint_marker_3 + WW(bar: 123, hello: spam(bing: 512)) -haml_lint_marker_5 -haml_lint_marker_6 -HL.out = spam(bing: 512) -haml_lint_marker_8 + haml_lint_marker_6 + haml_lint_marker_7 + HL.out = spam(bing: 512) + haml_lint_marker_9 +ensure + HL.noop +end --- %tag{bar: 123, hello: spam(bing: 512)}= spam(bing: 512) diff --git a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_script_examples.txt b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_script_examples.txt index 0c732643..013c21a3 100644 --- a/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_script_examples.txt +++ b/spec/haml_lint/linter/rubocop_autocorrect_examples/tag_script_examples.txt @@ -1,15 +1,23 @@ !!! fixes tag's script %tag= foo(:bar => 123) --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(:bar => 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(:bar => 123) + haml_lint_marker_5 +ensure + HL.noop +end --- -haml_lint_tag_1 -haml_lint_marker_2 -HL.out = foo(bar: 123) -haml_lint_marker_4 +begin + haml_lint_tag_2 + haml_lint_marker_3 + HL.out = foo(bar: 123) + haml_lint_marker_5 +ensure + HL.noop +end --- %tag= foo(bar: 123) @@ -20,20 +28,28 @@ haml_lint_marker_4 --- begin haml_lint_tag_2 - haml_lint_tag_3 $$2 - haml_lint_marker_4 - HL.out = foo(:bar => 123) - haml_lint_marker_6 + begin $$2 + haml_lint_tag_4 + haml_lint_marker_5 + HL.out = foo(:bar => 123) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end --- begin haml_lint_tag_2 - haml_lint_tag_3 - haml_lint_marker_4 - HL.out = foo(bar: 123) - haml_lint_marker_6 + begin + haml_lint_tag_4 + haml_lint_marker_5 + HL.out = foo(bar: 123) + haml_lint_marker_7 + ensure + HL.noop + end ensure HL.noop end @@ -49,19 +65,27 @@ end haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 $$2 - haml_lint_marker_5 - HL.out = foo(:bar => 123) - haml_lint_marker_7 + begin $$2 + haml_lint_tag_5 + haml_lint_marker_6 + HL.out = foo(:bar => 123) + haml_lint_marker_8 + ensure + HL.noop + end end --- haml_lint_marker_1 deeper do haml_lint_marker_3 - haml_lint_tag_4 - haml_lint_marker_5 - HL.out = foo(bar: 123) - haml_lint_marker_7 + begin + haml_lint_tag_5 + haml_lint_marker_6 + HL.out = foo(bar: 123) + haml_lint_marker_8 + ensure + HL.noop + end end --- - deeper do diff --git a/spec/haml_lint/linter/rubocop_spec.rb b/spec/haml_lint/linter/rubocop_spec.rb index 52f3ea48..c33dbe18 100644 --- a/spec/haml_lint/linter/rubocop_spec.rb +++ b/spec/haml_lint/linter/rubocop_spec.rb @@ -49,7 +49,7 @@ end it 'uses the source map to transform line numbers' do - subject.should report_lint line: 3 + subject.should report_lint line: 2 end context 'and the offence is from an ignored cop' do diff --git a/spec/haml_lint/ruby_extraction/chunk_extractor_spec.rb b/spec/haml_lint/ruby_extraction/chunk_extractor_spec.rb new file mode 100644 index 00000000..91ab22ec --- /dev/null +++ b/spec/haml_lint/ruby_extraction/chunk_extractor_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +describe HamlLint::RubyExtraction::ChunkExtractor do + let(:options) do + { + config: HamlLint::ConfigurationLoader.default_configuration, + } + end + + let(:document) { HamlLint::Document.new(normalize_indent(haml), options) } + let(:extractor) do + described_class.new(document, script_output_prefix: 'HL.out = ').tap(&:prepare_extract) + end + + describe '#extract_raw_ruby_lines' do + def do_test + expect(ruby_from_haml).to eq(expected_ruby_from_haml) + expect(extractor.extract_raw_ruby_lines(ruby_from_haml, 0)).to eq(expected_return) + end + + context 'with a silent script' do + let(:ruby_from_haml) do + document.tree.children.first.script + end + + context 'single-line' do + let(:haml) { <<~HAML } + - foo(bar: 42, spam: "hello") + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + \ foo(bar: 42, spam: "hello") + CODE + + let(:expected_return) { [2, <<~RET.split("\n")] } + foo(bar: 42, spam: "hello") + RET + + it { do_test } + end + + context 'multi-line using a comma' do + let(:haml) { <<~HAML } + - foo(bar: 42, + spam: "hello") + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + \ foo(bar: 42, spam: "hello") + CODE + + let(:expected_return) { [2, <<~RET.split("\n")] } + foo(bar: 42,\n spam: "hello") + RET + + it { do_test } + end + + context 'multi-line using a pipe' do + let(:haml) { <<~HAML } + - foo(bar: 42, spam: | + "hello") | + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + \ foo(bar: 42, spam: "hello") + CODE + + let(:expected_return) { [2, <<~RET.split("\n")] } + foo(bar: 42, spam:\n "hello") + RET + + it { do_test } + end + + context 'multi-line using a pipe then a comma' do + let(:haml) { <<~HAML } + - foo(bar: | + 42, | + spam: "hello") + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + \ foo(bar: 42, spam: "hello") + CODE + + let(:expected_return) { [2, <<~RET.split("\n")] } + foo(bar:\n 42,\n spam: "hello") + RET + + it { do_test } + end + end + end + + describe '#extract_raw_tag_attributes_ruby_lines' do + context 'with a tag attributes' do + def do_test + expect(ruby_from_haml).to eq(expected_ruby_from_haml) + expect(extractor.extract_raw_tag_attributes_ruby_lines(ruby_from_haml, 0)).to eq(expected_return) + end + + let(:ruby_from_haml) do + document.tree.children.first.dynamic_attributes_sources.first + end + + context 'hash-like' do + context 'single-line' do + let(:haml) { <<~HAML } + %tag{foo: bar} + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + foo: bar + CODE + + let(:expected_return) { [5, <<~RET.split("\n")] } + foo: bar + RET + + it { do_test } + end + + context 'multi-line with comma' do + let(:haml) { <<~HAML } + %tag{foo: bar, + abc: "hello"} + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + foo: bar, + abc: "hello" + CODE + + let(:expected_return) { [5, <<~RET.split("\n")] } + foo: bar, + abc: "hello" + RET + + it { do_test } + end + + context 'multi-line with pipe' do + let(:haml) { <<~HAML } + %tag{foo: | + bar, abc: "hello"} | + + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + foo: bar, abc: "hello" + CODE + + let(:expected_return) { [5, <<~RET.split("\n")] } + foo: + bar, abc: "hello" + RET + + it { do_test } + end + + context 'multi-line using a pipe then a comma' do + let(:haml) { <<~HAML } + %tag{foo: | + bar, | + abc: "hello"} + HAML + + let(:expected_ruby_from_haml) { <<~CODE.rstrip } + foo: bar,#{' '} + abc: "hello" + CODE + + let(:expected_return) { [5, <<~RET.split("\n")] } + foo: + bar, + abc: "hello" + RET + + it { do_test } + end + + # This is supported for tag attributes only... + context 'multi-line without pipe or comma' do + let(:haml) { <<~HAML } + %span{ foo: + "bar", + spam: + 42 } + HAML + + let(:expected_ruby_from_haml) { <<~CODE.chop } + \ foo: + "bar", + spam: + 42#{' '} + CODE + + let(:expected_return) { [7, <<~RET.split("\n")] } + foo: + "bar", + spam: + 42 + RET + + it { + if HamlLint::VersionComparer.for_haml < '5.2' + skip + end + do_test + } + end + end + end + end +end diff --git a/spec/haml_lint/ruby_extraction/script_chunk_spec.rb b/spec/haml_lint/ruby_extraction/script_chunk_spec.rb new file mode 100644 index 00000000..6f82d839 --- /dev/null +++ b/spec/haml_lint/ruby_extraction/script_chunk_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +describe HamlLint::RubyExtraction::ScriptChunk do + describe '.format_ruby_lines_to_haml_lines' do + def do_test + generated_haml = described_class.format_ruby_lines_to_haml_lines(ruby.split("\n"), + script_output_prefix: 'HL.out = ') + expect(generated_haml.join("\n")).to eq(expected_haml.chop) + end + + context 'with a begin and rescue' do + let(:ruby) { <<~RUBY } + begin + foo = 1 + rescue StandardError => e + foo = 2 + end + RUBY + + let(:expected_haml) { <<~HAML } + - begin + - foo = 1 + - rescue StandardError => e + - foo = 2 + HAML + + it { do_test } + end + + context 'with a rescue without preceding begin' do + let(:ruby) { <<~RUBY } + foo = 1 + rescue StandardError => e + foo = 2 + end + RUBY + + let(:expected_haml) { <<~HAML } + - foo = 1 + - rescue StandardError => e + - foo = 2 + HAML + + it { do_test } + end + + context 'with a ensure without preceding begin' do + let(:ruby) { <<~RUBY } + foo = 1 + ensure + foo = 2 + end + RUBY + + let(:expected_haml) { <<~HAML } + - foo = 1 + - ensure + - foo = 2 + HAML + + it { do_test } + end + + context 'with a elsif without preceding if' do + let(:ruby) { <<~RUBY } + HL.out = "hi" + elsif abc? + HL.out = "world" + RUBY + + let(:expected_haml) { <<~HAML } + = "hi" + - elsif abc? + = "world" + HAML + + it { do_test } + end + + context 'with a else without preceding if' do + let(:ruby) { <<~RUBY } + foo() + else + bar() + RUBY + + let(:expected_haml) { <<~HAML } + - foo() + - else + - bar() + HAML + + it { do_test } + end + + context 'with a else, end, else without preceding if' do + let(:ruby) { <<~RUBY } + foo() + else + bar() + end.join + else + more() + RUBY + + let(:expected_haml) { <<~HAML } + - foo() + - else + - bar() + - end.join + - else + - more() + HAML + + it { do_test } + end + + context 'with a when without preceding case' do + let(:ruby) { <<~RUBY } + foo() + when 123 + bar() + RUBY + + let(:expected_haml) { <<~HAML } + - foo() + - when 123 + - bar() + HAML + + it { do_test } + end + + context 'with consecutive multiline method calls' do + let(:ruby) { <<~RUBY } + foo( + ) + foo( + ) + RUBY + + let(:expected_haml) { <<~HAML } + - foo( | + ) | + + - foo( | + ) | + HAML + + it { do_test } + end + + context 'with multiline method call followed by block with arguments and spaces' do + let(:ruby) { <<~RUBY } + foo( + ) + hello do | hi | + RUBY + + let(:expected_haml) { <<~HAML } + - foo( | + ) | + - hello do | hi | + HAML + + it { do_test } + end + end +end diff --git a/spec/haml_lint/ruby_extractor_spec.rb b/spec/haml_lint/ruby_extractor_spec.rb deleted file mode 100644 index aeb8d2ae..00000000 --- a/spec/haml_lint/ruby_extractor_spec.rb +++ /dev/null @@ -1,621 +0,0 @@ -# frozen_string_literal: true - -describe HamlLint::RubyExtractor do - let(:extractor) { described_class.new } - - describe '#extract' do - let(:options) do - { - config: HamlLint::ConfigurationLoader.default_configuration, - } - end - - let(:tree) { HamlLint::Document.new(normalize_indent(haml), options) } - subject { extractor.extract(tree) } - - context 'with an empty HAML document' do - let(:haml) { '' } - its(:source) { should == '' } - its(:source_map) { should == {} } - end - - context 'with plain text' do - let(:haml) { <<-HAML } - Hello world - HAML - - its(:source) { should == '_haml_lint_puts_0' } - its(:source_map) { should == { 1 => 1 } } - end - - context 'with multiple lines of plain text' do - let(:haml) { <<-HAML } - Hello world - how are you? - HAML - - its(:source) { should == "_haml_lint_puts_0\n_haml_lint_puts_1" } - its(:source_map) { should == { 1 => 1, 2 => 2 } } - end - - context 'with only tags with text content' do - let(:haml) { <<-HAML } - %h1 Hello World - %p - Lorem - %b Ipsum - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # h1 - _haml_lint_puts_1 # h1/ - _haml_lint_puts_2 # p - _haml_lint_puts_3 - _haml_lint_puts_4 # b - _haml_lint_puts_5 # b/ - _haml_lint_puts_6 # p/ - RUBY - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 2, 4 => 3, 5 => 4, 6 => 4, 7 => 2 } } - end - - context 'with a silent script node' do - let(:haml) { <<-HAML } - - silent_script - HAML - - its(:source) { should == 'silent_script' } - its(:source_map) { should == { 1 => 1 } } - end - - context 'with a script node' do - let(:haml) { <<-HAML } - = script - HAML - - its(:source) { should == 'script' } - its(:source_map) { should == { 1 => 1 } } - end - - context 'with a script node that spans multiple lines' do - let(:haml) { <<-HAML } - = link_to 'Link', - path, - class: 'button' - HAML - - its(:source) { should == "link_to 'Link', path, class: 'button'" } - its(:source_map) { should == { 1 => 1 } } - end - - context 'with a tag containing a silent script node' do - let(:haml) { <<-HAML } - %tag - - script - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # tag - script - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 1 } } - end - - context 'with a tag containing a script node' do - let(:haml) { <<-HAML } - %tag - = script - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # tag - script - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 1 } } - end - - context 'with a tag containing inline script' do - let(:haml) { <<-HAML } - %tag= script - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # tag - script - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with hash attributes' do - let(:haml) { <<-HAML } - %tag{ one: 1, two: 2, 'three' => some_method } - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag(one: 1, two: 2, 'three' => some_method) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with hash attributes with hashrockets and questionable spacing' do - let(:haml) { <<-HAML } - %tag.class_one.class_two#with_an_id{:type=>'checkbox', 'special' => :true } - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - {:type=>'checkbox', 'special' => :true } - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with mixed-style hash attributes' do - let(:haml) { <<-HAML } - %tag.class_one.class_two#with_an_id{ :type=>'checkbox', special: 'true' } - HAML - - its(:source) { should == normalize_indent(<<-RUBY.rstrip) } - tag(:type=>'checkbox', special: 'true') - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with hash attributes with a method call' do - let(:haml) { <<-HAML } - %tag{ tag_options_method } - HAML - - its(:source) { should == normalize_indent(<<-RUBY.rstrip) } - tag(tag_options_method) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with HTML-style attributes' do - let(:haml) { <<-HAML } - %tag(one=1 two=2 three=some_method) - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag({\"one\" => 1,\"two\" => 2,\"three\" => some_method,}) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - - context 'with a tag with hash attributes and inline script' do - let(:haml) { <<-HAML } - %tag{ one: 1 }= script - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag(one: 1) - _haml_lint_puts_0 # tag - script - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1, 4 => 1 } } - end - - context 'with a tag with hash attributes that span multiple lines' do - let(:haml) { <<-HAML } - %tag{ one: 1, - two: 2, - 'three' => 3 } - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag(one: 1, - two: 2, - 'three' => 3) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1 } } - end - - # Multiline attributes were introduced in 5.2.1 - if Haml::VERSION >= '5.2.1' - context 'with a tag with hash attributes containing a hash with newlines' do - let(:haml) { <<-HAML } - %tag{class: some_method({ - one: 1, - two: 2 - })} - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag(class: some_method({ - one: 1, - two: 2 - })) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - end - - context 'with a tag with hash attributes containing a method call with newlines' do - let(:haml) { <<-HAML } - %tag{class: some_method( - 1, 2, 3 - )} - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - tag(class: some_method( - 1, 2, 3 - )) - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - end - end - - context 'with a tag with 1.8-style hash attributes of string key/values' do - let(:haml) { <<-HAML } - %tag{ 'one' => '1', 'two' => '2' } - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - { 'one' => '1', 'two' => '2' } - _haml_lint_puts_0 # tag - _haml_lint_puts_1 # tag/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - - context 'that span multiple lines' do - let(:haml) { <<-HAML } - %div{ 'one' => '1', - 'two' => '2' } - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - { 'one' => '1', 'two' => '2' } - _haml_lint_puts_0 # div - _haml_lint_puts_1 # div/ - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1 } } - end - end - - context 'with a HAML comment' do - let(:haml) { <<-HAML } - -# rubocop:disable SomeCop - = some_code - -# rubocop:enable SomeCop - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - # rubocop:disable SomeCop - some_code - # rubocop:enable SomeCop - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 3 } } - end - - context 'with a multiline HAML comment' do - let(:haml) { <<-HAML } - -# This is a HAML - comment spanning - multiple lines - = some_code - -# rubocop:enable SomeCop - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - # This is a HAML - # comment spanning - # multiple lines - some_code - # rubocop:enable SomeCop - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1, 4 => 4, 5 => 5 } } - - context 'with no leading spaces' do - let(:haml) { <<-HAML } - -# - This is a HAML - comment spanning - multiple lines - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - # - # This is a HAML - # comment spanning - # multiple lines - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 1, 3 => 1, 4 => 1 } } - end - - context 'with nested' do - let(:haml) { <<-HAML } - =some_code do - -# - This is a HAML - comment spanning - multiple lines - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - some_code do - # - # This is a HAML - # comment spanning - # multiple lines - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 2, 4 => 2, 5 => 2, 6 => 1 } } - end - end - - context 'with a block statement' do - let(:haml) { <<-HAML } - - if condition - - script_one - - elsif condition_two - - script_two - - else - - script_three - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - if condition - script_one - elsif condition_two - script_two - else - script_three - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 1 } } - end - - context 'with an anonymous block' do - let(:haml) { <<-HAML } - = link_to path do - = script - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - link_to path do - script - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 1 } } - end - - context 'with an anonymous block with a trailing comment' do - let(:haml) { <<-HAML } - - list.each do |var, var2| # Some comment - = something - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - list.each do |var, var2| # Some comment - something - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 1 } } - end - - context 'with a for loop' do - let(:haml) { <<-HAML } - - for value in list - = value - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - for value in list - value - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 1 } } - end - - context 'with a while loop' do - let(:haml) { <<-HAML } - - while value < 10 - = value - - value += 1 - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - while value < 10 - value - value += 1 - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 3, 4 => 1 } } - end - - context 'with a Ruby filter' do - let(:haml) { <<-HAML } - :ruby - method_one - if condition - method_two - end - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - method_one - if condition - method_two - end - RUBY - - its(:source_map) { should == { 1 => 2, 2 => 3, 3 => 4, 4 => 5 } } - end - - context 'with a Ruby filter containing block keywords' do - let(:haml) { <<-HAML } - :ruby - if condition - do_something - else - do_something_else - end - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - if condition - do_something - else - do_something_else - end - RUBY - - its(:source_map) { should == { 1 => 2, 2 => 3, 3 => 4, 4 => 5, 5 => 6 } } - - context 'and the filter is nested' do - let(:haml) { <<-HAML } - - something do - :ruby - if condition - do_something - else - do_something_else - end - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - something do - if condition - do_something - else - do_something_else - end - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 3, 3 => 4, 4 => 5, 5 => 6, 6 => 7, 7 => 1 } } - end - end - - context 'with a Ruby filter containing block keywords' do - let(:haml) { <<-HAML } - :ruby - def foo - 42 - end - - def bar - 23 - end - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - def foo - 42 - end - - def bar - 23 - end - RUBY - end - - context 'with a filter with interpolated values' do - let(:haml) { <<-HAML } - :filter - Some text \#{some_method} with interpolation. - Some more text \#{some_other_method} with interpolation. - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # :filter - some_method - some_other_method - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 3 } } - end - - context 'with a filter with interpolated values containing quotes' do - let(:haml) { <<-HAML } - :filter - Some text \#{some_method("hello")} - Some text \#{some_other_method('world')} - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # :filter - some_method("hello") - some_other_method('world') - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 3 } } - end - - context 'with a filter with interpolated values spanning multiple lines' do - let(:haml) { <<-HAML } - :filter - Some text \#{some_method('hello', - 'world')} - HAML - - # TODO: Figure out if it's worth normalizing indentation for the generated - # code in this interpolated context - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - _haml_lint_puts_0 # :filter - some_method('hello', - 'world') - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 2 } } - end - - context 'with an if/else block containing only filters' do - let(:haml) { <<-HAML } - - if condition - :filter - Some text - - else - :filter - Some other text - HAML - - its(:source) { should == normalize_indent(<<-RUBY).rstrip } - if condition - _haml_lint_puts_0 # :filter - else - _haml_lint_puts_1 # :filter - end - RUBY - - its(:source_map) { should == { 1 => 1, 2 => 2, 3 => 4, 4 => 5, 5 => 1 } } - end - end -end