UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

1,012 lines 63 kB
/****************************************************************************** * Copyright 2021 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ import { DiagnosticTag } from 'vscode-languageserver-types'; import * as ast from '../../languages/generated/ast.js'; import { getContainerOfType, streamAllContents } from '../../utils/ast-utils.js'; import { MultiMap } from '../../utils/collections.js'; import { toDocumentSegment } from '../../utils/cst-utils.js'; import { assertUnreachable } from '../../utils/errors.js'; import { findNameAssignment, findNodeForKeyword, findNodeForProperty, getAllReachableRules, getAllRulesUsedForCrossReferences, isArrayCardinality, isDataTypeRule, isOptionalCardinality, terminalRegex } from '../../utils/grammar-utils.js'; import { stream } from '../../utils/stream.js'; import { UriUtils } from '../../utils/uri-utils.js'; import { diagnosticData } from '../../validation/validation-registry.js'; import { getTypeNameWithoutError, hasDataTypeReturn, isPrimitiveGrammarType, isStringGrammarType, resolveImport, resolveTransitiveImports } from '../internal-grammar-util.js'; import { typeDefinitionToPropertyType } from '../type-system/type-collector/declared-types.js'; import { flattenPlainType, isPlainReferenceType } from '../type-system/type-collector/plain-types.js'; import { isDeclared, isInferred } from '../workspace/documents.js'; export function registerValidationChecks(services) { const registry = services.validation.ValidationRegistry; const validator = services.validation.LangiumGrammarValidator; const checks = { Action: [ validator.checkAssignmentReservedName, ], AbstractRule: validator.checkRuleName, Assignment: [ validator.checkAssignmentWithFeatureName, validator.checkAssignmentToFragmentRule, validator.checkAssignmentTypes, validator.checkAssignmentReservedName, validator.checkPredicateNotSupported ], ParserRule: [ validator.checkParserRuleDataType, validator.checkRuleParameters, validator.checkEmptyParserRule, validator.checkParserRuleReservedName, validator.checkOperatorMultiplicitiesForMultiAssignments, ], InfixRule: [ validator.checkInfixRuleDataType, ], TerminalRule: [ validator.checkTerminalRuleReturnType, validator.checkHiddenTerminalRule, validator.checkEmptyTerminalRule ], InferredType: validator.checkTypeReservedName, Keyword: [ validator.checkKeyword, validator.checkPredicateNotSupported ], UnorderedGroup: validator.checkUnorderedGroup, Grammar: [ validator.checkGrammarName, validator.checkEntryGrammarRule, validator.checkUniqueRuleName, validator.checkUniqueTypeName, validator.checkUniqueTypeAndGrammarNames, validator.checkUniqueImportedRules, validator.checkDuplicateImportedGrammar, validator.checkGrammarForUnusedRules, validator.checkGrammarTypeInfer, validator.checkClashingTerminalNames, ], GrammarImport: validator.checkPackageImport, CharacterRange: validator.checkInvalidCharacterRange, Interface: [ validator.checkTypeReservedName, validator.checkInterfacePropertyTypes, ], Type: [ validator.checkTypeReservedName, ], TypeAttribute: validator.checkTypeReservedName, RuleCall: [ validator.checkUsedHiddenTerminalRule, validator.checkUsedFragmentTerminalRule, validator.checkRuleCallParameters, validator.checkMultiRuleCallsAreAssigned, validator.checkPredicateNotSupported ], TerminalRuleCall: validator.checkUsedHiddenTerminalRule, CrossReference: [ validator.checkCrossReferenceSyntax, validator.checkCrossRefNameAssignment, validator.checkCrossRefTerminalType, validator.checkCrossRefType, validator.checkCrossReferenceToTypeUnion ], SimpleType: validator.checkFragmentsInTypes, ReferenceType: validator.checkReferenceTypeUnion, RegexToken: [ validator.checkInvalidRegexFlags, validator.checkDirectlyUsedRegexFlags ], Group: validator.checkPredicateNotSupported }; registry.register(checks, validator); } export var IssueCodes; (function (IssueCodes) { IssueCodes.GrammarNameUppercase = 'grammar-name-uppercase'; IssueCodes.RuleNameUppercase = 'rule-name-uppercase'; IssueCodes.UseRegexTokens = 'use-regex-tokens'; IssueCodes.EntryRuleTokenSyntax = 'entry-rule-token-syntax'; IssueCodes.CrossRefTokenSyntax = 'cross-ref-token-syntax'; IssueCodes.ParserRuleToTypeDecl = 'parser-rule-to-type-decl'; IssueCodes.UnnecessaryFileExtension = 'unnecessary-file-extension'; IssueCodes.InvalidReturns = 'invalid-returns'; IssueCodes.InvalidInfers = 'invalid-infers'; IssueCodes.MissingInfer = 'missing-infer'; IssueCodes.MissingReturns = 'missing-returns'; IssueCodes.MissingCrossRefTerminal = 'missing-cross-ref-terminal'; IssueCodes.SuperfluousInfer = 'superfluous-infer'; IssueCodes.OptionalUnorderedGroup = 'optional-unordered-group'; IssueCodes.ParsingRuleEmpty = 'parsing-rule-empty'; })(IssueCodes || (IssueCodes = {})); export class LangiumGrammarValidator { constructor(services) { this.options = {}; this.references = services.references.References; this.nodeLocator = services.workspace.AstNodeLocator; this.documents = services.shared.workspace.LangiumDocuments; } checkGrammarName(grammar, accept) { if (grammar.name) { const firstChar = grammar.name.substring(0, 1); if (firstChar.toUpperCase() !== firstChar) { accept('warning', 'Grammar name should start with an upper case letter.', { node: grammar, property: 'name', data: diagnosticData(IssueCodes.GrammarNameUppercase) }); } // this grammar and all its transitively imported grammars need to have different names for (const otherGrammar of resolveTransitiveImports(this.documents, grammar) /* never contains the given initial grammar! */) { if (otherGrammar.name === grammar.name) { accept('error', `This grammar name '${grammar.name}' is also used by the grammar in '${UriUtils.basename(otherGrammar.$document.uri)}'.`, { node: grammar, property: 'name', }); } } } } checkEntryGrammarRule(grammar, accept) { if (grammar.isDeclared && !grammar.name) { // Incomplete syntax: grammar without a name. return; } const entryRules = grammar.rules.filter(e => ast.isParserRule(e) && e.entry); if (grammar.isDeclared && entryRules.length === 0) { const possibleEntryRule = grammar.rules.find(e => ast.isParserRule(e) && !isDataTypeRule(e)); if (possibleEntryRule) { accept('error', 'The grammar is missing an entry parser rule. This rule can be an entry one.', { node: possibleEntryRule, property: 'name', data: diagnosticData(IssueCodes.EntryRuleTokenSyntax) }); } else { accept('error', 'This grammar is missing an entry parser rule.', { node: grammar, property: 'name' }); } } else if (!grammar.isDeclared && entryRules.length >= 1) { entryRules.forEach(rule => accept('error', 'Cannot declare entry rules for unnamed grammars.', { node: rule, property: 'name' })); } else if (entryRules.length > 1) { entryRules.forEach(rule => accept('error', 'The entry rule has to be unique.', { node: rule, property: 'name' })); } else if (entryRules.length === 1 && isDataTypeRule(entryRules[0])) { accept('error', 'The entry rule cannot be a data type rule.', { node: entryRules[0], property: 'name' }); } } /** * Check whether any rule defined in this grammar is a duplicate of an already defined rule or an imported rule */ checkUniqueRuleName(grammar, accept) { const extractor = (grammar) => stream(grammar.rules).filter(rule => !isEmptyRule(rule)); this.checkUniqueName(grammar, accept, extractor, 'rule'); } /** * Check whether any type defined in this grammar is a duplicate of an already defined type or an imported type */ checkUniqueTypeName(grammar, accept) { const extractor = (grammar) => stream(grammar.types).concat(grammar.interfaces); this.checkUniqueName(grammar, accept, extractor, 'type'); } checkUniqueName(grammar, accept, extractor, uniqueObjName) { const map = new MultiMap(); extractor(grammar).forEach(e => map.add(e.name, e)); for (const [, types] of map.entriesGroupedByKey()) { if (types.length > 1) { types.forEach(e => { accept('error', `A ${uniqueObjName}'s name has to be unique.`, { node: e, property: 'name' }); }); } } const imported = new Set(); const resolvedGrammars = resolveTransitiveImports(this.documents, grammar); for (const resolvedGrammar of resolvedGrammars) { extractor(resolvedGrammar).forEach(e => imported.add(e.name)); } for (const name of map.keys()) { if (imported.has(name)) { const types = map.get(name); types.forEach(e => { accept('error', `A ${uniqueObjName} with the name '${e.name}' already exists in an imported grammar.`, { node: e, property: 'name' }); }); } } } // ensures for the set of transitively imported grammars that all their resulting types have names which are different compared to the given input grammar name checkUniqueTypeAndGrammarNames(inputGrammar, accept) { // Collect all relevant grammars. Grammars without name are ignored. const allGrammars = [inputGrammar, ...resolveTransitiveImports(this.documents, inputGrammar)].filter(g => g.name); // Try to find types which are declared in or inferred by the current grammar and all its transitively imported grammars, which have the same name as one of the grammars. // Reuse precomputed types in order not to `streamAllContents` of all grammars to improve performance. // (`streamContents` is not sufficient since actions might infer new types, but are no top-level elements!) // Since there are validations which ensure that types and rules have unique names, multiple resulting types with the same name don't need to be considered here. const declaredOrInferredTypes = inputGrammar.$document.validationResources?.typeToValidationInfo ?? new Map(); for (const grammar of allGrammars) { const type = declaredOrInferredTypes.get(grammar.name); if (type !== undefined) { if (isDeclared(type)) { // Type, Interface reportNonUniqueName(grammar, type.declaredNode, 'name'); } if (isInferred(type)) { for (const node of type.inferredNodes) { if (ast.isParserRule(node)) { // ParserRule reportNonUniqueName(grammar, node, 'name'); } else if (ast.isAction(node)) { // Action if (node.inferredType) { reportNonUniqueName(grammar, node, 'inferredType'); } else { reportNonUniqueName(grammar, node, 'type'); } } else { assertUnreachable(node); } } } } } function reportNonUniqueName(notUniquelyNamedGrammar, node, property) { const nodeGrammar = getContainerOfType(node, ast.isGrammar); if (nodeGrammar === inputGrammar) { // the node is in the current grammar => report the issue at the node if (notUniquelyNamedGrammar === inputGrammar) { // the own grammar has the critical name accept('error', `'${notUniquelyNamedGrammar.name}' is already used here as grammar name.`, { node, property }); } else { // the grammar with the critical name is transitively imported accept('error', `'${notUniquelyNamedGrammar.name}' is already used as grammar name in '${UriUtils.basename(notUniquelyNamedGrammar.$document.uri)}'.`, { node, property }); } } else { // the node is transitively imported => it is not possible to report the issue at the node if (nodeGrammar === notUniquelyNamedGrammar) { // the name of the node is in conflict with the name of its own grammar => report the issue at the node when validating the node's grammar => do nothing here } else if (notUniquelyNamedGrammar === inputGrammar) { // the imported node is in conflict with the current grammar to validate => report the issue at the name of the current grammar accept('error', `'${notUniquelyNamedGrammar.name}' is already used as ${node.$type} name in '${UriUtils.basename(nodeGrammar.$document.uri)}'.`, { node: inputGrammar, property: 'name' }); } else { // the imported node is in conflict with a transitively imported grammar => report the issue for the current grammar in general accept('error', `'${UriUtils.basename(nodeGrammar.$document.uri)}' contains the ${node.$type} with the name '${notUniquelyNamedGrammar.name}', which is already the name of the grammar in '${UriUtils.basename(notUniquelyNamedGrammar.$document.uri)}'.`, { node: inputGrammar, keyword: 'grammar' }); } } } } checkDuplicateImportedGrammar(grammar, accept) { const importMap = new MultiMap(); for (const imp of grammar.imports) { const resolvedGrammar = resolveImport(this.documents, imp); if (resolvedGrammar) { importMap.add(resolvedGrammar, imp); } } for (const [, imports] of importMap.entriesGroupedByKey()) { if (imports.length > 1) { imports.forEach((imp, i) => { if (i > 0) { accept('warning', 'The grammar is already being directly imported.', { node: imp, tags: [DiagnosticTag.Unnecessary] }); } }); } } } /** * Compared to the validation above, this validation only checks whether two imported grammars export the same grammar rule. */ checkUniqueImportedRules(grammar, accept) { const imports = new Map(); for (const imp of grammar.imports) { const importedGrammars = resolveTransitiveImports(this.documents, imp); imports.set(imp, importedGrammars); } const allDuplicates = new MultiMap(); for (const outerImport of grammar.imports) { const outerGrammars = imports.get(outerImport); for (const innerImport of grammar.imports) { if (outerImport === innerImport) { continue; } const innerGrammars = imports.get(innerImport); const duplicates = this.getDuplicateExportedRules(outerGrammars, innerGrammars); for (const duplicate of duplicates) { allDuplicates.add(outerImport, duplicate); } } } for (const imp of grammar.imports) { const duplicates = allDuplicates.get(imp); if (duplicates.length > 0) { accept('error', 'Some rules exported by this grammar are also included in other imports: ' + stream(duplicates).distinct().join(', '), { node: imp, property: 'path' }); } } } getDuplicateExportedRules(outer, inner) { const exclusiveOuter = outer.filter(g => !inner.includes(g)); const outerRules = exclusiveOuter.flatMap(e => e.rules); const innerRules = inner.flatMap(e => e.rules); const duplicates = new Set(); for (const outerRule of outerRules) { const outerName = outerRule.name; for (const innerRule of innerRules) { const innerName = innerRule.name; if (outerName === innerName) { duplicates.add(innerRule.name); } } } return duplicates; } checkGrammarTypeInfer(grammar, accept) { const typesOption = this.options.types || 'normal'; const types = new Set(); for (const type of grammar.types) { types.add(type.name); } for (const interfaceType of grammar.interfaces) { types.add(interfaceType.name); } // Collect type/interface definitions from imported grammars for (const importedGrammar of resolveTransitiveImports(this.documents, grammar)) { importedGrammar.types.forEach(type => types.add(type.name)); importedGrammar.interfaces.forEach(iface => types.add(iface.name)); } for (const rule of grammar.rules.filter(ast.isAbstractParserRule)) { if (isEmptyRule(rule)) { continue; } const isDataType = ast.isParserRule(rule) && isDataTypeRule(rule); const isInfers = !rule.returnType && !rule.dataType; const ruleTypeName = getTypeNameWithoutError(rule); if (typesOption === 'strict' && isInfers) { accept('error', 'Inferred types are not allowed in strict mode.', { node: rule.inferredType ?? rule, property: 'name', data: diagnosticData(IssueCodes.InvalidInfers) }); } else if (!isDataType && ruleTypeName && types.has(ruleTypeName) === isInfers) { if ((isInfers || rule.returnType?.ref !== undefined) && rule.inferredType === undefined) { // report missing returns (a type of the same name is declared) accept('error', getMessage(ruleTypeName, isInfers), { node: rule, property: 'name', data: diagnosticData(IssueCodes.MissingReturns) }); } else if (isInfers || rule.returnType?.ref !== undefined) { // report bad infers (should be corrected to 'returns' to match existing type) const infersNode = findNodeForKeyword(rule.inferredType.$cstNode, 'infers'); accept('error', getMessage(ruleTypeName, isInfers), { node: rule.inferredType, property: 'name', data: { code: IssueCodes.InvalidInfers, actionSegment: toDocumentSegment(infersNode) } }); } } else if (isDataType && isInfers) { const inferNode = findNodeForKeyword(rule.$cstNode, 'infer'); accept('error', 'Data type rules cannot infer a type.', { node: rule, property: 'inferredType', data: { code: IssueCodes.InvalidInfers, actionSegment: toDocumentSegment(inferNode) } }); } } for (const action of streamAllContents(grammar).filter(ast.isAction)) { const actionType = this.getActionType(action); if (actionType) { const isInfers = action.inferredType !== undefined; const typeName = getTypeNameWithoutError(action); if (typesOption === 'strict' && isInfers) { accept('error', 'Inferred types are not allowed in strict mode.', { node: action.inferredType, property: 'name', data: diagnosticData(IssueCodes.InvalidInfers) }); } else if (action.type && typeName && types.has(typeName) === isInfers) { const keywordNode = isInfers ? findNodeForKeyword(action.$cstNode, 'infer') : findNodeForKeyword(action.$cstNode, '{'); accept('error', getMessage(typeName, isInfers), { node: action, property: 'type', data: { code: isInfers ? IssueCodes.SuperfluousInfer : IssueCodes.MissingInfer, actionSegment: toDocumentSegment(keywordNode) } }); } else if (actionType && typeName && types.has(typeName) && isInfers) { // error: action infers type that is already defined if (action.$cstNode) { const inferredTypeNode = findNodeForProperty(action.inferredType?.$cstNode, 'name'); const keywordNode = findNodeForKeyword(action.$cstNode, '{'); if (inferredTypeNode && keywordNode) { // remove everything from the opening { up to the type name // we may lose comments in-between, but this can be undone as needed accept('error', `${typeName} is a declared type and cannot be redefined.`, { node: action, property: 'type', data: { code: IssueCodes.SuperfluousInfer, actionRange: { start: keywordNode.range.end, end: inferredTypeNode.range.start } } }); } } } } } function getMessage(name, infer) { if (infer) { return `The type '${name}' is already explicitly declared and cannot be inferred.`; } else { return `The type '${name}' is not explicitly declared and must be inferred.`; } } } getActionType(rule) { if (rule.type) { return rule.type?.ref; } else if (rule.inferredType) { return rule.inferredType; } return undefined; } checkHiddenTerminalRule(terminalRule, accept) { if (terminalRule.hidden && terminalRule.fragment) { accept('error', 'Cannot use terminal fragments as hidden tokens.', { node: terminalRule, property: 'hidden' }); } } checkEmptyTerminalRule(terminalRule, accept) { try { const regex = terminalRegex(terminalRule); if (new RegExp(regex).test('')) { accept('error', 'This terminal could match an empty string.', { node: terminalRule, property: 'name' }); } } catch { // In case the terminal can't be transformed into a regex, we throw an error // As this indicates unresolved cross references or parser errors, we can ignore this here } } checkEmptyParserRule(parserRule, accept) { // Rule body needs to be set; // Entry rules and fragments may consume no input. if (!parserRule.definition || parserRule.entry || parserRule.fragment) { return; } const consumesAnything = (element) => { // First, check cardinality of the element. if (element.cardinality === '?' || element.cardinality === '*') { return false; } // Actions themselves count as optional. if (ast.isAction(element)) { return false; } // Unordered groups act as alternatives surrounded by `*` if (ast.isUnorderedGroup(element)) { return false; } // Only one element of the group needs to consume something if (ast.isGroup(element)) { return element.elements.some(consumesAnything); } // Every altneratives needs to consume something if (ast.isAlternatives(element)) { return element.elements.every(consumesAnything); } // If the element is a direct rule call // We need to check whether the element consumes anything if (ast.isRuleCall(element)) { if (ast.isInfixRule(element.rule.ref)) { // Infix rules always at least consume their operators return true; } else if (element.rule.ref?.definition) { return consumesAnything(element.rule.ref.definition); } } // Else, assert that we consume something. return true; }; if (!consumesAnything(parserRule.definition)) { accept('warning', 'This parser rule potentially consumes no input.', { node: parserRule, property: 'name', code: IssueCodes.ParsingRuleEmpty }); } } checkInvalidRegexFlags(token, accept) { const regex = token.regex; if (regex) { const slashIndex = regex.lastIndexOf('/'); const flags = regex.substring(slashIndex + 1); // global/multiline/sticky are valid, but not supported const unsupportedFlags = 'gmy'; // only case-insensitive/dotall/unicode are really supported const supportedFlags = 'isu'; const allFlags = unsupportedFlags + supportedFlags; const errorFlags = new Set(); const warningFlags = new Set(); for (let i = 0; i < flags.length; i++) { const flag = flags.charAt(i); if (!allFlags.includes(flag)) { errorFlags.add(flag); } else if (unsupportedFlags.includes(flag)) { warningFlags.add(flag); } } const range = this.getFlagRange(token); if (range) { if (errorFlags.size > 0) { accept('error', `'${Array.from(errorFlags).join('')}' ${errorFlags.size > 1 ? 'are' : 'is'} not valid regular expression flag${errorFlags.size > 1 ? 's' : ''}.`, { node: token, range }); } else if (warningFlags.size > 0) { accept('warning', `'${Array.from(warningFlags).join('')}' regular expression flag${warningFlags.size > 1 ? 's' : ''} will be ignored by Langium.`, { node: token, range }); } } } } checkDirectlyUsedRegexFlags(token, accept) { const regex = token.regex; if (!ast.isTerminalRule(token.$container) && regex) { const slashIndex = regex.lastIndexOf('/'); const flags = regex.substring(slashIndex + 1); const range = this.getFlagRange(token); if (range && flags) { accept('warning', 'Regular expression flags are only applied if the terminal is not a composition.', { node: token, range }); } } } getFlagRange(token) { const regexCstNode = findNodeForProperty(token.$cstNode, 'regex'); if (!regexCstNode || !token.regex) { return undefined; } const regex = token.regex; const slashIndex = regex.lastIndexOf('/') + 1; const range = { start: { line: regexCstNode.range.end.line, character: regexCstNode.range.end.character - regex.length + slashIndex }, end: regexCstNode.range.end }; return range; } checkUsedHiddenTerminalRule(ruleCall, accept) { const parentRule = getContainerOfType(ruleCall, (n) => ast.isTerminalRule(n) || ast.isParserRule(n)); if (parentRule) { if ('hidden' in parentRule && parentRule.hidden) { return; } if ('lookahead' in ruleCall && ruleCall.lookahead) { return; } const terminalGroup = findLookAheadGroup(ruleCall); if (terminalGroup && terminalGroup.lookahead) { return; } const ref = ruleCall.rule.ref; if (ast.isTerminalRule(ref) && ref.hidden) { accept('error', 'Cannot use hidden terminal in non-hidden rule', { node: ruleCall, property: 'rule' }); } } } checkUsedFragmentTerminalRule(ruleCall, accept) { const terminal = ruleCall.rule.ref; if (ast.isTerminalRule(terminal) && terminal.fragment) { const parentRule = getContainerOfType(ruleCall, ast.isParserRule); if (parentRule) { accept('error', 'Cannot use terminal fragments as part of parser rules.', { node: ruleCall, property: 'rule' }); } } } checkCrossReferenceSyntax(crossRef, accept) { if (crossRef.deprecatedSyntax) { accept('error', "'|' is deprecated. Please, use ':' instead.", { node: crossRef, property: 'deprecatedSyntax', data: diagnosticData(IssueCodes.CrossRefTokenSyntax) }); } } checkPackageImport(imp, accept) { const resolvedGrammar = resolveImport(this.documents, imp); if (resolvedGrammar === undefined) { accept('error', 'Import cannot be resolved.', { node: imp, property: 'path' }); } else if (imp.path.endsWith('.langium')) { accept('warning', 'Imports do not need file extensions.', { node: imp, property: 'path', data: diagnosticData(IssueCodes.UnnecessaryFileExtension) }); } } checkInvalidCharacterRange(range, accept) { if (range.right) { const message = 'Character ranges cannot use more than one character'; let invalid = false; if (range.left.value.length > 1) { invalid = true; accept('error', message, { node: range.left, property: 'value' }); } if (range.right.value.length > 1) { invalid = true; accept('error', message, { node: range.right, property: 'value' }); } if (!invalid) { accept('hint', 'Consider using regex instead of character ranges', { node: range, data: diagnosticData(IssueCodes.UseRegexTokens) }); } } } checkGrammarForUnusedRules(grammar, accept) { const reachableRules = getAllReachableRules(grammar, true); const parserRulesUsedByCrossReferences = getAllRulesUsedForCrossReferences(grammar); for (const rule of grammar.rules) { if (ast.isTerminalRule(rule) && rule.hidden || isEmptyRule(rule)) { continue; } if (!reachableRules.has(rule)) { if (ast.isParserRule(rule) && parserRulesUsedByCrossReferences.has(rule)) { accept('hint', 'This parser rule is not used for parsing, but referenced by cross-references. Consider to replace this rule by a type declaration.', { node: rule, data: diagnosticData(IssueCodes.ParserRuleToTypeDecl) }); } else { accept('hint', 'This rule is declared but never referenced.', { node: rule, property: 'name', tags: [DiagnosticTag.Unnecessary] }); } } } } checkClashingTerminalNames(grammar, accept) { const localTerminals = new MultiMap(); const localKeywords = new Set(); // Collect locally defined terminals/keywords for (const rule of grammar.rules) { if (ast.isTerminalRule(rule) && rule.name) { localTerminals.add(rule.name, rule); } if (ast.isAbstractParserRule(rule)) { const keywords = streamAllContents(rule).filter(ast.isKeyword); keywords.forEach(e => localKeywords.add(e.value)); } } // Collect imported terminals/keywords and their respective imports const importedTerminals = new MultiMap(); const importedKeywords = new MultiMap(); for (const importNode of grammar.imports) { const importedGrammars = resolveTransitiveImports(this.documents, importNode); for (const importedGrammar of importedGrammars) { for (const rule of importedGrammar.rules) { if (ast.isTerminalRule(rule) && rule.name) { importedTerminals.add(rule.name, importNode); } else if (ast.isAbstractParserRule(rule) && rule.name) { const keywords = streamAllContents(rule).filter(ast.isKeyword); keywords.forEach(e => importedKeywords.add(e.value, importNode)); } } } } for (const localTerminal of localTerminals.values()) { if (localKeywords.has(localTerminal.name)) { // 1st case: Local terminal with local keyword (error on terminal) accept('error', 'Terminal name clashes with existing keyword.', { node: localTerminal, property: 'name' }); } else if (importedKeywords.has(localTerminal.name)) { const importNode = importedKeywords.get(localTerminal.name); // 2nd case: Local terminal with imported keyword (error on terminal) accept('error', `Terminal name clashes with imported keyword from "${importNode[0].path}".`, { node: localTerminal, property: 'name' }); } } // Collect all imported terminals that share a name with a local keyword const importTerminalMap = new MultiMap(); for (const localKeyword of localKeywords) { for (const importNode of importedTerminals.get(localKeyword)) { importTerminalMap.add(importNode, localKeyword); } } for (const [importNode, keywords] of importTerminalMap.entriesGroupedByKey()) { if (keywords.length > 0) { // 3rd case: Imported terminal with local keyword (error on import) accept('error', `Imported terminals (${keywords.join(', ')}) clash with locally defined keywords.`, { node: importNode, property: 'path' }); } } // Collect all imported terminals that share a name with imported keywords const importKeywordMap = new MultiMap(); for (const [name, imports] of importedTerminals.entriesGroupedByKey()) { const keywordImports = importedKeywords.get(name); if (keywordImports.length > 0) { imports // Exclude transitive keyword/terminal clashing // These errors are already shown in another file // So no need to validate these again here .filter(e => !keywordImports.includes(e)) .forEach(e => importKeywordMap.add(e, name)); } } for (const [importNode, keywords] of importKeywordMap.entriesGroupedByKey()) { if (keywords.length > 0) { // 4th case: Imported terminal with imported keyword (error on import) accept('error', `Imported terminals (${keywords.join(', ')}) clash with imported keywords.`, { node: importNode, property: 'path' }); } } } checkRuleName(rule, accept) { if (rule.name && !isEmptyRule(rule)) { const firstChar = rule.name.substring(0, 1); if (firstChar.toUpperCase() !== firstChar) { accept('warning', 'Rule name should start with an upper case letter.', { node: rule, property: 'name', data: diagnosticData(IssueCodes.RuleNameUppercase) }); } } } /** This validation checks, that parser rules which are called multiple times are assigned (except for fragments). */ checkMultiRuleCallsAreAssigned(call, accept) { const findContainerWithCardinality = (node) => { let result = node; while (result !== undefined) { if (ast.isAbstractElement(result) && (result.cardinality === '+' || result.cardinality === '*')) { break; } result = result.$container; } return result; }; const ref = call.rule.ref; // Parsing an unassigned terminal rule is fine. if (!ref || ast.isTerminalRule(ref) || ast.isInfixRule(ref)) { return; } // Fragment or data type rules are fine too. if (ref.fragment || isDataTypeRule(ref)) { return; } // Multiple unassigned calls if inside a data type rule is fine too. const parentRule = getContainerOfType(call, ast.isParserRule); if (!parentRule || isDataTypeRule(parentRule)) { return; } const appearsMultipleTimes = findContainerWithCardinality(call) !== undefined; const hasAssignment = getContainerOfType(call, ast.isAssignment) !== undefined; if (appearsMultipleTimes && !hasAssignment) { accept('error', `Rule call '${ref.name}' requires assignment when parsed multiple times.`, { node: call }); } } checkTypeReservedName(type, accept) { this.checkReservedName(type, 'name', accept); } checkAssignmentReservedName(assignment, accept) { this.checkReservedName(assignment, 'feature', accept); } checkParserRuleReservedName(rule, accept) { if (!rule.inferredType) { this.checkReservedName(rule, 'name', accept); } } checkReservedName(node, property, accept) { const name = node[property]; if (typeof name === 'string' && reservedNames.has(name)) { accept('error', `'${name}' is a reserved name of the JavaScript runtime.`, { node, property }); } } checkKeyword(keyword, accept) { if (getContainerOfType(keyword, ast.isAbstractParserRule)) { if (keyword.value.length === 0) { accept('error', 'Keywords cannot be empty.', { node: keyword }); } else if (keyword.value.trim().length === 0) { accept('error', 'Keywords cannot only consist of whitespace characters.', { node: keyword }); } else if (/\s/g.test(keyword.value)) { accept('warning', 'Keywords should not contain whitespace characters.', { node: keyword }); } } } checkUnorderedGroup(unorderedGroup, accept) { unorderedGroup.elements.forEach((ele) => { if (isOptionalCardinality(ele.cardinality)) { accept('error', 'Optional elements in Unordered groups are currently not supported', { node: ele, data: diagnosticData(IssueCodes.OptionalUnorderedGroup) }); } }); } checkRuleParameters(rule, accept) { const parameters = rule.parameters; if (parameters.length > 0) { // Check for duplicate parameter names const parameterNames = new MultiMap(); for (const parameter of parameters) { parameterNames.add(parameter.name, parameter); } for (const [name, params] of parameterNames.entriesGroupedByKey()) { if (params.length > 1) { params.forEach(param => { accept('error', `Parameter '${name}' is declared multiple times.`, { node: param, property: 'name' }); }); } } // Check for unused parameters const allReferences = streamAllContents(rule).filter(ast.isParameterReference); for (const parameter of parameters) { if (!allReferences.some(e => e.parameter.ref === parameter)) { accept('hint', `Parameter '${parameter.name}' is unused.`, { node: parameter, tags: [DiagnosticTag.Unnecessary] }); } } } } checkParserRuleDataType(rule, accept) { if (isEmptyRule(rule)) { return; } const hasDatatypeReturnType = hasDataTypeReturn(rule); const dataTypeRule = isDataTypeRule(rule); if (!hasDatatypeReturnType && dataTypeRule) { accept('error', 'This parser rule does not create an object. Add a primitive return type or an action to the start of the rule to force object instantiation.', { node: rule, property: 'name' }); } else if (hasDatatypeReturnType && !dataTypeRule) { accept('error', 'Normal parser rules are not allowed to return a primitive value. Use a datatype rule for that.', { node: rule, property: rule.dataType ? 'dataType' : 'returnType' }); } } checkInfixRuleDataType(rule, accept) { if (rule.dataType) { accept('error', 'Infix rules are not allowed to return a primitive value.', { node: rule, property: 'dataType' }); } } checkAssignmentToFragmentRule(assignment, accept) { if (!assignment.terminal) { return; } if (ast.isRuleCall(assignment.terminal) && ast.isParserRule(assignment.terminal.rule.ref) && assignment.terminal.rule.ref.fragment) { accept('error', `Cannot use fragment rule '${assignment.terminal.rule.ref.name}' for assignment of property '${assignment.feature}'.`, { node: assignment, property: 'terminal' }); } } checkAssignmentTypes(assignment, accept) { if (!assignment.terminal) { return; } let firstType; const foundMixed = streamAllContents(assignment.terminal) .map(node => ast.isCrossReference(node) ? 'ref' : 'other') .find(type => { if (!firstType) { firstType = type; return false; } return type !== firstType; }); if (foundMixed) { accept('error', this.createMixedTypeError(assignment.feature), { node: assignment, property: 'terminal' }); } } /** This validation recursively looks at all assignments (and rewriting actions) with '=' as assignment operator and checks, * whether the operator should be '+=' instead. */ checkOperatorMultiplicitiesForMultiAssignments(rule, accept) { // for usual parser rules AND for fragments, but not for data type rules! if (!rule.dataType) { this.checkOperatorMultiplicitiesForMultiAssignmentsIndependent([rule.definition], accept); } } checkOperatorMultiplicitiesForMultiAssignmentsIndependent(startNodes, accept, map = new Map()) { // check all starting nodes this.checkOperatorMultiplicitiesForMultiAssignmentsNested(startNodes, 1, map, accept); // create the warnings for (const entry of map.values()) { if (entry.counter >= 2) { for (const assignment of entry.assignments) { if (assignment.operator !== '+=') { accept('warning', `Found multiple assignments to '${assignment.feature}' with the '${assignment.operator}' assignment operator. Consider using '+=' instead to prevent data loss.`, { node: assignment, property: 'feature' } // use 'feature' instead of 'operator', since it is pretty hard to see ); } } } } } checkOperatorMultiplicitiesForMultiAssignmentsNested(nodes, parentMultiplicity, map, accept) { let resultCreatedNewObject = false; // check all given elements for (let i = 0; i < nodes.length; i++) { const currentNode = nodes[i]; // Tree-Rewrite-Actions are a special case: a new object is created => following assignments are put into the new object if (ast.isAction(currentNode) && currentNode.feature) { // (This does NOT count for unassigned actions, i.e. actions without feature name, since they change only the type of the current object.) const mapForNewObject = new Map(); storeAssignmentUse(mapForNewObject, currentNode.feature, 1, currentNode); // remember the special rewriting feature // all following nodes are put into the new object => check their assignments independently this.checkOperatorMultiplicitiesForMultiAssignmentsIndependent(nodes.slice(i + 1), accept, mapForNewObject); resultCreatedNewObject = true; break; // breaks the current loop } // all other elements don't create new objects themselves: // the current element can occur multiple times => its assignments can occur multiple times as well let currentMultiplicity = parentMultiplicity; if (ast.isAbstractElement(currentNode) && isArrayCardinality(currentNode.cardinality)) { currentMultiplicity *= 2; // note that the result is not exact (but it is sufficient for the current use case)! } // assignment if (ast.isAssignment(currentNode)) { storeAssignmentUse(map, currentNode.feature, currentMultiplicity, currentNode); } // Search for assignments in used fragments as well, since their property values are stored in the current object. // But do not search in calls of regular parser rules, since parser rules create new objects. if (ast.isRuleCall(currentNode) && ast.isParserRule(currentNode.rule.ref) && currentNode.rule.ref.fragment) { const createdNewObject = this.checkOperatorMultiplicitiesForMultiAssignmentsNested([currentNode.rule.ref.definition], currentMultiplicity, map, accept); resultCreatedNewObject = createdNewObject || resultCreatedNewObject; } // look for assignments to the same feature nested within groups if (ast.isGroup(currentNode) || ast.isUnorderedGroup(currentNode)) { // all members of the group are relavant => collect them all const mapGroup = new Map(); // store assignments for Alternatives separately const createdNewObject = this.checkOperatorMultiplicitiesForMultiAssignmentsNested(currentNode.elements, 1, mapGroup, accept); mergeAssignmentUse(mapGroup, map, createdNewObject ? (s, t) => (s + t) // if a new object is created in the group: ignore the current multiplicity, since a new object is created for each loop cycle! : (s, t) => (s * currentMultiplicity + t) // otherwise as usual: take the current multiplicity into account ); resultCreatedNewObject = createdNewObject || resultCreatedNewObject; } // for alternatives, only a single alternative is used => assume the worst case and ta