Skip to content

Commit e77037e

Browse files
authored
feat: various checks for annotations on parameters and results (#625)
Closes partially #543 ### Summary of Changes * Annotations must not be used on parameters of callable types * Annotations must not be used on results of callable types * Annotations must not be used on parameters of lambdas * `@Deprecated` must not be used on required parameters * `@Expert` must not be used on required parameters
1 parent 090fcc3 commit e77037e

File tree

12 files changed

+265
-22
lines changed

12 files changed

+265
-22
lines changed
+25-15
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,44 @@
1-
import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation } from '../generated/ast.js';
1+
import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsParameter } from '../generated/ast.js';
22
import { annotationCallsOrEmpty } from '../helpers/nodeProperties.js';
33
import { SafeDsModuleMembers } from './safe-ds-module-members.js';
44
import { resourceNameToUri } from '../../helpers/resources.js';
5+
import { URI } from 'langium';
56

67
const CORE_ANNOTATIONS_URI = resourceNameToUri('builtins/safeds/lang/coreAnnotations.sdsstub');
78

89
export class SafeDsAnnotations extends SafeDsModuleMembers<SdsAnnotation> {
910
isDeprecated(node: SdsAnnotatedObject | undefined): boolean {
10-
return annotationCallsOrEmpty(node).some((it) => {
11-
const annotation = it.annotation?.ref;
12-
return annotation === this.Deprecated;
13-
});
11+
return this.hasAnnotationCallOf(node, this.Deprecated);
1412
}
1513

16-
isExperimental(node: SdsAnnotatedObject | undefined): boolean {
17-
return annotationCallsOrEmpty(node).some((it) => {
18-
const annotation = it.annotation?.ref;
19-
return annotation === this.Experimental;
20-
});
14+
private get Deprecated(): SdsAnnotation | undefined {
15+
return this.getAnnotation(CORE_ANNOTATIONS_URI, 'Deprecated');
2116
}
2217

23-
private get Deprecated(): SdsAnnotation | undefined {
24-
return this.getAnnotation('Deprecated');
18+
isExperimental(node: SdsAnnotatedObject | undefined): boolean {
19+
return this.hasAnnotationCallOf(node, this.Experimental);
2520
}
2621

2722
private get Experimental(): SdsAnnotation | undefined {
28-
return this.getAnnotation('Experimental');
23+
return this.getAnnotation(CORE_ANNOTATIONS_URI, 'Experimental');
24+
}
25+
26+
isExpert(node: SdsParameter | undefined): boolean {
27+
return this.hasAnnotationCallOf(node, this.Expert);
28+
}
29+
30+
private get Expert(): SdsAnnotation | undefined {
31+
return this.getAnnotation(CORE_ANNOTATIONS_URI, 'Expert');
32+
}
33+
34+
private hasAnnotationCallOf(node: SdsAnnotatedObject | undefined, expected: SdsAnnotation | undefined): boolean {
35+
return annotationCallsOrEmpty(node).some((it) => {
36+
const actual = it.annotation?.ref;
37+
return actual === expected;
38+
});
2939
}
3040

31-
private getAnnotation(name: string): SdsAnnotation | undefined {
32-
return this.getModuleMember(CORE_ANNOTATIONS_URI, name, isSdsAnnotation);
41+
private getAnnotation(uri: URI, name: string): SdsAnnotation | undefined {
42+
return this.getModuleMember(uri, name, isSdsAnnotation);
3343
}
3444
}

src/language/validation/builtins/deprecated.ts

+17
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import {
77
SdsArgument,
88
SdsAssignee,
99
SdsNamedType,
10+
SdsParameter,
1011
SdsReference,
1112
} from '../../generated/ast.js';
1213
import { SafeDsServices } from '../../safe-ds-module.js';
14+
import { isRequiredParameter } from '../../helpers/nodeProperties.js';
15+
import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js';
1316

1417
export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result';
1518
export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation';
1619
export const CODE_DEPRECATED_CORRESPONDING_PARAMETER = 'deprecated/corresponding-parameter';
1720
export const CODE_DEPRECATED_REFERENCED_DECLARATION = 'deprecated/referenced-declaration';
21+
export const CODE_DEPRECATED_REQUIRED_PARAMETER = 'deprecated/required-parameter';
1822

1923
export const assigneeAssignedResultShouldNotBeDeprecated =
2024
(services: SafeDsServices) => (node: SdsAssignee, accept: ValidationAcceptor) => {
@@ -95,3 +99,16 @@ export const referenceTargetShouldNotBeDeprecated =
9599
});
96100
}
97101
};
102+
103+
export const requiredParameterMustNotBeDeprecated =
104+
(services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => {
105+
if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) {
106+
if (services.builtins.Annotations.isDeprecated(node)) {
107+
accept('error', 'A deprecated parameter must be optional.', {
108+
node,
109+
property: 'name',
110+
code: CODE_DEPRECATED_REQUIRED_PARAMETER,
111+
});
112+
}
113+
}
114+
};
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ValidationAcceptor } from 'langium';
2+
import { SdsParameter } from '../../generated/ast.js';
3+
import { SafeDsServices } from '../../safe-ds-module.js';
4+
import { isRequiredParameter } from '../../helpers/nodeProperties.js';
5+
import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js';
6+
7+
export const CODE_EXPERT_TARGET_PARAMETER = 'expert/target-parameter';
8+
9+
export const requiredParameterMustNotBeExpert =
10+
(services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => {
11+
if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) {
12+
if (services.builtins.Annotations.isExpert(node)) {
13+
accept('error', 'An expert parameter must be optional.', {
14+
node,
15+
property: 'name',
16+
code: CODE_EXPERT_TARGET_PARAMETER,
17+
});
18+
}
19+
}
20+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
isSdsCallable,
3+
isSdsCallableType,
4+
isSdsLambda,
5+
SdsCallableType,
6+
SdsLambda,
7+
SdsParameter,
8+
} from '../../../generated/ast.js';
9+
import { getContainerOfType, ValidationAcceptor } from 'langium';
10+
import { annotationCallsOrEmpty, parametersOrEmpty, resultsOrEmpty } from '../../../helpers/nodeProperties.js';
11+
12+
export const CODE_ANNOTATION_CALL_TARGET_PARAMETER = 'annotation-call/target-parameter';
13+
export const CODE_ANNOTATION_CALL_TARGET_RESULT = 'annotation-call/target-result';
14+
15+
export const callableTypeParametersMustNotBeAnnotated = (node: SdsCallableType, accept: ValidationAcceptor) => {
16+
for (const parameter of parametersOrEmpty(node)) {
17+
for (const annotationCall of annotationCallsOrEmpty(parameter)) {
18+
accept('error', 'Parameters of callable types must not be annotated.', {
19+
node: annotationCall,
20+
code: CODE_ANNOTATION_CALL_TARGET_PARAMETER,
21+
});
22+
}
23+
}
24+
};
25+
26+
export const callableTypeResultsMustNotBeAnnotated = (node: SdsCallableType, accept: ValidationAcceptor) => {
27+
for (const result of resultsOrEmpty(node.resultList)) {
28+
for (const annotationCall of annotationCallsOrEmpty(result)) {
29+
accept('error', 'Results of callable types must not be annotated.', {
30+
node: annotationCall,
31+
code: CODE_ANNOTATION_CALL_TARGET_RESULT,
32+
});
33+
}
34+
}
35+
};
36+
37+
export const lambdaParametersMustNotBeAnnotated = (node: SdsLambda, accept: ValidationAcceptor) => {
38+
for (const parameter of parametersOrEmpty(node)) {
39+
for (const annotationCall of annotationCallsOrEmpty(parameter)) {
40+
accept('error', 'Lambda parameters must not be annotated.', {
41+
node: annotationCall,
42+
code: CODE_ANNOTATION_CALL_TARGET_PARAMETER,
43+
});
44+
}
45+
}
46+
};
47+
48+
export const parameterCanBeAnnotated = (node: SdsParameter) => {
49+
const containingCallable = getContainerOfType(node, isSdsCallable);
50+
return !isSdsCallableType(containingCallable) && !isSdsLambda(containingCallable);
51+
};

src/language/validation/safe-ds-validator.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
assigneeAssignedResultShouldNotBeDeprecated,
5353
namedTypeDeclarationShouldNotBeDeprecated,
5454
referenceTargetShouldNotBeDeprecated,
55+
requiredParameterMustNotBeDeprecated,
5556
} from './builtins/deprecated.js';
5657
import {
5758
annotationCallAnnotationShouldNotBeExperimental,
@@ -64,6 +65,12 @@ import { placeholderShouldBeUsed } from './other/declarations/placeholders.js';
6465
import { segmentParameterShouldBeUsed, segmentResultMustBeAssignedExactlyOnce } from './other/declarations/segments.js';
6566
import { lambdaParameterMustNotHaveConstModifier } from './other/expressions/lambdas.js';
6667
import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeature.js';
68+
import { requiredParameterMustNotBeExpert } from './builtins/expert.js';
69+
import {
70+
callableTypeParametersMustNotBeAnnotated,
71+
callableTypeResultsMustNotBeAnnotated,
72+
lambdaParametersMustNotBeAnnotated,
73+
} from './other/declarations/annotationCalls.js';
6774

6875
/**
6976
* Register custom validation checks.
@@ -97,7 +104,9 @@ export const registerValidationChecks = function (services: SafeDsServices) {
97104
SdsCallableType: [
98105
callableTypeMustContainUniqueNames,
99106
callableTypeMustNotHaveOptionalParameters,
107+
callableTypeParametersMustNotBeAnnotated,
100108
callableTypeParameterMustNotHaveConstModifier,
109+
callableTypeResultsMustNotBeAnnotated,
101110
],
102111
SdsClass: [classMustContainUniqueNames],
103112
SdsClassBody: [classBodyShouldNotBeEmpty],
@@ -109,15 +118,19 @@ export const registerValidationChecks = function (services: SafeDsServices) {
109118
SdsExpressionLambda: [expressionLambdaMustContainUniqueNames],
110119
SdsFunction: [functionMustContainUniqueNames, functionResultListShouldNotBeEmpty],
111120
SdsIndexedAccess: [indexedAccessesShouldBeUsedWithCaution],
112-
SdsLambda: [lambdaParameterMustNotHaveConstModifier],
121+
SdsLambda: [lambdaParametersMustNotBeAnnotated, lambdaParameterMustNotHaveConstModifier],
113122
SdsMemberAccess: [memberAccessNullSafetyShouldBeNeeded(services)],
114123
SdsModule: [moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage],
115124
SdsNamedType: [
116125
namedTypeDeclarationShouldNotBeDeprecated(services),
117126
namedTypeDeclarationShouldNotBeExperimental(services),
118127
namedTypeTypeArgumentListShouldBeNeeded,
119128
],
120-
SdsParameter: [parameterMustHaveTypeHint],
129+
SdsParameter: [
130+
parameterMustHaveTypeHint,
131+
requiredParameterMustNotBeDeprecated(services),
132+
requiredParameterMustNotBeExpert(services),
133+
],
121134
SdsParameterList: [parameterListMustNotHaveRequiredParametersAfterOptionalParameters],
122135
SdsPipeline: [pipelineMustContainUniqueNames],
123136
SdsPlaceholder: [placeholderShouldBeUsed(services)],

src/resources/builtins/safeds/lang/coreAnnotations.sdsstub

+5
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ annotation Deprecated(
9090
])
9191
annotation Experimental
9292

93+
@Experimental
94+
@Description("This parameter should only be used by expert users.")
95+
@Target([AnnotationTarget.Parameter])
96+
annotation Expert
97+
9398
@Experimental
9499
@Description("The function has no side effects and returns the same results for the same arguments.")
95100
@Target([AnnotationTarget.Function])

src/resources/builtins/safeds/lang/documentation.sdsstub

-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,3 @@ annotation Since(
1111
@Description("The version in which a declaration was added.")
1212
version: String
1313
)
14-
15-
@Experimental
16-
@Description("This parameter should only be used by expert users.")
17-
@Target([AnnotationTarget.Parameter])
18-
annotation Expert
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package tests.validation.builtins.deprecated.mustNotBeUsedOnRequiredParameters
2+
3+
// $TEST$ error "A deprecated parameter must be optional."
4+
// $TEST$ no error "A deprecated parameter must be optional."
5+
annotation MyAnnotation(@Deprecated »a«: Int, @Deprecated »b«: Int = 3)
6+
7+
// $TEST$ error "A deprecated parameter must be optional."
8+
// $TEST$ no error "A deprecated parameter must be optional."
9+
class MyClass(@Deprecated »a«: Int, @Deprecated »b«: Int = 3) {
10+
11+
// $TEST$ error "A deprecated parameter must be optional."
12+
// $TEST$ no error "A deprecated parameter must be optional."
13+
class MyClass(@Deprecated »a«: Int, @Deprecated »b«: Int = 3)
14+
15+
// $TEST$ error "A deprecated parameter must be optional."
16+
// $TEST$ no error "A deprecated parameter must be optional."
17+
fun myFunction(@Deprecated »a«: Int, @Deprecated »b«: Int = 3)
18+
}
19+
20+
enum MyEnum {
21+
22+
// $TEST$ error "A deprecated parameter must be optional."
23+
// $TEST$ no error "A deprecated parameter must be optional."
24+
MyEnumVariant(@Deprecated »a«: Int, @Deprecated »b«: Int = 3)
25+
}
26+
27+
// $TEST$ error "A deprecated parameter must be optional."
28+
// $TEST$ no error "A deprecated parameter must be optional."
29+
fun myFunction(@Deprecated »a«: Int, @Deprecated »b«: Int = 3)
30+
31+
// $TEST$ error "A deprecated parameter must be optional."
32+
// $TEST$ no error "A deprecated parameter must be optional."
33+
segment mySegment1(@Deprecated »a«: Int, @Deprecated »b«: Int = 3) {}
34+
35+
// $TEST$ no error "A deprecated parameter must be optional."
36+
// $TEST$ no error "A deprecated parameter must be optional."
37+
segment mySegment2(
38+
f: (@Deprecated »a«: Int, @Deprecated »b«: Int = 3) -> ()
39+
) {
40+
41+
// $TEST$ no error "A deprecated parameter must be optional."
42+
// $TEST$ no error "A deprecated parameter must be optional."
43+
val g = (@Deprecated »a«: Int, @Deprecated »b«: Int = 3) {};
44+
45+
// $TEST$ no error "A deprecated parameter must be optional."
46+
// $TEST$ no error "A deprecated parameter must be optional."
47+
val h = (@Deprecated »a«: Int, @Deprecated »b«: Int = 3) -> 1;
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package tests.validation.builtins.expert.mustNotBeUsedOnRequiredParameters
2+
3+
// $TEST$ error "An expert parameter must be optional."
4+
// $TEST$ no error "An expert parameter must be optional."
5+
annotation MyAnnotation(@Expert »a«: Int, @Expert »b«: Int = 3)
6+
7+
// $TEST$ error "An expert parameter must be optional."
8+
// $TEST$ no error "An expert parameter must be optional."
9+
class MyClass(@Expert »a«: Int, @Expert »b«: Int = 3) {
10+
11+
// $TEST$ error "An expert parameter must be optional."
12+
// $TEST$ no error "An expert parameter must be optional."
13+
class MyClass(@Expert »a«: Int, @Expert »b«: Int = 3)
14+
15+
// $TEST$ error "An expert parameter must be optional."
16+
// $TEST$ no error "An expert parameter must be optional."
17+
fun myFunction(@Expert »a«: Int, @Expert »b«: Int = 3)
18+
}
19+
20+
enum MyEnum {
21+
22+
// $TEST$ error "An expert parameter must be optional."
23+
// $TEST$ no error "An expert parameter must be optional."
24+
MyEnumVariant(@Expert »a«: Int, @Expert »b«: Int = 3)
25+
}
26+
27+
// $TEST$ error "An expert parameter must be optional."
28+
// $TEST$ no error "An expert parameter must be optional."
29+
fun myFunction(@Expert »a«: Int, @Expert »b«: Int = 3)
30+
31+
// $TEST$ error "An expert parameter must be optional."
32+
// $TEST$ no error "An expert parameter must be optional."
33+
segment mySegment1(@Expert »a«: Int, @Expert »b«: Int = 3) {}
34+
35+
// $TEST$ no error "An expert parameter must be optional."
36+
// $TEST$ no error "An expert parameter must be optional."
37+
segment mySegment2(
38+
f: (@Expert »a«: Int, @Expert »b«: Int = 3) -> ()
39+
) {
40+
41+
// $TEST$ no error "An expert parameter must be optional."
42+
// $TEST$ no error "An expert parameter must be optional."
43+
val g = (@Expert »a«: Int, @Expert »b«: Int = 3) {};
44+
45+
// $TEST$ no error "An expert parameter must be optional."
46+
// $TEST$ no error "An expert parameter must be optional."
47+
val h = (@Expert »a«: Int, @Expert »b«: Int = 3) -> 1;
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package tests.validation.other.declarations.annotationCalls.mustNotBeUsedOnLambdaParameters
2+
3+
annotation MyAnnotation
4+
5+
pipeline myPipeline {
6+
7+
// $TEST$ error "Lambda parameters must not be annotated."
8+
// $TEST$ error "Lambda parameters must not be annotated."
9+
// $TEST$ error "Lambda parameters must not be annotated."
10+
val f = (»@MyAnnotation« »@MyAnnotation« a: Int, »@MyAnnotation« b: Int = 3) {};
11+
12+
// $TEST$ error "Lambda parameters must not be annotated."
13+
// $TEST$ error "Lambda parameters must not be annotated."
14+
// $TEST$ error "Lambda parameters must not be annotated."
15+
val g = (»@MyAnnotation« »@MyAnnotation« a: Int, »@MyAnnotation« b: Int = 3) -> 1;
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package tests.validation.other.declarations.annotationCalls.mustNotBeUsedOnParametersOfCallableTypes
2+
3+
annotation MyAnnotation
4+
5+
// $TEST$ error "Parameters of callable types must not be annotated."
6+
// $TEST$ error "Parameters of callable types must not be annotated."
7+
// $TEST$ error "Parameters of callable types must not be annotated."
8+
segment mySegment(
9+
f: (»@MyAnnotation« »@MyAnnotation« a: Int, »@MyAnnotation« b: Int = 3) -> ()
10+
) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package tests.validation.other.declarations.annotationCalls.mustNotBeUsedOnResultsOfCallableTypes
2+
3+
annotation MyAnnotation
4+
5+
// $TEST$ error "Results of callable types must not be annotated."
6+
// $TEST$ error "Results of callable types must not be annotated."
7+
// $TEST$ error "Results of callable types must not be annotated."
8+
segment mySegment(
9+
f: () -> (»@MyAnnotation« »@MyAnnotation« a: Int, »@MyAnnotation« b: Int)
10+
) {}

0 commit comments

Comments
 (0)