Skip to content

Commit 1113d80

Browse files
committed
Fix recursive root-accessible grammar check
This fixes a bug in how the `@root` grammar validation was done. It was not properly handling recursive rules because when walking the tree it would find itself, and thus mark it as "used". What we really want is to only walk starting from the root set, and don't remove recursive definitions from the root set.
1 parent a24e6e9 commit 1113d80

File tree

3 files changed

+30
-8
lines changed

3 files changed

+30
-8
lines changed

mdbook-spec/src/grammar.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,31 @@ fn check_undefined_nt(grammar: &Grammar, diag: &mut Diagnostics) {
195195
/// This is intended to help catch any unexpected misspellings, orphaned
196196
/// productions, or general mistakes.
197197
fn check_unexpected_roots(grammar: &Grammar, diag: &mut Diagnostics) {
198+
// `set` starts with every production name.
198199
let mut set: HashSet<_> = grammar.name_order.iter().map(|s| s.as_str()).collect();
199-
grammar.visit_nt(&mut |nt| {
200-
set.remove(nt);
201-
});
200+
fn remove(set: &mut HashSet<&str>, grammar: &Grammar, prod: &Production, root_name: &str) {
201+
prod.expression.visit_nt(&mut |nt| {
202+
// Leave the root name in the set if we find it recursively.
203+
if nt == root_name {
204+
return;
205+
}
206+
if !set.remove(nt) {
207+
return;
208+
}
209+
if let Some(nt_prod) = grammar.productions.get(nt) {
210+
remove(set, grammar, nt_prod, root_name);
211+
}
212+
});
213+
}
214+
// Walk the productions starting from the root nodes, and remove every
215+
// non-terminal from `set`. What's left must be the set of roots.
216+
grammar
217+
.productions
218+
.values()
219+
.filter(|prod| prod.is_root)
220+
.for_each(|root| {
221+
remove(&mut set, grammar, root, &root.name);
222+
});
202223
let expected: HashSet<_> = grammar
203224
.productions
204225
.values()
@@ -210,17 +231,18 @@ fn check_unexpected_roots(grammar: &Grammar, diag: &mut Diagnostics) {
210231
if !new.is_empty() {
211232
warn_or_err!(
212233
diag,
213-
"New grammar production detected that is not used in any other\n\
234+
"New grammar production detected that is not used in any root-accessible\n\
214235
production. If this is expected, mark the production with\n\
215236
`@root`. If not, make sure it is spelled correctly and used in\n\
216-
another production.\n\
237+
another root-accessible production.\n\
217238
\n\
218239
The new names are: {new:?}\n"
219240
);
220241
} else if !removed.is_empty() {
221242
warn_or_err!(
222243
diag,
223-
"Old grammar production root seems to have been removed.\n\
244+
"Old grammar production root seems to have been removed\n\
245+
(it is used in some other production that is root-accessible).\n\
224246
If this is expected, remove `@root` from the production.\n\
225247
\n\
226248
The removed names are: {removed:?}\n"

src/attributes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ attributes]. It has the following grammar:
115115

116116
r[attributes.meta.syntax]
117117
```grammar,attributes
118-
MetaItem ->
118+
@root MetaItem ->
119119
SimplePath
120120
| SimplePath `=` Expression
121121
| SimplePath `(` MetaSeq? `)`

src/comments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ OUTER_BLOCK_DOC ->
3030
( BlockCommentOrDoc | ~[`*/` CR] )*
3131
`*/`
3232
33-
BlockCommentOrDoc ->
33+
@root BlockCommentOrDoc ->
3434
BLOCK_COMMENT
3535
| OUTER_BLOCK_DOC
3636
| INNER_BLOCK_DOC

0 commit comments

Comments
 (0)