typir
Version:
General purpose type checking library
194 lines • 11.6 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 { ValidationProblem } from '../../services/validation.js';
import { checkTypes, checkValueForConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js';
import { assertUnreachable, toArray } from '../../utils/utils.js';
/**
* This validation uses the inference rules for all available function calls to check, whether ...
* - the given arguments for a function call fit to one of the defined function signature
* - and validates this call according to the specific validation rules for this function call.
* There is only one instance of this class for each function kind/manager.
*/
export class FunctionCallArgumentsValidation {
constructor(services, functions) {
this.services = services;
this.functions = functions;
}
onAddedRule(_rule, diffOptions) {
// this rule needs to be registered also for all the language keys of the new inner function call rule
this.services.validation.Collector.addValidationRule(this, Object.assign(Object.assign({}, diffOptions), { boundToType: undefined }));
}
onRemovedRule(_rule, diffOptions) {
// remove this "composite" rule for all language keys for which no function call rules are registered anymore
if (diffOptions.languageKey === undefined) {
if (this.noFunctionCallRulesForThisLanguageKey(undefined)) {
this.services.validation.Collector.removeValidationRule(this, Object.assign(Object.assign({}, diffOptions), { languageKey: undefined, boundToType: undefined }));
}
}
else {
const languageKeysToUnregister = toArray(diffOptions.languageKey).filter(key => this.noFunctionCallRulesForThisLanguageKey(key));
this.services.validation.Collector.removeValidationRule(this, Object.assign(Object.assign({}, diffOptions), { languageKey: languageKeysToUnregister, boundToType: undefined }));
}
}
noFunctionCallRulesForThisLanguageKey(key) {
for (const overloads of this.functions.getAllOverloads()) {
if (overloads[1].details.getRulesByLanguageKey(key).length >= 1) {
return false;
}
}
return true;
}
validation(languageNode, accept, _typir) {
// determine all keys to check
const keysToApply = [];
const languageKey = this.services.Language.getLanguageNodeKey(languageNode);
if (languageKey === undefined) {
keysToApply.push(undefined);
}
else {
keysToApply.push(languageKey); // execute the rules which are associated to the key of the current language node
keysToApply.push(...this.services.Language.getAllSuperKeys(languageKey)); // apply all rules which are associated to super-keys
keysToApply.push(undefined); // rules associated with 'undefined' are applied to all language nodes, apply these rules at the end
}
// execute all rules wich are associated to the relevant language keys
const alreadyExecutedRules = new Set();
// for each (overloaded) function
for (const [overloadedName, overloadedFunctions] of this.functions.getAllOverloads()) { // this grouping is not required here (but for other use cases) and does not hurt here
const resultOverloaded = [];
// for each language key
for (const key of keysToApply) {
for (const singleFunction of overloadedFunctions.details.getRulesByLanguageKey(key)) {
if (alreadyExecutedRules.has(singleFunction.inferenceRuleForCalls)) { // TODO funktioniert das überhaupt, sprich: wird immer ein neues Objekt erstellt oder das aus der Konfiguration durchgereicht? zumindestens für Operatoren
// don't execute rules multiple times, if they are associated with multiple keys (with overlapping sub-keys)
}
else {
const exactMatch = this.executeSingleRule(singleFunction, languageNode, resultOverloaded);
if (exactMatch) {
// found exact match => execute the validation rules which are specific for this function call ...
for (const specificValidation of toArray(singleFunction.inferenceRuleForCalls.validation)) {
specificValidation.call(specificValidation, languageNode, singleFunction.functionType, accept, this.services);
}
return; // ... and ignore the other function call rules
}
alreadyExecutedRules.add(singleFunction.inferenceRuleForCalls);
}
}
}
// Since none of the function signatures match, report one validation issue (with sub-problems) for each function signature (and for each language key)
if (resultOverloaded.length >= 1) {
this.reportOperandsMismatch(languageNode, overloadedName, overloadedFunctions, resultOverloaded, accept);
}
}
}
reportOperandsMismatch(languageNode, overloadedName, overloadedFunctions, resultOverloaded, accept) {
accept({
languageNode: languageNode,
severity: 'error',
message: `The given operands for the call of ${overloadedFunctions.overloadedFunctions.length >= 2 ? 'the overload ' : ''}'${overloadedName}' don't match.`,
subProblems: resultOverloaded,
});
}
/**
* Checks whether the given inference rule for function calls matches the given language node.
* @param singleFunction the current function and its inference rule for calls of it
* @param languageNode the current language node, which might or might not represent a function call
* @param resultOverloaded receives a validation issue, if there is at least one conflict between given arguments and expected parameters
* @returns true, if the given function signature exactly matches the current function call, false otherwise
*/
executeSingleRule(singleFunction, languageNode, resultOverloaded) {
const inferenceRule = singleFunction.inferenceRuleForCalls;
const functionType = singleFunction.functionType;
if (inferenceRule.filter !== undefined && inferenceRule.filter(languageNode) === false) {
return false; // rule does not match at all => no constraints apply here => no error to show here
}
if (inferenceRule.matching !== undefined && inferenceRule.matching(languageNode, functionType) === false) {
return false; // false => does slightly not match => no constraints apply here => no error to show here
}
// Now, check that the given arguments fit to the expected parameters and collect all problems
// (Since the arguments should be validated, it is no option to skip the inference of arguments, as it is done as shortcut for the inference!)
const currentProblems = [];
const inputArguments = inferenceRule.inputArguments(languageNode);
const expectedParameterTypes = functionType.getInputs();
// check, that the given number of parameters is the same as the expected number of input parameters
const parameterLength = checkValueForConflict(expectedParameterTypes.length, inputArguments.length, 'number of input parameter values');
if (parameterLength.length >= 1) {
this.reportWrongNumberOfArguments(languageNode, parameterLength, currentProblems);
}
else {
// compare arguments with their corresponding parameters
const inferredParameterTypes = inputArguments.map(p => this.services.Inference.inferType(p));
for (let i = 0; i < inputArguments.length; i++) {
const expectedType = expectedParameterTypes[i];
const inferredType = inferredParameterTypes[i];
const parameterProblems = checkTypes(inferredType, expectedType, createTypeCheckStrategy('ASSIGNABLE_TYPE', this.services), true);
if (parameterProblems.length >= 1) {
// the value is not assignable to the type of the input parameter
// create one ValidationProblem for each problematic parameter!
this.reportWrongArgument(languageNode, inputArguments[i], i, inferredType, expectedType, parameterProblems, currentProblems);
}
else {
// this parameter value is fine
}
}
}
// summarize all parameters of the current function overload/signature
if (currentProblems.length >= 1) {
// some problems with parameters => this signature does not match
if (this.validateArgumentsOfFunctionCalls(inferenceRule, languageNode)) {
this.reportMismatchingSignature(languageNode, functionType, currentProblems, resultOverloaded);
}
else {
// ignore this variant for validation
}
return false;
}
else {
return true; // 100% match found => there are no validation issues to show!
}
}
validateArgumentsOfFunctionCalls(rule, languageNode) {
if (rule.validateArgumentsOfFunctionCalls === undefined) {
return false; // the default value
}
else if (typeof rule.validateArgumentsOfFunctionCalls === 'boolean') {
return rule.validateArgumentsOfFunctionCalls;
}
else if (typeof rule.validateArgumentsOfFunctionCalls === 'function') {
return rule.validateArgumentsOfFunctionCalls(languageNode);
}
else {
assertUnreachable(rule.validateArgumentsOfFunctionCalls);
}
}
reportWrongNumberOfArguments(languageNode, parameterLength, currentProblems) {
currentProblems.push({
$problem: ValidationProblem,
languageNode: languageNode,
severity: 'error',
message: 'The number of given parameter values does not match the expected number of input parameters.',
subProblems: parameterLength,
});
}
reportWrongArgument(languageNode, inputArgument, index, inferredType, expectedType, parameterProblems, currentProblems) {
currentProblems.push({
$problem: ValidationProblem,
languageNode: inputArgument,
severity: 'error',
message: `The parameter '${expectedType.name}' at index ${index} got a value with a wrong type.`,
subProblems: parameterProblems,
});
}
reportMismatchingSignature(languageNode, functionType, currentProblems, resultOverloaded) {
resultOverloaded.push({
$problem: ValidationProblem,
languageNode: languageNode,
severity: 'error',
message: `The given arguments don't match the parameters of '${this.services.Printer.printTypeUserRepresentation(functionType)}'.`,
subProblems: currentProblems,
});
}
}
//# sourceMappingURL=function-validation-calls.js.map