UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

742 lines 27.1 kB
/****************************************************************************** * 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