UNPKG

antlr-ng

Version:

Next generation ANTLR Tool

513 lines (512 loc) 16.9 kB
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 };