Module: Ace::Review::Molecules::SubjectFilter

Defined in:
lib/ace/review/molecules/subject_filter.rb

Overview

Pure module for filtering review subjects based on file patterns.

Provides file pattern matching logic for filtering diff content, file lists, and bundle sections. Uses glob patterns with include/exclude semantics following the standard: include patterns whitelist files, exclude patterns blacklist them.

Examples:

Filtering a diff

patterns = { "include" => ["lib/**/*.rb"], "exclude" => ["**/*_test.rb"] }
SubjectFilter.filter(diff_string, patterns)

Checking file match

SubjectFilter.matches_file?("lib/models/user.rb", patterns)
#=> true

Constant Summary collapse

FNMATCH_FLAGS =

File.fnmatch flags for glob pattern matching

File::FNM_PATHNAME | File::FNM_EXTGLOB

Class Method Summary collapse

Class Method Details

.filter(subject, file_patterns) ⇒ String, Hash

Filter subject content based on file patterns

Dispatches to appropriate filter method based on subject type. Returns subject unchanged if no patterns configured.

Parameters:

  • subject (String, Hash)

    Subject to filter (diff string or hash)

  • file_patterns (Hash, nil)

    File patterns with include/exclude arrays

Returns:

  • (String, Hash)

    Filtered subject



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/ace/review/molecules/subject_filter.rb', line 33

def self.filter(subject, file_patterns)
  return subject unless has_patterns?(file_patterns)

  case subject
  when String
    filter_diff(subject, file_patterns)
  when Hash
    filter_hash(subject, file_patterns)
  else
    subject
  end
end

.filter_bundle_sections(sections, file_patterns) ⇒ Hash

Filter bundle sections based on file patterns

Recursively filters files arrays within each section.

Parameters:

  • sections (Hash)

    Bundle sections

  • file_patterns (Hash)

    File patterns with include/exclude arrays

Returns:

  • (Hash)

    Filtered sections



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ace/review/molecules/subject_filter.rb', line 102

def self.filter_bundle_sections(sections, file_patterns)
  filtered = {}

  sections.each do |name, section|
    section = normalize_keys(section) if section.is_a?(Hash)

    if section.is_a?(Hash) && section["files"].is_a?(Array)
      filtered_files = section["files"].select { |f| matches_file?(f.to_s, file_patterns) }
      next if filtered_files.empty?

      filtered[name] = section.merge("files" => filtered_files)
    else
      filtered[name] = section
    end
  end

  filtered
end

.filter_diff(diff_content, file_patterns) ⇒ String

Filter diff content based on file patterns

Splits diff into per-file chunks, filters by patterns, and rejoins. Uses the destination (b/) path for consistency with renamed files.

Parameters:

  • diff_content (String)

    Git diff content

  • file_patterns (Hash)

    File patterns with include/exclude arrays

Returns:

  • (String)

    Filtered diff with only matching files



54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/ace/review/molecules/subject_filter.rb', line 54

def self.filter_diff(diff_content, file_patterns)
  return diff_content unless has_patterns?(file_patterns)

  # Split diff into file chunks
  chunks = split_diff_into_chunks(diff_content)

  # Filter chunks based on file patterns
  filtered_chunks = chunks.select do |chunk|
    file_path = extract_file_path_from_chunk(chunk)
    file_path ? matches_file?(file_path, file_patterns) : true
  end

  filtered_chunks.join
end

.filter_hash(subject, file_patterns) ⇒ Hash

Filter subject hash based on file patterns

Filters files arrays and bundle sections within the hash.

Parameters:

  • subject (Hash)

    Subject hash with files or sections

  • file_patterns (Hash)

    File patterns with include/exclude arrays

Returns:

  • (Hash)

    Filtered subject



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ace/review/molecules/subject_filter.rb', line 76

def self.filter_hash(subject, file_patterns)
  return subject unless has_patterns?(file_patterns)

  result = normalize_keys(subject.dup)

  # Filter files array if present
  if result["files"].is_a?(Array)
    result["files"] = result["files"].select { |f| matches_file?(f.to_s, file_patterns) }
  end

  # Filter bundle sections if present
  if result["bundle"].is_a?(Hash) && result["bundle"]["sections"].is_a?(Hash)
    result["bundle"] = normalize_keys(result["bundle"].dup)
    result["bundle"]["sections"] = filter_bundle_sections(result["bundle"]["sections"], file_patterns)
  end

  result
end

.has_patterns?(file_patterns) ⇒ Boolean

Check if file patterns are configured and non-empty

Parameters:

  • file_patterns (Hash, nil)

    File patterns hash

Returns:

  • (Boolean)

    True if patterns are configured



150
151
152
153
154
155
# File 'lib/ace/review/molecules/subject_filter.rb', line 150

def self.has_patterns?(file_patterns)
  return false unless file_patterns.is_a?(Hash)

  (file_patterns["include"].is_a?(Array) && file_patterns["include"].any?) ||
    (file_patterns["exclude"].is_a?(Array) && file_patterns["exclude"].any?)
end

.matches_file?(file_path, file_patterns) ⇒ Boolean

Check if a file path matches the given patterns

Include patterns: file must match at least one (if any exist) Exclude patterns: file must not match any

Parameters:

  • file_path (String)

    File path to check

  • file_patterns (Hash)

    File patterns with include/exclude arrays

Returns:

  • (Boolean)

    True if file matches patterns



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/ace/review/molecules/subject_filter.rb', line 129

def self.matches_file?(file_path, file_patterns)
  return true unless has_patterns?(file_patterns)

  includes = file_patterns["include"] || []
  excludes = file_patterns["exclude"] || []

  # If include patterns exist, file must match at least one
  if includes.any?
    return false unless includes.any? { |pattern| File.fnmatch?(pattern, file_path, FNMATCH_FLAGS) }
  end

  # File must not match any exclude pattern
  return false if excludes.any? { |pattern| File.fnmatch?(pattern, file_path, FNMATCH_FLAGS) }

  true
end