Skip to content

Commit 8f5d57a

Browse files
feat: port additional validation checks to `Langium (#576)
Closes partially #543 ### Summary of Changes Show an error if: * A union type has no type arguments * A callable type has optional parameters * Positional type arguments occur after named type arguments * Positional arguments occur after named arguments * A parameter is variadic and optional --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent 47ce782 commit 8f5d57a

File tree

22 files changed

+297
-27
lines changed

22 files changed

+297
-27
lines changed

src/language/grammar/safe-ds.langium

+10-10
Original file line numberDiff line numberDiff line change
@@ -1045,16 +1045,16 @@ terminal TEMPLATE_STRING_END returns string: TEMPLATE_EXPRESSION_END STRING_TEXT
10451045
terminal CALL_TYPE_ARGUMENT_LIST_START:
10461046
'<'
10471047
(?=
1048-
/\s*/
1049-
( '*' // Star projection as positional type argument
1050-
| 'in' // Contravariant type projection as positional type argument
1051-
| 'out' // Covariant type projection as positional type argument
1052-
| 'literal' // Invariant literal type as positional type argument
1053-
| 'union' // Invariant union type as positional type argument
1054-
| '>' // Empty type argument list
1055-
| ID /\s*/
1056-
( '=' // Named type argument
1057-
| ('.' /\s*/ ID /\s*/)* (',' | '>') // Invariant type projection as positional type argument
1048+
/[\s»«]*/
1049+
( '*' // Star projection as positional type argument
1050+
| 'in' // Contravariant type projection as positional type argument
1051+
| 'out' // Covariant type projection as positional type argument
1052+
| 'literal' // Invariant literal type as positional type argument
1053+
| 'union' // Invariant union type as positional type argument
1054+
| '>' // Empty type argument list
1055+
| ID /[\s»«]*/
1056+
( '=' // Named type argument
1057+
| ('.' /[\s»«]*/ ID /[\s»«]*/)* (',' | '>') // Invariant type projection as positional type argument
10581058
)
10591059
)
10601060
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SdsArgumentList } from '../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
4+
export const CODE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED = 'argument-list/positional-after-named';
5+
6+
export const argumentListMustNotHavePositionalArgumentsAfterNamedArguments = (
7+
node: SdsArgumentList,
8+
accept: ValidationAcceptor,
9+
): void => {
10+
let foundNamed = false;
11+
for (const argument of node.arguments) {
12+
if (argument.parameter) {
13+
foundNamed = true;
14+
} else if (foundNamed) {
15+
accept('error', 'After the first named argument all arguments must be named.', {
16+
node: argument,
17+
code: CODE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED,
18+
});
19+
}
20+
}
21+
};

src/language/validation/other/declarations/parameterLists.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export const parameterListMustNotHaveOptionalAndVariadicParameters = (
1111
) => {
1212
const hasOptional = node.parameters.find((p) => p.defaultValue);
1313
if (hasOptional) {
14-
const variadicParameters = node.parameters.filter((p) => p.variadic);
14+
const variadicRequiredParameters = node.parameters.filter((p) => p.variadic && !p.defaultValue);
1515

16-
for (const variadic of variadicParameters) {
16+
for (const variadic of variadicRequiredParameters) {
1717
accept('error', 'A callable with optional parameters must not have a variadic parameter.', {
1818
node: variadic,
1919
property: 'name',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SdsParameter } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
4+
export const CODE_PARAMETER_VARIADIC_AND_OPTIONAL = 'parameter/variadic-and-optional';
5+
6+
export const parameterMustNotBeVariadicAndOptional = (node: SdsParameter, accept: ValidationAcceptor) => {
7+
if (node.variadic && node.defaultValue) {
8+
accept('error', 'Variadic parameters must not be optional.', {
9+
node,
10+
property: 'name',
11+
code: CODE_PARAMETER_VARIADIC_AND_OPTIONAL,
12+
});
13+
}
14+
};

src/language/validation/imports.ts src/language/validation/other/imports.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ValidationAcceptor } from 'langium';
2-
import { SdsImportAlias } from '../generated/ast.js';
3-
import { isWildcardImport } from '../ast/checks.js';
2+
import { SdsImportAlias } from '../../generated/ast.js';
3+
import { isWildcardImport } from '../../ast/checks.js';
44

55
export const CODE_IMPORT_WILDCARD_IMPORT_WITH_ALIAS = 'import/wildcard-import-with-alias';
66

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { SdsCallableType } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
import { parametersOrEmpty } from '../../../ast/shortcuts.js';
4+
5+
export const CODE_CALLABLE_TYPE_NO_OPTIONAL_PARAMETERS = 'callable-type/no-optional-parameters';
6+
7+
export const callableTypeMustNotHaveOptionalParameters = (node: SdsCallableType, accept: ValidationAcceptor): void => {
8+
for (const parameter of parametersOrEmpty(node.parameterList)) {
9+
if (parameter.defaultValue && !parameter.variadic) {
10+
accept('error', 'A callable type must not have optional parameters.', {
11+
node: parameter,
12+
property: 'defaultValue',
13+
code: CODE_CALLABLE_TYPE_NO_OPTIONAL_PARAMETERS,
14+
});
15+
}
16+
}
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SdsTypeArgumentList } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
4+
export const CODE_TYPE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED = 'type-argument-list/positional-after-named';
5+
6+
export const typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments = (
7+
node: SdsTypeArgumentList,
8+
accept: ValidationAcceptor,
9+
): void => {
10+
let foundNamed = false;
11+
for (const typeArgument of node.typeArguments) {
12+
if (typeArgument.typeParameter) {
13+
foundNamed = true;
14+
} else if (foundNamed) {
15+
accept('error', 'After the first named type argument all type arguments must be named.', {
16+
node: typeArgument,
17+
code: CODE_TYPE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED,
18+
});
19+
}
20+
}
21+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SdsUnionType } from '../../../generated/ast.js';
2+
import { ValidationAcceptor } from 'langium';
3+
import { typeArgumentsOrEmpty } from '../../../ast/shortcuts.js';
4+
import { isEmpty } from 'radash';
5+
6+
export const CODE_UNION_TYPE_MISSING_TYPE_ARGUMENTS = 'union-type/missing-type-arguments';
7+
8+
export const unionTypeMustHaveTypeArguments = (node: SdsUnionType, accept: ValidationAcceptor): void => {
9+
if (isEmpty(typeArgumentsOrEmpty(node.typeArgumentList))) {
10+
accept('error', 'A union type must have least one type argument.', {
11+
node,
12+
property: 'typeArgumentList',
13+
code: CODE_UNION_TYPE_MISSING_TYPE_ARGUMENTS,
14+
});
15+
}
16+
};

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ import {
3737
parameterListMustNotHaveRequiredParametersAfterOptionalParameters,
3838
parameterListVariadicParameterMustBeLast,
3939
} from './other/declarations/parameterLists.js';
40-
import { importAliasMustNotBeUsedForWildcardImports } from './imports.js';
40+
import { importAliasMustNotBeUsedForWildcardImports } from './other/imports.js';
41+
import { unionTypeMustHaveTypeArguments } from './other/types/unionTypes.js';
42+
import { callableTypeMustNotHaveOptionalParameters } from './other/types/callableTypes.js';
43+
import { typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/types/typeArgumentLists.js';
44+
import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js';
45+
import { parameterMustNotBeVariadicAndOptional } from './other/declarations/parameters.js';
4146

4247
/**
4348
* Register custom validation checks.
@@ -48,9 +53,10 @@ export const registerValidationChecks = function (services: SafeDsServices) {
4853
const checks: ValidationChecks<SafeDsAstType> = {
4954
SdsAssignment: [assignmentShouldHaveMoreThanWildcardsAsAssignees],
5055
SdsAnnotation: [annotationMustContainUniqueNames, annotationParameterListShouldNotBeEmpty],
56+
SdsArgumentList: [argumentListMustNotHavePositionalArgumentsAfterNamedArguments],
5157
SdsAttribute: [attributeMustHaveTypeHint],
5258
SdsBlockLambda: [blockLambdaMustContainUniqueNames],
53-
SdsCallableType: [callableTypeMustContainUniqueNames],
59+
SdsCallableType: [callableTypeMustContainUniqueNames, callableTypeMustNotHaveOptionalParameters],
5460
SdsClass: [classMustContainUniqueNames],
5561
SdsClassBody: [classBodyShouldNotBeEmpty],
5662
SdsConstraintList: [constraintListShouldNotBeEmpty],
@@ -62,7 +68,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
6268
SdsFunction: [functionMustContainUniqueNames, functionResultListShouldNotBeEmpty],
6369
SdsImportAlias: [importAliasMustNotBeUsedForWildcardImports],
6470
SdsModule: [moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage],
65-
SdsParameter: [parameterMustHaveTypeHint],
71+
SdsParameter: [parameterMustHaveTypeHint, parameterMustNotBeVariadicAndOptional],
6672
SdsParameterList: [
6773
parameterListMustNotHaveOptionalAndVariadicParameters,
6874
parameterListMustNotHaveRequiredParametersAfterOptionalParameters,
@@ -72,9 +78,10 @@ export const registerValidationChecks = function (services: SafeDsServices) {
7278
SdsResult: [resultMustHaveTypeHint],
7379
SdsSegment: [segmentMustContainUniqueNames, segmentResultListShouldNotBeEmpty],
7480
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
81+
SdsTypeArgumentList: [typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments],
7582
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
7683
SdsTypeParameterList: [typeParameterListShouldNotBeEmpty],
77-
SdsUnionType: [unionTypeShouldNotHaveASingularTypeArgument],
84+
SdsUnionType: [unionTypeMustHaveTypeArguments, unionTypeShouldNotHaveASingularTypeArgument],
7885
SdsYield: [yieldMustNotBeUsedInPipeline],
7986
};
8087
registry.register(checks, validator);

src/language/validation/value.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// TODO: implement value-related checks here, including:
2+
// - division by zero
3+
// - must be constant

tests/language/validation/testValidation.test.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, describe, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, it } from 'vitest';
22
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
33
import { URI } from 'vscode-uri';
44
import { NodeFileSystem } from 'langium/node';
@@ -11,6 +11,11 @@ import { locationToString } from '../../helpers/location.js';
1111
const services = createSafeDsServices(NodeFileSystem).SafeDs;
1212

1313
describe('validation', async () => {
14+
beforeEach(async () => {
15+
// Load the builtin library
16+
await services.shared.workspace.WorkspaceManager.initializeWorkspace([]);
17+
});
18+
1419
afterEach(async () => {
1520
await clearDocuments(services);
1621
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// $TEST$ no_syntax_error
2+
3+
pipeline myPipeline {
4+
f<»T = Int«>();
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// $TEST$ no_syntax_error
2+
3+
pipeline myPipeline {
4+
f<»Int«>();
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package tests.validation.other.argumentLists.mustNotHavePositionalArgumentAfterNamedArgument
2+
3+
annotation MyAnnotation(a: Int, b: Int = 0, c: Int = 0, d: Int = 0, vararg e: Int)
4+
5+
// $TEST$ no error "After the first named argument all arguments must be named."
6+
// $TEST$ no error "After the first named argument all arguments must be named."
7+
// $TEST$ error "After the first named argument all arguments must be named."
8+
// $TEST$ no error "After the first named argument all arguments must be named."
9+
// $TEST$ error "After the first named argument all arguments must be named."
10+
@MyAnnotation(»0«, »a = 1«, »2«, »b = 3«, »4«)
11+
class MyClass1
12+
13+
// $TEST$ no error "After the first named argument all arguments must be named."
14+
@MyAnnotation(»0«)
15+
class MyClass2
16+
17+
// $TEST$ no error "After the first named argument all arguments must be named."
18+
@MyAnnotation(»a = 0«)
19+
class MyClass3
20+
21+
// $TEST$ no error "After the first named argument all arguments must be named."
22+
// $TEST$ no error "After the first named argument all arguments must be named."
23+
// $TEST$ error "After the first named argument all arguments must be named."
24+
// $TEST$ no error "After the first named argument all arguments must be named."
25+
@UnresolvedAnnotation(»0«, »a = 1«, »2«, »b = 3«)
26+
class MyClass4
27+
28+
// $TEST$ no error "After the first named argument all arguments must be named."
29+
@UnresolvedAnnotation(»0«)
30+
class MyClass5
31+
32+
// $TEST$ no error "After the first named argument all arguments must be named."
33+
@UnresolvedAnnotation(»a = 0«)
34+
class MyClass3
35+
36+
fun f(a: Int, b: Int = 0, c: Int = 0, d: Int = 0, vararg e: Int)
37+
38+
pipeline myPipeline {
39+
// $TEST$ no error "After the first named argument all arguments must be named."
40+
// $TEST$ no error "After the first named argument all arguments must be named."
41+
// $TEST$ error "After the first named argument all arguments must be named."
42+
// $TEST$ no error "After the first named argument all arguments must be named."
43+
// $TEST$ error "After the first named argument all arguments must be named."
44+
f(»0«, »a = 1«, »2«, »b = 3«, »4«);
45+
46+
// $TEST$ no error "After the first named argument all arguments must be named."
47+
f(»0«);
48+
49+
// $TEST$ no error "After the first named argument all arguments must be named."
50+
f(»a = 0«);
51+
52+
// $TEST$ no error "After the first named argument all arguments must be named."
53+
// $TEST$ no error "After the first named argument all arguments must be named."
54+
// $TEST$ error "After the first named argument all arguments must be named."
55+
// $TEST$ no error "After the first named argument all arguments must be named."
56+
unresolvedCallable(»0«, »a = 1«, »2«, »b = 3«);
57+
58+
// $TEST$ no error "After the first named argument all arguments must be named."
59+
unresolvedCallable(»0«);
60+
61+
// $TEST$ no error "After the first named argument all arguments must be named."
62+
unresolvedCallable(»a = 0«);
63+
}

tests/resources/validation/other/declarations/parameter lists/must not mix optional and variadic/main.sdstest

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ annotation MyAnnotation1(»a«: Int, »b«: Int = 1, »c«: Int, »d«: Int = 2,
99

1010
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
1111
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
12-
annotation MyAnnotation2(»a«: Int, vararg »b«: Int)
12+
annotation MyAnnotation2(»a«: Int, vararg »b«: Int = 1)
1313

1414
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
1515
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -26,7 +26,7 @@ class MyClass1(»a«: Int, »b«: Int = 1, »c«: Int, »d«: Int = 2, vararg »
2626

2727
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
2828
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
29-
class MyClass2(»a«: Int, vararg »b«: Int)
29+
class MyClass2(»a«: Int, vararg »b«: Int = 1)
3030

3131
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
3232
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -44,7 +44,7 @@ enum MyEnum {
4444

4545
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
4646
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
47-
MyEnumVariant2(»a«: Int, vararg »b«: Int)
47+
MyEnumVariant2(»a«: Int, vararg »b«: Int = 1)
4848

4949
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
5050
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -62,7 +62,7 @@ fun myFunction1(»a«: Int, »b«: Int = 1, »c«: Int, »d«: Int = 2, vararg
6262

6363
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
6464
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
65-
fun myFunction2(»a«: Int, vararg »b«: Int)
65+
fun myFunction2(»a«: Int, vararg »b«: Int = 1)
6666

6767
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
6868
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -79,7 +79,7 @@ segment mySegment1(»a«: Int, »b«: Int = 1, »c«: Int, »d«: Int = 2, varar
7979

8080
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
8181
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
82-
segment mySegment2(»a«: Int, vararg »b«: Int) {}
82+
segment mySegment2(»a«: Int, vararg »b«: Int = 1) {}
8383

8484
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
8585
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -97,7 +97,7 @@ pipeline myPipeline {
9797

9898
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
9999
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
100-
(»a«: Int, vararg »b«: Int) {};
100+
(»a«: Int, vararg »b«: Int = 1) {};
101101

102102
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
103103
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -114,7 +114,7 @@ pipeline myPipeline {
114114

115115
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
116116
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
117-
(»a«: Int, vararg »b«: Int) -> 1;
117+
(»a«: Int, vararg »b«: Int = 1) -> 1;
118118

119119
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
120120
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
@@ -133,7 +133,7 @@ fun myFunction4(
133133

134134
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
135135
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
136-
p2: (»a«: Int, vararg »b«: Int) -> (),
136+
p2: (»a«: Int, vararg »b«: Int = 1) -> (),
137137

138138
// $TEST$ error "A callable with optional parameters must not have a variadic parameter."
139139
// $TEST$ no error "A callable with optional parameters must not have a variadic parameter."
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package tests.validation.declarations.parameters.variadicMustNotBeOptional
2+
3+
fun f(
4+
// $TEST$ no error "Variadic parameters must not be optional."
5+
»a«: Int,
6+
// $TEST$ no error "Variadic parameters must not be optional."
7+
»b«: Int = 1,
8+
// $TEST$ no error "Variadic parameters must not be optional."
9+
vararg »c«: Int,
10+
// $TEST$ error "Variadic parameters must not be optional."
11+
vararg »d«: Int = 2
12+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package tests.validation.types.callableTypes.mustNotHaveOptionalParameters
2+
3+
fun f(
4+
g: (
5+
// $TEST$ error "A callable type must not have optional parameters."
6+
p: Int = »1«,
7+
) -> ()
8+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package tests.validation.types.callableTypes.mustNotHaveOptionalParameters
2+
3+
fun f(
4+
g: (
5+
// $TEST$ no error "A callable type must not have optional parameters."
6+
p: Int,
7+
) -> ()
8+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package tests.validation.types.callableTypes.mustNotHaveOptionalParameters
2+
3+
fun f(
4+
g: (
5+
// $TEST$ no error "A callable type must not have optional parameters."
6+
vararg p: Int = 1,
7+
) -> ()
8+
)

0 commit comments

Comments
 (0)