fix(generate): validate symbol-only grammar fields

Require `word`, `conflicts`, `inline`, and `supertypes` callbacks
to return named grammar symbols instead of silently accepting values
that produce undefined names.

Restrict `precedences` lists to named rules and precedence names, and
preserve the existing handling of undefined and duplicate `inline`
rules.
This commit is contained in:
Will Lillis 2026-08-23 16:35:41 -04:00
parent 7ca101cb7e
commit 1cb91662a5
2 changed files with 40 additions and 13 deletions

View file

@ -380,14 +380,16 @@ function grammar(baseGrammar, options) {
let word = baseGrammar.word;
if (options.word) {
word = options.word.call(ruleBuilder, ruleBuilder).name;
if (typeof word != 'string') {
const wordRule = options.word.call(ruleBuilder, ruleBuilder);
if (wordRule?.name === 'ReferenceError') {
throw new Error("Grammar's 'word' property must be a valid named rule.");
}
if (wordRule?.type !== 'SYMBOL' || typeof wordRule.name !== 'string') {
throw new Error("Grammar's 'word' property must be a named rule.");
}
if (word === 'ReferenceError') {
throw new Error("Grammar's 'word' property must be a valid rule name.");
}
word = wordRule.name;
}
let conflicts = baseGrammar.conflicts;
@ -408,7 +410,13 @@ function grammar(baseGrammar, options) {
throw new Error("Grammar's conflicts must be an array of arrays of rules.");
}
return conflictSet.map(symbol => normalize(symbol).name);
return conflictSet.map(symbol => {
const rule = normalize(symbol);
if (rule.type !== 'SYMBOL') {
throw new Error("Grammar's conflicts must contain only named rules.");
}
return rule.name;
});
});
}
@ -426,12 +434,15 @@ function grammar(baseGrammar, options) {
}
inline = inlineRules.filter((symbol, index, self) => {
if (self.findIndex(s => s.name === symbol.name) !== index) {
console.log(`Warning: duplicate inline rule '${symbol.name}'`);
if (symbol?.name === 'ReferenceError') {
console.log(`Warning: inline rule '${symbol.symbol.name}' is not defined.`);
return false;
}
if (symbol.name === 'ReferenceError') {
console.log(`Warning: inline rule '${symbol.symbol.name}' is not defined.`);
if (symbol?.type !== 'SYMBOL' || typeof symbol.name !== 'string') {
throw new Error("Grammar's inline property must contain only named rules.");
}
if (self.findIndex(s => s?.name === symbol.name) !== index) {
console.log(`Warning: duplicate inline rule '${symbol.name}'`);
return false;
}
return true;
@ -452,9 +463,12 @@ function grammar(baseGrammar, options) {
}
supertypes = supertypeRules.map(symbol => {
if (symbol.name === 'ReferenceError') {
if (symbol?.name === 'ReferenceError') {
throw new Error(`Supertype rule \`${symbol.symbol.name}\` is not defined.`);
}
if (symbol?.type !== 'SYMBOL' || typeof symbol.name !== 'string') {
throw new Error("Grammar's supertypes property must contain only named rules.");
}
return symbol.name;
});
}
@ -472,7 +486,15 @@ function grammar(baseGrammar, options) {
if (!Array.isArray(list)) {
throw new Error("Grammar's precedences must be an array of arrays of rules.");
}
return list.map(normalize);
return list.map(entry => {
const rule = normalize(entry);
if (rule.type !== 'STRING' && rule.type !== 'SYMBOL') {
throw new Error(
"Grammar's precedences must contain only precedence names or named rules."
);
}
return rule;
});
});
}

View file

@ -395,7 +395,11 @@ mod tests {
r"
module.exports = grammar({
name: 'test',
rules: { source_file: $ => 'hello' }
word: $ => $.identifier,
rules: {
source_file: $ => $.identifier,
identifier: $ => 'hello'
}
});
",
)
@ -403,6 +407,7 @@ mod tests {
let json = execute_native_runtime(&grammar_path).expect("Failed to execute grammar");
assert!(json.contains("\"name\": \"test\""));
assert!(json.contains("\"word\": \"identifier\""));
assert!(json.contains("\"hello\""));
});
}