Skip to content

Commit 09bfb38

Browse files
authored
feat: check template expressions inside @PythonCall (#686)
### Summary of Changes Show an error if the call specification inside a `@PythonCall` cannot be interpreted.
1 parent d22c446 commit 09bfb38

File tree

3 files changed

+84
-0
lines changed

3 files changed

+84
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { hasContainerOfType, ValidationAcceptor } from 'langium';
2+
import { isSdsClass, SdsFunction } from '../../generated/ast.js';
3+
import { SafeDsServices } from '../../safe-ds-module.js';
4+
import { findFirstAnnotationCallOf, getParameters } from '../../helpers/nodeProperties.js';
5+
import { pluralize } from '../../../helpers/stringUtils.js';
6+
7+
export const CODE_PYTHON_CALL_INVALID_TEMPLATE_EXPRESSION = 'python-call/invalid-template-expression';
8+
9+
export const pythonCallMustOnlyContainValidTemplateExpressions = (services: SafeDsServices) => {
10+
const builtinAnnotations = services.builtins.Annotations;
11+
12+
return (node: SdsFunction, accept: ValidationAcceptor) => {
13+
const pythonCall = builtinAnnotations.getPythonCall(node);
14+
if (!pythonCall) {
15+
return;
16+
}
17+
18+
// Get actual template expressions
19+
const match = pythonCall.matchAll(/\$[_a-zA-Z][_a-zA-Z0-9]*/gu);
20+
const actualTemplateExpressions = [...match].map((it) => it[0]);
21+
22+
// Compute valid template expressions
23+
const validTemplateExpressions = new Set(getParameters(node).map((it) => `\$${it.name}`));
24+
if (hasContainerOfType(node, isSdsClass)) {
25+
validTemplateExpressions.add('$this');
26+
}
27+
28+
// Compute invalid template expressions
29+
const invalidTemplateExpressions = actualTemplateExpressions.filter((it) => !validTemplateExpressions.has(it));
30+
31+
// Report invalid template expressions
32+
if (invalidTemplateExpressions.length > 0) {
33+
const kind = pluralize(invalidTemplateExpressions.length, 'template expression');
34+
const invalidTemplateExpressionsString = invalidTemplateExpressions.map((it) => `'${it}'`).join(', ');
35+
36+
accept('error', `The ${kind} ${invalidTemplateExpressionsString} cannot be interpreted.`, {
37+
node: findFirstAnnotationCallOf(node, builtinAnnotations.PythonCall)!,
38+
property: 'annotation',
39+
code: CODE_PYTHON_CALL_INVALID_TEMPLATE_EXPRESSION,
40+
});
41+
}
42+
};
43+
};

src/language/validation/safe-ds-validator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ import {
139139
literalTypeShouldNotHaveDuplicateLiteral,
140140
} from './other/types/literalTypes.js';
141141
import { annotationCallMustHaveCorrectTarget, targetShouldNotHaveDuplicateEntries } from './builtins/target.js';
142+
import { pythonCallMustOnlyContainValidTemplateExpressions } from './builtins/pythonCall.js';
142143

143144
/**
144145
* Register custom validation checks.
@@ -215,6 +216,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
215216
SdsFunction: [
216217
functionMustContainUniqueNames,
217218
functionResultListShouldNotBeEmpty,
219+
pythonCallMustOnlyContainValidTemplateExpressions(services),
218220
pythonNameMustNotBeSetIfPythonCallIsSet(services),
219221
],
220222
SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package tests.validation.builtins.pythonCall
2+
3+
class MyClass {
4+
// $TEST$ no error r"The template expressions? .* cannot be interpreted."
5+
@»PythonCall«("myMethod1($param)")
6+
fun myMethod1(param: Int)
7+
8+
// $TEST$ no error r"The template expressions? .* cannot be interpreted."
9+
@»PythonCall«("myMethod2($this)")
10+
fun myMethod2(this: Int)
11+
12+
// $TEST$ no error "The template expression '$this' cannot be interpreted."
13+
@»PythonCall«("myMethod3($this)")
14+
fun myMethod3()
15+
16+
// $TEST$ error "The template expressions '$param1', '$param2' cannot be interpreted."
17+
@»PythonCall«("myMethod4($param1, $param2)")
18+
fun myMethod4()
19+
}
20+
21+
// $TEST$ no error r"The template expressions? .* cannot be interpreted."
22+
@»PythonCall«("myFunction1($param)")
23+
fun myFunction1(param: Int)
24+
25+
// $TEST$ no error r"The template expressions? .* cannot be interpreted."
26+
@»PythonCall«("myFunction2($this)")
27+
fun myFunction2(this: Int)
28+
29+
// $TEST$ error "The template expression '$this' cannot be interpreted."
30+
@»PythonCall«("myFunction3($this)")
31+
fun myFunction3()
32+
33+
// $TEST$ error "The template expressions '$param1', '$param2' cannot be interpreted."
34+
@»PythonCall«("myFunction4($param1, $param2)")
35+
fun myFunction4()
36+
37+
// $TEST$ no error "An expert parameter must be optional."
38+
@»PythonCall«("$this")
39+
annotation MyAnnotation()

0 commit comments

Comments
 (0)