lib/simplecov/filter.rb



# frozen_string_literal: true

module SimpleCov
  #
  # Base filter class. Inherit from this to create custom filters,
  # and overwrite the matches?(source_file) instance method
  #
  # # A sample class that rejects all source files.
  # class StupidFilter < SimpleCov::Filter
  #   def matches?(source_file)
  #     false
  #   end
  # end
  #
  class Filter
    attr_reader :filter_argument

    def initialize(filter_argument)
      @filter_argument = filter_argument
    end

    def matches?(_source_file)
      raise NotImplementedError, "The base filter class is not intended for direct use"
    end

    def self.build_filter(filter_argument)
      return filter_argument if filter_argument.is_a?(SimpleCov::Filter)

      class_for_argument(filter_argument).new(filter_argument)
    end

    def self.class_for_argument(filter_argument)
      filter_classes_by_argument_type.find { |type, _| filter_argument.is_a?(type) }&.last ||
        raise(SimpleCov::ConfigurationError, "You have provided an unrecognized filter type")
    end

    def self.filter_classes_by_argument_type
      @filter_classes_by_argument_type ||= {
        String => SimpleCov::StringFilter,
        Regexp => SimpleCov::RegexFilter,
        Array => SimpleCov::ArrayFilter,
        Proc => SimpleCov::BlockFilter
      }.freeze
    end
    private_class_method :filter_classes_by_argument_type
  end

  # Filter that matches when the source file's project path contains the
  # configured string at a path-segment boundary.
  class StringFilter < SimpleCov::Filter
    # Returns true when the given source file's filename matches the
    # string configured when initializing this Filter with StringFilter.new('somestring').
    # Matching is path-segment-aware: the argument must appear immediately after a "/"
    # and be followed by "/" or end-of-string, so "lib" matches "/lib/foo.rb" but not
    # "/app/models/library.rb".
    def matches?(source_file)
      source_file.project_filename.match?(segment_pattern)
    end

  private

    def segment_pattern
      @segment_pattern ||= compute_segment_pattern
    end

    def compute_segment_pattern
      normalized = filter_argument.delete_prefix("/")
      escaped    = Regexp.escape(normalized)
      boundary   = '(?:\A|/)'

      if normalized.include?(".")
        # Filename pattern (e.g. "test.rb" matches "faked_test.rb"): allow
        # substring match within the last path segment, anchored to a
        # segment boundary.
        %r{#{boundary}[^/]*#{escaped}}
      elsif normalized.end_with?("/")
        # Trailing slash signals directory-only matching.
        /#{boundary}#{escaped}/
      else
        # Directory or path: require a segment-boundary match so "lib"
        # matches "lib/" but not "library/".
        %r{#{boundary}#{escaped}(?=[/.]|\z)}
      end
    end
  end

  # Filter that matches when the source file's project path matches the
  # configured Regexp.
  class RegexFilter < SimpleCov::Filter
    # Returns true when the given source file's filename matches the
    # regex configured when initializing this Filter with RegexFilter.new(/someregex/).
    # Uses `Regexp#match?` so the predicate returns a real boolean — `=~`
    # would return the match position (an Integer or nil), which trips
    # rspec-mocks 4's stricter predicate-matcher type check.
    def matches?(source_file)
      filter_argument.match?(source_file.project_filename)
    end
  end

  # Filter that matches when the configured block returns truthy for the
  # source file.
  class BlockFilter < SimpleCov::Filter
    # Returns true if the block given when initializing this filter with BlockFilter.new {|src_file| ... }
    # returns true for the given source file.
    def matches?(source_file)
      filter_argument.call(source_file)
    end
  end

  # Filter that matches when the source file's project path matches the
  # configured shell glob (e.g. "lib/**/*.rb"). Used by `cover` and
  # `skip` when callers want glob semantics instead of the substring
  # match of `StringFilter`.
  class GlobFilter < SimpleCov::Filter
    def matches?(source_file)
      File.fnmatch?(filter_argument, source_file.project_filename, File::FNM_PATHNAME | File::FNM_EXTGLOB)
    end
  end

  # Filter that matches when any of its component filters (built from the
  # array's elements) match the source file.
  class ArrayFilter < SimpleCov::Filter
    def initialize(filter_argument)
      filter_objects = filter_argument.map do |arg|
        Filter.build_filter(arg)
      end

      super(filter_objects)
    end

    # Returns true if any of the filters in the array match the given source file.
    # Configure this Filter like StringFilter.new(['some/path', /^some_regex/, Proc.new {|src_file| ... }])
    def matches?(source_files_list)
      filter_argument.any? do |arg|
        arg.matches?(source_files_list)
      end
    end
  end
end