Skip to content

Commit b72768c

Browse files
feat: validation for type arguments of named types (#632)
Closes partially #543 ### Summary of Changes Show an error if * the entire type argument list is missing * a type argument is missing * a type parameter is set multiple times * too many type arguments are given. --------- Co-authored-by: megalinter-bot <[email protected]>
1 parent 17a5b7a commit b72768c

File tree

11 files changed

+334
-26
lines changed

11 files changed

+334
-26
lines changed

src/language/helpers/stringUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Based on the given count, returns the singular or plural form of the given word.
3+
*/
4+
export const pluralize = (count: number, singular: string, plural: string = `${singular}s`): string => {
5+
return count === 1 ? singular : plural;
6+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { SdsNamedType } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
import { SafeDsServices } from '../../../safe-ds-module.js';
4+
import { typeArgumentsOrEmpty, typeParametersOrEmpty } from '../../../helpers/nodeProperties.js';
5+
import { duplicatesBy } from '../../../helpers/collectionUtils.js';
6+
import { pluralize } from '../../../helpers/stringUtils.js';
7+
8+
export const CODE_NAMED_TYPE_DUPLICATE_TYPE_PARAMETER = 'named-type/duplicate-type-parameter';
9+
export const CODE_NAMED_TYPE_POSITIONAL_AFTER_NAMED = 'named-type/positional-after-named';
10+
export const CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS = 'named-type/too-many-type-arguments';
11+
12+
export const namedTypeMustNotSetTypeParameterMultipleTimes = (services: SafeDsServices) => {
13+
const nodeMapper = services.helpers.NodeMapper;
14+
const typeArgumentToTypeParameterOrUndefined = nodeMapper.typeArgumentToTypeParameterOrUndefined.bind(nodeMapper);
15+
16+
return (node: SdsNamedType, accept: ValidationAcceptor): void => {
17+
const typeArguments = typeArgumentsOrEmpty(node.typeArgumentList);
18+
const duplicates = duplicatesBy(typeArguments, typeArgumentToTypeParameterOrUndefined);
19+
20+
for (const duplicate of duplicates) {
21+
const correspondingTypeParameter = typeArgumentToTypeParameterOrUndefined(duplicate)!;
22+
accept('error', `The type parameter '${correspondingTypeParameter.name}' is already set.`, {
23+
node: duplicate,
24+
code: CODE_NAMED_TYPE_DUPLICATE_TYPE_PARAMETER,
25+
});
26+
}
27+
};
28+
};
29+
30+
export const namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments = (
31+
node: SdsNamedType,
32+
accept: ValidationAcceptor,
33+
): void => {
34+
const typeArgumentList = node.typeArgumentList;
35+
if (!typeArgumentList) {
36+
return;
37+
}
38+
39+
let foundNamed = false;
40+
for (const typeArgument of typeArgumentList.typeArguments) {
41+
if (typeArgument.typeParameter) {
42+
foundNamed = true;
43+
} else if (foundNamed) {
44+
accept('error', 'After the first named type argument all type arguments must be named.', {
45+
node: typeArgument,
46+
code: CODE_NAMED_TYPE_POSITIONAL_AFTER_NAMED,
47+
});
48+
}
49+
}
50+
};
51+
52+
export const namedTypeMustNotHaveTooManyTypeArguments = (node: SdsNamedType, accept: ValidationAcceptor): void => {
53+
// If the declaration is unresolved, we already show another error
54+
if (!node.declaration.ref) {
55+
return;
56+
}
57+
58+
const typeParameters = typeParametersOrEmpty(node.declaration.ref);
59+
const typeArguments = typeArgumentsOrEmpty(node.typeArgumentList);
60+
61+
if (typeArguments.length > typeParameters.length) {
62+
const kind = pluralize(typeParameters.length, 'type argument');
63+
accept('error', `Expected ${typeParameters.length} ${kind} but got ${typeArguments.length}.`, {
64+
node,
65+
property: 'typeArgumentList',
66+
code: CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS,
67+
});
68+
}
69+
};

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

Lines changed: 0 additions & 21 deletions
This file was deleted.

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ import {
3434
} from './style.js';
3535
import { templateStringMustHaveExpressionBetweenTwoStringParts } from './other/expressions/templateStrings.js';
3636
import { assignmentAssigneeMustGetValue, yieldMustNotBeUsedInPipeline } from './other/statements/assignments.js';
37-
import { attributeMustHaveTypeHint, parameterMustHaveTypeHint, resultMustHaveTypeHint } from './types.js';
37+
import {
38+
attributeMustHaveTypeHint,
39+
namedTypeMustSetAllTypeParameters,
40+
parameterMustHaveTypeHint,
41+
resultMustHaveTypeHint,
42+
} from './types.js';
3843
import { moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage } from './other/modules.js';
3944
import { typeParameterConstraintLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterConstraints.js';
4045
import { parameterListMustNotHaveRequiredParametersAfterOptionalParameters } from './other/declarations/parameterLists.js';
@@ -43,7 +48,6 @@ import {
4348
callableTypeMustNotHaveOptionalParameters,
4449
callableTypeParameterMustNotHaveConstModifier,
4550
} from './other/types/callableTypes.js';
46-
import { typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/types/typeArgumentLists.js';
4751
import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js';
4852
import {
4953
referenceMustNotBeFunctionPointer,
@@ -77,6 +81,11 @@ import {
7781
import { memberAccessMustBeNullSafeIfReceiverIsNullable } from './other/expressions/memberAccesses.js';
7882
import { importPackageMustExist, importPackageShouldNotBeEmpty } from './other/imports.js';
7983
import { singleUseAnnotationsMustNotBeRepeated } from './builtins/repeatable.js';
84+
import {
85+
namedTypeMustNotHaveTooManyTypeArguments,
86+
namedTypeMustNotSetTypeParameterMultipleTimes,
87+
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
88+
} from './other/types/namedTypes.js';
8089

8190
/**
8291
* Register custom validation checks.
@@ -135,7 +144,11 @@ export const registerValidationChecks = function (services: SafeDsServices) {
135144
SdsNamedType: [
136145
namedTypeDeclarationShouldNotBeDeprecated(services),
137146
namedTypeDeclarationShouldNotBeExperimental(services),
147+
namedTypeMustNotHaveTooManyTypeArguments,
148+
namedTypeMustNotSetTypeParameterMultipleTimes(services),
149+
namedTypeMustSetAllTypeParameters(services),
138150
namedTypeTypeArgumentListShouldBeNeeded,
151+
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
139152
],
140153
SdsParameter: [
141154
parameterMustHaveTypeHint,
@@ -159,7 +172,6 @@ export const registerValidationChecks = function (services: SafeDsServices) {
159172
segmentResultListShouldNotBeEmpty,
160173
],
161174
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
162-
SdsTypeArgumentList: [typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments],
163175
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
164176
SdsTypeParameterList: [typeParameterListShouldNotBeEmpty],
165177
SdsUnionType: [unionTypeMustHaveTypeArguments, unionTypeShouldNotHaveASingularTypeArgument],

src/language/validation/types.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
11
import { getContainerOfType, ValidationAcceptor } from 'langium';
2-
import { isSdsCallable, isSdsLambda, SdsAttribute, SdsParameter, SdsResult } from '../generated/ast.js';
2+
import { isSdsCallable, isSdsLambda, SdsAttribute, SdsNamedType, SdsParameter, SdsResult } from '../generated/ast.js';
3+
import { typeArgumentsOrEmpty, typeParametersOrEmpty } from '../helpers/nodeProperties.js';
4+
import { isEmpty } from 'radash';
5+
import { SafeDsServices } from '../safe-ds-module.js';
6+
import { pluralize } from '../helpers/stringUtils.js';
37

8+
export const CODE_TYPE_MISSING_TYPE_ARGUMENTS = 'type/missing-type-arguments';
49
export const CODE_TYPE_MISSING_TYPE_HINT = 'type/missing-type-hint';
510

11+
// -----------------------------------------------------------------------------
12+
// Missing type arguments
13+
// -----------------------------------------------------------------------------
14+
15+
export const namedTypeMustSetAllTypeParameters =
16+
(services: SafeDsServices) =>
17+
(node: SdsNamedType, accept: ValidationAcceptor): void => {
18+
const expectedTypeParameters = typeParametersOrEmpty(node.declaration.ref);
19+
if (isEmpty(expectedTypeParameters)) {
20+
return;
21+
}
22+
23+
if (node.typeArgumentList) {
24+
const actualTypeParameters = typeArgumentsOrEmpty(node.typeArgumentList).map((it) =>
25+
services.helpers.NodeMapper.typeArgumentToTypeParameterOrUndefined(it),
26+
);
27+
28+
const missingTypeParameters = expectedTypeParameters.filter((it) => !actualTypeParameters.includes(it));
29+
if (!isEmpty(missingTypeParameters)) {
30+
const kind = pluralize(missingTypeParameters.length, 'type parameter');
31+
const missingTypeParametersString = missingTypeParameters.map((it) => `'${it.name}'`).join(', ');
32+
33+
accept('error', `The ${kind} ${missingTypeParametersString} must be set here.`, {
34+
node,
35+
property: 'typeArgumentList',
36+
code: CODE_TYPE_MISSING_TYPE_ARGUMENTS,
37+
});
38+
}
39+
} else {
40+
accept(
41+
'error',
42+
`The type '${node.declaration.$refText}' is parameterized, so a type argument list must be added.`,
43+
{
44+
node,
45+
code: CODE_TYPE_MISSING_TYPE_ARGUMENTS,
46+
},
47+
);
48+
}
49+
};
50+
651
// -----------------------------------------------------------------------------
752
// Missing type hints
853
// -----------------------------------------------------------------------------
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { pluralize } from '../../../src/language/helpers/stringUtils.js';
3+
4+
describe('pluralize', () => {
5+
it.each([
6+
{
7+
count: 0,
8+
singular: 'apple',
9+
plural: 'apples',
10+
expected: 'apples',
11+
},
12+
{
13+
count: 1,
14+
singular: 'apple',
15+
plural: 'apple',
16+
expected: 'apple',
17+
},
18+
{
19+
count: 2,
20+
singular: 'apple',
21+
plural: 'apples',
22+
expected: 'apples',
23+
},
24+
{
25+
count: 0,
26+
singular: 'apple',
27+
expected: 'apples',
28+
},
29+
{
30+
count: 1,
31+
singular: 'apple',
32+
expected: 'apple',
33+
},
34+
{
35+
count: 2,
36+
singular: 'apple',
37+
expected: 'apples',
38+
},
39+
])('should return the singular or plural form based on the count', ({ count, singular, plural, expected }) => {
40+
expect(pluralize(count, singular, plural)).toBe(expected);
41+
});
42+
});

tests/resources/validation/other/statements/assignments/nothing assigned/main.sdstest

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ segment mySegment() -> (
5858
// $TEST$ error "No value is assigned to this assignee."
5959
»val k«, »val l« = unresolved();
6060

61-
6261
// $TEST$ error "No value is assigned to this assignee."
6362
»yield r1« = noResults();
6463
// $TEST$ no error "No value is assigned to this assignee."
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package tests.validation.other.types.typeArgumentLists.duplicateTypeParameters
2+
3+
class MyClass<A, B>
4+
5+
fun myFunction(
6+
f: MyClass<
7+
// $TEST$ no error r"The type parameter '\w+' is already set\."
8+
»Int«,
9+
// $TEST$ error "The type parameter 'A' is already set."
10+
»A = Int«
11+
>,
12+
g: MyClass<
13+
// $TEST$ no error r"The type parameter '\w+' is already set\."
14+
»B = Int«,
15+
// $TEST$ error "The type parameter 'B' is already set."
16+
»B = Int«
17+
>,
18+
h: MyClass<
19+
// $TEST$ no error r"The type parameter '\w+' is already set\."
20+
»A = Int«,
21+
// $TEST$ no error r"The type parameter '\w+' is already set\."
22+
»B = Int«
23+
>,
24+
i: MyClass<
25+
// $TEST$ no error r"The type parameter '\w+' is already set\."
26+
»Int«,
27+
// $TEST$ no error r"The type parameter '\w+' is already set\."
28+
»Int«
29+
>,
30+
j: MyClass<
31+
// $TEST$ no error r"The type parameter '\w+' is already set\."
32+
»Unresolved = Int«,
33+
// $TEST$ no error r"The type parameter '\w+' is already set\."
34+
»Unresolved = Int«
35+
>
36+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package tests.validation.other.types.typeArgumentLists.tooManyTypeArguments
2+
3+
class MyClass1<T>
4+
class MyClass2<A, B>
5+
6+
fun myFunction(
7+
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
8+
f: MyClass1»<>«,
9+
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
10+
g: MyClass1»<Int>«,
11+
// $TEST$ error "Expected 1 type argument but got 2."
12+
h: MyClass1»<Int, Int>«,
13+
// $TEST$ error "Expected 2 type arguments but got 3."
14+
i: MyClass2»<Int, Int, Int>«,
15+
// $TEST$ no error r"Expected \d* type arguments? but got \d*\."
16+
j: Unresolved»<Int, Int>«
17+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package tests.validation.types.namedTypes.missingTypeArgumentList
2+
3+
class MyClassWithoutTypeParameters
4+
class MyClassWithTypeParameters<T>
5+
6+
enum MyEnum {
7+
MyEnumVariantWithoutTypeParameters
8+
MyEnumVariantWithTypeParameters<T>
9+
}
10+
11+
fun myFunction(
12+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
13+
a1: »MyClassWithoutTypeParameters«,
14+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
15+
b1: »MyClassWithoutTypeParameters«<>,
16+
17+
// $TEST$ error "The type 'MyClassWithTypeParameters' is parameterized, so a type argument list must be added."
18+
c1: »MyClassWithTypeParameters«,
19+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
20+
d1: »MyClassWithTypeParameters«<>,
21+
22+
23+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
24+
a2: MyEnum.»MyEnumVariantWithoutTypeParameters«,
25+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
26+
b2: MyEnum.»MyEnumVariantWithoutTypeParameters«<>,
27+
28+
// $TEST$ error "The type 'MyEnumVariantWithTypeParameters' is parameterized, so a type argument list must be added."
29+
c2: MyEnum.»MyEnumVariantWithTypeParameters«,
30+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
31+
d2: MyEnum.»MyEnumVariantWithTypeParameters«<>,
32+
33+
34+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
35+
e: »UnresolvedClass«,
36+
// $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\."
37+
f: »UnresolvedClass«<>,
38+
)

0 commit comments

Comments
 (0)