blob: 28478933c0fc1672d2201fd557c521de6f5b2ff1 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import re
import sys
from typing import Final, NoReturn, Protocol, TextIO, Any
from antlr4.error.ErrorStrategy import BailErrorStrategy, DefaultErrorStrategy
from antlr4 import InputStream, CommonTokenStream
from hrw4u.errors import Hrw4uSyntaxError, ThrowingErrorListener, ErrorCollector, CollectingErrorListener
from hrw4u.types import MagicStrings
class RegexPatterns:
"""Compiled regex patterns for reuse across modules"""
SIMPLE_TOKEN: Final = re.compile(r'^[@a-zA-Z0-9_-]+$')
HTTP_TOKEN: Final = re.compile(r'^[!#$%&\'*+.^_`|~0-9A-Za-z-]+$')
HTTP_HEADER_NAME: Final = re.compile(r'^(?:@[!#$%&\'*+^_`|~0-9A-Za-z-]+|[!#$%&\'*+^_`|~0-9A-Za-z-]+)$')
REGEX_LITERAL: Final = re.compile(r'^/(?:\\.|[^/\r\n])+/$')
PERCENT_BLOCK: Final = re.compile(r"^\%\{([A-Z0-9_-]+)(?::(.*))?\}$")
PERCENT_INLINE: Final = re.compile(r"%\{([A-Z0-9_-]+)(?::(.*?))?\}")
PERCENT_PATTERN: Final = re.compile(r'%\{([^}]+)\}')
SUBSTITUTE_PATTERN: Final = re.compile(
r"""(?<!%)\{\s*(?P<func>[a-zA-Z_][a-zA-Z0-9_-]*)\s*\((?P<args>[^)]*)\)\s*\}
|
(?<!%)\{(?P<var>[^{}()]+)\}
""",
re.VERBOSE,
)
# Additional performance patterns
IDENTIFIER: Final = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
WHITESPACE: Final = re.compile(r'\s+')
COMMENT_BLOCK: Final = re.compile(r'/\*.*?\*/', re.DOTALL)
STRING_INTERPOLATION: Final = re.compile(r'\{([a-zA-Z_][a-zA-Z0-9_.-]*(?:\([^)]*\))?)\}', re.MULTILINE)
class SystemDefaults:
"""System-wide default constants"""
DEFAULT_FILENAME: Final = "<stdin>"
DEFAULT_DEBUG: Final = False
DEFAULT_CONFIGURABLE: Final = False
LINE_NUMBER_WIDTH: Final = 4
INDENT_SPACES: Final = 4
DEBUG_PREFIX: Final = "[debug]"
class HeaderOperations:
"""Operation constants for various resource types"""
OPERATIONS: Final = (MagicStrings.RM_HEADER.value, MagicStrings.SET_HEADER.value)
ADD_OPERATION: Final = MagicStrings.ADD_HEADER.value
COOKIE_OPERATIONS: Final = (MagicStrings.RM_COOKIE.value, MagicStrings.SET_COOKIE.value)
DESTINATION_OPERATIONS: Final = (MagicStrings.RM_DESTINATION.value, MagicStrings.SET_DESTINATION.value)
class LexerProtocol(Protocol):
"""Protocol for ANTLR lexers."""
def removeErrorListeners(self) -> None:
...
def addErrorListener(self, listener: Any) -> None:
...
class ParserProtocol(Protocol):
"""Protocol for ANTLR parsers."""
def removeErrorListeners(self) -> None:
...
def addErrorListener(self, listener: Any) -> None:
...
def program(self) -> Any:
...
errorHandler: BailErrorStrategy | DefaultErrorStrategy
class VisitorProtocol(Protocol):
"""Protocol for ANTLR visitors."""
def visit(self, tree: Any) -> list[str]:
...
def fatal(message: str) -> NoReturn:
"""Print error message and exit with failure code."""
print(message, file=sys.stderr)
sys.exit(1)
def create_base_parser(description: str) -> tuple[argparse.ArgumentParser, argparse._MutuallyExclusiveGroup]:
"""Create base argument parser with common options."""
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"input_file",
help="The input file to parse (default: stdin)",
nargs="?",
type=argparse.FileType("r", encoding="utf-8"),
default=sys.stdin)
output_group = parser.add_mutually_exclusive_group()
output_group.add_argument("--ast", action="store_true", help="Produce the ANTLR parse tree only")
parser.add_argument("--debug", action="store_true", help="Enable debug output")
parser.add_argument(
"--stop-on-error", action="store_true", help="Stop processing on first error (default: collect and report multiple errors)")
return parser, output_group
def process_input(input_file: TextIO) -> tuple[str, str]:
"""Read input content and determine filename."""
content = input_file.read()
if input_file is not sys.stdin:
filename = input_file.name
input_file.close()
else:
filename = SystemDefaults.DEFAULT_FILENAME
return content, filename
def create_parse_tree(
content: str,
filename: str,
lexer_class: type[LexerProtocol],
parser_class: type[ParserProtocol],
error_prefix: str,
collect_errors: bool = True) -> tuple[Any, ParserProtocol, ErrorCollector | None]:
"""Create ANTLR parse tree from input content with optional error collection."""
input_stream = InputStream(content)
error_collector = None
if collect_errors:
error_collector = ErrorCollector()
error_listener = CollectingErrorListener(filename=filename, error_collector=error_collector)
else:
error_listener = ThrowingErrorListener(filename=filename)
lexer = lexer_class(input_stream)
lexer.removeErrorListeners()
lexer.addErrorListener(error_listener)
token_stream = CommonTokenStream(lexer)
parser_obj = parser_class(token_stream)
parser_obj.removeErrorListeners()
parser_obj.addErrorListener(error_listener)
if collect_errors:
parser_obj.errorHandler = DefaultErrorStrategy()
else:
parser_obj.errorHandler = BailErrorStrategy()
try:
tree = parser_obj.program()
return tree, parser_obj, error_collector
except Hrw4uSyntaxError as e:
if collect_errors:
if error_collector:
error_collector.add_error(e)
return None, parser_obj, error_collector
else:
fatal(str(e))
except Exception as e:
if collect_errors:
if error_collector:
syntax_error = Hrw4uSyntaxError(filename, 0, 0, f"{error_prefix} error: {e}", "")
error_collector.add_error(syntax_error)
return None, parser_obj, error_collector
else:
fatal(f"{filename}:0:0 - {error_prefix} error: {e}")
def generate_output(
tree: Any,
parser_obj: ParserProtocol,
visitor_class: type[VisitorProtocol],
filename: str,
args: Any,
error_collector: ErrorCollector | None = None) -> None:
"""Generate and print output based on mode with optional error collection."""
if args.ast:
if tree is not None:
print(tree.toStringTree(recog=parser_obj))
elif error_collector and error_collector.has_errors():
print("Parse tree not available due to syntax errors.")
else:
if tree is not None:
preserve_comments = not getattr(args, 'no_comments', False)
visitor = visitor_class(
filename=filename, debug=args.debug, error_collector=error_collector, preserve_comments=preserve_comments)
try:
result = visitor.visit(tree)
if result:
print("\n".join(result))
except Exception as e:
if error_collector:
syntax_error = Hrw4uSyntaxError(filename, 0, 0, f"Visitor error: {e}", "")
if hasattr(e, '__notes__') and e.__notes__:
for note in e.__notes__:
syntax_error.add_note(note)
error_collector.add_error(syntax_error)
else:
fatal(str(e))
if error_collector and error_collector.has_errors():
print(error_collector.get_error_summary(), file=sys.stderr)
if not args.ast and tree is None:
sys.exit(1)