langium
Version:
A language engineering tool for the Language Server Protocol
742 lines • 27.1 kB
JavaScript
/******************************************************************************
* Copyright 2022 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 { isNamed } from '../../../references/name-provider.js';
import { MultiMap } from '../../../utils/collections.js';
import { isAlternatives, isKeyword, isParserRule, isAction, isGroup, isUnorderedGroup, isAssignment, isRuleCall, isCrossReference, isTerminalRule, isAbstractParserRule } from '../../../languages/generated/ast.js';
import { getTypeNameWithoutError, isPrimitiveGrammarType } from '../../internal-grammar-util.js';
import { mergePropertyTypes } from './plain-types.js';
import { isOptionalCardinality, terminalRegex, getRuleTypeName, getTypeName } from '../../../utils/grammar-utils.js';
class TypeGraph {
constructor(context, root) {
this.context = context;
this.root = root;
}
getTypes() {
return this.iterate(this.root, [{
alt: {
name: this.root.name,
properties: this.root.properties,
ruleCalls: this.root.ruleCalls,
super: []
},
current: this.root,
next: this.root.children
}]);
}
iterate(root, paths) {
const finished = paths.filter(e => e.next.length === 0);
do {
const next = this.recurse(root, paths);
const unfinished = [];
for (const path of next) {
if (path.next.length > 0) {
unfinished.push(path);
}
else {
finished.push(path);
}
}
paths = unfinished;
} while (paths.length > 0);
return finished;
}
recurse(root, paths, end) {
const all = [];
for (const path of paths) {
const node = path.current;
if (node !== end && node.children.length > 0) {
const nextPaths = this.applyNext(root, path);
const subPaths = this.recurse(root, nextPaths, node.end ?? end);
all.push(...subPaths);
}
else {
all.push(path);
}
}
const map = new MultiMap();
for (const path of all) {
map.add(path.current, path);
}
const unique = [];
for (const [node, groupedPaths] of map.entriesGroupedByKey()) {
unique.push(...flattenTypes(groupedPaths, node));
}
return unique;
}
applyNext(root, nextPath) {
const splits = this.splitType(nextPath.alt, nextPath.next.length);
const paths = [];
for (let i = 0; i < nextPath.next.length; i++) {
const split = splits[i];
const part = nextPath.next[i];
if (part.actionWithAssignment) {
// If the path enters an action with an assignment which changes the current name
// We already add a new path, since the next part of the part refers to a new inferred type
paths.push({
alt: copyTypeAlternative(split),
current: part,
next: [],
comment: split.comment,
});
}
if (part.name !== undefined && part.name !== split.name) {
if (part.actionWithAssignment) {
// We reset all properties, super types and ruleCalls since we are now in a new inferred type
split.properties = [];
split.ruleCalls = [];
split.super = [root.name];
split.name = part.name;
}
else {
split.super = [split.name, ...split.ruleCalls];
split.properties = [];
split.ruleCalls = [];
split.name = part.name;
}
}
split.properties.push(...part.properties);
split.ruleCalls.push(...part.ruleCalls);
const path = {
alt: split,
current: part,
next: part.children,
comment: split.comment
};
path.alt.super = path.alt.super.filter(e => e !== path.alt.name);
paths.push(path);
}
return paths;
}
splitType(type, count) {
const alternatives = [];
for (let i = 0; i < count; i++) {
alternatives.push(copyTypeAlternative(type));
}
return alternatives;
}
getSuperTypes(node) {
const set = new Set();
this.collectSuperTypes(node, node, set);
return Array.from(set);
}
collectSuperTypes(original, part, set) {
if (part.ruleCalls.length > 0) {
// Each unassigned rule call corresponds to a super type
for (const ruleCall of part.ruleCalls) {
set.add(ruleCall);
}
return;
}
for (const parent of part.parents) {
if (original.name === undefined) {
this.collectSuperTypes(parent, parent, set);
}
else if (parent.name !== undefined && parent.name !== original.name) {
set.add(parent.name);
}
else {
this.collectSuperTypes(original, parent, set);
}
}
if (part.parents.length === 0 && part.name) {
set.add(part.name);
}
}
connect(parent, children) {
children.parents.push(parent);
parent.children.push(children);
return children;
}
merge(...parts) {
if (parts.length === 1) {
return parts[0];
}
else if (parts.length === 0) {
throw new Error('No parts to merge');
}
const node = newTypePart();
node.parents = parts;
for (const parent of parts) {
parent.children.push(node);
}
return node;
}
hasLeafNode(part) {
return this.partHasLeafNode(part);
}
partHasLeafNode(part, ignore) {
if (part.children.some(e => e !== ignore)) {
return true;
}
else if (part.name) {
return false;
}
else {
return part.parents.some(e => this.partHasLeafNode(e, part));
}
}
}
function copyTypePart(value) {
return {
name: value.name,
children: [],
parents: [],
actionWithAssignment: value.actionWithAssignment,
ruleCalls: value.ruleCalls.slice(),
properties: value.properties.map(copyProperty),
};
}
function copyTypeAlternative(value) {
return {
name: value.name,
super: value.super,
ruleCalls: value.ruleCalls.slice(),
properties: value.properties.map(e => copyProperty(e)),
comment: value.comment,
};
}
function copyProperty(value) {
return {
name: value.name,
optional: value.optional,
type: value.type,
astNodes: value.astNodes,
comment: value.comment,
};
}
export function collectInferredTypes(parserRules, datatypeRules, infixRules, declared, services) {
const commentProvider = services?.documentation.CommentProvider;
// extract interfaces and types from parser rules
const allTypes = [];
const context = {
fragments: new Map()
};
for (const rule of parserRules) {
const comment = commentProvider?.getComment(rule);
allTypes.push(...getRuleTypes(context, rule, services).map(typePath => ({ ...typePath, comment })));
}
const infixInterfaces = calculateInfixInterfaces(infixRules);
const interfaces = calculateInterfaces(allTypes, infixInterfaces);
const unions = buildSuperUnions(interfaces);
const astTypes = extractUnions(interfaces, unions, declared);
// extract types from datatype rules
for (const rule of datatypeRules) {
const type = getDataRuleType(rule);
astTypes.unions.push({
name: rule.name,
declared: false,
type,
subTypes: new Set(),
superTypes: new Set(),
dataType: rule.dataType,
comment: commentProvider?.getComment(rule),
});
}
return astTypes;
}
function calculateInfixInterfaces(rules) {
const interfaces = [];
for (const infixRule of rules) {
const on = infixRule.call.rule.ref;
const onName = isAbstractParserRule(on) ? getTypeName(on) : on?.name;
if (onName && infixRule.name) {
const operators = infixRule.operators.precedences
.flatMap(e => e.operators).map(e => e.value).sort();
const expressionProperty = {
astNodes: new Set(),
optional: false,
type: {
value: onName
}
};
const interfaceType = {
name: getTypeName(infixRule),
declared: false,
abstract: false,
properties: [
{
...expressionProperty,
name: 'left'
},
{
...expressionProperty,
name: 'right'
},
{
name: 'operator',
astNodes: new Set(),
optional: false,
type: {
types: operators.map(operator => ({
string: operator
}))
}
}
],
subTypes: new Set(),
superTypes: new Set()
};
interfaces.push(interfaceType);
}
}
return interfaces;
}
function getDataRuleType(rule) {
if (rule.dataType && rule.dataType !== 'string') {
return {
primitive: rule.dataType
};
}
let cancelled = false;
const cancel = () => {
cancelled = true;
return {
primitive: 'unknown'
};
};
const type = buildDataRuleType(rule.definition, cancel);
if (cancelled) {
return {
primitive: 'string'
};
}
else {
return type;
}
}
function buildDataRuleType(element, cancel) {
if (element.cardinality) {
// Multiplicity/optionality is not supported for types
return cancel();
}
if (isAlternatives(element)) {
return {
types: element.elements.map(e => buildDataRuleType(e, cancel))
};
}
else if (isGroup(element) || isUnorderedGroup(element)) {
if (element.elements.length !== 1) {
return cancel();
}
else {
return buildDataRuleType(element.elements[0], cancel);
}
}
else if (isRuleCall(element)) {
const ref = element.rule?.ref;
if (ref) {
if (isTerminalRule(ref)) {
let regex;
try {
regex = terminalRegex(ref).toString();
}
catch {
// If the regex cannot be built, we assume it's just a string
regex = undefined;
}
return {
primitive: ref.type?.name ?? 'string',
regex
};
}
else {
return {
value: ref.name
};
}
}
else {
return cancel();
}
}
else if (isKeyword(element)) {
return {
string: element.value
};
}
return cancel();
}
function getRuleTypes(context, rule, services) {
const type = newTypePart(rule);
const graph = new TypeGraph(context, type);
if (rule.definition) {
type.end = collectElement(graph, graph.root, rule.definition, services);
}
return flattenTypes(graph.getTypes(), type.end ?? newTypePart());
}
function newTypePart(element) {
return {
name: isAbstractParserRule(element) || isAction(element) ? getTypeNameWithoutError(element) : element,
properties: [],
ruleCalls: [],
children: [],
parents: [],
actionWithAssignment: false
};
}
/**
* Collects all possible type branches of a given parser rule element.
*
* @param state State to walk over element's graph.
* @param type Element that collects a current type branch for the given element.
* @param element The given AST element, from which it's necessary to extract the type.
*/
function collectElement(graph, current, element, services) {
const optional = isOptionalCardinality(element.cardinality, element);
if (isAlternatives(element)) {
const children = [];
if (optional) {
// Create a new empty node
children.push(graph.connect(current, newTypePart()));
}
for (const alt of element.elements) {
const altType = graph.connect(current, newTypePart());
children.push(collectElement(graph, altType, alt, services));
}
const mergeNode = graph.merge(...children);
current.end = mergeNode;
return mergeNode;
}
else if (isGroup(element) || isUnorderedGroup(element)) {
let groupNode = graph.connect(current, newTypePart());
let skipNode;
if (optional) {
skipNode = graph.connect(current, newTypePart());
}
for (const item of element.elements) {
groupNode = collectElement(graph, groupNode, item, services);
}
if (skipNode) {
const mergeNode = graph.merge(skipNode, groupNode);
current.end = mergeNode;
return mergeNode;
}
else {
return groupNode;
}
}
else if (isAction(element)) {
return addAction(graph, current, element, services);
}
else if (isAssignment(element)) {
addAssignment(current, element, services);
}
else if (isRuleCall(element)) {
addRuleCall(graph, current, element, services);
}
return current;
}
function addAction(graph, parent, action, services) {
const commentProvider = services?.documentation.CommentProvider;
// We create a copy of the current type part
// This is essentially a leaf node of the current type
// Otherwise we might lose information, such as properties
// We do this if there's no leaf node for the current type yet
if (!graph.hasLeafNode(parent)) {
const copy = copyTypePart(parent);
graph.connect(parent, copy);
}
const typeNode = graph.connect(parent, newTypePart(action));
if (action.type) {
const type = action.type?.ref;
if (type && isNamed(type))
// cs: if the (named) type could be resolved properly also set the name on 'typeNode'
// for the sake of completeness and better comprehensibility during debugging,
// it's not supposed to have a effect on the flow of control!
typeNode.name = type.name;
}
if (action.feature && action.operator) {
typeNode.actionWithAssignment = true;
typeNode.properties.push({
name: action.feature,
optional: false,
type: toPropertyType(action.operator === '+=', undefined, graph.root.ruleCalls.length !== 0 ? graph.root.ruleCalls : graph.getSuperTypes(typeNode)),
astNodes: new Set([action]),
comment: commentProvider?.getComment(action),
});
}
return typeNode;
}
function addAssignment(current, assignment, services) {
const commentProvider = services?.documentation.CommentProvider;
const typeItems = { types: new Set() };
findTypes(assignment.terminal, typeItems);
const type = toPropertyType(assignment.operator === '+=', typeItems.reference, assignment.operator === '?=' ? ['boolean'] : Array.from(typeItems.types));
current.properties.push({
name: assignment.feature,
optional: isOptionalCardinality(assignment.cardinality),
type,
astNodes: new Set([assignment]),
comment: commentProvider?.getComment(assignment),
});
}
function findTypes(terminal, types) {
if (isAlternatives(terminal) || isUnorderedGroup(terminal) || isGroup(terminal)) {
for (const element of terminal.elements) {
findTypes(element, types);
}
}
else if (isKeyword(terminal)) {
types.types.add(`'${terminal.value}'`);
}
else if (isRuleCall(terminal) && terminal.rule.ref) {
types.types.add(getRuleTypeName(terminal.rule.ref));
}
else if (isCrossReference(terminal) && terminal.type.ref) {
const refTypeName = getTypeNameWithoutError(terminal.type.ref);
if (refTypeName) {
types.types.add(refTypeName);
}
types.reference ?? (types.reference = {});
if (terminal.isMulti) {
types.reference.isMulti = true;
}
else {
types.reference.isSingle = true;
}
}
}
function addRuleCall(graph, current, ruleCall, services) {
const rule = ruleCall.rule.ref;
// Add all properties of fragments to the current type
if (isParserRule(rule) && rule.fragment) {
const properties = getFragmentProperties(rule, graph.context, services);
if (isOptionalCardinality(ruleCall.cardinality)) {
current.properties.push(...properties.map(e => ({
...e,
optional: true
})));
}
else {
current.properties.push(...properties);
}
}
else if (isAbstractParserRule(rule)) {
current.ruleCalls.push(getRuleTypeName(rule));
}
}
function getFragmentProperties(fragment, context, services) {
const existing = context.fragments.get(fragment);
if (existing) {
return existing;
}
const properties = [];
context.fragments.set(fragment, properties);
const fragmentName = getTypeNameWithoutError(fragment);
const typeAlternatives = getRuleTypes(context, fragment, services).filter(e => e.alt.name === fragmentName);
properties.push(...typeAlternatives.flatMap(e => e.alt.properties));
return properties;
}
/**
* Calculate interfaces from all possible type branches.
* [some of these interfaces will become types in the generated AST]
* @param alternatives The type branches that will be squashed in interfaces.
* @returns Interfaces.
*/
function calculateInterfaces(alternatives, otherInterfaces) {
const interfaces = new Map(otherInterfaces.map(e => [e.name, e]));
const ruleCallAlternatives = [];
const flattened = alternatives.length > 0
? flattenTypes(alternatives, alternatives[0].current).map(e => e.alt)
: [];
for (const flat of flattened) {
const interfaceType = {
name: flat.name,
properties: flat.properties,
superTypes: new Set(flat.super),
subTypes: new Set(),
declared: false,
abstract: false,
comment: flat.comment,
};
interfaces.set(interfaceType.name, interfaceType);
if (flat.ruleCalls.length > 0) {
ruleCallAlternatives.push(flat);
flat.ruleCalls.forEach(e => {
if (e !== interfaceType.name) { // An interface cannot subtype itself
interfaceType.subTypes.add(e);
}
});
}
// all other cases assume we have a data type rule
// we do not generate an AST type for data type rules
}
for (const ruleCallType of ruleCallAlternatives) {
for (const ruleCall of ruleCallType.ruleCalls) {
const calledInterface = interfaces.get(ruleCall);
if (calledInterface) {
if (calledInterface.name !== ruleCallType.name) {
calledInterface.superTypes.add(ruleCallType.name);
}
}
}
}
return Array.from(interfaces.values());
}
function flattenTypes(alternatives, part) {
var _a;
const nameToAlternatives = alternatives.reduce((acc, e) => acc.add(e.alt.name, e), new MultiMap());
const types = [];
for (const [name, namedAlternatives] of nameToAlternatives.entriesGroupedByKey()) {
const properties = [];
const ruleCalls = new Set();
const type = { alt: { name, properties, ruleCalls: [], super: [] }, next: [], current: part };
for (const path of namedAlternatives) {
const alt = path.alt;
type.comment ?? (type.comment = path.comment);
(_a = type.alt).comment ?? (_a.comment = path.comment);
type.alt.super.push(...alt.super);
type.next.push(...path.next);
const altProperties = alt.properties;
for (const altProperty of altProperties) {
const existingProperty = properties.find(e => e.name === altProperty.name);
if (existingProperty) {
existingProperty.type = mergePropertyTypes(existingProperty.type, altProperty.type);
altProperty.astNodes.forEach(e => existingProperty.astNodes.add(e));
}
else {
properties.push({ ...altProperty });
}
}
alt.ruleCalls.forEach(ruleCall => ruleCalls.add(ruleCall));
}
for (const path of namedAlternatives) {
type.next = Array.from(new Set(type.next));
const alt = path.alt;
// A type with rule calls is not a real member of the type
// Any missing properties are therefore not associated with the current type
if (alt.ruleCalls.length === 0) {
for (const property of properties) {
if (!alt.properties.find(e => e.name === property.name)) {
property.optional = true;
}
}
}
}
type.alt.ruleCalls = Array.from(ruleCalls);
types.push(type);
}
return types;
}
function buildSuperUnions(interfaces) {
const interfaceMap = new Map(interfaces.map(e => [e.name, e]));
const unions = [];
const allSupertypes = new MultiMap();
for (const interfaceType of interfaces) {
for (const superType of interfaceType.superTypes) {
allSupertypes.add(superType, interfaceType.name);
}
}
for (const [superType, types] of allSupertypes.entriesGroupedByKey()) {
if (!interfaceMap.has(superType)) {
const union = {
declared: false,
name: superType,
subTypes: new Set(),
superTypes: new Set(),
type: toPropertyType(false, undefined, types)
};
unions.push(union);
}
}
return unions;
}
/**
* Filters interfaces, transforming some of them in unions.
* The transformation criterion: no properties, but have subtypes.
* @param interfaces The interfaces that have to be transformed on demand.
* @returns Types and not transformed interfaces.
*/
function extractUnions(interfaces, unions, declared) {
const subTypes = new MultiMap();
for (const interfaceType of interfaces) {
for (const superTypeName of interfaceType.superTypes) {
subTypes.add(superTypeName, interfaceType.name);
}
}
const declaredInterfaces = new Set(declared.interfaces.map(e => e.name));
const astTypes = { interfaces: [], unions };
const unionTypes = new Map(unions.map(e => [e.name, e]));
for (const interfaceType of interfaces) {
const interfaceSubTypes = new Set(subTypes.get(interfaceType.name));
// Convert an interface into a union type if it has subtypes and no properties on its own
if (interfaceType.properties.length === 0 && interfaceSubTypes.size > 0) {
// In case we have an explicitly declared interface
// Mark the interface as `abstract` and do not create a union type
if (declaredInterfaces.has(interfaceType.name)) {
interfaceType.abstract = true;
astTypes.interfaces.push(interfaceType);
}
else {
const interfaceTypeValue = toPropertyType(false, undefined, Array.from(interfaceSubTypes));
const existingUnion = unionTypes.get(interfaceType.name);
if (existingUnion) {
existingUnion.type = mergePropertyTypes(existingUnion.type, interfaceTypeValue);
}
else {
const unionType = {
name: interfaceType.name,
declared: false,
subTypes: interfaceSubTypes,
superTypes: interfaceType.superTypes,
type: interfaceTypeValue,
comment: interfaceType.comment,
};
astTypes.unions.push(unionType);
unionTypes.set(interfaceType.name, unionType);
}
}
}
else {
astTypes.interfaces.push(interfaceType);
}
}
// After converting some interfaces into union types, these interfaces are no longer valid super types
for (const interfaceType of astTypes.interfaces) {
interfaceType.superTypes = new Set([...interfaceType.superTypes].filter(superType => !unionTypes.has(superType)));
}
return astTypes;
}
function toPropertyType(array, reference, types) {
if (array) {
return {
elementType: toPropertyType(false, reference, types)
};
}
else if (reference) {
const isMulti = reference.isMulti ?? false;
const isSingle = reference.isSingle ?? !isMulti;
return {
referenceType: toPropertyType(false, undefined, types),
isMulti,
isSingle
};
}
else if (types.length === 1) {
const type = types[0];
if (type.startsWith("'")) {
return {
string: type.substring(1, type.length - 1)
};
}
if (isPrimitiveGrammarType(type)) {
return {
primitive: type
};
}
else {
return {
value: type
};
}
}
else {
return {
types: types.map(e => toPropertyType(false, undefined, [e]))
};
}
}
//# sourceMappingURL=inferred-types.js.map