lib/todoist/diff_todo_finder.rb in danger-todoist-1.2.3 vs lib/todoist/diff_todo_finder.rb in danger-todoist-1.3.0

- old
+ new

@@ -1,46 +1,111 @@ module Danger # Identify todos in a set of diffs class DiffTodoFinder def initialize(keywords) - @keywords = keywords + @regexp = todo_regexp(keywords) end - def find_diffs_containing_todos(diffs) - todos = [] - regexp = todo_regexp - diffs.each do |diff| - matches = diff.patch.scan(regexp) - next if matches.empty? - - matches.each do |match| - todos << Danger::Todo.new(diff.path, clean_todo_text(match)) - end - end - todos + def call(diffs) + diffs + .map { |diff| MatchesInDiff.new(diff, diff.patch.scan(@regexp)) } + .select(&:todo_matches?) + .map(&:all_todos) + .flatten end private - def clean_todo_text(match) - comment_indicator, _, entire_todo = match - entire_todo.gsub(comment_indicator, "") - .delete("\n") - .strip - end - # this is quite a mess now ... I knew it would haunt me. # to aid debugging, this online regexr can be # used: http://rubular.com/r/DPkoE2ztpn # the regexp uses backreferences to match the comment indicator multiple # times if possible - def todo_regexp + def todo_regexp(keywords) / (?<comment_indicator>^\+\s*[^a-z0-9\+\s]+) (\n\+)?\s+ - (?<todo_indicator>#{@keywords.join("|")})[\s:]{1} + (?<todo_indicator>#{keywords.join("|")})[\s:]{1} (?<entire_text>(?<text>[^\n]*) - (?<rest>\n\k<comment_indicator>\s*[\w .]*)*) + (?<rest>\n\k<comment_indicator>\s*[\w .']*)*) /ixm end + end + + # Identify todos in a single diff + class MatchesInDiff < Struct.new(:diff, :matches) + def todo_matches? + !matches.empty? + end + + def all_todos + matches.map { |match| build_todo(diff.path, match) } + end + + private + + def line_number(match) + _, _, _, first_text = match + Patch.new(diff.patch).changed_lines.each do |line| + return line.number if line.content.include? first_text + end + raise TextNotFoundInPatchError + end + + def build_todo(path, match) + Danger::Todo.new(path, cleaned_todo_text(match), line_number(match)) + end + + def cleaned_todo_text(match) + comment_indicator, _, entire_todo = match + entire_todo.gsub(comment_indicator, "") + .delete("\n") + .strip + end + + # Parsed patch + class Patch + RANGE_INFORMATION_LINE = /^@@ .+\+(?<line_number>\d+),/ + MODIFIED_LINE = /^\+(?!\+|\+)/ + REMOVED_LINE = /^[-]/ + NOT_REMOVED_LINE = /^[^-]/ + + def initialize(body) + @body = body + end + + # rubocop:disable Metrics/MethodLength + def changed_lines + line_number = 0 + + lines_with_index + .each_with_object([]) do |(content, patch_position), lines| + case content + when RANGE_INFORMATION_LINE + line_number = Regexp.last_match[:line_number].to_i + when MODIFIED_LINE + lines << Line.new(content, line_number, patch_position) + line_number += 1 + when NOT_REMOVED_LINE + line_number += 1 + end + end + end + # rubocop:enable Metrics/MethodLength + + def lines + @body.lines + end + + def lines_with_index + lines.each_with_index + end + end + + # Parsed line + class Line < Struct.new(:content, :number, :patch_position) + end + end + + class TextNotFoundInPatchError < RuntimeError end end