Files
mongo/buildscripts/idl/idl/parser.py
Billy Donahue 2961453a36 SERVER-118562 improve dbtests integration with gtest/unittest frameworks (#47458)
GitOrigin-RevId: 7c2a1f82c44882bd13ed16c381c6e11927a508db
2026-02-04 21:46:09 +00:00

1257 lines
46 KiB
Python

# Copyright (C) 2018-present MongoDB, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the Server Side Public License, version 1,
# as published by MongoDB, Inc.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Server Side Public License for more details.
#
# You should have received a copy of the Server Side Public License
# along with this program. If not, see
# <http://www.mongodb.com/licensing/server-side-public-license>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the Server Side Public License in all respects for
# all of the code used other than as permitted herein. If you modify file(s)
# with this exception, you may extend this exception to your version of the
# file(s), but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version. If you delete this
# exception statement from all source files in the program, then also delete
# it in the license file.
#
"""
IDL Parser.
Converts a YAML document to an idl.syntax tree.
Only validates the document is syntatically correct, not semantically.
"""
import re
from abc import ABCMeta, abstractmethod
import yaml
from . import common, cpp_types, errors, syntax
class _RuleDesc(object):
"""
Describe a simple parser rule for the generic YAML node parser.
node_type is either (scalar, bool_scalar, int_scalar, scalar_or_sequence, sequence, or mapping)
- bool_scalar - means a scalar node which is a valid bool, populates a bool
- int_scalar - means a scalar node which is a valid non-negative int, populates a int
- scalar_or_sequence - means a scalar or sequence node, populates a list
- sequence - a sequence node, populates a list
- mapping - a mapping node, calls another parser
- scalar_or_mapping - means a scalar of mapping node, populates a struct
mapping_parser_func is only called when parsing a mapping or scalar_or_mapping yaml node.
Similar for sequence_parser_func.
"""
# TODO: after porting to Python 3, use an enum
REQUIRED = 1
OPTIONAL = 2
def __init__(
self, node_type, required=OPTIONAL, mapping_parser_func=None, sequence_parser_func=None
):
# type: (str, int, Callable[[errors.ParserContext,yaml.nodes.MappingNode], Any], Callable[[errors.ParserContext,yaml.nodes.SequenceNode], Any]) -> None
"""Construct a parser rule description."""
assert required in (_RuleDesc.REQUIRED, _RuleDesc.OPTIONAL)
self.node_type = node_type # type: str
self.required = required # type: int
self.mapping_parser_func = mapping_parser_func # type: Callable[[errors.ParserContext,yaml.nodes.MappingNode], Any]
def default_seq_parser(ctxt, node):
return ctxt.get_list(node)
self.sequence_parser_func = sequence_parser_func or default_seq_parser # type: Callable[[errors.ParserContext,yaml.nodes.SequenceNode], Any]
def _has_field(
node, # type: Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]
field_name, # type: str
): # type: (...) -> bool
return any(kv[0].value == field_name for kv in node.value)
def _generic_parser(
ctxt, # type: errors.ParserContext
node, # type: Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]
syntax_node_name, # type: str
syntax_node, # type: Any
mapping_rules, # type: Dict[str, _RuleDesc]
): # type: (...) -> None
field_name_set = set() # type: Set[str]
for [first_node, second_node] in node.value:
first_name = first_node.value
if first_name in field_name_set:
ctxt.add_duplicate_error(first_node, first_name)
continue
if first_name in mapping_rules:
rule_desc = mapping_rules[first_name]
if rule_desc.node_type == "scalar":
if ctxt.is_scalar_node(second_node, first_name):
syntax_node.__dict__[first_name] = second_node.value
elif rule_desc.node_type == "bool_scalar":
if ctxt.is_scalar_bool_node(second_node, first_name):
syntax_node.__dict__[first_name] = ctxt.get_bool(second_node)
elif rule_desc.node_type == "int_scalar":
if ctxt.is_scalar_non_negative_int_node(second_node, first_name):
syntax_node.__dict__[first_name] = ctxt.get_non_negative_int(second_node)
elif rule_desc.node_type == "scalar_or_sequence":
if ctxt.is_scalar_sequence_or_scalar_node(second_node, first_name):
syntax_node.__dict__[first_name] = rule_desc.sequence_parser_func(
ctxt, second_node
)
elif rule_desc.node_type == "sequence":
if ctxt.is_scalar_sequence(second_node, first_name):
syntax_node.__dict__[first_name] = rule_desc.sequence_parser_func(
ctxt, second_node
)
elif rule_desc.node_type == "sequence_mapping":
if ctxt.is_sequence_mapping(second_node, first_name):
syntax_node.__dict__[first_name] = rule_desc.sequence_parser_func(
ctxt, second_node
)
elif rule_desc.node_type == "scalar_or_mapping":
if ctxt.is_scalar_or_mapping_node(second_node, first_name):
syntax_node.__dict__[first_name] = rule_desc.mapping_parser_func(
ctxt, second_node
)
elif rule_desc.node_type == "mapping":
if ctxt.is_mapping_node(second_node, first_name):
syntax_node.__dict__[first_name] = rule_desc.mapping_parser_func(
ctxt, second_node
)
elif rule_desc.node_type == "required_bool_scalar":
syntax_node.__dict__[first_name] = ctxt.get_required_bool(second_node)
else:
raise errors.IDLError(
"Unknown node_type '%s' for parser rule" % (rule_desc.node_type)
)
else:
ctxt.add_unknown_node_error(first_node, syntax_node_name)
field_name_set.add(first_name)
# Check for any missing required fields
for name, rule_desc in list(mapping_rules.items()):
if not rule_desc.required == _RuleDesc.REQUIRED:
continue
# A bool is never "None" like other types, it simply defaults to "false".
# It means "if bool is None" will always return false and there is no support for required
# 'bool' at this time. Use the node type 'required_bool_scalar' if this behavior is not desired.
if not rule_desc.node_type == "bool_scalar":
if syntax_node.__dict__[name] is None:
ctxt.add_missing_required_field_error(node, syntax_node_name, name)
else:
raise errors.IDLError(
"Unknown node_type '%s' for parser required rule" % (rule_desc.node_type)
)
def _parse_mapping(
ctxt, # type: errors.ParserContext
spec, # type: syntax.IDLSpec
node, # type: Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]
syntax_node_name, # type: str
func, # type: Callable[[errors.ParserContext,syntax.IDLSpec,str,Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]], None]
): # type: (...) -> None
"""Parse a top-level mapping section in the IDL file."""
if not ctxt.is_mapping_node(node, syntax_node_name):
return
for [first_node, second_node] in node.value:
first_name = first_node.value
func(ctxt, spec, first_name, second_node)
def _parse_initializer(ctxt, node):
# type: (errors.ParserContext, Union[yaml.nodes.ScalarNode, yaml.nodes.MappingNode]) -> syntax.GlobalInitializer
"""Parse a global initializer."""
init = syntax.GlobalInitializer(ctxt.file_name, node.start_mark.line, node.start_mark.column)
if node.id == "scalar":
init.name = node.value
return init
_generic_parser(
ctxt,
node,
"initializer",
init,
{
"register": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"store": _RuleDesc("scalar"),
},
)
return init
def _parse_config_global(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.ConfigGlobal
"""Parse global settings for config options."""
config = syntax.ConfigGlobal(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"configs",
config,
{
"section": _RuleDesc("scalar"),
"source": _RuleDesc("scalar_or_sequence"),
"initializer": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_initializer),
},
)
return config
_mod_visibility_pattern = re.compile(
r"(pub|public|public_for_technical_reasons|private|file_private|needs_replacement|unfortunately_open|use_replacement\(.+\))"
)
def _validate_mod_visibility(
ctxt: errors.ParserContext, mod_visibility: str, node: yaml.nodes.Node
):
if mod_visibility is not None and not _mod_visibility_pattern.fullmatch(mod_visibility):
ctxt.add_bad_visibility(node, mod_visibility)
def _parse_global(ctxt, spec, node):
# type: (errors.ParserContext, syntax.IDLSpec, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a global section in the IDL file."""
if not ctxt.is_mapping_node(node, "global"):
return
idlglobal = syntax.Global(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"global",
idlglobal,
{
"cpp_namespace": _RuleDesc("scalar"),
"cpp_includes": _RuleDesc("scalar_or_sequence"),
"configs": _RuleDesc("mapping", mapping_parser_func=_parse_config_global),
"mod_visibility": _RuleDesc("scalar"),
},
)
_validate_mod_visibility(ctxt, idlglobal.mod_visibility, node)
spec.globals = idlglobal
def _parse_imports(ctxt, spec, node):
# type: (errors.ParserContext, syntax.IDLSpec, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse an imports section in the IDL file."""
if not ctxt.is_scalar_sequence(node, "imports"):
imports = syntax.Import(ctxt.file_name, None, None)
imports.imports = []
spec.imports = imports
else:
imports = syntax.Import(ctxt.file_name, node.start_mark.line, node.start_mark.column)
imports.imports = ctxt.get_list(node)
spec.imports = imports
def _parse_type(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a type section in the IDL file."""
if not ctxt.is_mapping_node(node, "type"):
return
idltype = syntax.Type(ctxt.file_name, node.start_mark.line, node.start_mark.column)
idltype.name = name
_generic_parser(
ctxt,
node,
"type",
idltype,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"cpp_type": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"bson_serialization_type": _RuleDesc("scalar_or_sequence", _RuleDesc.REQUIRED),
"is_view": _RuleDesc("bool_scalar"),
"bindata_subtype": _RuleDesc("scalar"),
"serializer": _RuleDesc("scalar"),
"deserializer": _RuleDesc("scalar"),
"deserialize_with_tenant": _RuleDesc("bool_scalar"),
"internal_only": _RuleDesc("bool_scalar"),
"default": _RuleDesc("scalar"),
},
)
spec.symbols.add_type(ctxt, idltype)
def _parse_expression(ctxt, node):
# type: (errors.ParserContext, Union[yaml.nodes.ScalarNode,yaml.nodes.MappingNode]) -> syntax.Expression
"""Parse an expression as either a scalar or a mapping."""
expr = syntax.Expression(ctxt.file_name, node.start_mark.line, node.start_mark.column)
if node.id == "scalar":
expr.literal = node.value
return expr
_generic_parser(
ctxt,
node,
"expr",
expr,
{
"expr": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"is_constexpr": _RuleDesc("bool_scalar"),
},
)
return expr
def _parse_validator(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.Validator
"""Parse a validator for a field."""
validator = syntax.Validator(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"validator",
validator,
{
"gt": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"lt": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"gte": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"lte": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"callback": _RuleDesc("scalar"),
},
)
return validator
def _parse_condition(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.Condition
"""Parse a condition."""
condition = syntax.Condition(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"condition",
condition,
{
"preprocessor": _RuleDesc("scalar"),
"constexpr": _RuleDesc("scalar"),
"expr": _RuleDesc("scalar"),
"feature_flag": _RuleDesc("scalar"),
"min_fcv": _RuleDesc("scalar"),
},
)
return condition
def _parse_variant_alternatives(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.SequenceNode) -> List[syntax.FieldType]
"""Parse a variant field type's alternative types."""
return [_parse_field_type(ctxt, child) for child in node.value]
def _parse_field_type(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.FieldType
"""Parse a struct field's type.
Can be a scalar like "string", or a mapping like {variant: ["string", "int"]}.
"""
if node.id == "mapping":
# For now, FieldTypeVariant is the only non-scalar node.
variant = syntax.FieldTypeVariant(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
_generic_parser(
ctxt,
node,
"type",
variant,
{"variant": _RuleDesc("sequence", sequence_parser_func=_parse_variant_alternatives)},
)
return variant
else:
assert node.id == "scalar"
if node.value.startswith("array<variant<"):
variant_types = syntax.parse_array_variant_types(node.value)
variant = syntax.FieldTypeVariant(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
if variant_types is None:
location = common.SourceLocation(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
ctxt.add_bad_array_variant_types_error(location, node.value)
else:
for variant_type in variant_types:
single = syntax.FieldTypeSingle(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
single.type_name = variant_type
variant.variant.append(single)
return syntax.FieldTypeArray(variant)
single = syntax.FieldTypeSingle(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
if node.value.startswith("array<"):
single.type_name = syntax.parse_array_type(node.value)
return syntax.FieldTypeArray(single)
single.type_name = node.value
return single
def _parse_field(ctxt, name, node):
# type: (errors.ParserContext, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> syntax.Field
"""Parse a field in a struct/command in the IDL file."""
field = syntax.Field(ctxt.file_name, node.start_mark.line, node.start_mark.column)
field.name = name
_generic_parser(
ctxt,
node,
"field",
field,
{
"description": _RuleDesc("scalar"),
"cpp_name": _RuleDesc("scalar"),
"type": _RuleDesc(
"scalar_or_mapping", _RuleDesc.REQUIRED, mapping_parser_func=_parse_field_type
),
"ignore": _RuleDesc("bool_scalar"),
"optional": _RuleDesc("bool_scalar"),
"default": _RuleDesc("scalar"),
"supports_doc_sequence": _RuleDesc("bool_scalar"),
"comparison_order": _RuleDesc("int_scalar"),
"validator": _RuleDesc("mapping", mapping_parser_func=_parse_validator),
"non_const_getter": _RuleDesc("bool_scalar"),
# Allow both 'unstable' and the new alternative 'stability' options to support IDL compatibility tests with old IDLs.
"unstable": _RuleDesc("bool_scalar"),
"stability": _RuleDesc("scalar"),
"always_serialize": _RuleDesc("bool_scalar"),
"forward_to_shards": _RuleDesc("bool_scalar"),
"forward_from_shards": _RuleDesc("bool_scalar"),
"query_shape": _RuleDesc("scalar"),
},
)
return field
def _parse_fields(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> List[syntax.Field]
"""Parse a fields section in a struct in the IDL file."""
fields = []
field_name_set = set() # type: Set[str]
for [first_node, second_node] in node.value:
first_name = first_node.value
if first_name in field_name_set:
ctxt.add_duplicate_error(first_node, first_name)
continue
if second_node.id == "scalar":
# Like "fieldName: typeName".
field = syntax.Field(ctxt.file_name, node.start_mark.line, node.start_mark.column)
field.name = first_name
single_type = syntax.FieldTypeSingle(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
array_type_name = syntax.parse_array_type(second_node.value)
if array_type_name:
single_type.type_name = array_type_name
array_type = syntax.FieldTypeArray(single_type)
field.type = array_type
else:
single_type.type_name = second_node.value
field.type = single_type
else:
# Like "fieldName: { ... options ... }".
field = _parse_field(ctxt, first_name, second_node)
fields.append(field)
field_name_set.add(first_name)
for field in fields:
if field.unstable is not None and field.stability is not None:
ctxt.add_duplicate_unstable_stability(field)
# Convert the deprecated 'unstable' option to the new 'stability' option to keep support for the IDL compatibility tests.
if field.unstable is not None:
if field.unstable:
field.stability = "unstable"
else:
field.stability = "stable"
field.unstable = None
if field.stability is not None and field.stability not in (
"stable",
"unstable",
"internal",
):
ctxt.add_stability_unknown_value(field)
return fields
def _parse_chained_struct(ctxt, name, node):
# type: (errors.ParserContext, str, yaml.nodes.MappingNode) -> syntax.ChainedStruct
"""Parse a chained struct in a struct in the IDL file."""
chain = syntax.ChainedStruct(ctxt.file_name, node.start_mark.line, node.start_mark.column)
chain.name = name
_generic_parser(
ctxt,
node,
"chain",
chain,
{
"cpp_name": _RuleDesc("scalar"),
},
)
return chain
def _parse_chained_structs(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> List[syntax.ChainedStruct]
"""Parse a chained structs in a struct in the IDL file."""
chained_items = []
field_name_set = set() # type: Set[str]
for [first_node, second_node] in node.value:
first_name = first_node.value
if first_name in field_name_set:
ctxt.add_duplicate_error(first_node, first_name)
continue
# Simple Scalar
if second_node.id == "scalar":
chain = syntax.ChainedStruct(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
chain.name = first_name
chain.cpp_name = second_node.value
chained_items.append(chain)
else:
chain = _parse_chained_struct(ctxt, first_name, second_node)
chained_items.append(chain)
field_name_set.add(first_name)
return chained_items
def _parse_struct(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a struct section in the IDL file."""
if not ctxt.is_mapping_node(node, "struct"):
return
struct = syntax.Struct(ctxt.file_name, node.start_mark.line, node.start_mark.column)
struct.name = name
_generic_parser(
ctxt,
node,
"struct",
struct,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"fields": _RuleDesc("mapping", mapping_parser_func=_parse_fields),
"chained_structs": _RuleDesc("mapping", mapping_parser_func=_parse_chained_structs),
"mod_visibility": _RuleDesc("scalar"),
"strict": _RuleDesc("bool_scalar"),
"inline_chained_structs": _RuleDesc("bool_scalar"),
"immutable": _RuleDesc("bool_scalar"),
"generate_comparison_operators": _RuleDesc("bool_scalar"),
"non_const_getter": _RuleDesc("bool_scalar"),
"cpp_validator_func": _RuleDesc("scalar"),
"is_command_reply": _RuleDesc("bool_scalar"),
"is_catalog_ctxt": _RuleDesc("bool_scalar"),
"is_generic_cmd_list": _RuleDesc("scalar"),
"query_shape_component": _RuleDesc("bool_scalar"),
"unsafe_dangerous_disable_extra_field_duplicate_checks": _RuleDesc("bool_scalar"),
},
)
if (
struct.generate_comparison_operators
and struct.fields
and any(isinstance(f.type, syntax.FieldTypeVariant) for f in struct.fields)
):
ctxt.add_variant_comparison_error(struct)
return
_validate_mod_visibility(ctxt, struct.mod_visibility, node)
spec.symbols.add_struct(ctxt, struct)
if struct.is_generic_cmd_list:
if struct.is_generic_cmd_list == "arg":
spec.symbols.add_generic_argument_list(struct)
elif struct.is_generic_cmd_list == "reply":
spec.symbols.add_generic_reply_field_list(struct)
def _parse_arbitrary_value(ctxt, node):
# type: (errors.ParserContext, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> Any
"""Parse a generic YAML type to a Python type."""
if node.id == "mapping":
return {k.value: _parse_arbitrary_value(ctxt, v) for (k, v) in node.value}
elif node.id == "sequence":
return [_parse_arbitrary_value(ctxt, node) for node in node.value]
elif ctxt.is_scalar_node(node, "node"):
return node.value
else:
# Error added to context by is_scalar_node case above
return None
def _parse_enum_values(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> List[syntax.EnumValue]
"""Parse a values section in an enum in the IDL file."""
enum_values = []
field_name_set = set() # type: Set[str]
for [first_node, second_node] in node.value:
first_name = first_node.value
if first_name in field_name_set:
ctxt.add_duplicate_error(first_node, first_name)
continue
enum_value = syntax.EnumValue(ctxt.file_name, node.start_mark.line, node.start_mark.column)
enum_value.name = first_name
if second_node.id == "mapping":
_generic_parser(
ctxt,
second_node,
first_name,
enum_value,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"value": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"extra_data": _RuleDesc("mapping", mapping_parser_func=_parse_arbitrary_value),
},
)
elif ctxt.is_scalar_node(second_node, first_name):
enum_value.value = second_node.value
enum_values.append(enum_value)
field_name_set.add(first_name)
return enum_values
def _parse_enum(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse an enum section in the IDL file."""
if not ctxt.is_mapping_node(node, "enum"):
return
idl_enum = syntax.Enum(ctxt.file_name, node.start_mark.line, node.start_mark.column)
idl_enum.name = name
_generic_parser(
ctxt,
node,
"enum",
idl_enum,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"type": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"mod_visibility": _RuleDesc("scalar"),
"values": _RuleDesc("mapping", mapping_parser_func=_parse_enum_values),
},
)
if idl_enum.values is None:
ctxt.add_empty_enum_error(node, idl_enum.name)
_validate_mod_visibility(ctxt, idl_enum.mod_visibility, node)
spec.symbols.add_enum(ctxt, idl_enum)
def _parse_privilege(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.Privilege
"""Parse a access check section in a struct in the IDL file."""
if not ctxt.is_mapping_node(node, "privilege"):
return None
privilege = syntax.Privilege(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"privilege",
privilege,
{
"resource_pattern": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"action_type": _RuleDesc("scalar_or_sequence", _RuleDesc.REQUIRED),
"agg_stage": _RuleDesc("scalar", _RuleDesc.OPTIONAL),
},
)
return privilege
def _parse_privilege_or_check(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.AccessCheck
"""Parse a privilege section in an access_check in the IDL file."""
access_check = syntax.AccessCheck(ctxt.file_name, node.start_mark.line, node.start_mark.column)
_generic_parser(
ctxt,
node,
"privilege_or_check",
access_check,
{
"check": _RuleDesc("scalar"),
"privilege": _RuleDesc("mapping", mapping_parser_func=_parse_privilege),
},
)
if (access_check.check is None and access_check.privilege is None) or (
access_check.check is not None and access_check.privilege is not None
):
ctxt.add_either_check_or_privilege(access_check)
return None
return access_check
def _parse_complex_sequence(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.SequenceNode) -> List[syntax.AccessCheck]
"""Parse a variant field type's alternative types."""
return [_parse_privilege_or_check(ctxt, child) for child in node.value]
def _parse_access_checks(ctxt, node):
# type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.AccessChecks
"""Parse an access check section in a struct in the IDL file."""
access_checks = syntax.AccessChecks(
ctxt.file_name, node.start_mark.line, node.start_mark.column
)
_generic_parser(
ctxt,
node,
"access_check",
access_checks,
{
"ignore": _RuleDesc("bool_scalar"),
"none": _RuleDesc("bool_scalar"),
"simple": _RuleDesc("mapping", mapping_parser_func=_parse_privilege_or_check),
"complex": _RuleDesc("sequence_mapping", sequence_parser_func=_parse_complex_sequence),
},
)
if ctxt.errors.has_errors():
return None
if (
bool(access_checks.ignore)
+ bool(access_checks.none)
+ bool(access_checks.simple)
+ bool(access_checks.complex)
) != 1:
ctxt.add_empty_access_check(access_checks)
return None
return access_checks
def _parse_command(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a command section in the IDL file."""
if not ctxt.is_mapping_node(node, "command"):
return
command = syntax.Command(ctxt.file_name, node.start_mark.line, node.start_mark.column)
command.name = name
_generic_parser(
ctxt,
node,
"command",
command,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"chained_structs": _RuleDesc("mapping", mapping_parser_func=_parse_chained_structs),
"fields": _RuleDesc("mapping", mapping_parser_func=_parse_fields),
"mod_visibility": _RuleDesc("scalar"),
"namespace": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"cpp_name": _RuleDesc("scalar"),
"type": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_field_type),
"command_name": _RuleDesc("scalar"),
"command_alias": _RuleDesc("scalar"),
"reply_type": _RuleDesc("scalar"),
"api_version": _RuleDesc("scalar"),
"is_deprecated": _RuleDesc("bool_scalar"),
"strict": _RuleDesc("bool_scalar"),
"inline_chained_structs": _RuleDesc("bool_scalar"),
"immutable": _RuleDesc("bool_scalar"),
"generate_comparison_operators": _RuleDesc("bool_scalar"),
"allow_global_collection_name": _RuleDesc("bool_scalar"),
"non_const_getter": _RuleDesc("bool_scalar"),
"access_check": _RuleDesc("mapping", mapping_parser_func=_parse_access_checks),
},
)
valid_commands = [
common.COMMAND_NAMESPACE_CONCATENATE_WITH_DB,
common.COMMAND_NAMESPACE_IGNORED,
common.COMMAND_NAMESPACE_TYPE,
common.COMMAND_NAMESPACE_CONCATENATE_WITH_DB_OR_UUID,
]
if not command.command_name:
ctxt.add_missing_required_field_error(node, "command", "command_name")
if command.api_version is None:
ctxt.add_missing_required_field_error(node, "command", "api_version")
if command.command_alias and command.command_alias == command.command_name:
ctxt.add_duplicate_command_name_and_alias(node)
if command.namespace:
if command.namespace not in valid_commands:
ctxt.add_bad_command_namespace_error(
command, command.name, command.namespace, valid_commands
)
# type property must be specified for a namespace = type
if command.namespace == common.COMMAND_NAMESPACE_TYPE and not command.type:
ctxt.add_missing_required_field_error(node, "command", "type")
if command.namespace != common.COMMAND_NAMESPACE_TYPE and command.type:
ctxt.add_extranous_command_type(command, command.name)
if command.api_version and command.reply_type is None:
ctxt.add_missing_reply_type(command, command.name)
_validate_mod_visibility(ctxt, command.mod_visibility, node)
# Commands may only have the first parameter, ensure the fields property is an empty array.
if not command.fields:
command.fields = []
if not command.api_version:
for field in command.fields:
if field.stability is not None and field.stability != "stable":
ctxt.add_stability_no_api_version(field, command.name)
spec.symbols.add_command(ctxt, command)
def _parse_server_parameter_class(ctxt, node):
# type: (errors.ParserContext, Union[yaml.nodes.ScalarNode,yaml.nodes.MappingNode]) -> syntax.ServerParameterClass
"""Parse a server_parameter.cpp_class as either a scalar or a mapping."""
spc = syntax.ServerParameterClass(ctxt.file_name, node.start_mark.line, node.start_mark.column)
if node.id == "scalar":
spc.name = node.value
return spc
_generic_parser(
ctxt,
node,
"cpp_class",
spc,
{
"name": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"data": _RuleDesc("scalar"),
"override_ctor": _RuleDesc("bool_scalar"),
"override_set": _RuleDesc("bool_scalar"),
"override_validate": _RuleDesc("bool_scalar"),
"override_warn_if_deprecated": _RuleDesc("bool_scalar"),
},
)
return spc
def _parse_server_parameter(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a server_parameters section in the IDL file."""
if not ctxt.is_mapping_node(node, "server_parameters"):
return
param = syntax.ServerParameter(ctxt.file_name, node.start_mark.line, node.start_mark.column)
param.name = name
# Declare as local to avoid ugly formatting with long line.
map_class = _parse_server_parameter_class
_generic_parser(
ctxt,
node,
"server_parameters",
param,
{
"set_at": _RuleDesc("scalar_or_sequence", _RuleDesc.REQUIRED),
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"cpp_vartype": _RuleDesc("scalar"),
"cpp_varname": _RuleDesc("scalar"),
"mod_visibility": _RuleDesc("scalar"),
"condition": _RuleDesc("mapping", mapping_parser_func=_parse_condition),
"redact": _RuleDesc("required_bool_scalar", _RuleDesc.REQUIRED),
"default": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"test_only": _RuleDesc("bool_scalar"),
"deprecated_name": _RuleDesc("scalar_or_sequence"),
"validator": _RuleDesc("mapping", mapping_parser_func=_parse_validator),
"on_update": _RuleDesc("scalar"),
"omit_in_ftdc": _RuleDesc("bool_scalar"),
"cpp_class": _RuleDesc("scalar_or_mapping", mapping_parser_func=map_class),
"is_deprecated": _RuleDesc("bool_scalar"),
},
)
_validate_mod_visibility(ctxt, param.mod_visibility, node)
spec.server_parameters.append(param)
def _parse_feature_flag(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a feature_flags section in the IDL file."""
if not ctxt.is_mapping_node(node, "feature_flags"):
return
param = syntax.FeatureFlag(ctxt.file_name, node.start_mark.line, node.start_mark.column)
param.name = name
_generic_parser(
ctxt,
node,
"feature_flags",
param,
{
"description": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"cpp_varname": _RuleDesc("scalar"),
"mod_visibility": _RuleDesc("scalar"),
"default": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"version": _RuleDesc("scalar"),
"fcv_gated": _RuleDesc(
"scalar_or_mapping", _RuleDesc.REQUIRED, mapping_parser_func=_parse_expression
),
"enable_on_transitional_fcv_UNSAFE": _RuleDesc("bool_scalar"),
"incremental_rollout_phase": _RuleDesc("scalar"),
"check_against_fcv": _RuleDesc("scalar"),
},
)
_validate_mod_visibility(ctxt, param.mod_visibility, node)
spec.feature_flags.append(param)
def _parse_config_option(ctxt, spec, name, node):
# type: (errors.ParserContext, syntax.IDLSpec, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> None
"""Parse a configs section in the IDL file."""
if not ctxt.is_mapping_node(node, "configs"):
return
option = syntax.ConfigOption(ctxt.file_name, node.start_mark.line, node.start_mark.column)
option.name = name
_generic_parser(
ctxt,
node,
"configs",
option,
{
"short_name": _RuleDesc("scalar"),
"single_name": _RuleDesc("scalar"),
"deprecated_name": _RuleDesc("scalar_or_sequence"),
"deprecated_short_name": _RuleDesc("scalar_or_sequence"),
"description": _RuleDesc("scalar_or_mapping", _RuleDesc.REQUIRED, _parse_expression),
"section": _RuleDesc("scalar"),
"arg_vartype": _RuleDesc("scalar", _RuleDesc.REQUIRED),
"cpp_vartype": _RuleDesc("scalar"),
"cpp_varname": _RuleDesc("scalar"),
"mod_visibility": _RuleDesc("scalar"),
"condition": _RuleDesc("mapping", mapping_parser_func=_parse_condition),
"conflicts": _RuleDesc("scalar_or_sequence"),
"requires": _RuleDesc("scalar_or_sequence"),
"hidden": _RuleDesc("bool_scalar"),
"redact": _RuleDesc("bool_scalar"),
"default": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"implicit": _RuleDesc("scalar_or_mapping", mapping_parser_func=_parse_expression),
"source": _RuleDesc("scalar_or_sequence"),
"canonicalize": _RuleDesc("scalar"),
"duplicate_behavior": _RuleDesc("scalar"),
"positional": _RuleDesc("scalar"),
"validator": _RuleDesc("mapping", mapping_parser_func=_parse_validator),
},
)
_validate_mod_visibility(ctxt, option.mod_visibility, node)
spec.configs.append(option)
def _prefix_with_namespace(cpp_namespace, cpp_name):
# type: (str, str) -> str
"""Preface a C++ type name with a namespace if not already qualified or a primitive type."""
if "::" in cpp_name or cpp_types.is_primitive_scalar_type(cpp_name):
return cpp_name
return cpp_namespace + "::" + cpp_name
def _propagate_globals(spec):
# type: (syntax.IDLSpec) -> None
"""Propagate the globals information to each type and struct as needed."""
if not spec.globals or not spec.globals.cpp_namespace:
return
cpp_namespace = spec.globals.cpp_namespace
for struct in spec.symbols.structs:
struct.cpp_namespace = cpp_namespace
for command in spec.symbols.commands:
command.cpp_namespace = cpp_namespace
for idlenum in spec.symbols.enums:
idlenum.cpp_namespace = cpp_namespace
for idltype in spec.symbols.types:
idltype.cpp_type = _prefix_with_namespace(cpp_namespace, idltype.cpp_type)
def parse_file(stream, error_file_name, parse_non_forward_compatible_section=True):
# type: (Any, str, bool) -> syntax.IDLParsedSpec
"""
Parse a YAML document into an idl.syntax tree.
If parse_non_forward_compatible_section is False, don't attempt to parse the feature flag type.
stream: is a io.Stream.
error_file_name: just a file name for error messages to use.
"""
# This will raise an exception if the YAML parse fails
root_node = yaml.compose(stream)
ctxt = errors.ParserContext(error_file_name, errors.ParserErrorCollection())
spec = syntax.IDLSpec()
# If the document is empty, we are done
if not root_node:
return syntax.IDLParsedSpec(spec, None)
if not root_node.id == "mapping":
raise errors.IDLError(
"Expected a YAML mapping node as root node of IDL document, got '%s' instead"
% root_node.id
)
field_name_set = set() # type: Set[str]
for [first_node, second_node] in root_node.value:
first_name = first_node.value
if first_name in field_name_set:
ctxt.add_duplicate_error(first_node, first_name)
continue
if first_name == "global":
_parse_global(ctxt, spec, second_node)
elif first_name == "imports":
_parse_imports(ctxt, spec, second_node)
elif first_name == "enums":
_parse_mapping(ctxt, spec, second_node, "enums", _parse_enum)
elif first_name == "types":
_parse_mapping(ctxt, spec, second_node, "types", _parse_type)
elif first_name == "structs":
_parse_mapping(ctxt, spec, second_node, "structs", _parse_struct)
elif first_name == "commands":
_parse_mapping(ctxt, spec, second_node, "commands", _parse_command)
elif first_name == "server_parameters":
_parse_mapping(ctxt, spec, second_node, "server_parameters", _parse_server_parameter)
elif first_name == "configs":
_parse_mapping(ctxt, spec, second_node, "configs", _parse_config_option)
elif first_name == "feature_flags":
if parse_non_forward_compatible_section:
_parse_mapping(ctxt, spec, second_node, "feature_flags", _parse_feature_flag)
else:
ctxt.add_unknown_root_node_error(first_node)
field_name_set.add(first_name)
if ctxt.errors.has_errors():
return syntax.IDLParsedSpec(None, ctxt.errors)
_propagate_globals(spec)
return syntax.IDLParsedSpec(spec, None)
class ImportResolverBase(object, metaclass=ABCMeta):
"""Base class for resolving imported files."""
def __init__(self):
# type: () -> None
"""Construct a ImportResolver."""
pass
@abstractmethod
def resolve(self, base_file, imported_file_name):
# type: (str, str) -> str
"""Return the complete path to an imported file name."""
pass
@abstractmethod
def open(self, resolved_file_name):
# type: (str) -> Any
"""Return an io.Stream for the requested file."""
pass
def parse(stream, input_file_name, resolver, parse_non_forward_compatible_section=True):
# type: (Any, str, ImportResolverBase, bool) -> syntax.IDLParsedSpec
"""
Parse a YAML document into an idl.syntax tree.
If parse_non_forward_compatible_section is False, don't attempt to parse the feature flag type.
stream: is a io.Stream.
input_file_name: a file name for error messages to use, and to help resolve imported files.
"""
root_doc = parse_file(stream, input_file_name, parse_non_forward_compatible_section)
if root_doc.errors:
return root_doc
imports = [] # type: List[Tuple[common.SourceLocation, str, str]]
if root_doc.spec.symbols.commands:
imports.append((None, input_file_name, common.GENERIC_ARGS_FILENAME))
needs_include = [] # type: List[str]
if root_doc.spec.imports:
imports += [
(root_doc.spec.imports, input_file_name, import_file_name)
for import_file_name in root_doc.spec.imports.imports
]
resolved_file_names = [] # type: List[str]
ctxt = errors.ParserContext(input_file_name, errors.ParserErrorCollection())
# Process imports in a breadth-first search
while imports:
file_import_tuple = imports[0]
imports = imports[1:]
import_location = file_import_tuple[0]
base_file_name = file_import_tuple[1]
imported_file_name = file_import_tuple[2]
# Check for already resolved file
resolved_file_name = resolver.resolve(base_file_name, imported_file_name)
if not resolved_file_name:
ctxt.add_cannot_find_import(import_location, imported_file_name)
return syntax.IDLParsedSpec(None, ctxt.errors)
if resolved_file_name in resolved_file_names:
continue
resolved_file_names.append(resolved_file_name)
# Parse imported file
with resolver.open(resolved_file_name) as file_stream:
parsed_doc = parse_file(
file_stream, resolved_file_name, parse_non_forward_compatible_section
)
# Check for errors
if parsed_doc.errors:
return parsed_doc
# We need to generate includes for imported IDL files which have structs or enums.
if base_file_name == input_file_name and (
parsed_doc.spec.symbols.structs or parsed_doc.spec.symbols.enums
):
needs_include.append(imported_file_name)
# Add other imported files to the list of files to parse
if parsed_doc.spec.imports:
imports += [
(parsed_doc.spec.imports, resolved_file_name, import_file_name)
for import_file_name in parsed_doc.spec.imports.imports
]
# Merge cpp_includes as needed
if parsed_doc.spec.globals and parsed_doc.spec.globals.cpp_includes:
root_doc.spec.globals.cpp_includes = list(
set(root_doc.spec.globals.cpp_includes + parsed_doc.spec.globals.cpp_includes)
)
# Merge symbol tables together
root_doc.spec.symbols.add_imported_symbol_table(ctxt, parsed_doc.spec.symbols)
if ctxt.errors.has_errors():
return syntax.IDLParsedSpec(None, ctxt.errors)
# Resolve the direct imports which contain structs for root document so they can be translated
# into include file paths in generated code.
for needs_include_name in needs_include:
resolved_file_name = resolver.resolve(base_file_name, needs_include_name)
root_doc.spec.imports.resolved_imports.append(resolved_file_name)
if root_doc.spec.imports:
root_doc.spec.imports.dependencies = resolved_file_names
return root_doc