Skip to content

Commit

Permalink
feat: check template expressions inside @PythonCall
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-reimann committed Oct 23, 2023
1 parent d22c446 commit 9095618
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/language/validation/builtins/pythonCall.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
};
};
2 changes: 2 additions & 0 deletions src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -215,6 +216,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsFunction: [
functionMustContainUniqueNames,
functionResultListShouldNotBeEmpty,
pythonCallMustOnlyContainValidTemplateExpressions(services),
pythonNameMustNotBeSetIfPythonCallIsSet(services),
],
SdsImport: [importPackageMustExist(services), importPackageShouldNotBeEmpty(services)],
Expand Down
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 9095618

Please sign in to comment.