Class: Ace::Lint::Atoms::PatternMatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/lint/atoms/pattern_matcher.rb

Overview

Matches file paths against glob patterns with specificity scoring Used for determining which validator group applies to a file

Class Method Summary collapse

Class Method Details

.best_group_match(path, groups) ⇒ Array<Symbol, Hash>?

Find the best matching group for a path

Parameters:

  • path (String)

    File path to match

  • groups (Hash)

    Group name => { patterns: […], … }

Returns:

  • (Array<Symbol, Hash>, nil)
    group_name, group_config

    or nil



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/ace/lint/atoms/pattern_matcher.rb', line 73

def self.best_group_match(path, groups)
  return nil if groups.nil? || groups.empty?

  best_group = nil
  best_score = -Float::INFINITY

  groups.each do |name, config|
    patterns = config[:patterns] || config["patterns"] || []
    pattern = best_match(path, patterns)
    next unless pattern

    score = specificity(pattern)
    if score > best_score
      best_score = score
      best_group = [name.to_sym, config]
    end
  end

  best_group
end

.best_match(path, patterns) ⇒ String?

Find the best matching pattern for a path

Parameters:

  • path (String)

    File path to match

  • patterns (Array<String>)

    List of patterns to check

Returns:

  • (String, nil)

    Best matching pattern or nil if no match



59
60
61
62
63
64
65
66
67
# File 'lib/ace/lint/atoms/pattern_matcher.rb', line 59

def self.best_match(path, patterns)
  return nil if patterns.nil? || patterns.empty?

  matching = patterns.select { |p| matches?(path, p) }
  return nil if matching.empty?

  # Return pattern with highest specificity
  matching.max_by { |p| specificity(p) }
end

.matches?(path, pattern) ⇒ Boolean

Check if a path matches a pattern

Parameters:

  • path (String)

    File path to check

  • pattern (String)

    Glob pattern

Returns:

  • (Boolean)

    True if path matches pattern



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ace/lint/atoms/pattern_matcher.rb', line 43

def self.matches?(path, pattern)
  return false if path.nil? || pattern.nil?

  # Normalize path (remove leading ./)
  normalized_path = path.sub(%r{^\./}, "")

  # File.fnmatch with FNM_PATHNAME for proper ** handling
  # FNM_EXTGLOB for brace expansion {rb,rake}
  File.fnmatch(pattern, normalized_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
    File.fnmatch(pattern, File.basename(normalized_path), File::FNM_PATHNAME | File::FNM_EXTGLOB)
end

.specificity(pattern) ⇒ Integer

Score a pattern based on specificity (higher = more specific)

Parameters:

  • pattern (String)

    Glob pattern to score

Returns:

  • (Integer)

    Specificity score



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/ace/lint/atoms/pattern_matcher.rb', line 12

def self.specificity(pattern)
  return 0 if pattern.nil? || pattern.empty?

  score = 0

  # Exact filename match (no glob chars) gets highest score
  unless pattern.include?("*") || pattern.include?("?") || pattern.include?("[")
    return 1000 + pattern.length
  end

  # Directory depth: +100 per path segment
  score += pattern.count("/") * 100

  # Double-star penalty: -50 per **
  score -= pattern.scan("**").count * 50

  # Single-star bonus: +10 per * (but not **)
  single_stars = pattern.gsub("**", "").count("*")
  score += single_stars * 10

  # Literal prefix length: +1 per char before first glob
  literal_prefix = pattern.split(/[*?\[]/).first || ""
  score += literal_prefix.length

  score
end