This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 7cd2c9a71c hrw4u: Fix section validations and operators (#12820)
7cd2c9a71c is described below

commit 7cd2c9a71cd6106687f10f1f2c9c2cef76fa4980
Author: Leif Hedstrom <[email protected]>
AuthorDate: Tue Jan 20 13:52:11 2026 -0700

    hrw4u: Fix section validations and operators (#12820)
    
    * hrw4u: Fix section validations and operators
    
    * Fixes per review
---
 tools/hrw4u/.gitignore                             |   3 +-
 tools/hrw4u/pyproject.toml                         |   2 +-
 tools/hrw4u/src/lsp/completions.py                 |   8 +-
 tools/hrw4u/src/symbols.py                         |  14 +--
 tools/hrw4u/src/symbols_base.py                    |   4 +-
 tools/hrw4u/src/tables.py                          | 114 +++++++++++----------
 tools/hrw4u/src/visitor.py                         |   5 +-
 .../hrw4u/tests/data/examples/all-nonsense.ast.txt |   2 +-
 .../tests/data/examples/all-nonsense.input.txt     |   2 +-
 .../tests/data/examples/all-nonsense.output.txt    |   2 +-
 .../data/hooks/inbound_resp_section.fail.error.txt |   3 +
 .../data/hooks/inbound_resp_section.fail.input.txt |   3 +
 .../hooks/outbound_resp_section.fail.error.txt     |   3 +
 .../hooks/outbound_resp_section.fail.input.txt     |   3 +
 tools/hrw4u/tests/data/ops/skip-remap.ast.txt      |   2 +-
 tools/hrw4u/tests/data/ops/skip-remap.input.txt    |   2 +-
 tools/hrw4u/tests/data/ops/skip-remap.output.txt   |   2 +-
 .../data/ops/skip_remap_quoted_bool.fail.input.txt |   2 +-
 tools/hrw4u/tests/test_lsp.py                      |  42 +++-----
 19 files changed, 115 insertions(+), 103 deletions(-)

diff --git a/tools/hrw4u/.gitignore b/tools/hrw4u/.gitignore
index 5df9179ad7..c61b1049d7 100644
--- a/tools/hrw4u/.gitignore
+++ b/tools/hrw4u/.gitignore
@@ -1,2 +1,3 @@
 build/
-dist/
\ No newline at end of file
+dist/
+uv.lock
diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml
index bf6babdf57..ce607fd5b6 100644
--- a/tools/hrw4u/pyproject.toml
+++ b/tools/hrw4u/pyproject.toml
@@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "hrw4u"
-version = "1.4.0"
+version = "1.4.1"
 description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
 authors = [
     {name = "Leif Hedstrom", email = "[email protected]"}
diff --git a/tools/hrw4u/src/lsp/completions.py 
b/tools/hrw4u/src/lsp/completions.py
index 1e8cf04197..42771ee66f 100644
--- a/tools/hrw4u/src/lsp/completions.py
+++ b/tools/hrw4u/src/lsp/completions.py
@@ -100,7 +100,7 @@ class CompletionBuilder:
             cls, label: str, commands: str | list[str] | tuple[str, ...], 
sections: set[SectionType] | None,
             current_section: SectionType | None, replacement_range: dict[str, 
Any]) -> CompletionItem | None:
         """Create completion item for operators."""
-        if sections and current_section and current_section in sections:
+        if sections and current_section and current_section not in sections:
             return None
 
         cmd_str = commands if isinstance(commands, str) else " / 
".join(commands)
@@ -108,7 +108,7 @@ class CompletionBuilder:
 
         if sections:
             section_names = [s.value for s in sections]
-            detail += f" (Restricted in: {', '.join(section_names)})"
+            detail += f" (Available in: {', '.join(section_names)})"
 
         documentation = cls.create_markdown_doc(f"**{label}** - HRW4U 
Operator\n\nMaps to: `{cmd_str}`")
 
@@ -127,14 +127,14 @@ class CompletionBuilder:
             cls, label: str, tag: str, sections: set[SectionType] | None, 
current_section: SectionType | None,
             replacement_range: dict[str, Any]) -> CompletionItem | None:
         """Create completion item for conditions."""
-        if sections and current_section and current_section in sections:
+        if sections and current_section and current_section not in sections:
             return None
 
         detail = f"Condition: {tag}"
 
         if sections:
             section_names = [s.value for s in sections]
-            detail += f" (Restricted in: {', '.join(section_names)})"
+            detail += f" (Available in: {', '.join(section_names)})"
 
         documentation = cls.create_markdown_doc(f"**{label}** - HRW4U 
Condition\n\nMaps to: `{tag}`")
 
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index c40010c64d..bb5210923f 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -140,8 +140,8 @@ class SymbolResolver(SymbolResolverBase):
 
             if params := self._lookup_condition_cached(name):
                 tag = params.target if params else None
-                restricted = params.sections if params else None
-                self.validate_section_access(name, section, restricted)
+                allowed_sections = params.sections if params else None
+                self.validate_section_access(name, section, allowed_sections)
                 # For exact matches, default_expr is determined by whether 
it's a prefix pattern
                 return tag, False
 
@@ -150,9 +150,9 @@ class SymbolResolver(SymbolResolverBase):
             for prefix, params in prefix_matches:
                 tag = params.target if params else None
                 validator = params.validate if params else None
-                restricted = params.sections if params else None
+                allowed_sections = params.sections if params else None
 
-                self.validate_section_access(name, section, restricted)
+                self.validate_section_access(name, section, allowed_sections)
                 suffix = name[len(prefix):]
                 suffix_norm = suffix.upper() if (params and params.upper) else 
suffix
                 if validator:
@@ -190,9 +190,11 @@ class SymbolResolver(SymbolResolverBase):
                 error.add_symbol_suggestion(suggestions)
             raise error
 
-    def resolve_statement_func(self, func_name: str, args: list[str]) -> str:
-        with self.debug_context("resolve_statement_func", func_name, args):
+    def resolve_statement_func(self, func_name: str, args: list[str], section: 
SectionType | None = None) -> str:
+        with self.debug_context("resolve_statement_func", func_name, args, 
section):
             if params := self._lookup_statement_function_cached(func_name):
+                allowed_sections = params.sections if params else None
+                self.validate_section_access(func_name, section, 
allowed_sections)
                 command = params.target
                 validator = params.validate
                 if validator:
diff --git a/tools/hrw4u/src/symbols_base.py b/tools/hrw4u/src/symbols_base.py
index 5749065074..1fb95cd60c 100644
--- a/tools/hrw4u/src/symbols_base.py
+++ b/tools/hrw4u/src/symbols_base.py
@@ -57,8 +57,8 @@ class SymbolResolverBase:
     def _reverse_resolution_map(self) -> dict[str, Any]:
         return tables.REVERSE_RESOLUTION_MAP
 
-    def validate_section_access(self, name: str, section: SectionType | None, 
restricted: set[SectionType] | None) -> None:
-        if section and restricted and section in restricted:
+    def validate_section_access(self, name: str, section: SectionType | None, 
allowed_sections: set[SectionType] | None) -> None:
+        if section and allowed_sections and section not in allowed_sections:
             raise SymbolResolutionError(name, f"{name} is not available in the 
{section.value} section")
 
     @lru_cache(maxsize=256)
diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py
index 85d93394ba..94253fba0a 100644
--- a/tools/hrw4u/src/tables.py
+++ b/tools/hrw4u/src/tables.py
@@ -24,43 +24,51 @@ from hrw4u.types import MapParams, SuffixGroup
 from hrw4u.states import SectionType
 from hrw4u.common import HeaderOperations
 
+# Common section sets for validation
+# HTTP_SECTIONS: All hooks where HTTP transaction data is available (excludes 
TXN_START/TXN_CLOSE)
+HTTP_SECTIONS: Final[frozenset[SectionType]] = frozenset(
+    {
+        SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST, SectionType.READ_RESPONSE,
+        SectionType.SEND_RESPONSE
+    })
+
 # yapf: disable
 OPERATOR_MAP: dict[str, MapParams] = {
-    "http.cntl.": MapParams(target="set-http-cntl", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS)),
-    "http.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple()),
-    "http.status": MapParams(target="set-status", validate=Validator.range(0, 
999)),
-    "inbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6)),
-    "inbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32)),
-    "outbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
-    "inbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token()),
-    "inbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name()),
-    "inbound.resp.body": MapParams(target="set-body", 
validate=Validator.quoted_or_simple()),
-    "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name()),
-    "inbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple()),
-    "inbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999)),
-    "inbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
-    "outbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST})
+    "http.cntl.": MapParams(target="set-http-cntl", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS), 
sections=HTTP_SECTIONS),
+    "http.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS),
+    "http.status": MapParams(target="set-status", validate=Validator.range(0, 
999), sections=HTTP_SECTIONS),
+    "inbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6), sections=HTTP_SECTIONS),
+    "inbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32), sections=HTTP_SECTIONS),
+    "outbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
+    "outbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
+    "inbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token(), sections=HTTP_SECTIONS),
+    "inbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections=HTTP_SECTIONS),
+    "inbound.resp.body": MapParams(target="set-body", 
validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS),
+    "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "inbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS),
+    "inbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999), sections=HTTP_SECTIONS),
+    "inbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
+    "outbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
+    "outbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
+    "outbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS),
+    "outbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999), sections=HTTP_SECTIONS),
+    "outbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST})
 }
 
 STATEMENT_FUNCTION_MAP: dict[str, MapParams] = {
-    "add-header": MapParams(target="add-header", 
validate=Validator.arg_count(2).arg_at(0, 
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple())),
+    "add-header": MapParams(target="add-header", 
validate=Validator.arg_count(2).arg_at(0, 
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple()), 
sections=HTTP_SECTIONS),
     "counter": MapParams(target="counter", 
validate=Validator.arg_count(1).quoted_or_simple()),
     "set-debug": MapParams(target="set-debug", 
validate=Validator.arg_count(0)),
     "no-op": MapParams(target="no-op", validate=Validator.arg_count(0)),
-    "remove_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple()),
-    "keep_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple()),
-    "run-plugin": MapParams(target="run-plugin", 
validate=Validator.min_args(1).quoted_or_simple()),
-    "set-body-from": MapParams(target="set-body-from", 
validate=Validator.arg_count(1).quoted_or_simple()),
-    "set-config": MapParams(target="set-config", 
validate=Validator.arg_count(2).quoted_or_simple()),
-    "set-redirect": MapParams(target="set-redirect", 
validate=Validator.arg_count(2).arg_at(0, Validator.range(300, 399)).arg_at(1, 
Validator.quoted_or_simple())),
-    "skip-remap": MapParams(target="skip-remap", 
validate=Validator.arg_count(1).suffix_group(SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0))),
-    "set-plugin-cntl": MapParams(target="set-plugin-cntl", 
validate=Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(0, 
Validator.suffix_group(SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(Validator.conditional_arg_validation(SuffixGroup.PLUGIN_CNTL_MAPPING.value))),
+    "remove_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS),
+    "keep_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS),
+    "run-plugin": MapParams(target="run-plugin", 
validate=Validator.min_args(1).quoted_or_simple(), sections=HTTP_SECTIONS),
+    "set-body-from": MapParams(target="set-body-from", 
validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS),
+    "set-config": MapParams(target="set-config", 
validate=Validator.arg_count(2).quoted_or_simple(), sections=HTTP_SECTIONS),
+    "set-redirect": MapParams(target="set-redirect", 
validate=Validator.arg_count(2).arg_at(0, Validator.range(300, 399)).arg_at(1, 
Validator.quoted_or_simple()), sections=HTTP_SECTIONS),
+    "skip-remap": MapParams(target="skip-remap", 
validate=Validator.arg_count(1).suffix_group(SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0)),
 sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "set-plugin-cntl": MapParams(target="set-plugin-cntl", 
validate=Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(0, 
Validator.suffix_group(SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(Validator.conditional_arg_validation(SuffixGroup.PLUGIN_CNTL_MAPPING.value)),
 sections=HTTP_SECTIONS),
 }
 
 FUNCTION_MAP: dict[str, MapParams] = {
@@ -76,21 +84,21 @@ FUNCTION_MAP: dict[str, MapParams] = {
 CONDITION_MAP: dict[str, MapParams] = {
     # Exact matches with reverse mapping info
     "inbound.ip": MapParams(target="%{IP:CLIENT}", rev={"reverse_tag": "IP", 
"reverse_payload": "CLIENT"}),
-    "inbound.method": MapParams(target="%{METHOD}", rev={"reverse_tag": 
"METHOD", "ambiguous": True}),
+    "inbound.method": MapParams(target="%{METHOD}", sections=HTTP_SECTIONS, 
rev={"reverse_tag": "METHOD", "ambiguous": True}),
     "inbound.server": MapParams(target="%{IP:INBOUND}", rev={"reverse_tag": 
"IP", "reverse_payload": "INBOUND"}),
-    "inbound.status": MapParams(target="%{STATUS}", rev={"reverse_tag": 
"STATUS", "ambiguous": True}),
+    "inbound.status": MapParams(target="%{STATUS}", sections=HTTP_SECTIONS, 
rev={"reverse_tag": "STATUS", "ambiguous": True}),
     "now": MapParams(target="%{NOW}"),
-    "outbound.ip": MapParams(target="%{IP:SERVER}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "IP", "reverse_payload": "SERVER"}),
-    "outbound.method": MapParams(target="%{METHOD}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "METHOD", "ambiguous": True}),
-    "outbound.server": MapParams(target="%{IP:OUTBOUND}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "IP", "reverse_payload": "OUTBOUND"}),
-    "outbound.status": MapParams(target="%{STATUS}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST}, rev={"reverse_tag": "STATUS", "ambiguous": True}),
+    "outbound.ip": MapParams(target="%{IP:SERVER}", 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_tag": "IP", "reverse_payload": 
"SERVER"}),
+    "outbound.method": MapParams(target="%{METHOD}", 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_tag": "METHOD", "ambiguous": True}),
+    "outbound.server": MapParams(target="%{IP:OUTBOUND}", 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_tag": "IP", "reverse_payload": 
"OUTBOUND"}),
+    "outbound.status": MapParams(target="%{STATUS}", 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_tag": "STATUS", "ambiguous": True}),
     "tcp.info": MapParams(target="%{TCP-INFO}"),
 
     # Prefix matches
     "capture.": MapParams(target="LAST-CAPTURE", prefix=True, 
validate=Validator.range(0, 9)),
-    "from.url.": MapParams(target="FROM-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "from.url.": MapParams(target="FROM-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
     "geo.": MapParams(target="GEO", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.GEO_FIELDS)),
-    "http.cntl.": MapParams(target="HTTP-CNTL", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS)),
+    "http.cntl.": MapParams(target="HTTP-CNTL", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS), 
sections=HTTP_SECTIONS),
     "id.": MapParams(target="ID", upper=True, 
validate=Validator.suffix_group(SuffixGroup.ID_FIELDS)),
     "inbound.conn.client-cert.SAN.": 
MapParams(target="INBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
     "inbound.conn.server-cert.SAN.": 
MapParams(target="INBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
@@ -99,23 +107,23 @@ CONDITION_MAP: dict[str, MapParams] = {
     "inbound.conn.client-cert.": MapParams(target="INBOUND:CLIENT-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
     "inbound.conn.server-cert.": MapParams(target="INBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
     "inbound.conn.": MapParams(target="INBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS)),
-    "inbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), rev={"reverse_fallback": "inbound.cookie."}),
-    "inbound.req.": MapParams(target="CLIENT-HEADER", prefix=True, 
validate=Validator.http_header_name(), rev={"reverse_fallback": 
"inbound.req."}),
-    "inbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), rev={"reverse_context": 
"header_condition"}),
-    "inbound.url.": MapParams(target="CLIENT-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "inbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), sections=HTTP_SECTIONS, 
rev={"reverse_fallback": "inbound.cookie."}),
+    "inbound.req.": MapParams(target="CLIENT-HEADER", prefix=True, 
validate=Validator.http_header_name(), sections=HTTP_SECTIONS, 
rev={"reverse_fallback": "inbound.req."}),
+    "inbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_context": "header_condition"}),
+    "inbound.url.": MapParams(target="CLIENT-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
     "now.": MapParams(target="NOW", upper=True, 
validate=Validator.suffix_group(SuffixGroup.DATE_FIELDS)),
-    "outbound.conn.client-cert.SAN.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.server-cert.SAN.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.client-cert.san.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.server-cert.san.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.client-cert.": MapParams(target="OUTBOUND:CLIENT-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_fallback": 
"inbound.cookie."}),
-    "outbound.req.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_context": 
"header_condition"}),
-    "outbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}, 
rev={"reverse_context": "header_condition"}),
-    "outbound.url.": MapParams(target="NEXT-HOP", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "to.url.": MapParams(target="TO-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "outbound.conn.client-cert.SAN.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.server-cert.SAN.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.client-cert.san.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.server-cert.san.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.client-cert.": MapParams(target="OUTBOUND:CLIENT-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": 
"inbound.cookie."}),
+    "outbound.req.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_context": 
"header_condition"}),
+    "outbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_context": "header_condition"}),
+    "outbound.url.": MapParams(target="NEXT-HOP", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "to.url.": MapParams(target="TO-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
 }
 
 FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = {
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index a3cbc285da..922187a6f7 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -162,7 +162,8 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
                     return replacement
                 if m.group("var"):
                     var_name = m.group("var").strip()
-                    replacement, _ = self._cached_symbol_resolution(var_name, 
self.current_section.value)
+                    # Use resolve_condition directly to properly validate 
section restrictions
+                    replacement, _ = 
self.symbol_resolver.resolve_condition(var_name, self.current_section)
                     self.debug_log(f"substitute: {{{var_name}}} -> 
{replacement}")
                     return replacement
                 raise SymbolResolutionError(m.group(0), "Unrecognized 
substitution format")
@@ -340,7 +341,7 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
                     subst_args = [
                         self._substitute_strings(arg, ctx) if 
arg.startswith('"') and arg.endswith('"') else arg for arg in args
                     ]
-                    symbol = self.symbol_resolver.resolve_statement_func(func, 
subst_args)
+                    symbol = self.symbol_resolver.resolve_statement_func(func, 
subst_args, self.current_section)
                     self.emit_statement(symbol)
                     return
 
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
index 92dbbd6345..bc4bfcfed1 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
@@ -1 +1 @@
-(program (programItem (section (varSection VARS { (variables (variablesItem 
(commentLine # Boolean and integer state you can flip/use across sections)) 
(variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl 
FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem 
(variableDecl Big16 : int16 ;))) }))) (programItem (section TXN_START { 
(sectionBody (commentLine # Plugin controls)) (sectionBody (statement 
http.cntl.TXN_DEBUG = (value true) ;)) (sectio [...]
+(program (programItem (section (varSection VARS { (variables (variablesItem 
(commentLine # Boolean and integer state you can flip/use across sections)) 
(variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl 
FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem 
(variableDecl Big16 : int16 ;))) }))) (programItem (section REMAP { 
(sectionBody (commentLine # Plugin controls)) (sectionBody (statement 
http.cntl.TXN_DEBUG = (value true) ;)) (sectionBod [...]
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
index f756ead1c7..e9d62ea221 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
@@ -6,7 +6,7 @@ VARS {
   Big16: int16;
 }
 
-TXN_START {
+REMAP {
   # Plugin controls
   http.cntl.TXN_DEBUG = true;
   http.cntl.LOGGING = true;
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
index ca3bf8e2c5..0134d0aa96 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
@@ -1,7 +1,7 @@
 # Boolean and integer state you can flip/use across sections
 
 
-cond %{TXN_START_HOOK} [AND]
+cond %{REMAP_PSEUDO_HOOK} [AND]
 # Plugin controls
     set-http-cntl TXN_DEBUG true
     set-http-cntl LOGGING true
diff --git a/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.error.txt 
b/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.error.txt
new file mode 100644
index 0000000000..5fa65ba46a
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.error.txt
@@ -0,0 +1,3 @@
+tests/data/hooks/inbound_resp_section.fail.input.txt:2:4: error: 
inbound.resp.X-Test is not available in the REMAP section
+   2 |     inbound.resp.X-Test = "should fail";
+     |     ^
diff --git a/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.input.txt 
b/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.input.txt
new file mode 100644
index 0000000000..30fcfd11c4
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/inbound_resp_section.fail.input.txt
@@ -0,0 +1,3 @@
+REMAP {
+    inbound.resp.X-Test = "should fail";
+}
diff --git a/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.error.txt 
b/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.error.txt
new file mode 100644
index 0000000000..0d33d3248f
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.error.txt
@@ -0,0 +1,3 @@
+tests/data/hooks/outbound_resp_section.fail.input.txt:2:4: error: symbol error 
in {}: outbound.resp.X-Origin is not available in the REMAP section
+   2 |     inbound.req.X-Test = "resp={outbound.resp.X-Origin}";
+     |     ^
diff --git a/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.input.txt 
b/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.input.txt
new file mode 100644
index 0000000000..78ddf6b985
--- /dev/null
+++ b/tools/hrw4u/tests/data/hooks/outbound_resp_section.fail.input.txt
@@ -0,0 +1,3 @@
+REMAP {
+    inbound.req.X-Test = "resp={outbound.resp.X-Origin}";
+}
diff --git a/tools/hrw4u/tests/data/ops/skip-remap.ast.txt 
b/tools/hrw4u/tests/data/ops/skip-remap.ast.txt
index 7a3f3fe731..f817d0995f 100644
--- a/tools/hrw4u/tests/data/ops/skip-remap.ast.txt
+++ b/tools/hrw4u/tests/data/ops/skip-remap.ast.txt
@@ -1 +1 @@
-(program (programItem (section SEND_REQUEST { (sectionBody (conditional 
(ifStatement if (condition (expression (term (factor (comparison (comparable 
inbound.req.path) ~ (regex /foo/)))))) (block { (blockItem (statement 
(functionCall skip-remap ( (argumentList (value true)) )) ;)) })))) })) <EOF>)
+(program (programItem (section REMAP { (sectionBody (conditional (ifStatement 
if (condition (expression (term (factor (comparison (comparable 
inbound.req.path) ~ (regex /foo/)))))) (block { (blockItem (statement 
(functionCall skip-remap ( (argumentList (value true)) )) ;)) })))) })) <EOF>)
diff --git a/tools/hrw4u/tests/data/ops/skip-remap.input.txt 
b/tools/hrw4u/tests/data/ops/skip-remap.input.txt
index c852f51b6a..ac8da25f33 100644
--- a/tools/hrw4u/tests/data/ops/skip-remap.input.txt
+++ b/tools/hrw4u/tests/data/ops/skip-remap.input.txt
@@ -1,4 +1,4 @@
-SEND_REQUEST {
+REMAP {
     if inbound.req.path ~ /foo/ {
         skip-remap(true);
     }
diff --git a/tools/hrw4u/tests/data/ops/skip-remap.output.txt 
b/tools/hrw4u/tests/data/ops/skip-remap.output.txt
index f9b11d1672..5f4a5194b2 100644
--- a/tools/hrw4u/tests/data/ops/skip-remap.output.txt
+++ b/tools/hrw4u/tests/data/ops/skip-remap.output.txt
@@ -1,3 +1,3 @@
-cond %{SEND_REQUEST_HDR_HOOK} [AND]
+cond %{REMAP_PSEUDO_HOOK} [AND]
 cond %{CLIENT-HEADER:path} /foo/
     skip-remap TRUE
diff --git a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.input.txt 
b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.input.txt
index a894cbb5c8..fdaf3fa533 100644
--- a/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.input.txt
+++ b/tools/hrw4u/tests/data/ops/skip_remap_quoted_bool.fail.input.txt
@@ -1,3 +1,3 @@
-SEND_REQUEST {
+REMAP {
     skip-remap("true");
 }
\ No newline at end of file
diff --git a/tools/hrw4u/tests/test_lsp.py b/tools/hrw4u/tests/test_lsp.py
index 81d86b6f52..5f2896bd9e 100644
--- a/tools/hrw4u/tests/test_lsp.py
+++ b/tools/hrw4u/tests/test_lsp.py
@@ -361,9 +361,9 @@ def shared_lsp_client():
 
 @pytest.mark.parametrize(
     "section,prefix,should_allow", [
+        ("REMAP", "inbound.req.", True),
+        ("SEND_REQUEST", "outbound.req.", True),
         ("SEND_REQUEST", "outbound.req.", True),
-        ("REMAP", "outbound.req.", False),
-        ("SEND_REQUEST", "outbound.resp.", False),
         ("READ_RESPONSE", "outbound.resp.", True),
     ])
 def test_section_restrictions_batch(shared_lsp_client, section, prefix, 
should_allow) -> None:
@@ -408,15 +408,16 @@ def 
test_multi_section_inbound_always_allowed(shared_lsp_client) -> None:
 
 
 def test_outbound_restrictions_batch(shared_lsp_client) -> None:
-    """Batch test outbound restrictions in early vs late sections."""
-    early_sections = ["PRE_REMAP", "REMAP", "READ_REQUEST"]
-    late_sections = ["SEND_REQUEST", "READ_RESPONSE"]
+    """Batch test outbound restrictions - outbound features have 
section-specific availability."""
+    # outbound.url. is available in PRE_REMAP through SEND_REQUEST, plus 
READ_RESPONSE, SEND_RESPONSE
+    # outbound.cookie. is only available from SEND_REQUEST onwards
+    http_sections = ["PRE_REMAP", "REMAP", "READ_REQUEST", "SEND_REQUEST", 
"READ_RESPONSE"]
 
-    for section in early_sections:
+    for section in http_sections:
         test_content = f"""{section} {{
     outbound.
 }}"""
-        uri = f"file:///test_early_{section.lower()}.hrw4u"
+        uri = f"file:///test_outbound_{section.lower()}.hrw4u"
         shared_lsp_client.open_document(uri, test_content)
 
         response = shared_lsp_client.request_completion(uri, 1, 13)
@@ -426,29 +427,16 @@ def test_outbound_restrictions_batch(shared_lsp_client) 
-> None:
         outbound_cookie_items = [item for item in items if 
item["label"].startswith("outbound.cookie.")]
         outbound_url_items = [item for item in items if 
item["label"].startswith("outbound.url.")]
 
-        assert len(outbound_cookie_items) == 0, f"outbound.cookie. should NOT 
be in {section}"
-        assert len(outbound_url_items) == 0, f"outbound.url. should NOT be in 
{section}"
-
-    for section in late_sections:
-        test_content = f"""{section} {{
-    outbound.
-}}"""
-        uri = f"file:///test_late_{section.lower()}.hrw4u"
-        shared_lsp_client.open_document(uri, test_content)
-
-        response = shared_lsp_client.request_completion(uri, 1, 13)
-        assert response is not None
-        items = response["result"]["items"]
-
-        outbound_conn_items = [item for item in items if 
item["label"].startswith("outbound.conn.")]
-        outbound_cookie_items = [item for item in items if 
item["label"].startswith("outbound.cookie.")]
-
-        assert len(outbound_conn_items) > 0, f"outbound.conn. should be in 
{section}"
-        assert len(outbound_cookie_items) > 0, f"outbound.cookie. should be in 
{section}"
+        # outbound.cookie. is only available from SEND_REQUEST onwards
+        if section in ["SEND_REQUEST", "READ_RESPONSE"]:
+            assert len(outbound_cookie_items) > 0, f"outbound.cookie. should 
be in {section}"
+        # outbound.url. is available in all these sections
+        assert len(outbound_url_items) > 0, f"outbound.url. should be in 
{section}"
 
 
 def test_specific_outbound_conn_completions(shared_lsp_client) -> None:
-    """Test specific outbound.conn completions"""
+    """Test specific outbound.conn completions (dscp/mark only available from 
SEND_REQUEST onwards)"""
+    # outbound.conn.dscp and outbound.conn.mark are only available in 
SEND_REQUEST, READ_RESPONSE, SEND_RESPONSE
     test_content = """SEND_REQUEST {
     outbound.conn.
 }"""


Reply via email to