Module: Hecks::Bluebook::PatternSubset
- Defined in:
- lib/hecks/bluebook/pattern_subset.rb
Overview
WHICH REGEXES A BLUEBOOK MAY SAY.
A pattern: is a fact about a value, carried in a bluebook — declared
data, not Ruby code, so it must not lean on what any one engine happens
to accept. Regex engines disagree in two different ways :
ONLY A BACKTRACKING ENGINE CAN MATCH IT — lookahead, lookbehind,
backreferences, atomic groups, possessive quantifiers. None of these
can be matched in linear time, and linear-time engines refuse them
outright. Refused here for the same reason.
EVERY ENGINE PARSES IT AND THEY MEAN DIFFERENT THINGS — the dangerous
half, because nothing errors. `\d` `\w` `\s` are ASCII in some engines
and Unicode in others ; `[:digit:]` and friends flip the same way in
the other direction. Both families are refused, and a domain spells
the range it means.
What remains — explicit ranges, alternation, quantifiers, anchors,
groups — reads identically everywhere, with ^ and $ as LINE anchors
(Ruby's reading). The evidence is spec/corpus/fixtures/patterns.json.
Defined Under Namespace
Classes: Rejection
Constant Summary collapse
- REASONS =
{ backreference: "backreferences cannot be matched in linear time and portable " \ "engines refuse them ; a declared pattern may not depend on one", named_backreference: "a named backreference is still a backreference — it cannot be " \ "matched in linear time ; a declared pattern may not depend on one", perl_class: "engines read it in OPPOSITE directions : ASCII in some and " \ "Unicode in others, so an Arabic-Indic digit satisfies one and not " \ "the other. Spell the range you mean — [0-9], [A-Za-z0-9_], [ \t] " \ "— which every engine reads the same way", posix_class: "[:digit:] and friends flip between ASCII and Unicode across " \ "engines — the mirror of the perl classes, and wrong in the same " \ "way. Spell the range you mean", lookahead: "lookahead cannot be matched in linear time and portable engines " \ "refuse it ; a declared pattern may not depend on it", lookbehind: "lookbehind cannot be matched in linear time and portable engines " \ "refuse it ; a declared pattern may not depend on it", atomic_group: "an atomic group is a backtracking-engine control knob — " \ "linear-time engines reject `(?>` as a syntax error", possessive: "a possessive quantifier is a backtracking-engine control knob — " \ "linear-time engines reject it as a syntax error" }.freeze
- CONSTRUCTS =
SPELLED OUT, not derived from the key : these strings are the refusal a caller reads.
{ backreference: "backreference", named_backreference: "named backreference", perl_class: "perl character class", posix_class: "posix bracket class", lookahead: "lookahead", lookbehind: "lookbehind", atomic_group: "atomic group", possessive: "possessive quantifier" }.freeze
Class Method Summary collapse
- .posix_class_at?(chars, index) ⇒ Boolean
- .refuse(key) ⇒ Object
-
.validate(pattern) ⇒ Object
nil when the pattern is admitted, a Rejection when it is not.
Class Method Details
.posix_class_at?(chars, index) ⇒ Boolean
111 112 113 114 115 116 117 |
# File 'lib/hecks/bluebook/pattern_subset.rb', line 111 def posix_class_at?(chars, index) return false unless chars[index] == "[" && chars[index + 1] == ":" cursor = index + 2 cursor += 1 while chars[cursor]&.match?(/[a-zA-Z]/) chars[cursor] == ":" && chars[cursor + 1] == "]" end |
.refuse(key) ⇒ Object
109 |
# File 'lib/hecks/bluebook/pattern_subset.rb', line 109 def refuse(key) = Rejection.new(CONSTRUCTS.fetch(key), REASONS.fetch(key)) |
.validate(pattern) ⇒ Object
nil when the pattern is admitted, a Rejection when it is not.
A CHARACTER WALK, deliberately plain : the subset is defined by this
walk, and a cleverer spelling would hide what it admits. An escaped
construct is a LITERAL, not a violation — \(\?= is the three
characters "(?=" and says nothing about lookahead — which is why this
steps over each backslash pair rather than matching the pattern as a
whole.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/hecks/bluebook/pattern_subset.rb', line 66 def validate(pattern) chars = pattern.to_s.chars index = 0 while index < chars.length if chars[index] == "\\" nxt = chars[index + 1] return refuse(:backreference) if nxt&.match?(/[1-9]/) return refuse(:named_backreference) if %w[k g].include?(nxt) return refuse(:perl_class) if %w[d D w W s S].include?(nxt) index += nxt ? 2 : 1 next end if chars[index] == "(" && chars[index + 1] == "?" return refuse(:lookahead) if %w[= !].include?(chars[index + 2]) return refuse(:lookbehind) if chars[index + 2] == "<" && %w[= !].include?(chars[index + 3]) return refuse(:atomic_group) if chars[index + 2] == ">" end return refuse(:posix_class) if posix_class_at?(chars, index) return refuse(:possessive) if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+" index += 1 end nil end |