From 909561848e5b05ddc49a30aa2959d427b54c8f7b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 23 Oct 2023 22:23:36 +0200 Subject: [PATCH] feat: check template expressions inside `@PythonCall` --- .../validation/builtins/pythonCall.ts | 43 +++++++++++++++++++ src/language/validation/safe-ds-validator.ts | 2 + .../annotations/pythonCall/main.sdstest | 39 +++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/language/validation/builtins/pythonCall.ts create mode 100644 tests/resources/validation/builtins/annotations/pythonCall/main.sdstest diff --git a/src/language/validation/builtins/pythonCall.ts b/src/language/validation/builtins/pythonCall.ts new file mode 100644 index 000000000..09eab3ee0 --- /dev/null +++ b/src/language/validation/builtins/pythonCall.ts @@ -0,0 +1,43 @@ +import { hasContainerOfType, ValidationAcceptor } from 'langium'; +import { isSdsClass, SdsFunction } from '../../generated/ast.js'; +import { SafeDsServices } from '../../safe-ds-module.js'; +import { findFirstAnnotationCallOf, getParameters } from '../../helpers/nodeProperties.js'; +import { pluralize } from '../../../helpers/stringUtils.js'; + +export const CODE_PYTHON_CALL_INVALID_TEMPLATE_EXPRESSION = 'python-call/invalid-template-expression'; + +export const pythonCallMustOnlyContainValidTemplateExpressions = (services: SafeDsServices) => { + const builtinAnnotations = services.builtins.Annotations; + + return (node: SdsFunction, accept: ValidationAcceptor) => { + const pythonCall = builtinAnnotations.getPythonCall(node); + if (!pythonCall) { + return; + } + + // Get actual template expressions + const match = pythonCall.matchAll(/\$[_a-zA-Z][_a-zA-Z0-9]*/gu); + const actualTemplateExpressions = [...match].map((it) => it[0]); + + // Compute valid template expressions + const validTemplateExpressions = new Set(getParameters(node).map((it) => `\$${it.name}`)); + if (hasContainerOfType(node, isSdsClass)) { + validTemplateExpressions.add('$this'); + } + + // Compute invalid template expressions + const invalidTemplateExpressions = actualTemplateExpressions.filter((it) => !validTemplateExpressions.has(it)); + + // Report invalid template expressions + if (invalidTemplateExpressions.length > 0) { + const kind = pluralize(invalidTemplateExpressions.length, 'template expression'); + const invalidTemplateExpressionsString = invalidTemplateExpressions.map((it) => `'${it}'`).join(', '); + + accept('error', `The ${kind} ${invalidTemplateExpressionsString} cannot be interpreted.`, { + node: findFirstAnnotationCallOf(node, builtinAnnotations.PythonCall)!, + property: 'annotation', + code: CODE_PYTHON_CALL_INVALID_TEMPLATE_EXPRESSION, + }); + } + }; +}; diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 2c65cb745..91bb31926 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -139,6 +139,7 @@ import { literalTypeShouldNotHaveDuplicateLiteral, } from './other/types/literalTypes.js'; import { annotationCallMustHaveCorrectTarget, targetShouldNotHaveDuplicateEntries } from './builtins/target.js'; +import { pythonCallMustOnlyContainValidTemplateExpressions } from './builtins/pythonCall.js'; /** * Register custom validation checks. @@ -215,6 +216,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsFunction: [ functionMustContainUniqueNames, functionResultListShouldNotBeEmpty, + pythonCallMustOnlyContainValidTemplateExpressions(services), pythonNameMustNotBeSetIfPythonCallIsSet(services), ], SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)], diff --git a/tests/resources/validation/builtins/annotations/pythonCall/main.sdstest b/tests/resources/validation/builtins/annotations/pythonCall/main.sdstest new file mode 100644 index 000000000..80b97dcba --- /dev/null +++ b/tests/resources/validation/builtins/annotations/pythonCall/main.sdstest @@ -0,0 +1,39 @@ +package tests.validation.builtins.pythonCall + +class MyClass { + // $TEST$ no error r"The template expressions? .* cannot be interpreted." + @»PythonCall«("myMethod1($param)") + fun myMethod1(param: Int) + + // $TEST$ no error r"The template expressions? .* cannot be interpreted." + @»PythonCall«("myMethod2($this)") + fun myMethod2(this: Int) + + // $TEST$ no error "The template expression '$this' cannot be interpreted." + @»PythonCall«("myMethod3($this)") + fun myMethod3() + + // $TEST$ error "The template expressions '$param1', '$param2' cannot be interpreted." + @»PythonCall«("myMethod4($param1, $param2)") + fun myMethod4() +} + +// $TEST$ no error r"The template expressions? .* cannot be interpreted." +@»PythonCall«("myFunction1($param)") +fun myFunction1(param: Int) + +// $TEST$ no error r"The template expressions? .* cannot be interpreted." +@»PythonCall«("myFunction2($this)") +fun myFunction2(this: Int) + +// $TEST$ error "The template expression '$this' cannot be interpreted." +@»PythonCall«("myFunction3($this)") +fun myFunction3() + +// $TEST$ error "The template expressions '$param1', '$param2' cannot be interpreted." +@»PythonCall«("myFunction4($param1, $param2)") +fun myFunction4() + +// $TEST$ no error "An expert parameter must be optional." +@»PythonCall«("$this") +annotation MyAnnotation()