@@ -529,12 +529,13 @@ func extractLiterals(re *syntax.Regexp, ci bool) interface{} {
529529// select|sleep|substr → s(?:elect|leep|ubstr)
530530// AST: OpConcat([OpLiteral("s"), OpAlternate([...])])
531531//
532- // It prepends the raw prefix literal to every element returned by
533- // rawExtractSuffixes, recovering the full literal words. The result is returned
534- // as anyRequired so the caller knows "at least one of these must be present".
532+ // It prepends the raw prefix literal to every string that leadingLiterals says
533+ // the alternation must begin with, recovering the full literal words. The result
534+ // is returned as anyRequired so the caller knows "at least one of these must be
535+ // present".
535536//
536- // Handles arbitrary nesting depth: rawExtractSuffixes recurses back into
537- // extractLiterals → trieReconstruct for inner OpConcat nodes , so patterns like
537+ // Handles arbitrary nesting depth: leadingLiterals recurses through nested
538+ // alternations and concatenations , so patterns like
538539//
539540// select|set|sleep → s(?:e(?:lect|t)|leep)
540541//
@@ -555,100 +556,104 @@ func trieReconstruct(concat *syntax.Regexp, ci bool) anyRequired {
555556 return nil
556557 }
557558
558- // Collect suffix candidates from remaining children using the permissive
559- // extractor that allows short literals (they will be combined with prefix).
560- var suffixes []string
561- for _ , sub := range concat .Sub [1 :] {
562- lits := rawExtractSuffixes (sub , ci )
563- if lits == nil {
564- continue
565- }
566- suffixes = append (suffixes , lits ... )
567- }
559+ // The second child directly follows the prefix, so every match of the
560+ // concat contains prefix+s for one of the strings s that the child must
561+ // begin with. Only text at the very start of the child can be glued to the
562+ // prefix: a literal further in (after a star, an optional group, a class...)
563+ // is not adjacent to it and would make a phantom string, e.g. the "60;" of
564+ // �*60; must not become "#60;" because the input "<" has no such
565+ // substring.
566+ suffixes := leadingLiterals (concat .Sub [1 ], ci )
568567 if len (suffixes ) == 0 {
569568 return nil
570569 }
571570
572571 result := make (anyRequired , 0 , len (suffixes ))
573572 for _ , s := range suffixes {
574- if full := prefix + s ; len (full ) >= 2 {
575- result = append (result , full )
573+ full := prefix + s
574+ if len (full ) < 2 {
575+ // Cannot happen (both parts are non-empty), but never drop an
576+ // element of an anyRequired set: that would reject its matches.
577+ return nil
576578 }
577- }
578- if len (result ) == 0 {
579- return nil
579+ result = append (result , full )
580580 }
581581 return result
582582}
583583
584- // rawExtractSuffixes extracts literal candidates from a regex subtree for use
585- // as trie-reconstruction suffixes. Unlike extractLiterals it permits short
586- // literals (< 2 bytes) because they will be combined with a prefix by the
587- // caller, producing a longer and more selective string.
584+ // maxLeadingLiterals bounds the number of strings leadingLiterals builds.
585+ const maxLeadingLiterals = anyRequiredMaxN
586+
587+ // leadingLiterals returns a set of non-empty strings such that every match of
588+ // re begins with at least one of them, or nil when it cannot say. Unlike
589+ // extractLiterals it permits short literals (< 2 bytes), because the caller
590+ // (trieReconstruct) combines them with a prefix into a longer string.
588591//
589- // Returns nil if any branch of an alternation has no extractable literal (same
590- // semantics as extractLiterals for OpAlternate: we can't safely omit a branch).
591- func rawExtractSuffixes (re * syntax.Regexp , ci bool ) []string {
592+ // The strings are anchored at the start of the match. That is what makes it safe
593+ // to glue them to a literal that directly precedes re: nothing may be skipped
594+ // between the two. A node that can consume other text first (a star, an optional
595+ // or class, a zero-width assertion) stops the walk, and so does anything that
596+ // is not a fixed string, after contributing what it knows its match starts with.
597+ //
598+ // An alternation needs a leading set from every branch; one branch without it
599+ // makes the whole alternation nil, as in extractLiterals, because that branch
600+ // could match without any of the strings.
601+ func leadingLiterals (re * syntax.Regexp , ci bool ) []string {
592602 switch re .Op {
593603 case syntax .OpLiteral :
594- s := rawLiteral (re , ci )
595- if s == "" {
596- return nil
604+ if s := rawLiteral (re , ci ); s != "" {
605+ return []string {s }
597606 }
598- return []string {s }
607+ return nil
608+
609+ case syntax .OpCapture , syntax .OpPlus :
610+ return leadingLiterals (re .Sub [0 ], ci )
611+
612+ case syntax .OpRepeat :
613+ if re .Min >= 1 {
614+ return leadingLiterals (re .Sub [0 ], ci )
615+ }
616+ return nil
599617
600618 case syntax .OpAlternate :
601- var result []string
619+ var out []string
602620 for _ , sub := range re .Sub {
603- lits := rawExtractSuffixes (sub , ci )
621+ lits := leadingLiterals (sub , ci )
604622 if lits == nil {
605- return nil // one branch has no extractable suffix → bail
623+ return nil
624+ }
625+ out = append (out , lits ... )
626+ if len (out ) > maxLeadingLiterals {
627+ return nil
606628 }
607- result = append (result , lits ... )
608629 }
609- return result
630+ return out
610631
611632 case syntax .OpConcat :
612- // Try the full extractLiterals pipeline first (handles deeper nesting
613- // through the trieReconstruct fallback it already calls).
614- lits := extractLiterals (re , ci )
615- if lits != nil {
616- switch v := lits .(type ) {
617- case allRequired :
618- // Only safe to return a single trie suffix when the concat
619- // collapses to exactly one contiguous literal. Multiple
620- // allRequired elements mean there are wildcards between them
621- // (e.g. "elect.*from" → allRequired{"elect","from"}). Joining
622- // them would produce "electfrom" — a phantom string that never
623- // appears contiguously in a real input — causing false negatives
624- // on valid matches like "select x from". Return nil here so the
625- // caller falls back to the safer anyRequired propagation instead.
626- if len (v ) == 1 {
627- return []string {v [0 ]}
628- }
629- return nil
630- case anyRequired :
631- return []string (v )
632- case combinedRequired :
633- // For trie-reconstruction we need a suffix that is *always* present
634- // when this sub-concat fires. The .all elements are guaranteed;
635- // .any elements are only conditionally present (one of them must be
636- // present, but not a specific one). Returning a .any element would
637- // let the outer prefix combine with a wrong suffix (e.g. "s"+"execute"
638- // instead of "s"+"p_"+"execute" → "sp_execute"), producing a phantom
639- // literal that never appears contiguously in real input.
640- // Return the single longest .all element as the guaranteed suffix.
641- rep := longest ([]string (v .all ))
642- if rep == "" {
643- return nil
633+ // Start from the empty string and extend it child by child. A fixed
634+ // literal lets the walk continue to the next child; any other child
635+ // ends it after contributing its own leading strings.
636+ acc := []string {"" }
637+ for _ , sub := range re .Sub {
638+ lits := leadingLiterals (sub , ci )
639+ if lits == nil || len (acc )* len (lits ) > maxLeadingLiterals {
640+ break
641+ }
642+ next := make ([]string , 0 , len (acc )* len (lits ))
643+ for _ , a := range acc {
644+ for _ , l := range lits {
645+ next = append (next , a + l )
644646 }
645- return []string {rep }
647+ }
648+ acc = next
649+ if sub .Op != syntax .OpLiteral {
650+ break
646651 }
647652 }
648- return nil
649-
650- case syntax . OpCapture :
651- return rawExtractSuffixes ( re . Sub [ 0 ], ci )
653+ if len ( acc ) == 1 && acc [ 0 ] == "" {
654+ return nil
655+ }
656+ return acc
652657
653658 default :
654659 return nil
0 commit comments