@finos/legend-graph
Version:
Legend graph and graph manager
480 lines • 24.1 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Class } from '../metamodel/pure/packageableElements/domain/Class.js';
import { CORE_PURE_PATH, ELEMENT_PATH_DELIMITER, RESERVERD_PACKAGE_NAMES, MILESTONING_STEREOTYPE, PRIMITIVE_TYPE, MULTIPLICITY_INFINITE, PURE_DEPRECATED_STEREOTYPE, ROOT_PACKAGE_NAME, MILESTONING_VERSION_PROPERTY_SUFFIX, FUNCTION_SIGNATURE_MULTIPLICITY_INFINITE_TOKEN, PURE_DOC_TAG, } from '../MetaModelConst.js';
import { Package } from '../metamodel/pure/packageableElements/domain/Package.js';
import { AssertionError, assertNonEmptyString, assertTrue, guaranteeNonNullable, guaranteeType, uniqBy, UnsupportedOperationError, returnUndefOnError, filterByType, } from '@finos/legend-shared';
import { createPath } from '../MetaModelUtils.js';
import { Measure, Unit, } from '../metamodel/pure/packageableElements/domain/Measure.js';
import { Enumeration } from '../metamodel/pure/packageableElements/domain/Enumeration.js';
import { PrimitiveType } from '../metamodel/pure/packageableElements/domain/PrimitiveType.js';
import { Property } from '../metamodel/pure/packageableElements/domain/Property.js';
import { DerivedProperty } from '../metamodel/pure/packageableElements/domain/DerivedProperty.js';
import { GenericType } from '../metamodel/pure/packageableElements/domain/GenericType.js';
import { Multiplicity } from '../metamodel/pure/packageableElements/domain/Multiplicity.js';
import { extractDependencyGACoordinateFromRootPackageName } from '../DependencyManager.js';
import { FunctionAnalysisInfo, FunctionAnalysisParameterInfo, } from './FunctionAnalysis.js';
import { generateFunctionPrettyName } from './PureLanguageHelper.js';
export const addElementToPackage = (parent, element) => {
// To improve performance we won't do duplication check here
parent.children.push(element);
element.package = parent;
};
export const deleteElementFromPackage = (parent, packageableElement) => {
parent.children = parent.children.filter((child) => child !== packageableElement);
};
const getDescendantsOfPackage = (parent) => {
const descendants = new Set();
parent.children.forEach((c) => {
if (c instanceof Package) {
getDescendantsOfPackage(c).forEach((e) => descendants.add(e));
}
else {
descendants.add(c);
}
});
return descendants;
};
export const getAllDescendantsOfPackage = (parent, graph) => new Set(graph
.getPackages(parent.path)
.map((p) => [...getDescendantsOfPackage(p)])
.flat());
export const elementBelongsToPackage = (element, parent) => {
const elementPackage = element instanceof Package ? element : element.package;
if (!elementPackage) {
return false;
}
const elementPackagePath = elementPackage.path;
const parentPackage = parent.path;
return (elementPackagePath + ELEMENT_PATH_DELIMITER).startsWith(parentPackage + ELEMENT_PATH_DELIMITER);
};
export const getElementRootPackage = (element) => !element.package
? guaranteeType(element, Package)
: getElementRootPackage(element.package);
/**
* If package name is a path, continue to recursively
* traverse the package chain to find the leaf package
*
* NOTE: if we do not allow create new packages, errorcould be
* thrown if a package with the specified path is not found
*/
const _getOrCreatePackage = (parentPackage, relativePackagePath, createNewPackageIfNotFound, cache) => {
const index = relativePackagePath.indexOf(ELEMENT_PATH_DELIMITER);
const packageName = index === -1
? relativePackagePath
: relativePackagePath.substring(0, index);
// try to resolve when there is a cache miss
let pkg;
pkg = parentPackage.children.find((child) => child instanceof Package && child.name === packageName);
if (!pkg) {
if (!createNewPackageIfNotFound) {
throw new AssertionError(`Can't find child package '${packageName}' in package '${parentPackage.path}'`);
}
// create the node if it is not in parent package
assertTrue(!RESERVERD_PACKAGE_NAMES.includes(packageName), `Can't create package with reserved name '${packageName}'`);
pkg = new Package(packageName);
pkg.package = parentPackage;
// NOTE: here we directly push the element to the children array without any checks rather than use `addUniqueEntry` to improve performance.
// Duplication checks should be handled separately for speed
parentPackage.children.push(pkg);
}
// populate cache after resolving the package
if (cache) {
cache.set(createPath(parentPackage.path, packageName), pkg);
}
// traverse the package chain
if (index !== -1) {
return _getOrCreatePackage(pkg, relativePackagePath.substring(index + ELEMENT_PATH_DELIMITER.length), createNewPackageIfNotFound, cache);
}
return pkg;
};
export const getOrCreatePackage = (parentPackage, relativePackagePath, createNewPackageIfNotFound, cache) => {
// check cache to find the shortest chain of packages to find/build
if (cache) {
// short-circuit
const cachedPackage = cache.get(createPath(parentPackage.path, relativePackagePath));
if (cachedPackage) {
return cachedPackage;
}
// NOTE: to check the cache, we need to traverse from the full package path
// up its ancestor chain till we find a cache hit
let immediateParentPackageRelativePath = relativePackagePath;
while (immediateParentPackageRelativePath !== '') {
const fullPath = createPath(parentPackage.path, immediateParentPackageRelativePath);
const cachedParentPackage = cache.get(fullPath);
if (cachedParentPackage) {
return _getOrCreatePackage(cachedParentPackage, relativePackagePath.substring(immediateParentPackageRelativePath.length +
ELEMENT_PATH_DELIMITER.length, relativePackagePath.length), createNewPackageIfNotFound, cache);
}
const index = immediateParentPackageRelativePath.lastIndexOf(ELEMENT_PATH_DELIMITER);
immediateParentPackageRelativePath =
index !== -1
? immediateParentPackageRelativePath.substring(0, index)
: '';
}
}
return _getOrCreatePackage(parentPackage, relativePackagePath, createNewPackageIfNotFound, cache);
};
export const getOrCreateGraphPackage = (graph, packagePath, cache) => {
assertNonEmptyString(packagePath, 'Package path is required');
return getOrCreatePackage(graph.root, packagePath, true, cache);
};
export const getRawGenericType = (genericType, clazz) => guaranteeType(genericType.rawType, clazz);
export const isMainGraphElement = (element) => returnUndefOnError(() => getElementRootPackage(element))?.name ===
ROOT_PACKAGE_NAME.MAIN;
export const isElementReadOnly = (element) => !isMainGraphElement(element);
export const isDependencyElement = (element) => {
const rootPackage = returnUndefOnError(() => getElementRootPackage(element));
return (rootPackage?.name === ROOT_PACKAGE_NAME.PROJECT_DEPENDENCY_ROOT ||
(rootPackage !== undefined &&
Boolean(extractDependencyGACoordinateFromRootPackageName(rootPackage.name))));
};
export const isGeneratedElement = (element) => returnUndefOnError(() => getElementRootPackage(element))?.name ===
ROOT_PACKAGE_NAME.MODEL_GENERATION;
export const isSystemElement = (element) => {
const elementRootPackageName = returnUndefOnError(() => getElementRootPackage(element))?.name;
return (element instanceof PrimitiveType ||
elementRootPackageName === ROOT_PACKAGE_NAME.SYSTEM ||
elementRootPackageName === ROOT_PACKAGE_NAME.CORE);
};
/**
* Extract the type of temporal milestone the class is associated with (using stereotype).
*
* Whatever the type, it means the class is milestoned, which means:
* 1. properties of this type will now be considered milestoned, and will be automatically
* converted into a derived property which accept some temporal parameters (depending
* on the milestoning type)
* 2. when we query properties of this type, we can provide the values for these parameters
*/
export const getMilestoneTemporalStereotype = (val, graph) => {
const milestonedProfile = graph.getProfile(CORE_PURE_PATH.PROFILE_TEMPORAL);
let stereotype;
const profile = val.stereotypes.find((st) => st.ownerReference.value === milestonedProfile);
stereotype = Object.values(MILESTONING_STEREOTYPE).find((value) => value === profile?.value.value);
if (stereotype !== undefined) {
return stereotype;
}
val.generalizations.forEach((generalization) => {
const superType = generalization.value.rawType;
if (superType instanceof Class) {
const milestonedStereotype = getMilestoneTemporalStereotype(superType, graph);
if (milestonedStereotype !== undefined) {
stereotype = Object.values(MILESTONING_STEREOTYPE).find((value) => value === milestonedStereotype);
}
}
});
return stereotype;
};
export const getTag = (profile, value) => guaranteeNonNullable(profile.p_tags.find((tag) => tag.value === value), `Can't find tag '${value}' in profile '${profile.path}'`);
export const getStereotype = (profile, value) => guaranteeNonNullable(profile.p_stereotypes.find((stereotype) => stereotype.value === value), `Can't find stereotype '${value}' in profile '${profile.path}'`);
export const getEnumValueNames = (enumeration) => enumeration.values.map((value) => value.name).filter(Boolean);
export const getEnumValue = (enumeration, name) => guaranteeNonNullable(enumeration.values.find((value) => value.name === name), `Can't find enum value '${name}' in enumeration '${enumeration.path}'`);
export const getFirstAssociatedProperty = (association) => guaranteeNonNullable(association.properties[0]);
export const getSecondAssociatedProperty = (association) => guaranteeNonNullable(association.properties[1]);
export const getOtherAssociatedProperty = (association, property) => {
const idx = association.properties.findIndex((p) => p === property);
assertTrue(idx !== -1, `Can't find property '${property.name}' in association '${association.path}'`);
return guaranteeNonNullable(association.properties[(idx + 1) % 2]);
};
export const getAssociatedPropertyClass = (association, property) => {
if (property instanceof Property) {
return guaranteeType(getOtherAssociatedProperty(association, property).genericType
.ownerReference.value, Class, `Association property '${property.name}' must be of type 'class'`);
}
else if (property instanceof DerivedProperty) {
throw new UnsupportedOperationError(`Derived property is not currently supported in association`);
}
throw new UnsupportedOperationError(`Can't get associated class of property`, property);
};
/**
* Get all superclasses of a class, accounted for loop and duplication (which should be caught by compiler)
* NOTE: we intentionally leave out `Any`
*/
export const getAllSuperclasses = (c) => {
const visitedClasses = new Set();
visitedClasses.add(c);
const resolveSuperTypes = (_class) => {
_class.generalizations.forEach((gen) => {
const superType = getRawGenericType(gen.value, Class);
if (!visitedClasses.has(superType)) {
visitedClasses.add(superType);
resolveSuperTypes(superType);
}
});
};
resolveSuperTypes(c);
visitedClasses.delete(c);
return Array.from(visitedClasses);
};
/**
* Get all subclasses of a class, accounted for loop and duplication (which should be caught by compiler)
* NOTE: we intentionally leave out `Any`
*/
export const getAllSubclasses = (c) => {
const visitedClasses = new Set();
visitedClasses.add(c);
const resolveSubclasses = (_class) => {
_class._subclasses.forEach((subclass) => {
if (!visitedClasses.has(subclass)) {
visitedClasses.add(subclass);
resolveSubclasses(subclass);
}
});
};
resolveSubclasses(c);
visitedClasses.delete(c);
return Array.from(visitedClasses);
};
export const getMilestoningGeneratedProperties = (_class) => _class._generatedMilestonedProperties.filter(filterByType(Property));
/**
* Get class and its supertypes' properties recursively, duplications and loops are handled (Which should be caught by compiler)
*/
export const getAllClassProperties = (_class, includeGeneratedMilestoning) => uniqBy(getAllSuperclasses(_class)
.concat(_class)
.map((c) => c.propertiesFromAssociations.concat(c.properties))
.flat()
.concat(includeGeneratedMilestoning
? getMilestoningGeneratedProperties(_class)
: []), (property) => property.name);
export const getAllClassDerivedProperties = (_class) => uniqBy(getAllSuperclasses(_class)
.concat(_class)
.map((c) => c.derivedProperties)
.flat(), (property) => property.name);
export const getClassProperty = (_class, name) => guaranteeNonNullable(getAllClassProperties(_class, true).find((property) => property.name === name), `Can't find property '${name}' in class '${_class.path}'`);
export const getAllOwnClassProperties = (_class) => _class.properties
.concat(_class.propertiesFromAssociations)
.concat(_class.derivedProperties);
export const getOwnClassProperty = (_class, name) => guaranteeNonNullable(getAllOwnClassProperties(_class).find((property) => property.name === name), `Can't find property '${name}' in class '${_class.path}'`);
export const getAllClassConstraints = (_class) =>
// Perhaps we don't need to care about deduping constraints here like for properties
getAllSuperclasses(_class)
.concat(_class)
.map((c) => c.constraints)
.flat();
export const getOwnProperty = (propertyOwner, name) => guaranteeNonNullable(propertyOwner instanceof Class
? getAllOwnClassProperties(propertyOwner).find((property) => property.name === name)
: propertyOwner.properties.find((property) => property.name === name), `Can't find property '${name}' of '${propertyOwner.path}'`);
/**
* Check if the first type subtype of the second type
*
* NOTE: Use this for contravariant and covariant check
* See https://www.originate.com/cheat-codes-for-contravariance-and-covariance
* See https://en.wikipedia.org/wiki/Covariance_and_contravariance_of_vectors
*/
export const isSubType = (type1, type2) => {
if (type1 === type2) {
return true;
}
if (type1 instanceof Unit) {
return type1.measure === type2;
}
else if (type1 instanceof Measure) {
return false;
}
else if (type1 instanceof Enumeration) {
return false;
}
else if (type1 instanceof PrimitiveType) {
if (!(type2 instanceof PrimitiveType)) {
return false;
}
if (type2.name === PRIMITIVE_TYPE.NUMBER) {
return (type1.name === PRIMITIVE_TYPE.INTEGER ||
type1.name === PRIMITIVE_TYPE.FLOAT ||
type1.name === PRIMITIVE_TYPE.DECIMAL);
}
if (type2.name === PRIMITIVE_TYPE.DATE) {
return (type1.name === PRIMITIVE_TYPE.STRICTDATE ||
type1.name === PRIMITIVE_TYPE.DATETIME ||
type1.name === PRIMITIVE_TYPE.LATESTDATE);
}
}
else if (type1 instanceof Class) {
return (type1.path === CORE_PURE_PATH.ANY ||
(type2 instanceof Class && getAllSuperclasses(type2).includes(type1)));
}
return false;
};
/**
* Check if the first type supertype of the second type
*
* NOTE: Use this for contravariant and covariant check
* See https://www.originate.com/cheat-codes-for-contravariance-and-covariance
* See https://en.wikipedia.org/wiki/Covariance_and_contravariance_of_vectors
*/
export const isSuperType = (type1, type2) => {
if (type1 === type2) {
return true;
}
if (type1 instanceof Unit) {
return false;
}
else if (type1 instanceof Measure) {
return type2 instanceof Unit && type2.measure === type1;
}
else if (type1 instanceof Enumeration) {
return false;
}
else if (type1 instanceof PrimitiveType) {
if (!(type2 instanceof PrimitiveType)) {
return false;
}
if (type1.name === PRIMITIVE_TYPE.NUMBER) {
return (type2.name === PRIMITIVE_TYPE.INTEGER ||
type2.name === PRIMITIVE_TYPE.FLOAT ||
type2.name === PRIMITIVE_TYPE.DECIMAL);
}
if (type1.name === PRIMITIVE_TYPE.DATE) {
return (type2.name === PRIMITIVE_TYPE.STRICTDATE ||
type2.name === PRIMITIVE_TYPE.DATETIME ||
type2.name === PRIMITIVE_TYPE.LATESTDATE);
}
}
else if (type1 instanceof Class) {
return (type2.path === CORE_PURE_PATH.ANY ||
(type2 instanceof Class && getAllSubclasses(type2).includes(type1)));
}
return false;
};
export const getMultiplicityDescription = (multiplicity) => {
if (multiplicity.lowerBound === multiplicity.upperBound) {
return `[${multiplicity.lowerBound.toString()}] - Must have exactly ${multiplicity.lowerBound.toString()} value(s)`;
}
else if (multiplicity.lowerBound === 0 &&
multiplicity.upperBound === undefined) {
return `[${MULTIPLICITY_INFINITE}] - May have many values`;
}
return `[${multiplicity.lowerBound}..${multiplicity.upperBound ?? MULTIPLICITY_INFINITE}] - ${multiplicity.upperBound
? `Must have from ${multiplicity.lowerBound} to ${multiplicity.upperBound} value(s)`
: `Must have at least ${multiplicity.lowerBound} values(s)`}`;
};
export const getMultiplicityPrettyDescription = (multiplicity) => {
if (multiplicity === Multiplicity.ONE) {
return `[${multiplicity.lowerBound.toString()}] - Required`;
}
else if (multiplicity === Multiplicity.ZERO_MANY) {
return `[${MULTIPLICITY_INFINITE}] - List`;
}
else if (multiplicity === Multiplicity.ZERO_ONE) {
return `[${multiplicity.lowerBound}..${multiplicity.upperBound ?? MULTIPLICITY_INFINITE}] - Optional`;
}
return `[${multiplicity.lowerBound}..${multiplicity.upperBound ?? MULTIPLICITY_INFINITE}] - ${multiplicity.upperBound
? `Must have from ${multiplicity.lowerBound} to ${multiplicity.upperBound} value(s)`
: `Must have at least ${multiplicity.lowerBound} values(s)`}`;
};
export const areMultiplicitiesEqual = (mul1, mul2) => mul1.upperBound === mul2.upperBound && mul1.lowerBound === mul2.lowerBound;
export const isElementDeprecated = (element, graph) => element.stereotypes.some((st) => st.value ===
graph
.getProfile(CORE_PURE_PATH.PROFILE_DOC)
.p_stereotypes.find((s) => s.value === PURE_DEPRECATED_STEREOTYPE));
export const extractAnnotatedElementDocumentation = (el) => {
let result = undefined;
for (const taggedValue of el.taggedValues) {
if (taggedValue.tag.ownerReference.value.path ===
CORE_PURE_PATH.PROFILE_DOC &&
taggedValue.tag.value.value === PURE_DOC_TAG) {
result = taggedValue.value;
break;
}
}
return result;
};
/**
* Gets the generated milestoned properties of a property owner
*/
export const getGeneratedMilestonedPropertiesForAssociation = (propertyOwner, property) => propertyOwner._generatedMilestonedProperties.filter((prop) => prop.name !== property.name &&
prop.name !==
`${property.name}${MILESTONING_VERSION_PROPERTY_SUFFIX.ALL_VERSIONS}` &&
prop.name !==
`${property.name}${MILESTONING_VERSION_PROPERTY_SUFFIX.ALL_VERSIONS_IN_RANGE}`);
const getMultiplicityString = (lowerBound, upperBound) => {
if (lowerBound === upperBound) {
return lowerBound.toString();
}
else if (lowerBound === 0 && upperBound === undefined) {
return FUNCTION_SIGNATURE_MULTIPLICITY_INFINITE_TOKEN;
}
return `$${lowerBound}_${upperBound ?? 'MANY'}$`;
};
export const getFunctionSignature = (func) => `_${func.parameters
.map((p) => `${p.type.value.name}_${getMultiplicityString(p.multiplicity.lowerBound, p.multiplicity.upperBound)}_`)
.join('_')}_${func.returnType.value.rawType.name}_${getMultiplicityString(func.returnMultiplicity.lowerBound, func.returnMultiplicity.upperBound)}_`;
export const getFunctionName = (func, name) => name.substring(0, name.indexOf(getFunctionSignature(func)));
export const getFunctionNameWithPath = (func) => func.package?.path + ELEMENT_PATH_DELIMITER + func.functionName;
export const buildFunctionAnalysisInfoFromConcreteFunctionDefinition = (funcs, graph) => {
const functionInfos = funcs.map((func) => {
const functionInfo = new FunctionAnalysisInfo();
functionInfo.functionPath = func.path;
functionInfo.name = func.name;
functionInfo.functionName = func.functionName;
functionInfo.functionPrettyName = generateFunctionPrettyName(func, {
fullPath: true,
spacing: false,
});
functionInfo.packagePath = func.package?.path ?? '';
functionInfo.returnType = func.returnType.value.rawType.name;
functionInfo.parameterInfoList = func.parameters.map((param) => {
const paramInfo = new FunctionAnalysisParameterInfo();
paramInfo.multiplicity = graph.getMultiplicity(param.multiplicity.lowerBound, param.multiplicity.upperBound);
paramInfo.name = param.name;
paramInfo.type = param.type.value.name;
return paramInfo;
});
return functionInfo;
});
return functionInfos;
};
const _classHasCycle = (_class, __classesIndex, options) => {
if (__classesIndex.has(_class.path)) {
return true;
}
const excludedProperties = options?.excludedPaths?.get(_class.path) ?? [];
const properties = options?.traverseNonRequiredProperties
? getAllClassProperties(_class)
: getAllClassProperties(_class).filter((property) => property.multiplicity.lowerBound);
const complexPropertyTypes = properties
.filter((property) => !excludedProperties.includes(property.name))
.map((property) => property.genericType.value.rawType)
.filter(filterByType(Class));
if (complexPropertyTypes.length > 0) {
// we only count classes with complex properties in the cycle
__classesIndex.add(_class.path);
}
// we only check unique complex property classes; 2 same property classes on the same level do not count as a cycle
return Boolean(Array.from(new Set(complexPropertyTypes)).find((type) => _classHasCycle(type, __classesIndex, options)));
};
export const classHasCycle = (_class, options) => _classHasCycle(_class, new Set(), options);
export const getElementOrigin = (element, graph) => {
if (isSystemElement(element)) {
return 'system elements';
}
else if (isGeneratedElement(element)) {
return 'generation elements';
}
else if (isDependencyElement(element)) {
const name = graph.dependencyManager.getElementOrigin(element);
if (name) {
return `project dependency ${name}`;
}
}
return '';
};
export const newGenericType = (rawType, typeArguments) => {
const genericType = new GenericType(rawType);
genericType.typeArguments = typeArguments;
return genericType;
};
//# sourceMappingURL=DomainHelper.js.map