UNPKG

typir

Version:

General purpose type checking library

276 lines 13 kB
/****************************************************************************** * Copyright 2024 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 { WaitingForIdentifiableAndCompletedTypeReferences, WaitingForInvalidTypeReferences } from '../initialization/type-waiting.js'; import { assertTrue, assertUnreachable, removeFromArray } from '../utils/utils.js'; /** * Design decisions: * - features of types are realized/determined by their kinds * - Identifiers of types must be unique! */ export class Type { constructor(identifier, typeDetails) { // this is required only to apply graph algorithms in a generic way! // $relation is used as key this.edgesIncoming = new Map(); this.edgesOutgoing = new Map(); // store the state of the initialization process of this type this.initializationState = 'Invalid'; // manage listeners for updates of the initialization state this.stateListeners = []; this.identifier = identifier; this.associatedLanguageNode = typeDetails.associatedLanguageNode; } /** * Identifiers must be unique and stable for all types known in a single Typir instance, since they are used as key to store types in maps. * Identifiers might have a naming schema for calculatable values. */ getIdentifier() { // an Identifier must be available; note that the state might be 'Invalid' nevertheless, which is required to handle cyclic type definitions assertTrue(this.identifier !== undefined); return this.identifier; } getInitializationState() { return this.initializationState; } assertState(expectedState) { if (this.isInState(expectedState) === false) { throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but ${expectedState} is expected.`); } } assertNotState(expectedState) { if (this.isNotInState(expectedState) === false) { throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`); } } assertStateOrLater(expectedState) { if (this.isInStateOrLater(expectedState) === false) { throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`); } } isInState(state) { return this.initializationState === state; } isNotInState(state) { return this.initializationState !== state; } isInStateOrLater(state) { switch (state) { case 'Invalid': return true; case 'Identifiable': return this.initializationState !== 'Invalid'; case 'Completed': return this.initializationState === 'Completed'; default: assertUnreachable(state); } } addListener(newListeners, informIfNotInvalidAnymore) { this.stateListeners.push(newListeners); if (informIfNotInvalidAnymore) { const currentState = this.getInitializationState(); switch (currentState) { case 'Invalid': // don't inform about the Invalid state! break; case 'Identifiable': newListeners.onSwitchedToIdentifiable(this); break; case 'Completed': newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed! newListeners.onSwitchedToCompleted(this); break; default: assertUnreachable(currentState); } } } removeListener(listener) { removeFromArray(listener, this.stateListeners); } /** * Use this method to specify, how THIS new type should be initialized. * * This method has(!) to be called at the end(!) of the constructor of each specific Type implementation, even if nothing has to be specified, * since calling this method starts the initialization process! * If you forget the call this method, the new type remains invalid and invisible for Typir and you will not be informed about this problem! * * @param preconditions all possible options for the initialization process */ defineTheInitializationProcessOfThisType(preconditions) { var _a, _b, _c, _d, _e, _f, _g, _h; // store the reactions this.onIdentification = (_a = preconditions.onIdentifiable) !== null && _a !== void 0 ? _a : (() => { }); this.onCompletion = (_b = preconditions.onCompleted) !== null && _b !== void 0 ? _b : (() => { }); this.onInvalidation = (_c = preconditions.onInvalidated) !== null && _c !== void 0 ? _c : (() => { }); // preconditions for Identifiable this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences((_d = preconditions.preconditionsForIdentifiable) === null || _d === void 0 ? void 0 : _d.referencesToBeIdentifiable, (_e = preconditions.preconditionsForIdentifiable) === null || _e === void 0 ? void 0 : _e.referencesToBeCompleted); this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents // preconditions for Completed this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences((_f = preconditions.preconditionsForCompleted) === null || _f === void 0 ? void 0 : _f.referencesToBeIdentifiable, (_g = preconditions.preconditionsForCompleted) === null || _g === void 0 ? void 0 : _g.referencesToBeCompleted); this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents // preconditions for Invalid this.waitForInvalid = new WaitingForInvalidTypeReferences((_h = preconditions.referencesRelevantForInvalidation) !== null && _h !== void 0 ? _h : []); // eslint-disable-next-line @typescript-eslint/no-this-alias const thisType = this; // invalid --> identifiable this.waitForIdentifiable.addListener({ onFulfilled(_waiter) { thisType.switchFromInvalidToIdentifiable(); if (thisType.waitForCompleted.isFulfilled()) { // this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered thisType.switchFromIdentifiableToCompleted(); } }, onInvalidated(_waiter) { thisType.switchFromCompleteOrIdentifiableToInvalid(); }, }, true); // 'true' triggers the initialization process! // identifiable --> completed this.waitForCompleted.addListener({ onFulfilled(_waiter) { if (thisType.waitForIdentifiable.isFulfilled()) { thisType.switchFromIdentifiableToCompleted(); } else { // switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed } }, onInvalidated(_waiter) { thisType.switchFromCompleteOrIdentifiableToInvalid(); }, }, false); // not required, since 'waitForIdentifiable' will switch to Completed as well! // identifiable/completed --> invalid this.waitForInvalid.addListener(() => { this.switchFromCompleteOrIdentifiableToInvalid(); }, false); // no initial trigger, since 'Invalid' is the initial state } /** * This is an internal method to ignore some types during the initialization process in order to prevent dependency cycles. * Usually there is no need to call this method on your own. * @param additionalTypesToIgnore the new types to ignore during */ ignoreDependingTypesDuringInitialization(additionalTypesToIgnore) { this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore); this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); } dispose() { // clear everything this.stateListeners.splice(0, this.stateListeners.length); this.waitForInvalid.getWaitForRefsInvalid().forEach(ref => ref.dispose()); this.waitForIdentifiable.deconstruct(); this.waitForCompleted.deconstruct(); this.waitForInvalid.deconstruct(); // edges are already removed, when the type is removed from the graph, // but in some cases, the type was not (yet) added to the graph, but got already edges => these edges need to be removed now // e.g. "duplicated" types which are created and disposed by TypeInitializers this.edgesIncoming.clear(); this.edgesOutgoing.clear(); } switchFromInvalidToIdentifiable() { this.assertState('Invalid'); this.onIdentification(); this.initializationState = 'Identifiable'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications } switchFromIdentifiableToCompleted() { this.assertState('Identifiable'); this.onCompletion(); this.initializationState = 'Completed'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications } switchFromCompleteOrIdentifiableToInvalid() { if (this.isNotInState('Invalid')) { this.onInvalidation(); this.initializationState = 'Invalid'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications // add the types again, since the initialization process started again this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); } else { // is already 'Invalid' => do nothing } } addIncomingEdge(edge) { const key = edge.$relation; if (this.edgesIncoming.has(key)) { this.edgesIncoming.get(key).push(edge); } else { this.edgesIncoming.set(key, [edge]); } } addOutgoingEdge(edge) { const key = edge.$relation; if (this.edgesOutgoing.has(key)) { this.edgesOutgoing.get(key).push(edge); } else { this.edgesOutgoing.set(key, [edge]); } } removeIncomingEdge(edge) { const key = edge.$relation; const list = this.edgesIncoming.get(key); if (list) { const index = list.indexOf(edge); if (index >= 0) { list.splice(index, 1); if (list.length <= 0) { this.edgesIncoming.delete(key); } return true; } } return false; } removeOutgoingEdge(edge) { const key = edge.$relation; const list = this.edgesOutgoing.get(key); if (list) { const index = list.indexOf(edge); if (index >= 0) { list.splice(index, 1); if (list.length <= 0) { this.edgesOutgoing.delete(key); } return true; } } return false; } getIncomingEdges($relation) { var _a; return (_a = this.edgesIncoming.get($relation)) !== null && _a !== void 0 ? _a : []; } getOutgoingEdges($relation) { var _a; return (_a = this.edgesOutgoing.get($relation)) !== null && _a !== void 0 ? _a : []; } getEdges($relation) { return [ ...this.getIncomingEdges($relation), ...this.getOutgoingEdges($relation), ]; } getAllIncomingEdges() { return Array.from(this.edgesIncoming.values()).flat(); } getAllOutgoingEdges() { return Array.from(this.edgesOutgoing.values()).flat(); } getAllEdges() { return [ ...this.getAllIncomingEdges(), ...this.getAllOutgoingEdges(), ]; } } export function isType(type) { return typeof type === 'object' && type !== null && typeof type.getIdentifier === 'function' && typeof type.kind === 'object'; } //# sourceMappingURL=type-node.js.map