reek 1.3.4 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +5 -0
- data/README.md +27 -2
- data/features/command_line_interface/options.feature +5 -2
- data/features/command_line_interface/smells_count.feature +43 -45
- data/features/command_line_interface/stdin.feature +9 -15
- data/features/configuration_files/masking_smells.feature +9 -17
- data/features/rake_task/rake_task.feature +4 -4
- data/features/reports/reports.feature +80 -21
- data/features/samples.feature +8 -18
- data/features/step_definitions/reek_steps.rb +4 -0
- data/lib/reek/cli/application.rb +3 -6
- data/lib/reek/cli/command_line.rb +16 -6
- data/lib/reek/cli/reek_command.rb +4 -12
- data/lib/reek/cli/report.rb +61 -19
- data/lib/reek/config_file_exception.rb +5 -0
- data/lib/reek/smells/control_parameter.rb +45 -14
- data/lib/reek/smells/data_clump.rb +15 -39
- data/lib/reek/smells/duplicate_method_call.rb +76 -26
- data/lib/reek/source/config_file.rb +30 -19
- data/lib/reek/source/sexp_extensions.rb +139 -0
- data/lib/reek/source/sexp_node.rb +64 -0
- data/lib/reek/source/source_code.rb +1 -1
- data/lib/reek/source/tree_dresser.rb +30 -175
- data/lib/reek/spec/should_reek.rb +2 -5
- data/lib/reek/version.rb +1 -1
- data/reek.gemspec +1 -1
- data/spec/matchers/smell_of_matcher.rb +12 -15
- data/spec/reek/cli/report_spec.rb +10 -6
- data/spec/reek/core/code_parser_spec.rb +0 -6
- data/spec/reek/smells/control_parameter_spec.rb +195 -8
- data/spec/reek/smells/data_clump_spec.rb +28 -3
- data/spec/reek/smells/uncommunicative_method_name_spec.rb +7 -7
- data/spec/reek/source/sexp_extensions_spec.rb +290 -0
- data/spec/reek/source/sexp_node_spec.rb +28 -0
- data/spec/reek/source/source_code_spec.rb +59 -19
- data/spec/reek/source/tree_dresser_spec.rb +7 -314
- data/spec/reek/spec/should_reek_spec.rb +51 -64
- data/spec/samples/all_but_one_masked/dirty.rb +2 -2
- data/spec/samples/corrupt_config_file/dirty.rb +1 -0
- data/spec/samples/masked/dirty.rb +1 -1
- data/spec/samples/masked_by_dotfile/dirty.rb +2 -2
- data/spec/samples/no_config_file/dirty.rb +8 -0
- data/spec/samples/not_quite_masked/dirty.rb +0 -3
- data/spec/samples/three_smelly_files/dirty_one.rb +3 -0
- data/spec/samples/three_smelly_files/dirty_three.rb +5 -0
- data/spec/samples/three_smelly_files/dirty_two.rb +4 -0
- data/spec/spec_helper.rb +5 -0
- metadata +145 -137
- data/spec/reek/cli/reek_command_spec.rb +0 -46
    
        data/lib/reek/cli/application.rb
    CHANGED
    
    | @@ -23,7 +23,7 @@ module Reek | |
| 23 23 | 
             
                    begin
         | 
| 24 24 | 
             
                      cmd = @options.parse
         | 
| 25 25 | 
             
                      cmd.execute(self)
         | 
| 26 | 
            -
                    rescue  | 
| 26 | 
            +
                    rescue OptionParser::InvalidOption, ConfigFileException => error
         | 
| 27 27 | 
             
                      $stderr.puts "Error: #{error}"
         | 
| 28 28 | 
             
                      @status = STATUS_ERROR
         | 
| 29 29 | 
             
                    end
         | 
| @@ -42,11 +42,8 @@ module Reek | |
| 42 42 | 
             
                    @status = STATUS_SMELLS
         | 
| 43 43 | 
             
                  end
         | 
| 44 44 |  | 
| 45 | 
            -
                  def  | 
| 46 | 
            -
                     | 
| 47 | 
            -
                    total_smells_message += 's' unless total_smells_count == 1
         | 
| 48 | 
            -
                    total_smells_message += "\n"
         | 
| 49 | 
            -
                    output total_smells_message
         | 
| 45 | 
            +
                  def update_status(reporter)
         | 
| 46 | 
            +
                    reporter.has_smells? ? report_smells : report_success
         | 
| 50 47 | 
             
                  end
         | 
| 51 48 | 
             
                end
         | 
| 52 49 | 
             
              end
         | 
| @@ -17,10 +17,11 @@ module Reek | |
| 17 17 | 
             
                  def initialize(argv)
         | 
| 18 18 | 
             
                    @argv = argv
         | 
| 19 19 | 
             
                    @parser = OptionParser.new
         | 
| 20 | 
            -
                    @report_class =  | 
| 20 | 
            +
                    @report_class = QuietReport
         | 
| 21 21 | 
             
                    @warning_formatter = WarningFormatterWithLineNumbers
         | 
| 22 22 | 
             
                    @command_class = ReekCommand
         | 
| 23 23 | 
             
                    @config_files = []
         | 
| 24 | 
            +
                    @sort_by_issue_count = false
         | 
| 24 25 | 
             
                    set_options
         | 
| 25 26 | 
             
                  end
         | 
| 26 27 |  | 
| @@ -72,15 +73,24 @@ EOB | |
| 72 73 | 
             
                    end
         | 
| 73 74 |  | 
| 74 75 | 
             
                    @parser.separator "\nReport formatting:"
         | 
| 75 | 
            -
                    @parser.on("-q", "-- | 
| 76 | 
            -
                      @report_class =  | 
| 76 | 
            +
                    @parser.on("-q", "--quiet", "Suppress headings for smell-free source files (this is the default)") do |opt|
         | 
| 77 | 
            +
                      @report_class = QuietReport
         | 
| 77 78 | 
             
                    end
         | 
| 78 | 
            -
                    @parser.on("- | 
| 79 | 
            +
                    @parser.on("-V", "--no-quiet", "--verbose", "Show headings for smell-free source files") do |opt|
         | 
| 80 | 
            +
                      @report_class = VerboseReport
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                    @parser.on("-n", "--no-line-numbers", "Suppress line numbers from the output") do 
         | 
| 79 83 | 
             
                      @warning_formatter = SimpleWarningFormatter
         | 
| 80 84 | 
             
                    end
         | 
| 85 | 
            +
                    @parser.on("--line-numbers", "Show line numbers in the output (this is the default)") do 
         | 
| 86 | 
            +
                      @warning_formatter = WarningFormatterWithLineNumbers
         | 
| 87 | 
            +
                    end
         | 
| 81 88 | 
             
                    @parser.on("-s", "--single-line", "Show IDE-compatible single-line-per-warning") do 
         | 
| 82 89 | 
             
                      @warning_formatter = SingleLineWarningFormatter
         | 
| 83 90 | 
             
                    end        
         | 
| 91 | 
            +
                    @parser.on("-S", "--sort-by-issue-count", 'Sort by "issue-count", listing the "smelliest" files first') do
         | 
| 92 | 
            +
                      @sort_by_issue_count = true
         | 
| 93 | 
            +
                    end
         | 
| 84 94 | 
             
                    @parser.on("-y", "--yaml", "Report smells in YAML format") do
         | 
| 85 95 | 
             
                      @command_class = YamlCommand
         | 
| 86 96 | 
             
                      # SMELL: the args passed to the command should be tested, because it may
         | 
| @@ -100,8 +110,8 @@ EOB | |
| 100 110 | 
             
                      if @command_class == YamlCommand
         | 
| 101 111 | 
             
                        YamlCommand.create(sources, @config_files)
         | 
| 102 112 | 
             
                      else
         | 
| 103 | 
            -
                         | 
| 104 | 
            -
                        ReekCommand.create(sources,  | 
| 113 | 
            +
                        reporter = @report_class.new(@warning_formatter, ReportFormatter, @sort_by_issue_count)
         | 
| 114 | 
            +
                        ReekCommand.create(sources, reporter, @config_files)
         | 
| 105 115 | 
             
                      end
         | 
| 106 116 | 
             
                    end
         | 
| 107 117 | 
             
                  end
         | 
| @@ -18,20 +18,12 @@ module Reek | |
| 18 18 | 
             
                    @config_files = config_files
         | 
| 19 19 | 
             
                  end
         | 
| 20 20 |  | 
| 21 | 
            -
                  def execute( | 
| 22 | 
            -
                    total_smells_count = 0
         | 
| 21 | 
            +
                  def execute(app)
         | 
| 23 22 | 
             
                    @sources.each do |source|
         | 
| 24 | 
            -
                       | 
| 25 | 
            -
                      total_smells_count += examiner.smells_count
         | 
| 26 | 
            -
                      view.output @reporter.report(examiner)
         | 
| 23 | 
            +
                      @reporter.add_examiner(Examiner.new(source, @config_files))
         | 
| 27 24 | 
             
                    end
         | 
| 28 | 
            -
                     | 
| 29 | 
            -
             | 
| 30 | 
            -
                    else
         | 
| 31 | 
            -
                      view.report_success
         | 
| 32 | 
            -
                    end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                    view.output_smells_total(total_smells_count) if @sources.count > 1
         | 
| 25 | 
            +
                    app.update_status(@reporter)
         | 
| 26 | 
            +
                    @reporter.show
         | 
| 35 27 | 
             
                  end
         | 
| 36 28 | 
             
                end
         | 
| 37 29 | 
             
              end
         | 
    
        data/lib/reek/cli/report.rb
    CHANGED
    
    | @@ -31,36 +31,78 @@ module Reek | |
| 31 31 | 
             
                  def self.format(warning)
         | 
| 32 32 | 
             
                    "#{warning.source}:#{warning.lines.first}: #{SimpleWarningFormatter.format(warning)}"
         | 
| 33 33 | 
             
                  end
         | 
| 34 | 
            -
                end | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                class Report
         | 
| 37 | 
            +
                  def initialize(warning_formatter = SimpleWarningFormatter, report_formatter = ReportFormatter, sort_by_issue_count = false)
         | 
| 38 | 
            +
                    @warning_formatter   = warning_formatter
         | 
| 39 | 
            +
                    @report_formatter    = report_formatter
         | 
| 40 | 
            +
                    @examiners           = []
         | 
| 41 | 
            +
                    @total_smell_count   = 0
         | 
| 42 | 
            +
                    @sort_by_issue_count = sort_by_issue_count
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def add_examiner(examiner)
         | 
| 46 | 
            +
                    @total_smell_count += examiner.smells_count
         | 
| 47 | 
            +
                    @examiners << examiner
         | 
| 48 | 
            +
                    self
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def show
         | 
| 52 | 
            +
                    sort_examiners
         | 
| 53 | 
            +
                    display_summary
         | 
| 54 | 
            +
                    display_total_smell_count
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def has_smells?
         | 
| 58 | 
            +
                    @total_smell_count > 0
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                private
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def sort_examiners
         | 
| 64 | 
            +
                    @examiners.sort! {|a, b| b.smells_count <=> a.smells_count } if @sort_by_issue_count
         | 
| 65 | 
            +
                  end
         | 
| 35 66 |  | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
                #
         | 
| 39 | 
            -
                class VerboseReport
         | 
| 40 | 
            -
                  def initialize(warning_formatter = SimpleWarningFormatter, report_formatter = ReportFormatter)
         | 
| 41 | 
            -
                    @warning_formatter = warning_formatter
         | 
| 42 | 
            -
                    @report_formatter = report_formatter
         | 
| 67 | 
            +
                  def display_summary
         | 
| 68 | 
            +
                    print gather_results.reject(&:empty?).join("\n")
         | 
| 43 69 | 
             
                  end
         | 
| 44 70 |  | 
| 45 | 
            -
                  def  | 
| 71 | 
            +
                  def display_total_smell_count
         | 
| 72 | 
            +
                    if @examiners.size > 1
         | 
| 73 | 
            +
                      print "\n"
         | 
| 74 | 
            +
                      print total_smell_count_message
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def total_smell_count_message
         | 
| 79 | 
            +
                    "#{@total_smell_count} total warning#{'s' unless @total_smell_count == 1 }\n"
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  def summarize_single_examiner(examiner)
         | 
| 46 83 | 
             
                    result = @report_formatter.header examiner
         | 
| 47 84 | 
             
                    if examiner.smelly?
         | 
| 48 85 | 
             
                      formatted_list = @report_formatter.format_list examiner.smells, @warning_formatter
         | 
| 49 86 | 
             
                      result += ":\n#{formatted_list}"
         | 
| 50 87 | 
             
                    end
         | 
| 51 | 
            -
                    result | 
| 88 | 
            +
                    result
         | 
| 52 89 | 
             
                  end
         | 
| 53 90 | 
             
                end
         | 
| 54 91 |  | 
| 55 | 
            -
                 | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 92 | 
            +
                class VerboseReport < Report
         | 
| 93 | 
            +
                  def gather_results
         | 
| 94 | 
            +
                    @examiners.each_with_object([]) do |examiner, result|
         | 
| 95 | 
            +
                      result << summarize_single_examiner(examiner)
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                class QuietReport < Report
         | 
| 101 | 
            +
                  def gather_results
         | 
| 102 | 
            +
                    @examiners.each_with_object([]) do |examiner, result|
         | 
| 103 | 
            +
                      if examiner.smelly?
         | 
| 104 | 
            +
                        result << summarize_single_examiner(examiner)
         | 
| 105 | 
            +
                      end
         | 
| 64 106 | 
             
                    end
         | 
| 65 107 | 
             
                  end
         | 
| 66 108 | 
             
                end
         | 
| @@ -46,6 +46,7 @@ module Reek | |
| 46 46 | 
             
                  SMELL_CLASS = 'ControlCouple'
         | 
| 47 47 | 
             
                  SMELL_SUBCLASS = self.name.split(/::/)[-1]
         | 
| 48 48 | 
             
                  PARAMETER_KEY = 'parameter'
         | 
| 49 | 
            +
                  VALUE_POSITION = 1
         | 
| 49 50 |  | 
| 50 51 | 
             
                  #
         | 
| 51 52 | 
             
                  # Checks whether the given method chooses its execution path
         | 
| @@ -54,31 +55,61 @@ module Reek | |
| 54 55 | 
             
                  # @return [Array<SmellWarning>]
         | 
| 55 56 | 
             
                  #
         | 
| 56 57 | 
             
                  def examine_context(ctx)
         | 
| 57 | 
            -
                    control_parameters(ctx).map do | | 
| 58 | 
            -
                      param =  | 
| 58 | 
            +
                    control_parameters(ctx).map do |lvars, occurs|
         | 
| 59 | 
            +
                      param = lvars.format_ruby
         | 
| 59 60 | 
             
                      lines = occurs.map {|exp| exp.line}
         | 
| 60 61 | 
             
                      smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, lines,
         | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 62 | 
            +
                                               "is controlled by argument #{param}",
         | 
| 63 | 
            +
                                               @source, SMELL_SUBCLASS,
         | 
| 64 | 
            +
                                               {PARAMETER_KEY => param})
         | 
| 64 65 | 
             
                      smell
         | 
| 65 66 | 
             
                    end
         | 
| 66 67 | 
             
                  end
         | 
| 67 68 |  | 
| 68 | 
            -
             | 
| 69 | 
            +
                  private
         | 
| 69 70 |  | 
| 70 71 | 
             
                  def control_parameters(method_ctx)
         | 
| 71 | 
            -
                     | 
| 72 | 
            -
                     | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
                      cond = if_node[1]
         | 
| 76 | 
            -
                      if cond[0] == :lvar and params.include?(cond[1])
         | 
| 77 | 
            -
                        result[cond].push(cond)
         | 
| 78 | 
            -
                      end
         | 
| 72 | 
            +
                    result = Hash.new {|hash, key| hash[key] = []}
         | 
| 73 | 
            +
                    method_ctx.exp.parameter_names.each do |param|
         | 
| 74 | 
            +
                      next if used_outside_conditional?(method_ctx, param)
         | 
| 75 | 
            +
                      find_matchs(method_ctx, param).each {|match| result[match].push(match)}
         | 
| 79 76 | 
             
                    end
         | 
| 80 77 | 
             
                    result
         | 
| 81 78 | 
             
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  # Returns wether the parameter is used outside of the conditional statement.
         | 
| 81 | 
            +
                  def used_outside_conditional?(method_ctx, param)
         | 
| 82 | 
            +
                    method_ctx.exp.each_node(:lvar, [:if, :case, :and, :or, :args]) do |node|
         | 
| 83 | 
            +
                      return true if node.value == param
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                    false
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  # Find the use of the param that match the definition of a control parameter.
         | 
| 89 | 
            +
                  def find_matchs(method_ctx, param)
         | 
| 90 | 
            +
                    matchs = []
         | 
| 91 | 
            +
                    [:if, :case, :and, :or].each do |keyword|
         | 
| 92 | 
            +
                      method_ctx.local_nodes(keyword).each do |node|
         | 
| 93 | 
            +
                        return [] if used_besides_in_condition?(node, param)
         | 
| 94 | 
            +
                        node.each_node(:lvar, []) {|inner| matchs.push(inner) if inner.value == param}
         | 
| 95 | 
            +
                      end
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                    matchs
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  # Returns wether the parameter is used somewhere besides in the condition of the
         | 
| 101 | 
            +
                  # conditional statement.
         | 
| 102 | 
            +
                  def used_besides_in_condition?(node, param)
         | 
| 103 | 
            +
                    times_in_conditional, times_total = 0, 0
         | 
| 104 | 
            +
                    node.each_node(:lvar, [:if, :case]) {|inner| times_total +=1 if inner[VALUE_POSITION] == param}
         | 
| 105 | 
            +
                    if node.condition
         | 
| 106 | 
            +
                      times_in_conditional += 1 if node.condition[VALUE_POSITION] == param
         | 
| 107 | 
            +
                      node.condition.each do |inner|
         | 
| 108 | 
            +
                        times_in_conditional += 1 if inner.class == Sexp && inner[VALUE_POSITION] == param
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                    return times_total > times_in_conditional
         | 
| 112 | 
            +
                  end
         | 
| 82 113 | 
             
                end
         | 
| 83 114 | 
             
              end
         | 
| 84 115 | 
             
            end
         | 
| @@ -89,49 +89,29 @@ module Reek | |
| 89 89 | 
             
                def initialize(ctx, min_clump_size, max_copies)
         | 
| 90 90 | 
             
                  @min_clump_size = min_clump_size
         | 
| 91 91 | 
             
                  @max_copies = max_copies
         | 
| 92 | 
            -
                  @candidate_methods = ctx.local_nodes(:defn). | 
| 93 | 
            -
                     | 
| 94 | 
            -
                  end.map {|defn_node| CandidateMethod.new(defn_node)}
         | 
| 95 | 
            -
                  delete_infrequent_parameters
         | 
| 96 | 
            -
                  delete_small_methods
         | 
| 92 | 
            +
                  @candidate_methods = ctx.local_nodes(:defn).map {|defn_node|
         | 
| 93 | 
            +
                    CandidateMethod.new(defn_node)}
         | 
| 97 94 | 
             
                end
         | 
| 98 95 |  | 
| 99 | 
            -
                def  | 
| 100 | 
            -
                   | 
| 101 | 
            -
                     | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
                    end
         | 
| 106 | 
            -
                  end
         | 
| 96 | 
            +
                def candidate_clumps
         | 
| 97 | 
            +
                  @candidate_methods.each_cons(@max_copies + 1).map do |methods|
         | 
| 98 | 
            +
                    common_argument_names_for(methods)
         | 
| 99 | 
            +
                  end.select do |clump|
         | 
| 100 | 
            +
                    clump.length >= @min_clump_size
         | 
| 101 | 
            +
                  end.uniq
         | 
| 107 102 | 
             
                end
         | 
| 108 103 |  | 
| 109 | 
            -
                def  | 
| 110 | 
            -
                   | 
| 111 | 
            -
                  tail = methods[1..-1]
         | 
| 112 | 
            -
                  clumps_containing(methods[0], tail, results)
         | 
| 113 | 
            -
                  collect_clumps_in(tail, results)
         | 
| 114 | 
            -
                end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                def clumps
         | 
| 117 | 
            -
                  results = Hash.new([])
         | 
| 118 | 
            -
                  collect_clumps_in(@candidate_methods, results)
         | 
| 119 | 
            -
                  results.each_key { |key| results[key].uniq! }
         | 
| 120 | 
            -
                  results
         | 
| 104 | 
            +
                def common_argument_names_for(methods)
         | 
| 105 | 
            +
                  methods.collect(&:arg_names).inject(:&)
         | 
| 121 106 | 
             
                end
         | 
| 122 107 |  | 
| 123 | 
            -
                def  | 
| 124 | 
            -
                  @candidate_methods | 
| 125 | 
            -
                    meth.arg_names.length >= @min_clump_size
         | 
| 126 | 
            -
                  end
         | 
| 108 | 
            +
                def methods_containing_clump(clump)
         | 
| 109 | 
            +
                  @candidate_methods.select { |method| clump & method.arg_names == clump }
         | 
| 127 110 | 
             
                end
         | 
| 128 111 |  | 
| 129 | 
            -
                def  | 
| 130 | 
            -
                   | 
| 131 | 
            -
                     | 
| 132 | 
            -
                      occurs = @candidate_methods.inject(0) {|sum, cm| cm.arg_names.include?(param) ? sum+1 : sum}
         | 
| 133 | 
            -
                      meth.delete(param) if occurs <= @max_copies
         | 
| 134 | 
            -
                    end
         | 
| 112 | 
            +
                def clumps
         | 
| 113 | 
            +
                  candidate_clumps.map do |clump|
         | 
| 114 | 
            +
                    [clump, methods_containing_clump(clump)]
         | 
| 135 115 | 
             
                  end
         | 
| 136 116 | 
             
                end
         | 
| 137 117 | 
             
              end
         | 
| @@ -148,10 +128,6 @@ module Reek | |
| 148 128 | 
             
                  @params
         | 
| 149 129 | 
             
                end
         | 
| 150 130 |  | 
| 151 | 
            -
                def delete(param)
         | 
| 152 | 
            -
                  @params.delete(param)
         | 
| 153 | 
            -
                end
         | 
| 154 | 
            -
             | 
| 155 131 | 
             
                def line
         | 
| 156 132 | 
             
                  @defn.line
         | 
| 157 133 | 
             
                end
         | 
| @@ -18,7 +18,6 @@ module Reek | |
| 18 18 | 
             
                #   end
         | 
| 19 19 | 
             
                #
         | 
| 20 20 | 
             
                class DuplicateMethodCall < SmellDetector
         | 
| 21 | 
            -
             | 
| 22 21 | 
             
                  SMELL_CLASS = 'Duplication'
         | 
| 23 22 | 
             
                  SMELL_SUBCLASS = self.name.split(/::/)[-1]
         | 
| 24 23 |  | 
| @@ -51,39 +50,90 @@ module Reek | |
| 51 50 | 
             
                  # @return [Array<SmellWarning>]
         | 
| 52 51 | 
             
                  #
         | 
| 53 52 | 
             
                  def examine_context(ctx)
         | 
| 54 | 
            -
                     | 
| 55 | 
            -
                     | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
                      smell = SmellWarning.new(SMELL_CLASS, ctx.full_name, copies.map {|exp| exp.line},
         | 
| 63 | 
            -
                        "calls #{call} #{multiple}",
         | 
| 64 | 
            -
                        @source, SMELL_SUBCLASS,
         | 
| 65 | 
            -
                        {CALL_KEY => call, OCCURRENCES_KEY => occurs})
         | 
| 66 | 
            -
                      smell
         | 
| 53 | 
            +
                    max_allowed_calls = value(MAX_ALLOWED_CALLS_KEY, ctx, DEFAULT_MAX_CALLS)
         | 
| 54 | 
            +
                    allow_calls = value(ALLOW_CALLS_KEY, ctx, DEFAULT_ALLOW_CALLS)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    CallCollector.new(ctx, max_allowed_calls, allow_calls).smelly_calls.map do |found_call|
         | 
| 57 | 
            +
                      SmellWarning.new(SMELL_CLASS, ctx.full_name, found_call.lines,
         | 
| 58 | 
            +
                                       found_call.smell_message,
         | 
| 59 | 
            +
                                       @source, SMELL_SUBCLASS,
         | 
| 60 | 
            +
                                       {CALL_KEY => found_call.call, OCCURRENCES_KEY => found_call.occurs})
         | 
| 67 61 | 
             
                    end
         | 
| 68 62 | 
             
                  end
         | 
| 69 63 |  | 
| 70 | 
            -
             | 
| 64 | 
            +
                  # Collects information about a single found call
         | 
| 65 | 
            +
                  class FoundCall
         | 
| 66 | 
            +
                    def initialize(call_node)
         | 
| 67 | 
            +
                      @call_node = call_node
         | 
| 68 | 
            +
                      @occurences = []
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    def record(occurence)
         | 
| 72 | 
            +
                      @occurences.push occurence
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    def smell_message
         | 
| 76 | 
            +
                      multiple = occurs == 2 ? 'twice' : "#{occurs} times"
         | 
| 77 | 
            +
                      "calls #{call} #{multiple}"
         | 
| 78 | 
            +
                    end
         | 
| 71 79 |  | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
                    method_ctx.local_nodes(:call) do |call_node|
         | 
| 75 | 
            -
                      next if call_node.method_name == :new
         | 
| 76 | 
            -
                      next if call_node.receiver.nil? && call_node.args.empty?
         | 
| 77 | 
            -
                      result[call_node].push(call_node)
         | 
| 80 | 
            +
                    def call
         | 
| 81 | 
            +
                      @call ||= @call_node.format_ruby
         | 
| 78 82 | 
             
                    end
         | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 83 | 
            +
             | 
| 84 | 
            +
                    def occurs
         | 
| 85 | 
            +
                      @occurences.length
         | 
| 86 | 
            +
                    end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    def lines
         | 
| 89 | 
            +
                      @occurences.map {|exp| exp.line}
         | 
| 81 90 | 
             
                    end
         | 
| 82 | 
            -
                    result.to_a.sort_by {|call_exp, _| call_exp.format_ruby}
         | 
| 83 91 | 
             
                  end
         | 
| 84 92 |  | 
| 85 | 
            -
                   | 
| 86 | 
            -
             | 
| 93 | 
            +
                  # Collects all calls in a given context
         | 
| 94 | 
            +
                  class CallCollector
         | 
| 95 | 
            +
                    attr_reader :context
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    def initialize(context, max_allowed_calls, allow_calls)
         | 
| 98 | 
            +
                      @context = context
         | 
| 99 | 
            +
                      @max_allowed_calls = max_allowed_calls
         | 
| 100 | 
            +
                      @allow_calls = allow_calls
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    def calls
         | 
| 104 | 
            +
                      result = Hash.new {|hash,key| hash[key] = FoundCall.new(key)}
         | 
| 105 | 
            +
                      collect_calls(result)
         | 
| 106 | 
            +
                      collect_assignments(result)
         | 
| 107 | 
            +
                      result.values.sort_by {|found_call| found_call.call}
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    def smelly_calls
         | 
| 111 | 
            +
                      calls.select {|found_call| smelly_call? found_call }
         | 
| 112 | 
            +
                    end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                    private
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    def collect_assignments(result)
         | 
| 117 | 
            +
                      context.local_nodes(:attrasgn) do |asgn_node|
         | 
| 118 | 
            +
                        result[asgn_node].record(asgn_node) if asgn_node.args
         | 
| 119 | 
            +
                      end
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    def collect_calls(result)
         | 
| 123 | 
            +
                      context.local_nodes(:call) do |call_node|
         | 
| 124 | 
            +
                        next if call_node.method_name == :new
         | 
| 125 | 
            +
                        next if !call_node.receiver && call_node.args.empty?
         | 
| 126 | 
            +
                        result[call_node].record(call_node)
         | 
| 127 | 
            +
                      end
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    def smelly_call?(found_call)
         | 
| 131 | 
            +
                      found_call.occurs > @max_allowed_calls and not allow_calls?(found_call.call)
         | 
| 132 | 
            +
                    end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    def allow_calls?(method)
         | 
| 135 | 
            +
                      @allow_calls.any? { |allow| /#{allow}/ === method }
         | 
| 136 | 
            +
                    end
         | 
| 87 137 | 
             
                  end
         | 
| 88 138 | 
             
                end
         | 
| 89 139 | 
             
              end
         |