Skip to content

Commit

Permalink
GlobGitignore doesn't preprocess patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
robotdana committed Nov 23, 2023
1 parent c22b18d commit 81d2e32
Show file tree
Hide file tree
Showing 10 changed files with 580 additions and 95 deletions.
2 changes: 2 additions & 0 deletions .spellr_wordlists/english.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,15 @@ torvolds
tsx
ttributes
txt
umc
unanchorable
unc
unexpandable
unfuck
unnegated
unrecursive
unstaged
unstub
untr
upcase
urrent
Expand Down
2 changes: 1 addition & 1 deletion lib/path_list/autoloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def autoload(klass)
def class_from_path(path)
name = ::File.basename(path).delete_suffix('.rb')

if name == 'version' || name == 'expandable_path'
if name == 'version' || name == 'expandable_path' || name == 'scanner'
name.upcase
else
name.gsub(/(?:^|_)(\w)/, &:upcase).delete('_')
Expand Down
59 changes: 34 additions & 25 deletions lib/path_list/pattern_parser/gitignore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ class PatternParser
class Gitignore
Autoloader.autoload(self)

SCANNER = RuleScanner

# @api private
# @param pattern [String]
# @param polarity [:ignore, :allow]
# @param root [String]
def initialize(pattern, polarity, root)
@s = RuleScanner.new(pattern)
@s = self.class::SCANNER.new(pattern)
@default_polarity = polarity
@rule_polarity = polarity
@root = root
Expand Down Expand Up @@ -60,6 +62,7 @@ def prepare_regexp_builder
end

@start_any_dir_position = @re.length - 1
@re.delete_at(@start_any_dir_position) if @root && @anchored
end

def break!
Expand Down Expand Up @@ -94,7 +97,7 @@ def dir_only?

def anchored!
@anchored ||= begin
@re.delete_at(@start_any_dir_position)
@re.delete_at(@start_any_dir_position) if defined?(@re)
true
end
end
Expand All @@ -121,8 +124,8 @@ def emit_end
break!
end

def process_backslash
return unless @s.backslash?
def process_escape
return unless @s.escape?

if @re.append_string(@s.next_character)
emitted!
Expand All @@ -142,7 +145,7 @@ def process_character_class

until @s.character_class_end?
next if process_character_class_range
next if process_backslash
next if process_escape
next if append_string(@s.character_class_literal)

unmatchable_rule!
Expand All @@ -158,11 +161,9 @@ def process_character_class_range
start = @s.character_class_range_start
return unless start

start = start.delete_prefix('\\')

append_string(start)

finish = @s.character_class_range_end.delete_prefix('\\')
finish = @s.character_class_range_end

return true unless start < finish

Expand All @@ -184,31 +185,39 @@ def process_rule
catch :abort_build do
blank! if @s.hash?
negated! if @s.exclamation_mark?
prepare_regexp_builder
anchored! if !@anchored && @s.slash?
process_first_characters

catch :break do
loop do
next if process_backslash
next unmatchable_rule! if @s.star_star_slash_slash?
next append_part(:any) && dir_only! if @s.star_star_slash_end?
next append_part(:any_dir) && anchored! if @s.star_star_slash?
next unmatchable_rule! if @s.slash_slash?
next append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end?
next append_part(:any_non_dir) if @s.star?
next dir_only! if @s.slash_end?
next append_part(:dir) && anchored! if @s.slash?
next append_part(:one_non_dir) if @s.question_mark?
next if process_character_class
next if append_string(@s.literal)
next if append_string(@s.significant_whitespace)

process_end
process_next_characters
end
end
end
end

def process_first_characters
prepare_regexp_builder
anchored! if !@anchored && @s.slash?
end

def process_next_characters
return if process_escape
return unmatchable_rule! if @s.star_star_slash_slash?
return append_part(:any) && dir_only! if @s.star_star_slash_end?
return append_part(:any_dir) && anchored! if @s.star_star_slash?
return unmatchable_rule! if @s.slash_slash?
return append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end?
return append_part(:any_non_dir) if @s.star?
return dir_only! if @s.slash_end?
return append_part(:dir) && anchored! if @s.slash?
return append_part(:one_non_dir) if @s.question_mark?
return if process_character_class
return if append_string(@s.literal)
return if append_string(@s.significant_whitespace)

process_end
end

def build_matcher
@main_re ||= @re.dup.compress

Expand Down
46 changes: 43 additions & 3 deletions lib/path_list/pattern_parser/gitignore/rule_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,53 @@ def slash?
skip(%r{/})
end

# @return [String, nil]
def root
matched if scan(%r{/})
end

# @return [String, nil]
def home
scan(%r{~[^/]*})
end

# @return [Boolean]
def dot_slash?
skip(%r{\./})
end

# @return [Boolean]
def dot_end?
skip(/\.\s*\z/)
end

# @return [Boolean]
def dot_slash_end?
skip(%r{\./\s*\z})
end

# @return [Boolean]
def dot_dot_slash_end?
skip(%r{\.\./\s*\z})
end

# @return [Boolean]
def dot_dot_slash?
skip(%r{\.\./})
end

# @return [Boolean]
def dot_dot_end?
skip(/\.\.\s*\z/)
end

# @return [Boolean]
def slash_end?
skip(%r{/\s*\z})
end

# @return [Boolean]
def backslash?
def escape?
skip(/\\/)
end

Expand Down Expand Up @@ -84,7 +124,7 @@ def character_class_literal

# @return [String, nil]
def character_class_range_start
matched if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/)
matched.delete_prefix('\\') if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/)
end

# @return [String, nil]
Expand All @@ -93,7 +133,7 @@ def character_class_range_end
# with the lookahead in character_class_range_start
skip(/-/)
scan(/(\\.|[^\\\]])/)
matched
matched.delete_prefix('\\')
end

# @return [String, nil]
Expand Down
108 changes: 108 additions & 0 deletions lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# frozen_string_literal: true

require 'strscan'

class PathList
class PatternParser
class Gitignore
# @api private
class WindowsRuleScanner < RuleScanner
# @return [Boolean]
def slash?
skip(%r{[\\/]})
end

# @return [String, nil]
def home
# not sure this makes sense on windows, but just for similarity
scan(%r{~[^/\\]*})
end

# @return [String, nil]
def root
# / or \ or UMC path or driver letter
matched if scan(%r{(?:[\\/]{1,2}|[a-zA-Z]:[\\/])})
end

# @return [Boolean]
def dot_slash?
skip(%r{\.[\\/]})
end

# @return [Boolean]
def dot_slash_end?
skip(%r{\.[\\/]\s*\z})
end

# @return [Boolean]
def dot_dot_slash_end?
skip(%r{\.\.[\\/]\s*\z})
end

# @return [Boolean]
def dot_dot_slash?
skip(%r{\.\.[\\/]})
end

# @return [Boolean]
def slash_end?
skip(%r{[\\/]\s*\z})
end

# @return [Boolean]
def escape?
skip(/`/)
end

# @return [Boolean]
def star_star_slash_end?
skip(%r{\*{2,}[\\/]\s*\z})
end

# @return [Boolean]
def star_star_slash_slash?
skip(%r{\*{2,}[\\/]{2}})
end

# @return [Boolean]
def slash_slash?
skip(%r{[\\/]{2}})
end

# @return [Boolean]
def star_star_slash?
skip(%r{\*{2,}[\\/]})
end

# @return [Boolean]
def slash_star_star_end?
skip(%r{[\\/]\*{2,}\s*\z})
end

# @return [String, nil]
def character_class_literal
matched if scan(/[^\]`][^\]`-]*(?!-)/)
end

# @return [String, nil]
def character_class_range_start
matched.delete_prefix('`') if scan(/(`.|[^`\]])(?=-(`.|[^`\]]))/)
end

# @return [String, nil]
def character_class_range_end
# we already confirmed this was going to match
# with the lookahead in character_class_range_start
skip(/-/)
scan(/(`.|[^`\]])/)
matched.delete_prefix('`')
end

# @return [String, nil]
def literal
matched if scan(%r{[^*[\\/]?\[`\s]+})
end
end
end
end
end
Loading

0 comments on commit 81d2e32

Please sign in to comment.