Skip to content

Commit d76e597

Browse files
feat: various checks for argument lists (#648)
Closes partially #543 ### Summary of Changes Show an error if an argument list of an annotation call or a call * does not set required parameters, * sets a parameter multiple times, * passes too many arguments. --------- Co-authored-by: megalinter-bot <[email protected]>
1 parent 2d2ccc6 commit d76e597

File tree

12 files changed

+770
-43
lines changed

12 files changed

+770
-43
lines changed

esbuild.mjs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//@ts-check
22
import * as esbuild from 'esbuild';
33
import { copy } from 'esbuild-plugin-copy';
4+
import fs from 'fs/promises';
45

56
const watch = process.argv.includes('--watch');
67
const minify = process.argv.includes('--minify');
@@ -10,20 +11,18 @@ const success = watch ? 'Watch build succeeded' : 'Build succeeded';
1011
const getTime = function () {
1112
const date = new Date();
1213
return `[${`${padZeroes(date.getHours())}:${padZeroes(date.getMinutes())}:${padZeroes(date.getSeconds())}`}] `;
13-
}
14+
};
1415

1516
const padZeroes = function (i) {
1617
return i.toString().padStart(2, '0');
17-
}
18+
};
1819

1920
const plugins = [
2021
{
21-
name: 'watch-plugin',
22+
name: 'clean-old-builtins',
2223
setup(build) {
23-
build.onEnd(result => {
24-
if (result.errors.length === 0) {
25-
console.log(getTime() + success);
26-
}
24+
build.onStart(async () => {
25+
await fs.rm('./out/resources', { force: true, recursive: true });
2726
});
2827
},
2928
},
@@ -34,26 +33,36 @@ const plugins = [
3433
},
3534
watch,
3635
}),
36+
{
37+
name: 'watch-plugin',
38+
setup(build) {
39+
build.onEnd((result) => {
40+
if (result.errors.length === 0) {
41+
console.log(getTime() + success);
42+
}
43+
});
44+
},
45+
},
3746
];
3847

3948
const ctx = await esbuild.context({
4049
// Entry points for the vscode extension and the language server
4150
entryPoints: ['src/cli/main.ts', 'src/extension/main.ts', 'src/language/main.ts'],
4251
outdir: 'out',
4352
bundle: true,
44-
target: "ES2017",
53+
target: 'ES2017',
4554
// VSCode's extension host is still using cjs, so we need to transform the code
4655
format: 'cjs',
4756
// To prevent confusing node, we explicitly use the `.cjs` extension
4857
outExtension: {
49-
'.js': '.cjs'
58+
'.js': '.cjs',
5059
},
51-
loader: {'.ts': 'ts'},
60+
loader: { '.ts': 'ts' },
5261
external: ['vscode'],
5362
platform: 'node',
5463
sourcemap: !minify,
5564
minify,
56-
plugins
65+
plugins,
5766
});
5867

5968
if (watch) {

src/language/helpers/nodeProperties.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
isSdsAnnotation,
3+
isSdsArgumentList,
34
isSdsAssignment,
45
isSdsAttribute,
56
isSdsBlockLambda,
@@ -23,6 +24,7 @@ import {
2324
SdsAnnotation,
2425
SdsAnnotationCall,
2526
SdsArgument,
27+
SdsArgumentList,
2628
SdsAssignee,
2729
SdsAssignment,
2830
SdsBlock,
@@ -161,12 +163,18 @@ export const findFirstAnnotationCallOf = (
161163
});
162164
};
163165

164-
export const argumentsOrEmpty = (node: SdsAbstractCall | undefined): SdsArgument[] => {
165-
return node?.argumentList?.arguments ?? [];
166+
export const argumentsOrEmpty = (node: SdsArgumentList | SdsAbstractCall | undefined): SdsArgument[] => {
167+
if (isSdsArgumentList(node)) {
168+
return node.arguments;
169+
} else {
170+
return node?.argumentList?.arguments ?? [];
171+
}
166172
};
173+
167174
export const assigneesOrEmpty = (node: SdsAssignment | undefined): SdsAssignee[] => {
168175
return node?.assigneeList?.assignees ?? [];
169176
};
177+
170178
export const blockLambdaResultsOrEmpty = (node: SdsBlockLambda | undefined): SdsBlockLambdaResult[] => {
171179
return stream(statementsOrEmpty(node?.body))
172180
.filter(isSdsAssignment)

src/language/validation/builtins/repeatable.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const singleUseAnnotationsMustNotBeRepeated =
1616
for (const duplicate of duplicatesBy(callsOfSingleUseAnnotations, (it) => it.annotation?.ref)) {
1717
accept('error', `The annotation '${duplicate.annotation.$refText}' is not repeatable.`, {
1818
node: duplicate,
19+
property: 'annotation',
1920
code: CODE_ANNOTATION_NOT_REPEATABLE,
2021
});
2122
}

src/language/validation/other/argumentLists.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { SdsArgumentList } from '../../generated/ast.js';
2-
import { ValidationAcceptor } from 'langium';
1+
import { isSdsAnnotation, isSdsCall, SdsAbstractCall, SdsArgumentList } from '../../generated/ast.js';
2+
import { getContainerOfType, ValidationAcceptor } from 'langium';
3+
import { SafeDsServices } from '../../safe-ds-module.js';
4+
import { argumentsOrEmpty, isRequiredParameter, parametersOrEmpty } from '../../helpers/nodeProperties.js';
5+
import { duplicatesBy } from '../../helpers/collectionUtils.js';
6+
import { isEmpty } from 'radash';
7+
import { pluralize } from '../../helpers/stringUtils.js';
38

9+
export const CODE_ARGUMENT_LIST_DUPLICATE_PARAMETER = 'argument-list/duplicate-parameter';
10+
export const CODE_ARGUMENT_LIST_MISSING_REQUIRED_PARAMETER = 'argument-list/missing-required-parameter';
411
export const CODE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED = 'argument-list/positional-after-named';
12+
export const CODE_ARGUMENT_LIST_TOO_MANY_ARGUMENTS = 'argument-list/too-many-arguments';
513

614
export const argumentListMustNotHavePositionalArgumentsAfterNamedArguments = (
715
node: SdsArgumentList,
@@ -19,3 +27,107 @@ export const argumentListMustNotHavePositionalArgumentsAfterNamedArguments = (
1927
}
2028
}
2129
};
30+
31+
export const argumentListMustNotHaveTooManyArguments = (services: SafeDsServices) => {
32+
const nodeMapper = services.helpers.NodeMapper;
33+
34+
return (node: SdsAbstractCall, accept: ValidationAcceptor): void => {
35+
const actualArgumentCount = argumentsOrEmpty(node).length;
36+
37+
// We can never have too many arguments in this case
38+
if (actualArgumentCount === 0) {
39+
return;
40+
}
41+
42+
// We already report other errors in those cases
43+
const callable = nodeMapper.callToCallableOrUndefined(node);
44+
if (!callable || (isSdsCall(node) && isSdsAnnotation(callable))) {
45+
return;
46+
}
47+
48+
const parameters = parametersOrEmpty(callable);
49+
const maxArgumentCount = parameters.length;
50+
51+
// All is good
52+
if (actualArgumentCount <= maxArgumentCount) {
53+
return;
54+
}
55+
56+
const minArgumentCount = parameters.filter((it) => isRequiredParameter(it)).length;
57+
const kind = pluralize(Math.max(minArgumentCount, maxArgumentCount), 'argument');
58+
if (minArgumentCount === maxArgumentCount) {
59+
accept('error', `Expected exactly ${minArgumentCount} ${kind} but got ${actualArgumentCount}.`, {
60+
node,
61+
property: 'argumentList',
62+
code: CODE_ARGUMENT_LIST_TOO_MANY_ARGUMENTS,
63+
});
64+
} else {
65+
accept(
66+
'error',
67+
`Expected between ${minArgumentCount} and ${maxArgumentCount} ${kind} but got ${actualArgumentCount}.`,
68+
{
69+
node,
70+
property: 'argumentList',
71+
code: CODE_ARGUMENT_LIST_TOO_MANY_ARGUMENTS,
72+
},
73+
);
74+
}
75+
};
76+
};
77+
78+
export const argumentListMustNotSetParameterMultipleTimes = (services: SafeDsServices) => {
79+
const nodeMapper = services.helpers.NodeMapper;
80+
const argumentToParameterOrUndefined = nodeMapper.argumentToParameterOrUndefined.bind(nodeMapper);
81+
82+
return (node: SdsArgumentList, accept: ValidationAcceptor): void => {
83+
// We already report other errors in this case
84+
const containingCall = getContainerOfType(node, isSdsCall);
85+
const callable = nodeMapper.callToCallableOrUndefined(containingCall);
86+
if (isSdsAnnotation(callable)) {
87+
return;
88+
}
89+
90+
const args = argumentsOrEmpty(node);
91+
const duplicates = duplicatesBy(args, argumentToParameterOrUndefined);
92+
93+
for (const duplicate of duplicates) {
94+
const correspondingParameter = argumentToParameterOrUndefined(duplicate)!;
95+
accept('error', `The parameter '${correspondingParameter.name}' is already set.`, {
96+
node: duplicate,
97+
code: CODE_ARGUMENT_LIST_DUPLICATE_PARAMETER,
98+
});
99+
}
100+
};
101+
};
102+
103+
export const argumentListMustSetAllRequiredParameters = (services: SafeDsServices) => {
104+
const nodeMapper = services.helpers.NodeMapper;
105+
106+
return (node: SdsAbstractCall, accept: ValidationAcceptor): void => {
107+
const callable = nodeMapper.callToCallableOrUndefined(node);
108+
109+
// We already report other errors in those cases
110+
if (!callable || (isSdsCall(node) && isSdsAnnotation(callable))) {
111+
return;
112+
}
113+
114+
const expectedParameters = parametersOrEmpty(callable).filter((it) => isRequiredParameter(it));
115+
if (isEmpty(expectedParameters)) {
116+
return;
117+
}
118+
119+
const actualParameters = argumentsOrEmpty(node).map((it) => nodeMapper.argumentToParameterOrUndefined(it));
120+
121+
const missingTypeParameters = expectedParameters.filter((it) => !actualParameters.includes(it));
122+
if (!isEmpty(missingTypeParameters)) {
123+
const kind = pluralize(missingTypeParameters.length, 'parameter');
124+
const missingParametersString = missingTypeParameters.map((it) => `'${it.name}'`).join(', ');
125+
126+
accept('error', `The ${kind} ${missingParametersString} must be set here.`, {
127+
node,
128+
property: 'argumentList',
129+
code: CODE_ARGUMENT_LIST_MISSING_REQUIRED_PARAMETER,
130+
});
131+
}
132+
};
133+
};

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ import {
5858
callableTypeMustNotHaveOptionalParameters,
5959
callableTypeParameterMustNotHaveConstModifier,
6060
} from './other/types/callableTypes.js';
61-
import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js';
61+
import {
62+
argumentListMustNotHavePositionalArgumentsAfterNamedArguments,
63+
argumentListMustNotHaveTooManyArguments,
64+
argumentListMustNotSetParameterMultipleTimes,
65+
argumentListMustSetAllRequiredParameters,
66+
} from './other/argumentLists.js';
6267
import {
6368
referenceMustNotBeFunctionPointer,
6469
referenceMustNotBeStaticClassOrEnumReference,
@@ -127,6 +132,10 @@ export const registerValidationChecks = function (services: SafeDsServices) {
127132
assignmentShouldNotImplicitlyIgnoreResult(services),
128133
assignmentShouldHaveMoreThanWildcardsAsAssignees,
129134
],
135+
SdsAbstractCall: [
136+
argumentListMustNotHaveTooManyArguments(services),
137+
argumentListMustSetAllRequiredParameters(services),
138+
],
130139
SdsAnnotation: [
131140
annotationMustContainUniqueNames,
132141
annotationParameterListShouldNotBeEmpty,
@@ -143,7 +152,10 @@ export const registerValidationChecks = function (services: SafeDsServices) {
143152
argumentCorrespondingParameterShouldNotBeDeprecated(services),
144153
argumentCorrespondingParameterShouldNotBeExperimental(services),
145154
],
146-
SdsArgumentList: [argumentListMustNotHavePositionalArgumentsAfterNamedArguments],
155+
SdsArgumentList: [
156+
argumentListMustNotHavePositionalArgumentsAfterNamedArguments,
157+
argumentListMustNotSetParameterMultipleTimes(services),
158+
],
147159
SdsAttribute: [attributeMustHaveTypeHint],
148160
SdsBlockLambda: [blockLambdaMustContainUniqueNames],
149161
SdsCall: [

src/language/validation/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const callReceiverMustBeCallable = (services: SafeDsServices) => {
4646
}
4747

4848
const callable = nodeMapper.callToCallableOrUndefined(node);
49-
if (!callable) {
49+
if (!callable || isSdsAnnotation(callable)) {
5050
accept('error', 'This expression is not callable.', {
5151
node: node.receiver,
5252
code: CODE_TYPE_CALLABLE_RECEIVER,

tests/resources/validation/builtins/annotations/repeatable/main.sdstest

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ annotation SingleUse
66
annotation MultiUse
77

88
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
9-
»@SingleUse«
9+
SingleUse«
1010
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
11-
»@MultiUse«
11+
MultiUse«
1212
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
13-
»@MultiUse«
13+
MultiUse«
1414
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
15-
»@UnresolvedAnnotation«
15+
UnresolvedAnnotation«
1616
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
17-
»@UnresolvedAnnotation«
17+
UnresolvedAnnotation«
1818
class CorrectUse
1919

2020
// $TEST$ no error r"The annotation '\w*' is not repeatable\."
21-
»@SingleUse«
21+
SingleUse«
2222
// $TEST$ error "The annotation 'SingleUse' is not repeatable."
23-
»@SingleUse«
23+
SingleUse«
2424
class IncorrectUse

0 commit comments

Comments
 (0)