Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check template expressions inside @PythonCall #686

Merged
merged 1 commit into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()