Skip to content

Commit e846b59

Browse files
authored
feat: check context of union types (#677)
Closes #675 ### Summary of Changes Union types are now only allowed as parameters of annotations, classes, and functions. This way, no values with a union type appear inside pipelines or segments.
1 parent 4656c25 commit e846b59

File tree

12 files changed

+181
-22
lines changed

12 files changed

+181
-22
lines changed

src/language/validation/experimentalLanguageFeatures.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import { SdsIndexedAccess, SdsLiteralType, SdsMap, SdsUnionType } from '../generated/ast.js';
2-
import { ValidationAcceptor } from 'langium';
1+
import {
2+
isSdsIndexedAccess,
3+
isSdsMap,
4+
isSdsUnionType,
5+
SdsIndexedAccess,
6+
SdsLiteralType,
7+
SdsMap,
8+
SdsUnionType,
9+
} from '../generated/ast.js';
10+
import { hasContainerOfType, ValidationAcceptor } from 'langium';
311

412
export const CODE_EXPERIMENTAL_LANGUAGE_FEATURE = 'experimental/language-feature';
513

614
export const indexedAccessesShouldBeUsedWithCaution = (node: SdsIndexedAccess, accept: ValidationAcceptor): void => {
15+
if (hasContainerOfType(node.$container, isSdsIndexedAccess)) {
16+
return;
17+
}
18+
719
accept('warning', 'Indexed accesses are experimental and may change without prior notice.', {
820
node,
921
code: CODE_EXPERIMENTAL_LANGUAGE_FEATURE,
@@ -18,13 +30,21 @@ export const literalTypesShouldBeUsedWithCaution = (node: SdsLiteralType, accept
1830
};
1931

2032
export const mapsShouldBeUsedWithCaution = (node: SdsMap, accept: ValidationAcceptor): void => {
33+
if (hasContainerOfType(node.$container, isSdsMap)) {
34+
return;
35+
}
36+
2137
accept('warning', 'Map literals are experimental and may change without prior notice.', {
2238
node,
2339
code: CODE_EXPERIMENTAL_LANGUAGE_FEATURE,
2440
});
2541
};
2642

2743
export const unionTypesShouldBeUsedWithCaution = (node: SdsUnionType, accept: ValidationAcceptor): void => {
44+
if (hasContainerOfType(node.$container, isSdsUnionType)) {
45+
return;
46+
}
47+
2848
accept('warning', 'Union types are experimental and may change without prior notice.', {
2949
node,
3050
code: CODE_EXPERIMENTAL_LANGUAGE_FEATURE,

src/language/validation/names.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ import {
1717
} from '../generated/ast.js';
1818
import { getDocument, ValidationAcceptor } from 'langium';
1919
import {
20-
streamBlockLambdaResults,
21-
getMatchingClassMembers,
2220
getColumns,
2321
getEnumVariants,
2422
getImportedDeclarations,
2523
getImports,
26-
isStatic,
24+
getMatchingClassMembers,
2725
getModuleMembers,
2826
getPackageName,
2927
getParameters,
30-
streamPlaceholders,
3128
getResults,
3229
getTypeParameters,
30+
isStatic,
31+
streamBlockLambdaResults,
32+
streamPlaceholders,
3333
} from '../helpers/nodeProperties.js';
3434
import { duplicatesBy } from '../../helpers/collectionUtils.js';
3535
import { isInPipelineFile, isInStubFile, isInTestFile } from '../helpers/fileExtensions.js';
@@ -217,16 +217,20 @@ export const moduleMemberMustHaveNameThatIsUniqueInPackage = (services: SafeDsSe
217217
const builtinUris = new Set(listBuiltinFiles().map((it) => it.toString()));
218218

219219
return (node: SdsModule, accept: ValidationAcceptor): void => {
220+
const moduleUri = getDocument(node).uri?.toString();
221+
if (builtinUris.has(moduleUri)) {
222+
return;
223+
}
224+
220225
for (const member of getModuleMembers(node)) {
221226
const packageName = getPackageName(member) ?? '';
222227
const declarationsInPackage = packageManager.getDeclarationsInPackage(packageName);
223-
const memberUri = getDocument(member).uri?.toString();
224228

225229
if (
226230
declarationsInPackage.some(
227231
(it) =>
228232
it.name === member.name &&
229-
it.documentUri.toString() !== memberUri &&
233+
it.documentUri.toString() !== moduleUri &&
230234
!builtinUris.has(it.documentUri.toString()),
231235
)
232236
) {

src/language/validation/other/types/unionTypes.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1-
import { SdsUnionType } from '../../../generated/ast.js';
2-
import { ValidationAcceptor } from 'langium';
1+
import {
2+
isSdsAnnotation,
3+
isSdsCallable,
4+
isSdsClass,
5+
isSdsFunction,
6+
isSdsParameter,
7+
isSdsUnionType,
8+
SdsUnionType,
9+
} from '../../../generated/ast.js';
10+
import { getContainerOfType, hasContainerOfType, ValidationAcceptor } from 'langium';
311
import { getTypeArguments } from '../../../helpers/nodeProperties.js';
412
import { SafeDsServices } from '../../../safe-ds-module.js';
513
import { Type } from '../../../typing/model.js';
614
import { isEmpty } from '../../../../helpers/collectionUtils.js';
715

16+
export const CODE_UNION_TYPE_CONTEXT = 'union-type/context';
817
export const CODE_UNION_TYPE_DUPLICATE_TYPE = 'union-type/duplicate-type';
918
export const CODE_UNION_TYPE_MISSING_TYPES = 'union-type/missing-types';
1019

20+
export const unionTypeMustBeUsedInCorrectContext = (node: SdsUnionType, accept: ValidationAcceptor): void => {
21+
if (!contextIsCorrect(node)) {
22+
accept('error', 'Union types must only be used for parameters of annotations, classes, and functions.', {
23+
node,
24+
code: CODE_UNION_TYPE_CONTEXT,
25+
});
26+
}
27+
};
28+
29+
const contextIsCorrect = (node: SdsUnionType): boolean => {
30+
if (hasContainerOfType(node.$container, isSdsUnionType)) {
31+
return true;
32+
}
33+
34+
const container = node.$container;
35+
if (!isSdsParameter(container)) {
36+
return false;
37+
}
38+
39+
const containingCallable = getContainerOfType(container, isSdsCallable);
40+
return (
41+
!containingCallable ||
42+
isSdsAnnotation(containingCallable) ||
43+
isSdsClass(containingCallable) ||
44+
isSdsFunction(containingCallable)
45+
);
46+
};
47+
1148
export const unionTypeMustHaveTypes = (node: SdsUnionType, accept: ValidationAcceptor): void => {
1249
if (isEmpty(getTypeArguments(node.typeArgumentList))) {
1350
accept('error', 'A union type must have at least one type.', {

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ import {
5757
} from './other/modules.js';
5858
import { typeParameterConstraintLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterConstraints.js';
5959
import { parameterListMustNotHaveRequiredParametersAfterOptionalParameters } from './other/declarations/parameterLists.js';
60-
import { unionTypeMustHaveTypes, unionTypeShouldNotHaveDuplicateTypes } from './other/types/unionTypes.js';
60+
import {
61+
unionTypeMustBeUsedInCorrectContext,
62+
unionTypeMustHaveTypes,
63+
unionTypeShouldNotHaveDuplicateTypes,
64+
} from './other/types/unionTypes.js';
6165
import {
6266
callableTypeMustNotHaveOptionalParameters,
6367
callableTypeParameterMustNotHaveConstModifier,
@@ -273,6 +277,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
273277
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
274278
SdsTypeParameterList: [typeParameterListShouldNotBeEmpty],
275279
SdsUnionType: [
280+
unionTypeMustBeUsedInCorrectContext,
276281
unionTypeMustHaveTypes,
277282
unionTypesShouldBeUsedWithCaution,
278283
unionTypeShouldNotHaveDuplicateTypes(services),

tests/resources/validation/experimental language feature/indexed access/main.sdstest

+6
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ pipeline myPipeline {
66

77
// $TEST$ warning "Indexed accesses are experimental and may change without prior notice."
88
»{"a": "b"}["a"]«;
9+
10+
// $TEST$ no warning "Indexed accesses are experimental and may change without prior notice."
11+
[1, 2][»[1, 2][1]«];
12+
13+
// $TEST$ no warning "Indexed accesses are experimental and may change without prior notice."
14+
{"a": "b"}[»{"a": "b"}["a"]«];
915
}

tests/resources/validation/experimental language feature/maps/main.sdstest

+3
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ package tests.validation.experimentalLanguageFeature.maps
33
pipeline myPipeline {
44
// $TEST$ warning "Map literals are experimental and may change without prior notice."
55
»{"a": "b"}«;
6+
7+
// $TEST$ no warning "Map literals are experimental and may change without prior notice."
8+
{"a": »{}«};
69
}

tests/resources/validation/experimental language feature/union types/main.sdstest

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@ package tests.validation.experimentalLanguageFeature.unionTypes
22

33
fun myFunction(
44
// $TEST$ warning "Union types are experimental and may change without prior notice."
5-
p: »union<>«
5+
p: »union<Int, Float>«,
6+
7+
// $TEST$ no warning "Union types are experimental and may change without prior notice."
8+
q: union<»union<Int, Float>«, Int>,
9+
10+
// $TEST$ no warning "Union types are experimental and may change without prior notice."
11+
// $TEST$ no warning "Union types are experimental and may change without prior notice."
12+
r: union<(p: »union<Int, Float>«) -> (r: »union<Int, Float>«), Int>,
613
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package tests.validation.other.types.unionTypes.context
2+
3+
annotation MyAnnotation(
4+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
5+
p: »union<Int>«,
6+
)
7+
8+
class MyClass<T>(
9+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
10+
p: »union<Int>«,
11+
) {
12+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
13+
attr a: »union<Int>«
14+
}
15+
16+
enum MyEnum {
17+
MyEnumVariant<T>(
18+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
19+
p: »union<Int>«,
20+
)
21+
}
22+
23+
fun myFunction(
24+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
25+
p: »union<Int>«,
26+
) -> (
27+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
28+
r: »union<Int>«,
29+
)
30+
31+
segment mySegment1(
32+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
33+
p: »union<Int>«,
34+
) -> (
35+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
36+
r: »union<Int>«,
37+
) {}
38+
39+
segment mySegment2(
40+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
41+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
42+
c: (p: »union<Int>«) -> (r: »union<Int>«),
43+
) {
44+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
45+
(
46+
p: »union<Int>«,
47+
) {};
48+
49+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
50+
(
51+
p: »union<Int>«,
52+
) -> 1;
53+
}
54+
55+
segment mySegment3(
56+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
57+
p1: MyClass<»union<Int>«>,
58+
59+
// $TEST$ error "Union types must only be used for parameters of annotations, classes, and functions."
60+
p2: MyEnum.MyEnumVariant<»union<Int>«>,
61+
) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package tests.validation.other.types.unionTypes.context
2+
3+
/*
4+
* We already show an error for the outer union type, if it's used in the wrong context.
5+
*/
6+
7+
class MyClass1 {
8+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
9+
attr a: union<Int, »union<Int>«>
10+
}
11+
12+
class MyClass2 {
13+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
14+
// $TEST$ no error "Union types must only be used for parameters of annotations, classes, and functions."
15+
attr a: union<Int, (p: »union<Int>«) -> (r: »union<Int>«)>
16+
}

tests/resources/validation/other/types/union types/duplicate types/empty list.sdstest

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ package tests.validation.other.types.unionTypes.duplicateTypes
22

33
// $TEST$ no warning r"The type .* was already listed."
44

5-
segment mySegment1(
5+
fun myFunction1(
66
p: union<>
7-
) {}
7+
)

tests/resources/validation/other/types/union types/duplicate types/main.sdstest

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package tests.validation.other.types.unionTypes.duplicateTypes
22

3-
segment mySegment(
3+
fun myFunction2(
44
// $TEST$ no warning r"The type .* was already listed."
55
p: union<»Int«>,
66
q: union<
@@ -11,4 +11,4 @@ segment mySegment(
1111
// $TEST$ warning r"The type 'Int' was already listed."
1212
»Int«,
1313
>,
14-
) {}
14+
)
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package tests.validation.other.types.unionTypes.mustHaveTypes
22

33
// $TEST$ error "A union type must have at least one type."
4-
segment mySegment1(
4+
fun myFunction1(
55
p: union»<>«
6-
) {}
6+
)
77

88
// $TEST$ no error "A union type must have at least one type."
9-
segment mySegment2(
9+
fun myFunction2(
1010
p: union»<Int>«
11-
) {}
11+
)
1212

1313
// $TEST$ no error "A union type must have at least one type."
14-
segment mySegment3(
14+
fun myFunction3(
1515
p: union»<Int, Float>«
16-
) {}
16+
)

0 commit comments

Comments
 (0)