Skip to content

Commit f6f129a

Browse files
committed
DOP-3107: Validate list-table structure
1 parent e3c912d commit f6f129a

File tree

3 files changed

+56
-8
lines changed

3 files changed

+56
-8
lines changed

snooty/n.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,24 @@ def verify(self) -> None:
191191
for child in self.children:
192192
child.verify()
193193

194+
def check_tree(
195+
self, *types: "Type[Parent[Any]]"
196+
) -> "Optional[Tuple[Node, Type[Parent[Any]]]]":
197+
"""Ensure that this node's hierarchy matches, at each level, a given type. If there is a mismatch,
198+
return the first node that fails validation, and the type that was expected at that level."""
199+
if not types:
200+
return None
201+
202+
for child in self.children:
203+
if not isinstance(child, types[0]):
204+
return child, types[0]
205+
206+
result = child.check_tree(*types[1:])
207+
if result:
208+
return result
209+
210+
return None
211+
194212

195213
@dataclass
196214
class InlineParent(InlineNode, Parent[InlineNode]):

snooty/parser.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,17 @@ def dispatch_departure(self, node: docutils.nodes.Node) -> None:
448448
and popped.options["tabset"] != "tab"
449449
):
450450
self.handle_tabset(popped)
451+
elif isinstance(popped, n.Directive) and popped.name == "list-table":
452+
failed_node = popped.check_tree(
453+
n.ListNode, n.ListNodeItem, n.ListNode, n.ListNodeItem
454+
)
455+
if failed_node:
456+
self.diagnostics.append(
457+
InvalidTableStructure(
458+
f"Incorrect list-item directive child: expected {failed_node[1].__name__}, got {type(failed_node[0]).__name__}. List tables must contain a list of lists",
459+
failed_node[0].start[0],
460+
)
461+
)
451462

452463
elif isinstance(popped, n.Directive) and popped.name == "tabs":
453464
self.validate_tabs_children(popped)
@@ -610,10 +621,10 @@ def handle_directive(
610621
self.validate_and_add_asset(doc, argument_text, line)
611622

612623
elif name == "list-table":
613-
# Calculate the expected number of columns for this list-table structure.
614624
if not node.children:
615625
return doc
616626

627+
# Calculate the expected number of columns for this list-table structure.
617628
expected_num_columns = 0
618629
if "widths" in options:
619630
widths = re.split(r"[,\s][\s]?", options["widths"])
@@ -623,7 +634,9 @@ def handle_directive(
623634
if expected_num_columns == 0 and list_item.children:
624635
expected_num_columns = len(list_item.children[0].children)
625636
for bullets in list_item.children:
626-
self.validate_list_table(bullets, expected_num_columns)
637+
self.diagnostics.extend(
638+
self.validate_list_table_item(bullets, expected_num_columns)
639+
)
627640

628641
elif name == "openapi":
629642
# Parsing should be done by the OpenAPI renderer on the frontend by default
@@ -973,9 +986,10 @@ def validate_doc_role(self, node: docutils.nodes.Node) -> None:
973986
)
974987
)
975988

976-
def validate_list_table(
977-
self, node: docutils.nodes.Node, expected_num_columns: int
978-
) -> None:
989+
@staticmethod
990+
def validate_list_table_item(
991+
node: docutils.nodes.Node, expected_num_columns: int
992+
) -> Sequence[Diagnostic]:
979993
"""Validate list-table structure"""
980994
if (
981995
isinstance(node, docutils.nodes.bullet_list)
@@ -984,10 +998,11 @@ def validate_list_table(
984998
msg = (
985999
f'Expected "{expected_num_columns}" columns, saw "{len(node.children)}"'
9861000
)
987-
self.diagnostics.append(
1001+
return [
9881002
InvalidTableStructure(msg, util.get_line(node) + len(node.children) - 1)
989-
)
990-
return
1003+
]
1004+
1005+
return []
9911006

9921007
def validate_tabs_children(self, node: n.Directive) -> None:
9931008
new_children: List[n.Node] = []

snooty/test_parser.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
InvalidDirectiveStructure,
1313
InvalidField,
1414
InvalidLiteralInclude,
15+
InvalidTableStructure,
1516
InvalidURL,
1617
MakeCorrectionMixin,
1718
MalformedGlossary,
@@ -2489,6 +2490,20 @@ def test_list_table() -> None:
24892490
)
24902491
assert len(diagnostics) == 0
24912492

2493+
# Broken list-table
2494+
page, diagnostics = parse_rst(
2495+
parser,
2496+
path,
2497+
"""
2498+
.. list-table::
2499+
2500+
* -Java (Synchronous) version 4.7.0-beta0 or later.
2501+
- MongoCrypt version 1.5.0-rc2 or later
2502+
""",
2503+
)
2504+
print(ast_to_testing_string(page.ast))
2505+
assert [type(x) for x in diagnostics] == [InvalidTableStructure]
2506+
24922507

24932508
def test_footnote() -> None:
24942509
path = ROOT_PATH.joinpath(Path("test.rst"))

0 commit comments

Comments
 (0)