typir
Version:
General purpose type checking library
296 lines • 14.3 kB
JavaScript
/******************************************************************************
* Copyright 2025 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 { removeFromArray, toArray, toArrayWithValue } from './utils.js';
export class RuleRegistry {
constructor(services) {
/**
* language node type --> rules
* Improves the look-up of related rules, when doing type for a concrete language node.
* All rules are registered at least once in this map, since rules without dedicated language key are registered to 'undefined'. */
this.languageTypeToRules = new Map();
/**
* type identifier --> -> rules
* Improves the look-up for rules which are bound to types, when these types are removed.
* Only rules which are bound to at least one type in this map, types bound to no types are missing in this map. */
this.typirTypeToRules = new Map();
/**
* rule --> its collected options
* Contains the current set of all options for an rule. */
this.ruleToOptions = new Map();
/** Collects all unique rules, lazily managed. */
this.uniqueRules = new Set();
this.listeners = [];
services.infrastructure.Graph.addListener(this);
}
getRulesByLanguageKey(languageKey) {
const store = this.languageTypeToRules.get(languageKey);
if (store === undefined) {
return [];
}
return store;
}
/** Unique set of all registered rules. */
getUniqueRules() {
if (this.uniqueRules.size <= 0) {
// lazily fill the set of unique rules
Array.from(this.languageTypeToRules.values()).flatMap(v => v).forEach(v => this.uniqueRules.add(v));
}
return this.uniqueRules;
}
isEmpty() {
return this.languageTypeToRules.size <= 0;
}
getNumberUniqueRules() {
return this.getUniqueRules().size;
}
getRuleOptions(options) {
return Object.assign({
// default values ...
languageKey: undefined, boundToType: undefined }, options);
}
addRule(rule, givenOptions) {
var _a;
const newOptions = this.getRuleOptions(givenOptions);
const languageKeyUndefined = newOptions.languageKey === undefined;
const languageKeys = toArray(newOptions.languageKey, { newArray: true });
const existingOptions = this.ruleToOptions.get(rule);
const diffOptions = Object.assign(Object.assign({}, newOptions), { languageKey: [], boundToType: [] });
let added = false; // remember whether the rule is really new
// register the rule with the key(s) of the language node
if (languageKeyUndefined) {
// register this rule for 'undefined'
if (existingOptions === null || existingOptions === void 0 ? void 0 : existingOptions.languageKeyUndefined) {
// nothing to do, since this rule is already registered for 'undefined'
}
else {
// since the rule shall be registered for 'undefined', remove all existing specific language keys
this.removeRule(rule, { languageKey: (_a = existingOptions === null || existingOptions === void 0 ? void 0 : existingOptions.languageKeys) !== null && _a !== void 0 ? _a : [] });
// register this rule for 'undefined'
let rules = this.languageTypeToRules.get(undefined);
if (rules === undefined) {
rules = [];
this.languageTypeToRules.set(undefined, rules);
}
rules.push(rule);
if (existingOptions !== undefined) {
existingOptions.languageKeyUndefined = true;
}
added = true;
diffOptions.languageKey = undefined;
}
}
else {
// register this rule for some language keys
if (existingOptions === null || existingOptions === void 0 ? void 0 : existingOptions.languageKeyUndefined) {
// don't add the new language keys, since this rule is already registered for 'undefined'
}
else {
// add some more language keys
for (const key of languageKeys) {
let rules = this.languageTypeToRules.get(key);
if (rules === undefined) {
rules = [];
this.languageTypeToRules.set(key, rules);
}
if (existingOptions === undefined) {
// this rule is unknown until now
rules.push(rule);
added = true;
diffOptions.languageKey = toArrayWithValue(key, diffOptions.languageKey);
}
else {
if (existingOptions.languageKeys.includes(key)) {
// this rule is already registered with this language key => do nothing
}
else {
// this rule is known, but not registered for the current language key yet
rules.push(rule);
existingOptions.languageKeys.push(key);
added = true;
diffOptions.languageKey = toArrayWithValue(key, diffOptions.languageKey);
}
}
}
}
}
// register the rule to Typir types in order to easily remove them together with removed types
for (const boundToType of toArray(newOptions.boundToType)) {
const typeKey = this.getBoundToTypeKey(boundToType);
let rules = this.typirTypeToRules.get(typeKey);
if (rules === undefined) {
rules = [];
this.typirTypeToRules.set(typeKey, rules);
}
if (existingOptions === undefined) {
// this rule is unknown until now
rules.push(rule);
diffOptions.boundToType = toArrayWithValue(boundToType, diffOptions.boundToType);
added = true;
}
else {
if (existingOptions.boundToTypes.includes(boundToType)) {
// this rule is already bound to this type => do nothing
}
else {
// this rule is known, but not bound to the current type yet
existingOptions.boundToTypes.push(boundToType);
rules.push(rule);
diffOptions.boundToType = toArrayWithValue(boundToType, diffOptions.boundToType);
added = true;
}
}
}
if (existingOptions === undefined) {
// no options yet => use the new options
this.ruleToOptions.set(rule, {
languageKeyUndefined: languageKeyUndefined,
languageKeys: languageKeys,
boundToTypes: toArray(newOptions.boundToType, { newArray: true }),
});
}
else {
// the existing options are already updated above
}
// update the set of unique rules
if (this.uniqueRules.size >= 1) {
this.uniqueRules.add(rule);
}
else {
// otherwise the set is populated on the next request
}
// inform all listeners about the new rule
if (added) {
this.listeners.forEach(listener => listener.onAddedRule(rule, diffOptions));
}
}
removeRule(rule, optionsToRemove) {
const existingOptions = this.ruleToOptions.get(rule);
if (existingOptions === undefined) { // these options need to be updated (or completely removed at the end)
return; // the rule is unknown here => nothing to do
}
const languageKeyUndefined = optionsToRemove ? (optionsToRemove.languageKey === undefined) : true;
const languageKeys = toArray(optionsToRemove === null || optionsToRemove === void 0 ? void 0 : optionsToRemove.languageKey, { newArray: true });
const diffOptions = {
// ... maybe more options in the future ...
languageKey: [], // empty/nothing
boundToType: [], // empty/nothing
};
let removed = false;
// update 'language keys'
if (languageKeyUndefined) {
// deregister the rule for 'undefined'
if (existingOptions.languageKeyUndefined) {
const result = this.deregisterRuleForLanguageKey(rule, undefined);
if (result) {
removed = true;
diffOptions.languageKey = undefined;
}
}
else {
// deregister the rule for all existing language keys
languageKeys.push(...existingOptions.languageKeys);
}
existingOptions.languageKeyUndefined = false;
}
if (languageKeys.length >= 1) {
// remove the rule for some language keys
if (existingOptions.languageKeyUndefined) {
// since the rule is registered for 'undefined', i.e. all language keys, don't remove some language keys here
}
else {
for (const key of languageKeys) {
const result1 = this.deregisterRuleForLanguageKey(rule, key);
const result2 = removeFromArray(key, existingOptions.languageKeys); // update existing options
if (result1 !== result2) {
throw new Error();
}
if (result1) {
removed = true;
diffOptions.languageKey = toArrayWithValue(key, diffOptions.languageKey);
}
}
}
}
// update 'bounded types'
for (const boundToType of toArray(optionsToRemove === null || optionsToRemove === void 0 ? void 0 : optionsToRemove.boundToType)) {
const typeKey = this.getBoundToTypeKey(boundToType);
const rules = this.typirTypeToRules.get(typeKey);
if (rules) {
const result = removeFromArray(rule, rules);
if (result) {
removed = true;
diffOptions.boundToType = toArrayWithValue(boundToType, diffOptions.boundToType);
removeFromArray(boundToType, existingOptions.boundToTypes); // update existing options
if (rules.length <= 0) { // remove empty entries
this.typirTypeToRules.delete(typeKey);
}
}
}
}
// if the rule is not relevant anymore, clear the options map
if (existingOptions.languageKeyUndefined === false && existingOptions.languageKeys.length <= 0) {
this.ruleToOptions.delete(rule);
}
// update the set of unique rules
this.uniqueRules.clear(); // the set needs to be populated on the next request
// inform listeners
if (removed) {
this.listeners.forEach(listener => listener.onRemovedRule(rule, diffOptions));
}
}
deregisterRuleForLanguageKey(rule, languageKey) {
const rules = this.languageTypeToRules.get(languageKey);
if (rules) {
const result = removeFromArray(rule, rules);
if (rules.length <= 0) { // remove empty entries
this.languageTypeToRules.delete(languageKey);
}
return result;
}
return false;
}
getBoundToTypeKey(boundToType) {
var _a;
return (_a = boundToType === null || boundToType === void 0 ? void 0 : boundToType.getIdentifier()) !== null && _a !== void 0 ? _a : '';
}
/* Get informed about deleted types in order to remove rules which are bound to them. */
onRemovedType(type, _key) {
const typeKey = this.getBoundToTypeKey(type); // TODO only if "typeKey === _key" ?? this needs to be double-checked when making Alias types explicit!
const entriesToRemove = this.typirTypeToRules.get(typeKey);
if (entriesToRemove) {
this.typirTypeToRules.delete(typeKey);
// for each rule which was bound to the removed type:
for (const ruleToRemove of entriesToRemove) {
const existingOptions = this.ruleToOptions.get(ruleToRemove);
const removed = removeFromArray(type, existingOptions.boundToTypes);
if (removed) {
if (existingOptions.boundToTypes.length <= 0) {
// this rule is not bound to any existing type anymore => remove this rule completely
this.removeRule(ruleToRemove, {
// ... maybe additional properties in the future?
// boundToType: there are no bounded types anymore!
languageKey: existingOptions.languageKeyUndefined ? undefined : existingOptions.languageKeys,
});
}
else {
// inform listeners about removed rules
this.listeners.forEach(listener => listener.onRemovedRule(ruleToRemove, Object.assign(Object.assign({}, existingOptions), { languageKey: existingOptions.languageKeyUndefined ? undefined : existingOptions.languageKeys, boundToType: type })));
}
}
else {
throw new Error('Removed type does not exist here');
}
}
}
}
addListener(listener) {
this.listeners.push(listener);
}
removeListener(listener) {
removeFromArray(listener, this.listeners);
}
}
//# sourceMappingURL=rule-registration.js.map