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: warn about duplicate impurity reasons #773

Merged
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { EMPTY_STREAM, getContainerOfType, Stream, stream, URI } from 'langium';
import { EMPTY_STREAM, Stream, stream, URI } from 'langium';
import { resourceNameToUri } from '../../helpers/resources.js';
import {
isSdsAnnotation,
isSdsEnum,
SdsAnnotatedObject,
SdsAnnotation,
SdsEnumVariant,
Expand Down Expand Up @@ -71,21 +70,15 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
return hasAnnotationCallOf(node, this.Impure);
}

streamImpurityReasons(node: SdsFunction | undefined): Stream<SdsEnumVariant> {
streamImpurityReasons(node: SdsFunction | undefined): Stream<EvaluatedEnumVariant> {
// If allReasons are specified, but we could not evaluate them to a list, no reasons apply
const value = this.getParameterValue(node, this.Impure, 'allReasons');
if (!(value instanceof EvaluatedList)) {
return EMPTY_STREAM;
}

// Otherwise, filter the elements of the list and keep only variants of the ImpurityReason enum
return stream(value.elements)
.filter(
(it) =>
it instanceof EvaluatedEnumVariant &&
getContainerOfType(it.variant, isSdsEnum) === this.builtinEnums.ImpurityReason,
)
.map((it) => (<EvaluatedEnumVariant>it).variant);
return stream(value.elements).filter(this.builtinEnums.isEvaluatedImpurityReason);
}

get Impure(): SdsAnnotation | undefined {
Expand Down Expand Up @@ -161,12 +154,8 @@ export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {

// Otherwise, filter the elements of the list and keep only variants of the AnnotationTarget enum
return stream(value.elements)
.filter(
(it) =>
it instanceof EvaluatedEnumVariant &&
getContainerOfType(it.variant, isSdsEnum) === this.builtinEnums.AnnotationTarget,
)
.map((it) => (<EvaluatedEnumVariant>it).variant);
.filter(this.builtinEnums.isEvaluatedAnnotationTarget)
.map((it) => it.variant);
}

get Target(): SdsAnnotation | undefined {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { URI } from 'langium';
import { getContainerOfType, URI } from 'langium';
import { resourceNameToUri } from '../../helpers/resources.js';
import { isSdsEnum, SdsEnum } from '../generated/ast.js';
import { EvaluatedEnumVariant, EvaluatedNode } from '../partialEvaluation/model.js';
import { SafeDsModuleMembers } from './safe-ds-module-members.js';

const ANNOTATION_USAGE_URI = resourceNameToUri('builtins/safeds/lang/annotationUsage.sdsstub');
Expand All @@ -11,10 +12,16 @@ export class SafeDsEnums extends SafeDsModuleMembers<SdsEnum> {
return this.getEnum(ANNOTATION_USAGE_URI, 'AnnotationTarget');
}

isEvaluatedAnnotationTarget = (node: EvaluatedNode): node is EvaluatedEnumVariant =>
node instanceof EvaluatedEnumVariant && getContainerOfType(node.variant, isSdsEnum) === this.AnnotationTarget;

get ImpurityReason(): SdsEnum | undefined {
return this.getEnum(PURITY_URI, 'ImpurityReason');
}

isEvaluatedImpurityReason = (node: EvaluatedNode): node is EvaluatedEnumVariant =>
node instanceof EvaluatedEnumVariant && getContainerOfType(node.variant, isSdsEnum) === this.ImpurityReason;

private getEnum(uri: URI, name: string): SdsEnum | undefined {
return this.getModuleMember(uri, name, isSdsEnum);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getContainerOfType, stream, ValidationAcceptor } from 'langium';
import { isSdsCall, isSdsEnum, isSdsList, SdsFunction } from '../../generated/ast.js';
import type { SafeDsServices } from '../../safe-ds-module.js';
import { stream, ValidationAcceptor } from 'langium';
import { isSdsCall, isSdsList, SdsFunction } from '../../generated/ast.js';
import { findFirstAnnotationCallOf, getArguments, getParameters } from '../../helpers/nodeProperties.js';
import { EvaluatedEnumVariant, StringConstant } from '../../partialEvaluation/model.js';
import type { SafeDsServices } from '../../safe-ds-module.js';

export const CODE_IMPURE_DUPLICATE_REASON = 'impure/duplicate-reason';
export const CODE_IMPURE_PARAMETER_NAME = 'impure/parameter-name';

export const impurityReasonParameterNameMustBelongToParameter = (services: SafeDsServices) => {
Expand Down Expand Up @@ -38,10 +39,7 @@ export const impurityReasonParameterNameMustBelongToParameter = (services: SafeD

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

Expand Down Expand Up @@ -70,3 +68,43 @@ export const impurityReasonParameterNameMustBelongToParameter = (services: SafeD
}
};
};

export const impurityReasonShouldNotBeSetMultipleTimes = (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 knownReasons: EvaluatedEnumVariant[] = [];
for (const reason of allReasons.elements) {
// Check whether the reason is valid
const evaluatedReason = partialEvaluator.evaluate(reason);
if (!builtinEnums.isEvaluatedImpurityReason(evaluatedReason)) {
continue;
}

if (knownReasons.some((it) => it.equals(evaluatedReason))) {
accept('warning', `The impurity reason '${evaluatedReason}' was set already.`, {
node: reason,
code: CODE_IMPURE_DUPLICATE_REASON,
});
} else {
knownReasons.push(evaluatedReason);
}
}
};
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getContainerOfType, ValidationAcceptor } from 'langium';
import { ValidationAcceptor } from 'langium';
import {
isSdsAnnotation,
isSdsAttribute,
Expand All @@ -17,9 +17,8 @@ import {
SdsAnnotationCall,
SdsEnumVariant,
} from '../../generated/ast.js';
import { SafeDsServices } from '../../safe-ds-module.js';
import { findFirstAnnotationCallOf, getAnnotationCallTarget } from '../../helpers/nodeProperties.js';
import { EvaluatedEnumVariant } from '../../partialEvaluation/model.js';
import { SafeDsServices } from '../../safe-ds-module.js';

export const CODE_TARGET_DUPLICATE_TARGET = 'target/duplicate-target';
export const CODE_TARGET_WRONG_TARGET = 'target/wrong-target';
Expand All @@ -44,10 +43,7 @@ export const targetShouldNotHaveDuplicateEntries = (services: SafeDsServices) =>
const knownTargets = new Set<SdsEnumVariant>();
for (const target of targets.elements) {
const evaluatedTarget = partialEvaluator.evaluate(target);
if (
!(evaluatedTarget instanceof EvaluatedEnumVariant) ||
getContainerOfType(evaluatedTarget.variant, isSdsEnum) !== builtinEnums.AnnotationTarget
) {
if (!builtinEnums.isEvaluatedAnnotationTarget(evaluatedTarget)) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
referenceTargetShouldNotExperimental,
} from './builtins/experimental.js';
import { requiredParameterMustNotBeExpert } from './builtins/expert.js';
import {
impurityReasonParameterNameMustBelongToParameter,
impurityReasonShouldNotBeSetMultipleTimes,
} from './builtins/impure.js';
import { pureParameterMustHaveCallableType } from './builtins/pure.js';
import { pythonCallMustOnlyContainValidTemplateExpressions } from './builtins/pythonCall.js';
import { pythonModuleShouldDifferFromSafeDsPackage } from './builtins/pythonModule.js';
Expand Down Expand Up @@ -168,7 +172,6 @@ import {
resultMustHaveTypeHint,
yieldTypeMustMatchResultType,
} from './types.js';
import { impurityReasonParameterNameMustBelongToParameter } from './builtins/impure.js';

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

@Impure([
// $TEST$ no warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«
])

/*
* We already show another error if the `@Impure` annotation is called multiple times.
*/

@Impure([
// $TEST$ no warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
// $TEST$ no warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
])
fun testFunction1()

@Impure([
// $TEST$ no warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
// $TEST$ warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
])
fun testFunction2()

@Impure([
// $TEST$ no warning "The impurity reason 'FileWriteToConstantPath("text.txt")' was set already."
»ImpurityReason.FileWriteToConstantPath("text.txt")«,
// $TEST$ warning "The impurity reason 'FileWriteToConstantPath("text.txt")' was set already."
»ImpurityReason.FileWriteToConstantPath("text.txt")«,
// $TEST$ warning "The impurity reason 'FileWriteToConstantPath("text.txt")' was set already."
»ImpurityReason.FileWriteToConstantPath("text.txt")«,
// $TEST$ no warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
// $TEST$ warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
// $TEST$ warning "The impurity reason 'FileReadFromConstantPath("text.txt")' was set already."
»ImpurityReason.FileReadFromConstantPath("text.txt")«,
])
fun testFunction3()

/*
* We already show another error if an annotation is called with arguments of invalid type.
*/

@Impure(
// $TEST$ no warning "The impurity reason 'FileWriteToConstantPath("text.txt")' was set already."
»ImpurityReason.FileWriteToConstantPath("text.txt")«,
// $TEST$ no warning "The impurity reason 'FileWriteToConstantPath("text.txt")' was set already."
»ImpurityReason.FileWriteToConstantPath("text.txt")«
)
fun testFunction4()

@Impure([
// $TEST$ no warning r"The impurity reason '.*' was set already\."
»1«,
// $TEST$ no warning r"The impurity reason '.*' was set already\."
»1«,
])
fun testFunction5()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tests.validation.builtins.annotations.impure.duplicateReason

// $TEST$ no warning r"The impurity reason '.*' was set already\."

fun testFunction6()