move parser tolerance setting to Environment and tolerate more issues

This commit is contained in:
InsanePrawn 2025-05-05 02:52:48 +02:00
parent 05ddbbda8e
commit 42345cc1c4
3 changed files with 67 additions and 13 deletions

View File

@ -252,6 +252,10 @@ class Environment:
will reload the template. For higher performance it's possible to
disable that.
`parser_tolerate_faults`
Instruct the parser to tolerate some invalid constructs that don't cause much semantic uncertainty, useful for linters and LSP to provide output on incomplete templates.
Defaults to False.
`bytecode_cache`
If set to a bytecode cache object, this object will provide a
cache for the internal Jinja bytecode so that templates don't
@ -316,6 +320,7 @@ class Environment:
auto_reload: bool = True,
bytecode_cache: t.Optional["BytecodeCache"] = None,
enable_async: bool = False,
parser_tolerate_faults: bool = False,
):
# !!Important notice!!
# The constructor accepts quite a few arguments that should be
@ -360,6 +365,7 @@ class Environment:
self.auto_reload = auto_reload
# configurable policies
self.parser_tolerate_faults = parser_tolerate_faults
self.policies = DEFAULT_POLICIES.copy()
# load extensions

View File

@ -125,6 +125,7 @@ class Node(metaclass=NodeType):
lineno: int
environment: t.Optional["Environment"]
issues: list[t.Union["ParserIssue", "ExprIssue"]]
def __init__(self, *fields: t.Any, **attributes: t.Any) -> None:
if self.abstract:
@ -279,6 +280,13 @@ class Node(metaclass=NodeType):
return "".join(buf)
class ParserIssue(Node):
attributes: tuple[str, ...] = ("message", "lineno_end")
message: str
lineno_end: int | None
class Stmt(Node):
"""Base node for all statements."""
@ -493,13 +501,26 @@ class Expr(Node):
return False
class EmptyExpression(Expr):
class ExprIssue(Expr):
attributes: tuple[str, ...] = ("message", "lineno_end")
message: str
lineno_end: int | None
class EmptyExpression(ExprIssue):
"""Node where an expression should be but an empty expression was given.
Returned in Fault-tolerant Mode only
"""
comment: str
attributes: t.Tuple[str, ...] = ("comment",)
class InvalidExpression(ExprIssue):
"""Node where an expression should be but an unparsable expression was given.
Returned in Fault-tolerant Mode only
"""
attributes: tuple[str, ...] = ("original_str",)
original_str: str
class BinExpr(Expr):

View File

@ -57,9 +57,7 @@ class Parser:
name: str | None = None,
filename: str | None = None,
state: str | None = None,
fault_tolerant: bool = False,
) -> None:
self.fault_tolerant = fault_tolerant
self.environment = environment
self.stream = environment._tokenize(source, name, filename, state)
self.name = name
@ -401,9 +399,17 @@ class Parser:
node.with_context = False
return node
def parse_signature(self, node: _MacroCall) -> None:
def parse_signature(self, node: _MacroCall) -> None | nodes.EmptyExpression | nodes.InvalidExpression:
args = node.args = []
defaults = node.defaults = []
if self.environment.parser_tolerate_faults and self.stream.current.type != "lparen":
if self.stream.current.type == "block_end":
return nodes.EmptyExpression( # type: ignore[assignment]
lineno=node.lineno,
lineno_end=self.stream.current.lineno,
message="Empty signature",
)
self.stream.expect("lparen")
while self.stream.current.type != "rparen":
if args:
@ -413,14 +419,27 @@ class Parser:
if self.stream.skip_if("assign"):
defaults.append(self.parse_expression())
elif defaults:
self.fail("non-default argument follows default argument")
msg = "non-default argument follows default argument"
if not self.environment.parser_tolerate_faults:
self.fail(msg)
err = nodes.InvalidExpression(
lineno=arg.lineno,
lineno_end=self.stream.current.lineno,
message=msg,
original_str=arg.name,
)
arg.issues = [err]
args.append(arg)
self.stream.expect("rparen")
return None
def parse_call_block(self) -> nodes.CallBlock:
node = nodes.CallBlock(lineno=next(self.stream).lineno)
if self.stream.current.type == "lparen":
self.parse_signature(node)
signature_issue = self.parse_signature(node)
if signature_issue:
assert self.environment.parser_tolerate_faults
node.args = signature_issue # type: ignore[assignment]
else:
node.args = []
node.defaults = []
@ -439,9 +458,15 @@ class Parser:
return node
def parse_macro(self) -> nodes.Macro:
node = nodes.Macro(lineno=next(self.stream).lineno)
node = nodes.Macro(lineno=next(self.stream).lineno, issues=None)
node.name = self.parse_assign_target(name_only=True).name
self.parse_signature(node)
signature_issue = self.parse_signature(node)
if signature_issue:
assert self.environment.parser_tolerate_faults
node.args = signature_issue # type: ignore[assignment]
if node.issues is None:
node.issues = []
node.issues.append(signature_issue)
node.body = self.parse_statements(("name:endmacro",), drop_needle=True)
return node
@ -744,7 +769,9 @@ class Parser:
# tuple.
if not explicit_parentheses:
if allow_empty:
return nodes.EmptyExpression(lineno=lineno, comment="")
empty = nodes.EmptyExpression(lineno=lineno, message="Expected an expression")
empty.issues = [empty]
return empty
self.fail(
"Expected an expression,"
f" got {describe_token(self.stream.current)!r}"
@ -1014,9 +1041,9 @@ class Parser:
next(self.stream)
elif token.type == "variable_begin":
next(self.stream)
data = self.parse_tuple(with_condexpr=True, allow_empty=self.fault_tolerant)
data = self.parse_tuple(with_condexpr=True, allow_empty=self.environment.parser_tolerate_faults)
if isinstance(data, nodes.EmptyExpression):
data.comment = "Empty expression inside print statement"
data.message = "Empty expression inside print statement"
add_data(data)
self.stream.expect("variable_end")
elif token.type == "block_begin":