Skip to content

Commit

Permalink
feat: error if parameter name in impurity reason is invalid (#772)
Browse files Browse the repository at this point in the history
Closes #741

### Summary of Changes

A `parameterName` in an `ImpurityReason` must be the name of a parameter
in the annotated function. This PR adds validation for this.
  • Loading branch information
lars-reimann authored Nov 13, 2023
1 parent 87d2a48 commit faa2012
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 0 deletions.
72 changes: 72 additions & 0 deletions packages/safe-ds-lang/src/language/validation/builtins/impure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { getContainerOfType, stream, ValidationAcceptor } from 'langium';
import { isSdsCall, isSdsEnum, isSdsList, SdsFunction } from '../../generated/ast.js';
import type { SafeDsServices } from '../../safe-ds-module.js';
import { findFirstAnnotationCallOf, getArguments, getParameters } from '../../helpers/nodeProperties.js';
import { EvaluatedEnumVariant, StringConstant } from '../../partialEvaluation/model.js';

export const CODE_IMPURE_PARAMETER_NAME = 'impure/parameter-name';

export const impurityReasonParameterNameMustBelongToParameter = (services: SafeDsServices) => {
const builtinAnnotations = services.builtins.Annotations;
const builtinEnums = services.builtins.Enums;
const nodeMapper = services.helpers.NodeMapper;
const partialEvaluator = services.evaluation.PartialEvaluator;

return (node: SdsFunction, accept: ValidationAcceptor) => {
const annotationCall = findFirstAnnotationCallOf(node, builtinAnnotations.Impure);

// Don't further validate if the function is marked as impure and as pure
if (!annotationCall || builtinAnnotations.isPure(node)) {
return;
}

// Check whether allReasons is valid
const allReasons = nodeMapper.callToParameterValue(annotationCall, 'allReasons');
if (!isSdsList(allReasons)) {
return;
}

const parameterNames = stream(getParameters(node))
.map((it) => it.name)
.toSet();

for (const reason of allReasons.elements) {
// If it's not a call, no parameter name could've been provided
if (!isSdsCall(reason)) {
continue;
}

// Check whether the reason is valid
const evaluatedReason = partialEvaluator.evaluate(reason);
if (
!(evaluatedReason instanceof EvaluatedEnumVariant) ||
getContainerOfType(evaluatedReason.variant, isSdsEnum) !== builtinEnums.ImpurityReason
) {
continue;
}

// Check whether a parameter name was provided
const parameterName = nodeMapper.callToParameterValue(reason, 'parameterName');
if (!parameterName) {
continue;
}

// Check whether parameterName is valid
const evaluatedParameterName = partialEvaluator.evaluate(parameterName);
if (!(evaluatedParameterName instanceof StringConstant)) {
continue;
}

if (!parameterNames.has(evaluatedParameterName.value)) {
const parameterNameArgument = getArguments(reason).find(
(it) => nodeMapper.argumentToParameter(it)?.name === 'parameterName',
)!;

accept('error', `The parameter '${evaluatedParameterName.value}' does not exist.`, {
node: parameterNameArgument,
code: CODE_IMPURE_PARAMETER_NAME,
});
}
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import {
resultMustHaveTypeHint,
yieldTypeMustMatchResultType,
} from './types.js';
import { impurityReasonParameterNameMustBelongToParameter } from './builtins/impure.js';

/**
* Register custom validation checks.
Expand Down Expand Up @@ -249,6 +250,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
functionMustContainUniqueNames,
functionResultListShouldNotBeEmpty,
functionPurityMustBeSpecified(services),
impurityReasonParameterNameMustBelongToParameter(services),
pythonCallMustOnlyContainValidTemplateExpressions(services),
pythonNameMustNotBeSetIfPythonCallIsSet(services),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tests.validation.builtins.annotations.impure.parameterNamesMustBeValid

@Impure([
// $TEST$ no error r"The parameter '.*' does not exist\."
ImpurityReason.FileReadFromConstantPath(»"text.txt"«),
// $TEST$ no error "The parameter 'p' does not exist."
ImpurityReason.FileReadFromParameterizedPath(»"p"«),
// $TEST$ error "The parameter 'q' does not exist."
ImpurityReason.FileReadFromParameterizedPath(»"q"«),
// $TEST$ no error r"The parameter '.*' does not exist\."
ImpurityReason.FileWriteToConstantPath(»"text.txt"«),
// $TEST$ no error "The parameter 'p' does not exist."
ImpurityReason.FileWriteToParameterizedPath(»"p"«),
// $TEST$ error "The parameter 'q' does not exist."
ImpurityReason.FileWriteToParameterizedPath(»"q"«),
// $TEST$ no error "The parameter 'p' does not exist."
ImpurityReason.PotentiallyImpureParameterCall(»"p"«),
// $TEST$ error "The parameter 'q' does not exist."
ImpurityReason.PotentiallyImpureParameterCall(»"q"«),

// $TEST$ no error r"The parameter '.*' does not exist\."
ImpurityReason.PotentiallyImpureParameterCall(»1«),
// $TEST$ no error "The parameter 'q' does not exist."
AnotherEnum.SomeVariant(»"q"«)
])
fun f(p: Int)

enum AnotherEnum {
SomeVariant(parameterName: String)
}

0 comments on commit faa2012

Please sign in to comment.