route: merge rule_set branches into outer rules

Treat rule_set items as merged branches instead of standalone boolean
sub-items.

Evaluate each branch inside a referenced rule-set as if it were merged
into the outer rule and keep OR semantics between branches. This lets
outer grouped fields satisfy matching groups inside a branch without
introducing a standalone outer fallback or cross-branch state union.

Keep inherited grouped state outside inverted default and logical
branches. Negated rule-set branches now evaluate !(...) against their
own conditions and only reapply the outer grouped match after negation
succeeds, so configs like outer-group && !inner-condition continue to
work.

Add regression tests for same-group merged matches, cross-group and
extra-AND failures, DNS merged-branch behaviour, and inverted merged
branches. Update the route and DNS rule docs to clarify that rule-set
branches merge into the outer rule while keeping OR semantics between
branches.
This commit is contained in:
世界
2026-03-25 10:32:09 +08:00
parent fcdea284e1
commit fdfd0ffb07
10 changed files with 308 additions and 32 deletions

View File

@@ -87,22 +87,40 @@ type ruleStateMatcher interface {
matchStates(metadata *adapter.InboundContext) ruleMatchStateSet
}
type ruleStateMatcherWithBase interface {
matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet
}
func matchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
return matchHeadlessRuleStatesWithBase(rule, metadata, 0)
}
func matchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := rule.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := rule.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata)
return matcher.matchStates(metadata).withBase(base)
}
if rule.Match(metadata) {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(base)
}
return 0
}
func matchRuleItemStates(item RuleItem, metadata *adapter.InboundContext) ruleMatchStateSet {
return matchRuleItemStatesWithBase(item, metadata, 0)
}
func matchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet {
if matcher, isStateMatcher := item.(ruleStateMatcherWithBase); isStateMatcher {
return matcher.matchStatesWithBase(metadata, base)
}
if matcher, isStateMatcher := item.(ruleStateMatcher); isStateMatcher {
return matcher.matchStates(metadata)
return matcher.matchStates(metadata).withBase(base)
}
if item.Match(metadata) {
return emptyRuleMatchState()
return emptyRuleMatchState().withBase(base)
}
return 0
}