antlr-ng
Version:
Next generation ANTLR Tool
513 lines (512 loc) • 16.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
import { ANTLRv4Parser } from "../generated/ANTLRv4Parser.js";
import { GrammarTreeVisitor } from "../tree/walkers/GrammarTreeVisitor.js";
import { Constants } from "../Constants.js";
import { Utils } from "../misc/Utils.js";
import { Character } from "../support/Character.js";
import { GrammarType } from "../support/GrammarType.js";
import { isTokenName } from "../support/helpers.js";
import { IssueCode } from "../tool/Issues.js";
import { Grammar } from "../tool/Grammar.js";
import { RuleRefAST } from "../tool/ast/RuleRefAST.js";
import { TerminalAST } from "../tool/ast/TerminalAST.js";
class BasicSemanticChecks extends GrammarTreeVisitor {
static {
__name(this, "BasicSemanticChecks");
}
/**
* Set of valid imports. Maps delegate to set of delegator grammar types. `validDelegations.get(LEXER)` gives
* list of the kinds of delegators that can import lexers.
*/
static validImportTypes = /* @__PURE__ */ new Map([
[GrammarType.Lexer, [GrammarType.Lexer, GrammarType.Combined]],
[GrammarType.Parser, [GrammarType.Parser, GrammarType.Combined]],
[GrammarType.Combined, [GrammarType.Combined]]
]);
g;
ruleCollector;
/**
* When this is `true`, the semantic checks will report {@link IssueCode.UnrecognizedAsscoOption} where
* appropriate. This may be set to `false` to disable this specific check.
*
* The default value is `true`.
*/
checkAssocElementOption = true;
/** This field is used for reporting the {@link IssueCode.ModeWithoutRules} error when necessary. */
nonFragmentRuleCount;
/**
* This is `true` from the time {@link discoverLexerRule} is called for a lexer rule with the `fragment` modifier
* until {@link exitLexerRule} is called.
*/
inFragmentRule;
/** Value of caseInsensitive option (false if not defined) */
grammarCaseInsensitive = false;
constructor(g, ruleCollector) {
super(g.tool.errorManager);
this.g = g;
this.ruleCollector = ruleCollector;
}
process() {
this.visitGrammar(this.g.ast);
}
// Routines to route visitor traffic to the checking routines.
discoverGrammar(root, id) {
this.checkGrammarName(id.token);
}
finishPrequels(firstPrequel) {
if (firstPrequel === null) {
return;
}
const parent = firstPrequel.parent;
const options = parent.getAllChildrenWithType(ANTLRv4Parser.OPTIONS);
const imports = parent.getAllChildrenWithType(ANTLRv4Parser.IMPORT);
const tokens = parent.getAllChildrenWithType(ANTLRv4Parser.TOKENS);
this.checkNumPrequels(options, imports, tokens);
}
importGrammar(label, id) {
this.checkImport(id.token);
}
discoverRules(rules) {
this.checkNumRules(rules);
}
modeDef(m, id) {
if (!this.g.isLexer()) {
this.g.tool.errorManager.grammarError(
IssueCode.ModeNotInLexer,
this.g.fileName,
id.token,
id.token.text,
this.g
);
}
}
discoverRule(rule, id) {
this.checkInvalidRuleDef(id.token);
}
discoverLexerRule(rule, id, modifiers) {
this.checkInvalidRuleDef(id.token);
for (const tree of modifiers) {
if (tree.getType() === ANTLRv4Parser.FRAGMENT) {
this.inFragmentRule = true;
}
}
if (!this.inFragmentRule) {
this.nonFragmentRuleCount++;
}
}
ruleRef(ref) {
this.checkInvalidRuleRef(ref.token);
}
grammarOption(id, valueAST) {
this.checkOptions(this.g.ast, id.token, valueAST);
}
ruleOption(id, valueAST) {
this.checkOptions(id.getAncestor(ANTLRv4Parser.RULE), id.token, valueAST);
}
blockOption(id, valueAST) {
this.checkOptions(id.getAncestor(ANTLRv4Parser.BLOCK), id.token, valueAST);
}
defineToken(id) {
this.checkTokenDefinition(id.token);
}
defineChannel(id) {
this.checkChannelDefinition(id.token);
}
elementOption(t, id, valueAST) {
this.checkElementOptions(t, id, valueAST);
}
finishRule(rule) {
if (rule.isLexerRule()) {
return;
}
const blk = rule.getFirstChildWithType(ANTLRv4Parser.BLOCK);
const altCount = blk.children.length;
const idAST = rule.children[0];
for (let i = 0; i < altCount; i++) {
const altAST = blk.children[i];
if (altAST.altLabel) {
const altLabel = altAST.altLabel.getText();
const r = this.ruleCollector.nameToRuleMap.get(Utils.decapitalize(altLabel));
if (r) {
this.g.tool.errorManager.grammarError(
IssueCode.AltLabelConflictsWithRule,
this.g.fileName,
altAST.altLabel.token,
altLabel,
r.name
);
}
const prevRuleForLabel = this.ruleCollector.altLabelToRuleName.get(altLabel);
if (prevRuleForLabel && prevRuleForLabel !== rule.getRuleName()) {
this.g.tool.errorManager.grammarError(
IssueCode.AltLabelRedef,
this.g.fileName,
altAST.altLabel.token,
altLabel,
rule.getRuleName(),
prevRuleForLabel
);
}
}
}
const altLabels = this.ruleCollector.ruleToAltLabels.get(rule.getRuleName());
const numAltLabels = altLabels?.length ?? 0;
if (numAltLabels > 0 && altCount !== numAltLabels) {
this.g.tool.errorManager.grammarError(
IssueCode.RuleWithTooFewAltLabels,
this.g.fileName,
idAST.token,
rule.getRuleName()
);
}
}
actionInAlt(action) {
if (this.inFragmentRule) {
const fileName = action.token.inputStream.getSourceName();
const ruleName = this.currentRuleName;
this.g.tool.errorManager.grammarError(IssueCode.FragmentActionIgnored, fileName, action.token, ruleName);
}
}
label(op, id, element) {
switch (element.getType()) {
case ANTLRv4Parser.TOKEN_REF:
case ANTLRv4Parser.STRING_LITERAL:
case ANTLRv4Parser.RANGE:
case ANTLRv4Parser.SET:
case ANTLRv4Parser.NOT:
case ANTLRv4Parser.RULE_REF:
case ANTLRv4Parser.STAR: {
return;
}
default: {
const fileName = id.token.inputStream.getSourceName();
this.g.tool.errorManager.grammarError(
IssueCode.LabelBlockNotASet,
fileName,
id.token,
id.getText()
);
break;
}
}
}
enterMode(tree) {
this.nonFragmentRuleCount = 0;
}
exitMode(tree) {
if (this.nonFragmentRuleCount === 0) {
let token = tree.token;
let name = "?";
if (tree.children.length > 0) {
name = tree.children[0]?.getText() ?? "";
if (!name) {
name = "?";
}
token = tree.children[0].token;
}
this.g.tool.errorManager.grammarError(IssueCode.ModeWithoutRules, this.g.fileName, token, name, this.g);
}
}
exitLexerRule(tree) {
this.inFragmentRule = false;
}
enterChannelsSpec(tree) {
const errorType = this.g.isParser() ? IssueCode.ChannelsBlockInParserGrammar : this.g.isCombined() ? IssueCode.ChannelsBlockInCombinedGrammar : null;
if (errorType !== null) {
this.g.tool.errorManager.grammarError(errorType, this.g.fileName, tree.token);
}
}
// Routines to do the actual work of checking issues with a grammar.
// They are triggered by the visitor methods above.
checkGrammarName(nameToken) {
const fullyQualifiedName = nameToken.inputStream?.getSourceName();
if (!fullyQualifiedName) {
return;
}
if (this.g.originalGrammar) {
return;
}
const fileName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf("."));
if (fileName !== nameToken.text && fullyQualifiedName !== Constants.GrammarFromStringName) {
this.g.tool.errorManager.grammarError(
IssueCode.FileAndGrammarNameDiffer,
fullyQualifiedName,
nameToken,
nameToken.text,
fullyQualifiedName
);
}
}
checkNumRules(rulesNode) {
if (rulesNode.children.length === 0) {
const root = rulesNode.parent;
const idNode = root.children[0];
this.g.tool.errorManager.grammarError(
IssueCode.NoRules,
this.g.fileName,
null,
idNode.getText(),
this.g
);
}
}
checkNumPrequels(options, imports, tokens) {
const secondOptionTokens = new Array();
if (options.length > 1) {
secondOptionTokens.push(options[1].token);
}
if (imports.length > 1) {
secondOptionTokens.push(imports[1].token);
}
if (tokens.length > 1) {
secondOptionTokens.push(tokens[1].token);
}
for (const t of secondOptionTokens) {
const fileName = t.inputStream.getSourceName();
this.g.tool.errorManager.grammarError(IssueCode.RepeatedPrequel, fileName, t);
}
}
checkInvalidRuleDef(ruleID) {
const fileName = ruleID.inputStream?.getSourceName() ?? "<none>";
if (this.g.isLexer() && Character.isLowerCase(ruleID.text.codePointAt(0))) {
this.g.tool.errorManager.grammarError(IssueCode.ParserRuleNotAllowed, fileName, ruleID, ruleID.text);
}
if (this.g.isParser() && isTokenName(ruleID.text)) {
this.g.tool.errorManager.grammarError(IssueCode.LexerRulesNotAllowed, fileName, ruleID, ruleID.text);
}
}
checkInvalidRuleRef(ruleID) {
const fileName = ruleID.inputStream?.getSourceName();
if (this.g.isLexer() && Character.isLowerCase(ruleID.text.codePointAt(0))) {
this.g.tool.errorManager.grammarError(
IssueCode.ParserRuleRefInLexerRule,
fileName ?? "<none>",
ruleID,
ruleID.text,
this.currentRuleName
);
}
}
checkTokenDefinition(tokenID) {
const fileName = tokenID.inputStream?.getSourceName();
if (!isTokenName(tokenID.text)) {
this.g.tool.errorManager.grammarError(
IssueCode.TokenNamesMustStartUpper,
fileName ?? "<none>",
tokenID,
tokenID.text
);
}
}
checkChannelDefinition(tokenID) {
}
enterLexerElement(tree) {
}
enterLexerCommand(tree) {
this.checkElementIsOuterMostInSingleAlt(tree);
if (this.inFragmentRule) {
const fileName = tree.token?.inputStream?.getSourceName();
const ruleName = this.currentRuleName;
this.g.tool.errorManager.grammarError(
IssueCode.FragmentActionIgnored,
fileName ?? "<none>",
tree.token,
ruleName
);
}
}
/**
* Make sure that action is the last element in an outer alt. Here action, a2, z, and zz are bad, but a3 is ok:
* ```
* (RULE A (BLOCK (ALT {action} 'a')))
* (RULE B (BLOCK (ALT (BLOCK (ALT {a2} 'x') (ALT 'y')) {a3})))
* (RULE C (BLOCK (ALT 'd' {z}) (ALT 'e' {zz})))
* ```
*
* @param tree The action node to check.
*/
checkElementIsOuterMostInSingleAlt(tree) {
const alt = tree.parent;
const blk = alt.parent;
const outerMostAlt = blk.parent.getType() === ANTLRv4Parser.RULE;
const rule = tree.getAncestor(ANTLRv4Parser.RULE);
const fileName = tree.token?.inputStream?.getSourceName();
if (!outerMostAlt || blk.children.length > 1) {
const e = IssueCode.LexerCommandPlacementIssue;
this.g.tool.errorManager.grammarError(e, fileName ?? "<none>", tree.token, rule.children[0].getText());
}
}
enterTerminal(tree) {
const text = tree.getText();
if (text === "''") {
this.g.tool.errorManager.grammarError(
IssueCode.EmptyStringAndSetsNotAllowed,
this.g.fileName,
tree.token,
"''"
);
}
}
/**
* Checks that an option is appropriate for grammar, rule, subrule.
*
* @param parent The parent node of the option.
* @param optionID The ID of the option.
* @param valueAST The value of the option.
*/
checkOptions(parent, optionID, valueAST) {
let optionsToCheck = null;
const parentType = parent.getType();
switch (parentType) {
case ANTLRv4Parser.BLOCK: {
optionsToCheck = this.g.isLexer() ? Grammar.lexerBlockOptions : Grammar.parserBlockOptions;
break;
}
case ANTLRv4Parser.RULE: {
optionsToCheck = this.g.isLexer() ? Grammar.lexerRuleOptions : Grammar.parseRuleOptions;
break;
}
case ANTLRv4Parser.GRAMMAR: {
optionsToCheck = this.g.type === GrammarType.Lexer ? Grammar.lexerOptions : Grammar.parserOptions;
break;
}
default:
}
const optionName = optionID.text;
if (optionsToCheck !== null && !optionsToCheck.has(optionName)) {
this.g.tool.errorManager.grammarError(IssueCode.IllegalOption, this.g.fileName, optionID, optionName);
} else {
this.checkCaseInsensitiveOption(optionID, valueAST, parentType);
}
}
/**
* Checks that an option is appropriate for elem. Parent of ID is ELEMENT_OPTIONS.
*
* @param elem The element to check.
* @param id The ID of the option.
* @param valueAST The value of the option.
*
* @returns `true` if the option is valid for the element, `false` otherwise.
*/
checkElementOptions(elem, id, valueAST) {
if (this.checkAssocElementOption && id.getText() === "assoc") {
if (elem.getType() !== ANTLRv4Parser.ALT) {
const optionID = id.token;
const fileName = optionID.inputStream?.getSourceName();
this.g.tool.errorManager.grammarError(
IssueCode.UnrecognizedAsscoOption,
fileName ?? "<none>",
optionID,
this.currentRuleName
);
}
}
if (elem instanceof RuleRefAST) {
return this.checkRuleRefOptions(elem, id, valueAST);
}
if (elem instanceof TerminalAST) {
return this.checkTokenOptions(elem, id, valueAST);
}
if (elem.getType() === ANTLRv4Parser.ACTION) {
return false;
}
if (elem.getType() === ANTLRv4Parser.SEMPRED) {
const optionID = id.token;
const fileName = optionID.inputStream?.getSourceName();
if (valueAST !== null && !Grammar.semPredOptions.has(optionID.text)) {
this.g.tool.errorManager.grammarError(
IssueCode.IllegalOption,
fileName ?? "<none>",
optionID,
optionID.text
);
return false;
}
}
return false;
}
checkRuleRefOptions(elem, id, valueAST) {
const optionID = id.token;
const fileName = optionID.inputStream?.getSourceName();
if (valueAST !== null && !Grammar.ruleRefOptions.has(optionID.text)) {
this.g.tool.errorManager.grammarError(
IssueCode.IllegalOption,
fileName ?? "<none>",
optionID,
optionID.text
);
return false;
}
return true;
}
checkTokenOptions(elem, id, valueAST) {
const optionID = id.token;
const fileName = optionID.inputStream?.getSourceName();
if (valueAST !== null && !Grammar.tokenOptions.has(optionID.text)) {
this.g.tool.errorManager.grammarError(
IssueCode.IllegalOption,
fileName ?? "<none>",
optionID,
optionID.text
);
return false;
}
return true;
}
checkImport(importID) {
const delegate = this.g.getImportedGrammar(importID.text);
if (delegate === null) {
return;
}
const validDelegators = BasicSemanticChecks.validImportTypes.get(delegate.type);
if (validDelegators && !validDelegators.includes(this.g.type)) {
this.g.tool.errorManager.grammarError(
IssueCode.InvalidImport,
this.g.fileName,
importID,
this.g,
delegate
);
}
if (this.g.isCombined() && (delegate.name === this.g.name + Grammar.getGrammarTypeToFileNameSuffix(GrammarType.Lexer) || delegate.name === this.g.name + Grammar.getGrammarTypeToFileNameSuffix(GrammarType.Parser))) {
this.g.tool.errorManager.grammarError(
IssueCode.ImportNameClash,
this.g.fileName,
importID,
this.g,
delegate
);
}
}
checkCaseInsensitiveOption(optionID, valueAST, parentType) {
const optionName = optionID.text;
if (optionName === Grammar.caseInsensitiveOptionName) {
const valueText = valueAST.getText();
if (valueText === "true" || valueText === "false") {
const currentValue = valueText === "true";
if (parentType === ANTLRv4Parser.GRAMMAR) {
this.grammarCaseInsensitive = currentValue;
} else if (this.grammarCaseInsensitive === currentValue) {
this.g.tool.errorManager.grammarError(
IssueCode.RedundantCaseInsensitiveLexerRuleOption,
this.g.fileName,
optionID,
currentValue
);
}
} else {
this.g.tool.errorManager.grammarError(
IssueCode.IllegalOptionValue,
this.g.fileName,
valueAST.token,
optionName,
valueText
);
}
}
}
}
export {
BasicSemanticChecks
};