Skip to content

Commit

Permalink
feat: error if lambda is used in wrong context (#647)
Browse files Browse the repository at this point in the history
Closes #409
Closes partially #543

### Summary of Changes

Show an error if a lambda is used in the wrong context. They must be
assigned to a type parameter, either as its default value or an
argument.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot committed Oct 17, 2023
1 parent 097764d commit 2d2ccc6
Show file tree
Hide file tree
Showing 10 changed files with 464 additions and 10 deletions.
4 changes: 2 additions & 2 deletions src/language/grammar/safe-ds.langium
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ interface SdsIndexedAccess extends SdsChainedExpression {

interface SdsMemberAccess extends SdsChainedExpression {
isNullSafe: boolean
member: SdsReference
member?: SdsReference
}

SdsChainedExpression returns SdsExpression:
Expand Down Expand Up @@ -874,8 +874,8 @@ interface SdsType extends SdsObject {}
interface SdsNamedTypeDeclaration extends SdsDeclaration {}

interface SdsMemberType extends SdsType {
member: SdsNamedType
receiver: SdsType
member?: SdsNamedType
}

SdsType returns SdsType:
Expand Down
4 changes: 2 additions & 2 deletions src/language/scoping/safe-ds-scope-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
if (isSdsNamedType(node)) {
return node.declaration.ref;
} else if (isSdsMemberType(node)) {
return node.member.declaration.ref;
return node.member?.declaration?.ref;
} else {
return undefined;
}
Expand Down Expand Up @@ -233,7 +233,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
if (isSdsReference(node)) {
return node.target.ref;
} else if (isSdsMemberAccess(node)) {
return node.member.target.ref;
return node.member?.target?.ref;
} else {
return undefined;
}
Expand Down
31 changes: 30 additions & 1 deletion src/language/validation/other/expressions/lambdas.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import { SdsLambda } from '../../../generated/ast.js';
import { isSdsArgument, isSdsParameter, isSdsParenthesizedExpression, SdsLambda } from '../../../generated/ast.js';
import { ValidationAcceptor } from 'langium';
import { parametersOrEmpty } from '../../../helpers/nodeProperties.js';
import { SafeDsServices } from '../../../safe-ds-module.js';

export const CODE_LAMBDA_CONTEXT = 'lambda/context';
export const CODE_LAMBDA_CONST_MODIFIER = 'lambda/const-modifier';

export const lambdaMustBeAssignedToTypedParameter = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;

return (node: SdsLambda, accept: ValidationAcceptor): void => {
let context = node.$container;
while (isSdsParenthesizedExpression(context)) {
context = context.$container;
}

let contextIsValid = false;
if (isSdsParameter(context)) {
contextIsValid = context.type !== undefined;
} else if (isSdsArgument(context)) {
const parameter = nodeMapper.argumentToParameterOrUndefined(context);
// If the resolution of the parameter failed, we already show another error nearby
contextIsValid = parameter === undefined || parameter.type !== undefined;
}

if (!contextIsValid) {
accept('error', 'A lambda must be assigned to a typed parameter.', {
node,
code: CODE_LAMBDA_CONTEXT,
});
}
};
};

export const lambdaParameterMustNotHaveConstModifier = (node: SdsLambda, accept: ValidationAcceptor): void => {
for (const parameter of parametersOrEmpty(node)) {
if (parameter.isConstant) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const memberAccessOfEnumVariantMustNotLackInstantiation = (
node: SdsMemberAccess,
accept: ValidationAcceptor,
): void => {
const declaration = node.member.target.ref;
const declaration = node.member?.target?.ref;
if (!isSdsEnumVariant(declaration)) {
return;
}
Expand Down
11 changes: 9 additions & 2 deletions src/language/validation/safe-ds-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ import {
} from './builtins/experimental.js';
import { placeholderShouldBeUsed, placeholdersMustNotBeAnAlias } from './other/declarations/placeholders.js';
import { segmentParameterShouldBeUsed, segmentResultMustBeAssignedExactlyOnce } from './other/declarations/segments.js';
import { lambdaParameterMustNotHaveConstModifier } from './other/expressions/lambdas.js';
import {
lambdaMustBeAssignedToTypedParameter,
lambdaParameterMustNotHaveConstModifier,
} from './other/expressions/lambdas.js';
import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeatures.js';
import { requiredParameterMustNotBeExpert } from './builtins/expert.js';
import {
Expand Down Expand Up @@ -177,7 +180,11 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsImportedDeclaration: [importedDeclarationAliasShouldDifferFromDeclarationName],
SdsIndexedAccess: [indexedAccessesShouldBeUsedWithCaution],
SdsInfixOperation: [divisionDivisorMustNotBeZero(services), elvisOperatorShouldBeNeeded(services)],
SdsLambda: [lambdaParametersMustNotBeAnnotated, lambdaParameterMustNotHaveConstModifier],
SdsLambda: [
lambdaMustBeAssignedToTypedParameter(services),
lambdaParametersMustNotBeAnnotated,
lambdaParameterMustNotHaveConstModifier,
],
SdsMemberAccess: [
memberAccessMustBeNullSafeIfReceiverIsNullable(services),
memberAccessNullSafetyShouldBeNeeded(services),
Expand Down
4 changes: 2 additions & 2 deletions src/language/validation/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getContainerOfType, ValidationAcceptor } from 'langium';
import { AstNode, getContainerOfType, ValidationAcceptor } from 'langium';
import {
isSdsAnnotation,
isSdsCallable,
Expand Down Expand Up @@ -31,7 +31,7 @@ export const callReceiverMustBeCallable = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;

return (node: SdsCall, accept: ValidationAcceptor): void => {
let receiver = node.receiver;
let receiver: AstNode | undefined = node.receiver;
if (isSdsMemberAccess(receiver)) {
receiver = receiver.member;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package tests.validation.other.expressions.lambdas.assignedToTypedParameter

/*
* Lambdas passed as default values
*/

@Repeatable
annotation MyAnnotation(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = »() -> 1«
)

class MyClass(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
)

enum MyEnum {
MyEnumVariant(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
)
}

fun myFunction(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
)

segment mySegment1(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
) {}

segment mySegment2(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: (p: () -> () = »() {}«) -> (),
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: (p: () -> (r: Int) = ((»() -> 1«))) -> (),
) {
(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
) {};

(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f: () -> () = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g: () -> r: Int = ((»() -> 1«))
) -> 1;
}

/*
* Lambdas passed as arguments
*/

@MyAnnotation(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
)
@MyAnnotation(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
)
segment lambdasPassedAsArguments(
callableType: (p: () -> (), q: () -> (r: Int)) -> (),
) {
val blockLambda = (p: () -> (), q: () -> (r: Int)) {};
val expressionLambda = (p: () -> (), q: () -> (r: Int)) -> 1;

MyAnnotation(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
MyAnnotation(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
);

MyClass(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
MyClass(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
);

MyEnum.MyEnumVariant(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
MyEnum.MyEnumVariant(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
);

myFunction(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
myFunction(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
);

mySegment1(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
mySegment1(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
f = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
g = ((»() -> 1«))
);

callableType(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
callableType(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
p = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
q = ((»() -> 1«))
);

blockLambda(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
blockLambda(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
p = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
q = ((»() -> 1«))
);

expressionLambda(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
»() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
((»() -> 1«))
);
expressionLambda(
// $TEST$ no error "A lambda must be assigned to a typed parameter."
p = »() {}«,
// $TEST$ no error "A lambda must be assigned to a typed parameter."
q = ((»() -> 1«))
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tests.validation.other.expressions.lambdas.context.assignedToUnresolvedParameter

fun myFunction()

pipeline unresolvedParameter {
// $TEST$ no error "A lambda must be assigned to a typed parameter."
myFunction(»() {}«);
// $TEST$ no error "A lambda must be assigned to a typed parameter."
myFunction(»() -> 1«);

// $TEST$ no error "A lambda must be assigned to a typed parameter."
myFunction(unresolved = »() {}«);
// $TEST$ no error "A lambda must be assigned to a typed parameter."
myFunction(unresolved = »() -> 1«);

// $TEST$ no error "A lambda must be assigned to a typed parameter."
unresolved(»() {}«);
// $TEST$ no error "A lambda must be assigned to a typed parameter."
unresolved(»() -> 1«);
}
Loading

0 comments on commit 2d2ccc6

Please sign in to comment.