langium
Version:
A language engineering tool for the Language Server Protocol
292 lines • 11 kB
JavaScript
/******************************************************************************
* 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 * as ast from '../../languages/generated/ast.js';
import { isAstNode } from '../../syntax-tree.js';
import { getContainerOfType } from '../../utils/ast-utils.js';
import { getCrossReferenceTerminal, getExplicitRuleType, getTypeName, isArrayCardinality, isOptionalCardinality, terminalRegex } from '../../utils/grammar-utils.js';
/**
* Calculates any features that can follow the given feature stack.
* This also includes features following optional features and features from previously called rules that could follow the last feature.
* @param featureStack A stack of features starting at the entry rule and ending at the feature of the current cursor position.
* @param unparsedTokens All tokens which haven't been parsed successfully yet. This is the case when we call this function inside an alternative.
* @returns Any `AbstractElement` that could be following the given feature stack.
*/
export function findNextFeatures(featureStack, unparsedTokens) {
const context = {
stacks: featureStack,
tokens: unparsedTokens
};
interpretTokens(context);
// Reset the container property
context.stacks.flat().forEach(feature => { feature.property = undefined; });
const nextStacks = findNextFeatureStacks(context.stacks);
// We only need the last element of each stack
return nextStacks.map(e => e[e.length - 1]);
}
function findNextFeaturesInternal(options) {
const { next, cardinalities, visited, plus } = options;
const features = [];
const feature = next.feature;
if (visited.has(feature)) {
return [];
}
else if (!ast.isGroup(feature)) {
// Do not add the feature to the list if it is a group
// `findFirstFeaturesInternal` will take care of this
visited.add(feature);
}
let parent;
let item = feature;
while (item.$container) {
if (ast.isGroup(item.$container)) {
parent = item.$container;
break;
}
else if (ast.isAbstractElement(item.$container)) {
item = item.$container;
}
else {
break;
}
}
// First try to iterate the same element again
if (isArrayCardinality(item.cardinality)) {
const repeatingFeatures = findFirstFeaturesInternal({
next: {
feature: item,
type: next.type
},
cardinalities,
visited,
plus
});
for (const repeatingFeature of repeatingFeatures) {
plus.add(repeatingFeature.feature);
}
features.push(...repeatingFeatures);
}
if (parent) {
const ownIndex = parent.elements.indexOf(item);
// Find next elements of the same group
if (ownIndex !== undefined && ownIndex < parent.elements.length - 1) {
features.push(...findNextFeaturesInGroup({
feature: parent,
type: next.type
}, ownIndex + 1, cardinalities, visited, plus));
}
// Try to find the next elements of the parent
// Only do this if every following element is either optional or has been parsed as +
if (features.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)) || plus.has(e.feature))) {
features.push(...findNextFeaturesInternal({
next: {
feature: parent,
type: next.type
},
cardinalities,
visited,
plus
}));
}
}
return features;
}
/**
* Calculates the first child feature of any `AbstractElement`.
* @param next The `AbstractElement` whose first child features should be calculated.
*/
export function findFirstFeatures(next) {
if (isAstNode(next)) {
next = { feature: next };
}
return findFirstFeaturesInternal({ next, cardinalities: new Map(), visited: new Set(), plus: new Set() });
}
function findFirstFeaturesInternal(options) {
const { next, cardinalities, visited, plus } = options;
if (next === undefined) {
return [];
}
const { feature, type } = next;
if (ast.isGroup(feature)) {
if (visited.has(feature)) {
return [];
}
else {
visited.add(feature);
}
return findNextFeaturesInGroup(next, 0, cardinalities, visited, plus)
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
}
else if (ast.isAlternatives(feature) || ast.isUnorderedGroup(feature)) {
return feature.elements.flatMap(e => findFirstFeaturesInternal({
next: {
feature: e,
type,
property: next.property
},
cardinalities,
visited,
plus
}))
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
}
else if (ast.isAssignment(feature)) {
const assignmentNext = {
feature: feature.terminal,
type,
property: next.property ?? feature.feature
};
return findFirstFeaturesInternal({ next: assignmentNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
}
else if (ast.isAction(feature)) {
return findNextFeaturesInternal({
next: {
feature,
type: getTypeName(feature),
property: next.property ?? feature.feature
},
cardinalities,
visited,
plus
});
}
else if (ast.isRuleCall(feature) && ast.isParserRule(feature.rule.ref)) {
const rule = feature.rule.ref;
const ruleCallNext = {
feature: rule.definition,
type: rule.fragment || rule.dataType ? undefined : (getExplicitRuleType(rule) ?? rule.name),
property: next.property
};
return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
}
else if (ast.isRuleCall(feature) && ast.isInfixRule(feature.rule.ref)) {
const rule = feature.rule.ref;
const call = rule.call.rule.ref;
if (!ast.isParserRule(call)) {
console.error('Failed to resolve reference to ' + rule.call.rule.$refText);
return [];
}
const ruleCallNext = {
feature: call.definition,
type: getExplicitRuleType(call) ?? call.name,
property: 'parts'
};
return findFirstFeaturesInternal({ next: ruleCallNext, cardinalities, visited, plus })
.map(e => modifyCardinality(e, feature.cardinality, cardinalities));
}
else {
return [next];
}
}
/**
* Modifying the cardinality is necessary to identify which features are coming from an optional feature.
* Those features should be optional as well.
* @param next The next feature that could be made optionally.
* @param cardinality The cardinality of the calling (parent) object.
* @returns A new feature that could be now optional (`?` or `*`).
*/
function modifyCardinality(next, cardinality, cardinalities) {
cardinalities.set(next.feature, cardinality);
return next;
}
function findNextFeaturesInGroup(next, index, cardinalities, visited, plus) {
const features = [];
let firstFeature;
while (index < next.feature.elements.length) {
const feature = next.feature.elements[index++];
firstFeature = {
feature,
type: next.type
};
features.push(...findFirstFeaturesInternal({
next: firstFeature,
cardinalities,
visited,
plus
}));
if (!isOptionalCardinality(firstFeature.feature.cardinality ?? cardinalities.get(firstFeature.feature), firstFeature.feature)) {
break;
}
}
return features;
}
function interpretTokens(context) {
for (const token of context.tokens) {
const nextFeatureStacks = findNextFeatureStacks(context.stacks, token);
context.stacks = nextFeatureStacks;
}
}
function findNextFeatureStacks(stacks, token) {
const newStacks = [];
for (const stack of stacks) {
newStacks.push(...interpretStackToken(stack, token));
}
return newStacks;
}
function interpretStackToken(stack, token) {
const cardinalities = new Map();
const plus = new Set(stack.map(e => e.feature).filter(isPlusFeature));
const newStacks = [];
while (stack.length > 0) {
const top = stack.pop();
const allNextFeatures = findNextFeaturesInternal({
next: top,
cardinalities,
plus,
visited: new Set()
}).filter(next => token ? featureMatches(next.feature, token) : true);
for (const nextFeature of allNextFeatures) {
newStacks.push([...stack, nextFeature]);
}
if (!allNextFeatures.every(e => isOptionalCardinality(e.feature.cardinality, e.feature) || isOptionalCardinality(cardinalities.get(e.feature)))) {
break;
}
}
return newStacks;
}
function isPlusFeature(feature) {
if (feature.cardinality === '+') {
return true;
}
const assignment = getContainerOfType(feature, ast.isAssignment);
if (assignment && assignment.cardinality === '+') {
return true;
}
return false;
}
function featureMatches(feature, token) {
if (ast.isKeyword(feature)) {
const content = feature.value;
return content === token.image;
}
else if (ast.isRuleCall(feature)) {
return ruleMatches(feature.rule.ref, token);
}
else if (ast.isCrossReference(feature)) {
const crossRefTerminal = getCrossReferenceTerminal(feature);
if (crossRefTerminal) {
return featureMatches(crossRefTerminal, token);
}
}
return false;
}
function ruleMatches(rule, token) {
if (ast.isParserRule(rule)) {
const ruleFeatures = findFirstFeatures(rule.definition);
return ruleFeatures.some(e => featureMatches(e.feature, token));
}
else if (ast.isTerminalRule(rule)) {
// We have to take keywords into account
// e.g. most keywords are valid IDs as well
// Only return 'true' if this terminal does not match a keyword. TODO
return terminalRegex(rule).test(token.image);
}
else {
return false;
}
}
//# sourceMappingURL=follow-element-computation.js.map