xdrgen: Reject specifications that define a name twice

When an RPC specification defines the same type or constant name
more than once, currently xdrgen emits every definition without
complaint. The duplication surfaces later as a C compiler error
about a redefined struct or function that points at generated code
instead of the actual offending line in the .x source.

RFC 4506 Section 6.4 places constant and type identifiers in a
single name space that must be unique within a specification. Add
a semantic check that enforces this rule.

Link: https://patch.msgid.link/20260712203451.124902-4-cel@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
This commit is contained in:
Chuck Lever
2026-07-12 16:34:49 -04:00
parent e1391920c6
commit b75e1a256d
6 changed files with 101 additions and 11 deletions

View File

@@ -21,16 +21,15 @@ from generators.union import XdrUnionGenerator
from xdr_ast import transform_parse_tree, _RpcProgram, Specification
from xdr_ast import _XdrEnum, _XdrPointer, _XdrTypedef, _XdrStruct, _XdrUnion
from xdr_ast import XdrSemanticError
from xdr_parse import xdr_parser, set_xdr_annotate
from xdr_parse import make_error_handler, XdrParseError
from xdr_parse import handle_transform_error
from xdr_parse import handle_transform_error, handle_semantic_error
logger.setLevel(logging.INFO)
def emit_header_declarations(
root: Specification, language: str, peer: str
) -> None:
def emit_header_declarations(root: Specification, language: str, peer: str) -> None:
"""Emit header declarations"""
for definition in root.definitions:
if isinstance(definition.value, _XdrEnum):
@@ -68,6 +67,9 @@ def subcmd(args: Namespace) -> int:
except VisitError as e:
handle_transform_error(e, source, args.filename)
return 1
except XdrSemanticError as e:
handle_semantic_error(e, source, args.filename)
return 1
gen = XdrHeaderTopGenerator(args.language, args.peer)
gen.emit_declaration(args.filename, ast)

View File

@@ -21,12 +21,12 @@ from generators.typedef import XdrTypedefGenerator
from generators.struct import XdrStructGenerator
from generators.union import XdrUnionGenerator
from xdr_ast import transform_parse_tree, Specification
from xdr_ast import transform_parse_tree, Specification, XdrSemanticError
from xdr_ast import _RpcProgram, _XdrConstant, _XdrEnum, _XdrPassthru, _XdrPointer
from xdr_ast import _XdrTypedef, _XdrStruct, _XdrUnion
from xdr_parse import xdr_parser, set_xdr_annotate
from xdr_parse import make_error_handler, XdrParseError
from xdr_parse import handle_transform_error
from xdr_parse import handle_transform_error, handle_semantic_error
logger.setLevel(logging.INFO)
@@ -94,6 +94,9 @@ def subcmd(args: Namespace) -> int:
except VisitError as e:
handle_transform_error(e, source, args.filename)
return 1
except XdrSemanticError as e:
handle_semantic_error(e, source, args.filename)
return 1
gen = XdrHeaderTopGenerator(args.language, args.peer)
gen.emit_definition(args.filename, ast)

View File

@@ -11,8 +11,8 @@ from lark import logger
from lark.exceptions import VisitError
from xdr_parse import xdr_parser, make_error_handler, XdrParseError
from xdr_parse import handle_transform_error
from xdr_ast import transform_parse_tree
from xdr_parse import handle_transform_error, handle_semantic_error
from xdr_ast import transform_parse_tree, XdrSemanticError
logger.setLevel(logging.DEBUG)
@@ -34,5 +34,8 @@ def subcmd(args: Namespace) -> int:
except VisitError as e:
handle_transform_error(e, source, args.filename)
return 1
except XdrSemanticError as e:
handle_semantic_error(e, source, args.filename)
return 1
return 0

View File

@@ -21,11 +21,11 @@ from generators.union import XdrUnionGenerator
from xdr_ast import transform_parse_tree, _RpcProgram, Specification
from xdr_ast import _XdrAst, _XdrEnum, _XdrPassthru, _XdrPointer
from xdr_ast import _XdrStruct, _XdrTypedef, _XdrUnion
from xdr_ast import _XdrStruct, _XdrTypedef, _XdrUnion, XdrSemanticError
from xdr_parse import xdr_parser, set_xdr_annotate, set_xdr_enum_validation
from xdr_parse import make_error_handler, XdrParseError
from xdr_parse import handle_transform_error
from xdr_parse import handle_transform_error, handle_semantic_error
logger.setLevel(logging.INFO)
@@ -123,6 +123,9 @@ def subcmd(args: Namespace) -> int:
except VisitError as e:
handle_transform_error(e, source, args.filename)
return 1
except XdrSemanticError as e:
handle_semantic_error(e, source, args.filename)
return 1
match args.peer:
case "server":
generate_server_source(args.filename, ast, args.language)

View File

@@ -814,7 +814,9 @@ def _merge_consecutive_passthru(definitions: List[Definition]) -> List[Definitio
lines = [definitions[i].value.content]
meta = definitions[i].meta
j = i + 1
while j < len(definitions) and isinstance(definitions[j].value, _XdrPassthru):
while j < len(definitions) and isinstance(
definitions[j].value, _XdrPassthru
):
lines.append(definitions[j].value.content)
j += 1
merged = _XdrPassthru("\n".join(lines))
@@ -826,10 +828,67 @@ def _merge_consecutive_passthru(definitions: List[Definition]) -> List[Definitio
return result
def _meta_line(meta) -> int:
"""Return the 1-based source line for a node's meta, or 0 if unknown"""
try:
return meta.line
except AttributeError:
return 0
class XdrSemanticError(Exception):
"""A specification that parses but violates an XDR semantic rule.
Detection lives in the language-independent front end because a
duplicate name is malformed XDR regardless of the output language.
"""
def __init__(self, message: str, meta):
super().__init__(message)
self.message = message
self.line = _meta_line(meta)
self.column = getattr(meta, "column", 0)
def _introduced_names(value):
"""Yield (name, node) for each identifier a definition introduces."""
if isinstance(value, (_XdrStruct, _XdrUnion, _XdrPointer)):
yield value.name, value
elif isinstance(value, _XdrEnum):
yield value.name, value
for enumerator in value.enumerators:
yield enumerator.name, enumerator
elif isinstance(value, _XdrTypedef):
yield value.declaration.name, value.declaration
elif isinstance(value, _XdrConstant):
yield value.name, value
def check_duplicate_definitions(root: "Specification") -> None:
"""Reject a spec that declares an identifier more than once.
RFC 4506 Section 6.4 places constant and type identifiers in a
single name space that must be unique within a specification.
"""
seen = {}
for definition in root.definitions:
for name, node in _introduced_names(definition.value):
where = node if node.line else definition.meta
first = seen.get(name)
if first is not None:
raise XdrSemanticError(
f"duplicate identifier '{name}'"
f" (first declared at line {_meta_line(first)})",
where,
)
seen[name] = where
def transform_parse_tree(parse_tree):
"""Transform productions into an abstract syntax tree"""
ast = transformer.transform(parse_tree)
ast.definitions = _merge_consecutive_passthru(ast.definitions)
check_duplicate_definitions(ast)
return ast

View File

@@ -169,6 +169,26 @@ def handle_transform_error(e: VisitError, source: str, filename: str) -> None:
sys.stderr.write("\n".join(msg_parts) + "\n")
def handle_semantic_error(e, source: str, filename: str) -> None:
"""Report a semantic error (e.g., a duplicate name) with context.
Args:
e: The XdrSemanticError carrying message and source position
source: The XDR source text being parsed
filename: The name of the file being parsed
"""
lines = source.splitlines()
line_num = getattr(e, "line", 0)
column = getattr(e, "column", 0)
line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
msg_parts = [f"{filename}:{line_num}:{column}: semantic error", e.message]
if line_text:
msg_parts.extend(format_source_caret(line_text, column))
sys.stderr.write("\n".join(msg_parts) + "\n")
def xdr_parser() -> Lark:
"""Return a Lark parser instance configured with the XDR language grammar"""