diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 19510eb..31614b2 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -131,6 +131,16 @@ class AstNode: yield from child.iterate_children_recursive() + def validate_unique_in_parent(self, error, check=None): + for child in self.parent.children: + if child is self: + break + + if type(child) is type(self): + if check is None or check(child): + raise CompileError(error) + + def validate(token_name=None, end_token_name=None, skip_incomplete=False): """ Decorator for functions that validate an AST node. Exceptions raised during validation are marked with range information from the tokens. """ diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index a542901..c623b9a 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -112,6 +112,14 @@ class Property(AstNode): ) + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate property '{self.tokens['name']}'", + check=lambda child: child.tokens["name"] == self.tokens["name"] + ) + + @docs("name") def property_docs(self): if self.gir_property is not None: diff --git a/blueprintcompiler/language/gtk_a11y.py b/blueprintcompiler/language/gtk_a11y.py index 161c608..57bd6af 100644 --- a/blueprintcompiler/language/gtk_a11y.py +++ b/blueprintcompiler/language/gtk_a11y.py @@ -139,6 +139,13 @@ class A11yProperty(BaseTypedAttribute): did_you_mean=(self.tokens["name"], types.keys()), ) + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate accessibility attribute '{self.tokens['name']}'", + check=lambda child: child.tokens["name"] == self.tokens["name"], + ) + @docs("name") def prop_docs(self): if self.tokens["name"] in get_types(self.root.gir): @@ -156,6 +163,9 @@ class A11y(AstNode): def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "accessibility properties") + @validate("accessibility") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate accessibility block") def emit_xml(self, xml: XmlEmitter): xml.start_tag("accessibility") diff --git a/blueprintcompiler/language/gtk_combo_box_text.py b/blueprintcompiler/language/gtk_combo_box_text.py index 512a245..ecee31f 100644 --- a/blueprintcompiler/language/gtk_combo_box_text.py +++ b/blueprintcompiler/language/gtk_combo_box_text.py @@ -56,6 +56,9 @@ class Items(AstNode): def container_is_combo_box_text(self): validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") + @validate("items") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate items block") def emit_xml(self, xml: XmlEmitter): xml.start_tag("items") diff --git a/blueprintcompiler/language/gtk_file_filter.py b/blueprintcompiler/language/gtk_file_filter.py index 734df47..39e563e 100644 --- a/blueprintcompiler/language/gtk_file_filter.py +++ b/blueprintcompiler/language/gtk_file_filter.py @@ -27,6 +27,18 @@ class Filters(AstNode): def container_is_file_filter(self): validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") + @validate() + def unique_in_parent(self): + # The token argument to validate() needs to be calculated based on + # the instance, hence wrapping it like this. + @validate(self.tokens["tag_name"]) + def wrapped_validator(self): + self.validate_unique_in_parent( + f"Duplicate {self.tokens['tag_name']} block", + check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], + ) + wrapped_validator(self) + def emit_xml(self, xml: XmlEmitter): xml.start_tag(self.tokens["tag_name"]) for child in self.children: diff --git a/blueprintcompiler/language/gtk_layout.py b/blueprintcompiler/language/gtk_layout.py index f3b9160..8ccc136 100644 --- a/blueprintcompiler/language/gtk_layout.py +++ b/blueprintcompiler/language/gtk_layout.py @@ -31,6 +31,13 @@ class LayoutProperty(BaseAttribute): # there isn't really a way to validate these return None + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate layout property '{self.name}'", + check=lambda child: child.name == self.name, + ) + layout_prop = Group( LayoutProperty, @@ -53,6 +60,9 @@ class Layout(AstNode): def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "layout properties") + @validate("layout") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate layout block") def emit_xml(self, xml: XmlEmitter): xml.start_tag("layout") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 54e3d89..de766ed 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -55,6 +55,10 @@ class Widgets(AstNode): def container_is_size_group(self): validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") + @validate("widgets") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate widgets block") + def emit_xml(self, xml: XmlEmitter): xml.start_tag("widgets") for child in self.children: diff --git a/blueprintcompiler/language/gtk_string_list.py b/blueprintcompiler/language/gtk_string_list.py index d79b4ed..4f19190 100644 --- a/blueprintcompiler/language/gtk_string_list.py +++ b/blueprintcompiler/language/gtk_string_list.py @@ -51,6 +51,9 @@ class Strings(AstNode): def container_is_string_list(self): validate_parent_type(self, "Gtk", "StringList", "StringList items") + @validate("strings") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate strings block") def emit_xml(self, xml: XmlEmitter): xml.start_tag("items") diff --git a/blueprintcompiler/language/gtk_styles.py b/blueprintcompiler/language/gtk_styles.py index 346f61a..f93ddc0 100644 --- a/blueprintcompiler/language/gtk_styles.py +++ b/blueprintcompiler/language/gtk_styles.py @@ -41,6 +41,10 @@ class Styles(AstNode): def container_is_widget(self): validate_parent_type(self, "Gtk", "Widget", "style classes") + @validate("styles") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate styles block") + def emit_xml(self, xml: XmlEmitter): xml.start_tag("style") for child in self.children: diff --git a/tests/sample_errors/duplicates.blp b/tests/sample_errors/duplicates.blp new file mode 100644 index 0000000..45e691e --- /dev/null +++ b/tests/sample_errors/duplicates.blp @@ -0,0 +1,45 @@ +using Gtk 4.0; + +Label { + visible: true; + visible: false; + + styles [""] + styles [""] + + accessibility { + label: "label"; + label: "label"; + } + accessibility {} +} + +FileFilter { + suffixes [] + patterns [] + mime-types [] + suffixes [] + patterns [] + mime-types [] +} + +ComboBoxText { + layout { + orientation: vertical; + orientation: vertical; + } + layout {} + + items [] + items [] +} + +SizeGroup { + widgets [] + widgets [] +} + +StringList { + strings [] + strings [] +} diff --git a/tests/sample_errors/duplicates.err b/tests/sample_errors/duplicates.err new file mode 100644 index 0000000..65ed136 --- /dev/null +++ b/tests/sample_errors/duplicates.err @@ -0,0 +1,12 @@ +5,3,7,Duplicate property 'visible' +8,3,6,Duplicate styles block +12,5,5,Duplicate accessibility attribute 'label' +14,3,13,Duplicate accessibility block +21,3,8,Duplicate suffixes block +22,3,8,Duplicate patterns block +23,3,10,Duplicate mime-types block +29,5,11,Duplicate layout property 'orientation' +31,3,6,Duplicate layout block +34,3,5,Duplicate items block +39,3,7,Duplicate widgets block +44,3,7,Duplicate strings block \ No newline at end of file diff --git a/tests/test_samples.py b/tests/test_samples.py index c4a00a8..40e9d0b 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -179,6 +179,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("consecutive_unexpected_tokens") self.assert_sample_error("does_not_implement") self.assert_sample_error("duplicate_obj_id") + self.assert_sample_error("duplicates") self.assert_sample_error("enum_member_dne") self.assert_sample_error("filters_in_non_file_filter") self.assert_sample_error("gtk_3")