From 674cda89c45248a5a41b7afebeb2db4d6c1f8c96 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 8 Jan 2025 15:41:36 +0100 Subject: [PATCH] feat: optional parameters must be passed by name (#1307) Closes #1163 ### Summary of Changes Optional parameters must now be passed by name. This allows reordering them in the stubs and inserting new optional parameters anywhere instead of just at the end. We can now also deprecate all optional parameters without introducing a new function. The error has an associated quickfix (our first). This PR also adds a data-driven test system for quickfixes and other code actions. --- .github/workflows/main.yml | 2 +- .github/workflows/pr.yml | 2 +- .../testing/code-actions-testing.md | 49 ++++ .../development/testing/generation-testing.md | 20 +- .../development/testing/validation-testing.md | 2 +- docs/mkdocs.yml | 1 + .../src/language/codeActions/factories.ts | 28 +++ .../codeActions/quickfixes/arguments.ts | 50 ++++ .../quickfixes/safe-ds-quickfix-provider.ts | 34 +++ .../safe-ds-code-action-provider.ts | 31 +++ .../src/language/safe-ds-module.ts | 9 + .../other/declarations/annotationCalls.ts | 2 +- .../validation/other/expressions/arguments.ts | 38 +++ .../validation/other/expressions/calls.ts | 2 +- .../language/validation/safe-ds-validator.ts | 20 +- .../data/tabular/containers/Column.sdsstub | 2 +- .../tabular/containers/StringCell.sdsstub | 2 +- .../data/tabular/containers/Table.sdsstub | 2 +- .../transformation/Discretizer.sdsstub | 2 +- .../builtins/safeds/lang/coreClasses.sdsstub | 6 +- .../safe-ds-lang/tests/helpers/diagnostics.ts | 6 +- .../tests/language/codeActions/creator.ts | 156 +++++++++++++ .../safe-ds-code-action-provider.test.ts | 221 ++++++++++++++++++ .../input.sdsdev | 18 ++ .../skip-output/input.sdsdev | 18 ++ .../tests/generator/call/gen_input.py.map | 2 +- .../python/expressions/call/input.sdsdev | 8 +- .../tests/generator/safeds/gen_input.py | 2 +- .../tests/generator/safeds/gen_input.py.map | 2 +- .../python/imports/safeds/input.sdsdev | 2 +- .../python/macros/strings/gen_input.py.map | 2 +- .../python/macros/strings/input.sdsdev | 2 +- .../expressions/calls/main/gen_input.py.map | 2 +- .../expressions/calls/main/input.sdsdev | 8 +- .../annotation calls.sdsdev | 26 +++ .../calls.sdsdev | 26 +++ 36 files changed, 754 insertions(+), 51 deletions(-) create mode 100644 docs/development/testing/code-actions-testing.md create mode 100644 packages/safe-ds-lang/src/language/codeActions/factories.ts create mode 100644 packages/safe-ds-lang/src/language/codeActions/quickfixes/arguments.ts create mode 100644 packages/safe-ds-lang/src/language/codeActions/quickfixes/safe-ds-quickfix-provider.ts create mode 100644 packages/safe-ds-lang/src/language/codeActions/safe-ds-code-action-provider.ts create mode 100644 packages/safe-ds-lang/src/language/validation/other/expressions/arguments.ts create mode 100644 packages/safe-ds-lang/tests/language/codeActions/creator.ts create mode 100644 packages/safe-ds-lang/tests/language/codeActions/safe-ds-code-action-provider.test.ts create mode 100644 packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/input.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/skip-output/input.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/annotation calls.sdsdev create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/calls.sdsdev diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7bc4025a4..3d68895ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [ 18.x ] + node-version: [ 20.x ] steps: - name: Checkout source diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index be7de777f..83ef1fee0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - node-version: [ 18.x ] + node-version: [ 20.x ] steps: - name: Checkout source diff --git a/docs/development/testing/code-actions-testing.md b/docs/development/testing/code-actions-testing.md new file mode 100644 index 000000000..b7a2f32d6 --- /dev/null +++ b/docs/development/testing/code-actions-testing.md @@ -0,0 +1,49 @@ +# Code Actions Testing + +Code actions tests are data-driven instead of being specified explicitly. This document explains how to add a new +code action test. + +## Adding a code action test + +1. Create a new **folder** (not just a file!) in the `tests/resources/code actions` directory or any subdirectory. Give + the folder a descriptive name, since the folder name becomes part of the test name. + + !!! tip "Skipping a test" + + If you want to skip a test, add the prefix `skip-` to the folder name. + +2. Add files with the extension `.sdsdev`, `.sds`, or `.sdsstub` **directly inside the folder**. All files in a + folder will be loaded into the same workspace, so they can reference each other. Files in different folders are + loaded into different workspaces, so they cannot reference each other. +3. Add the Safe-DS code that you want to test to the files. +4. Specify the code actions to apply using test comments (see [below](#format-of-test-comments)) at the top of the file. +5. Run the tests. The test runner will automatically pick up the new test, and create a snapshot of the current output + after applying the selected code actions. +6. Verify that the snapshot is correct, modify it if needed, and commit it. + +## Format of test comments + +1. As usual, test comments are single-line comments that start with `$TEST$`. +2. Then, the keyword `apply` follows. +3. Finally, you must specify the title of the code action enclosed in double-quotes. You can also add an `r` before the + opening double-quote to indicate that the title should be interpreted as a regular expression that must match the + entire actual title. + +Here are some examples: + +```ts +// $TEST$ apply "Remove statement." +``` + +We apply all code actions with the exact title `Remove statement.`. + +```ts +// $TEST$ apply r"^Remove.*" +``` + +We apply all code actions with a title that starts with `Remove`. + + +## Updating the snapshots + +To quickly update the snapshots after changes to the code generator, run `vitest` with the `--update` flag. diff --git a/docs/development/testing/generation-testing.md b/docs/development/testing/generation-testing.md index 64db0a54f..69f73d077 100644 --- a/docs/development/testing/generation-testing.md +++ b/docs/development/testing/generation-testing.md @@ -28,14 +28,12 @@ document explains how to add a new generation test. ```ts // $TEST$ target ``` -5. Add another folder called `generated` inside the folder that you created in step 1. Place folders and Python files - inside the `generated` folder to specify the expected output of the program. The relative paths to the Python files - and the contents of the Python files will be compared to the actual generation output. -6. Run the tests. The test runner will automatically pick up the new test. +5. Run the tests. The test runner will automatically pick up the new test, and create a snapshot of the current output of the generator. +6. Verify that the snapshot is correct, modify it if needed, and commit it. -### Updating the expected output +### Updating the snapshots -To quickly update the expected output after changes to the code generator, run `vitest` with the `--update` flag. +To quickly update the snapshots after changes to the code generator, run `vitest` with the `--update` flag. ## Markdown Generation @@ -53,11 +51,9 @@ To quickly update the expected output after changes to the code generator, run ` loaded into different workspaces, so they cannot reference each other. Generation will be triggered for all files in the folder. 3. Add the Safe-DS code that you want to test to the files. -4. Add another folder called `generated` inside the folder that you created in step 1. Place folders and Python files - inside the `generated` folder to specify the expected output of the program. The relative paths to the Python files - and the contents of the Python files will be compared to the actual generation output. -5. Run the tests. The test runner will automatically pick up the new test. +4. Run the tests. The test runner will automatically pick up the new test, and create a snapshot of the current output of the generator. +5. Verify that the snapshot is correct, modify it if needed, and commit it. -### Updating the expected output +### Updating the snapshots -To quickly update the expected output after changes to the code generator, run `vitest` with the `--update` flag. +To quickly update the snapshots after changes to the code generator, run `vitest` with the `--update` flag. diff --git a/docs/development/testing/validation-testing.md b/docs/development/testing/validation-testing.md index 349358eb9..db8d1a441 100644 --- a/docs/development/testing/validation-testing.md +++ b/docs/development/testing/validation-testing.md @@ -32,7 +32,7 @@ validation test. ## Format of test comments 1. As usual, test comments are single-line comments that start with `$TEST$`. -2. Then, you specify whether the issue should be absent by writing `no` or present by writing nothing. +2. Then, you specify whether the issue should be absent by writing `no`, or present by writing nothing. 3. Next, you specify the severity of the issue by writing `error`, `warning`, `info`, or `hint`. 4. Finally, you can optionally specify the message of the issue enclosed in double-quotes. You can also add an `r` before the opening double-quote to indicate that the expected message should be interpreted as a regular expression diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index de789bf1d..da3f916ef 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -62,6 +62,7 @@ nav: - Development: - Testing: - Call Graph Testing: development/testing/call-graph-testing.md + - Code Actions Testing: development/testing/code-actions-testing.md - Formatting Testing: development/testing/formatting-testing.md - Generation Testing: development/testing/generation-testing.md - Grammar Testing: development/testing/grammar-testing.md diff --git a/packages/safe-ds-lang/src/language/codeActions/factories.ts b/packages/safe-ds-lang/src/language/codeActions/factories.ts new file mode 100644 index 000000000..150e93f3d --- /dev/null +++ b/packages/safe-ds-lang/src/language/codeActions/factories.ts @@ -0,0 +1,28 @@ +import { CodeAction, Diagnostic, TextEdit } from 'vscode-languageserver'; +import { LangiumDocument } from 'langium'; + +export const createQuickfixFromTextEditsToSingleDocument = ( + title: string, + diagnostic: Diagnostic, + document: LangiumDocument, + edits: TextEdit[], + isPreferred: boolean = false, +): CodeAction => { + return { + title, + kind: 'quickfix', + diagnostics: [diagnostic], + edit: { + documentChanges: [ + { + textDocument: { + uri: document.textDocument.uri, + version: document.textDocument.version, + }, + edits, + }, + ], + }, + isPreferred, + }; +}; diff --git a/packages/safe-ds-lang/src/language/codeActions/quickfixes/arguments.ts b/packages/safe-ds-lang/src/language/codeActions/quickfixes/arguments.ts new file mode 100644 index 000000000..b77b33f28 --- /dev/null +++ b/packages/safe-ds-lang/src/language/codeActions/quickfixes/arguments.ts @@ -0,0 +1,50 @@ +import { Diagnostic, TextEdit } from 'vscode-languageserver'; +import { LangiumDocument } from 'langium'; +import { SafeDsServices } from '../../safe-ds-module.js'; +import { isSdsArgumentList, SdsArgument } from '../../generated/ast.js'; +import { Argument, Parameter } from '../../helpers/nodeProperties.js'; +import { SafeDsNodeMapper } from '../../helpers/safe-ds-node-mapper.js'; +import { CodeActionAcceptor } from '../safe-ds-code-action-provider.js'; +import { createQuickfixFromTextEditsToSingleDocument } from '../factories.js'; + +export const makeArgumentsAssignedToOptionalParametersNamed = (services: SafeDsServices) => { + const locator = services.workspace.AstNodeLocator; + const nodeMapper = services.helpers.NodeMapper; + + return (diagnostic: Diagnostic, document: LangiumDocument, acceptor: CodeActionAcceptor) => { + const node = locator.getAstNode(document.parseResult.value, diagnostic.data.path); + if (!isSdsArgumentList(node)) { + /* c8 ignore next 2 */ + return; + } + + acceptor( + createQuickfixFromTextEditsToSingleDocument( + 'Add names to arguments that are assigned to optional parameters.', + diagnostic, + document, + node.arguments.flatMap((it) => ensureArgumentIsNamed(nodeMapper, it)), + true, + ), + ); + }; +}; + +const ensureArgumentIsNamed = (nodeMapper: SafeDsNodeMapper, argument: SdsArgument): TextEdit[] | TextEdit => { + const cstNode = argument.$cstNode; + if (!cstNode || Argument.isNamed(argument)) { + return []; + } + + const parameter = nodeMapper.argumentToParameter(argument); + if (!parameter || Parameter.isRequired(parameter)) { + return []; + } + + const text = argument.$cstNode.text; + + return { + range: cstNode.range, + newText: `${parameter.name} = ${text}`, + }; +}; diff --git a/packages/safe-ds-lang/src/language/codeActions/quickfixes/safe-ds-quickfix-provider.ts b/packages/safe-ds-lang/src/language/codeActions/quickfixes/safe-ds-quickfix-provider.ts new file mode 100644 index 000000000..8676fe475 --- /dev/null +++ b/packages/safe-ds-lang/src/language/codeActions/quickfixes/safe-ds-quickfix-provider.ts @@ -0,0 +1,34 @@ +import { Diagnostic } from 'vscode-languageserver'; +import { LangiumDocument } from 'langium'; +import { CODE_ARGUMENT_POSITIONAL } from '../../validation/other/expressions/arguments.js'; +import { SafeDsServices } from '../../safe-ds-module.js'; +import { makeArgumentsAssignedToOptionalParametersNamed } from './arguments.js'; +import { CodeActionAcceptor } from '../safe-ds-code-action-provider.js'; + +export class SafeDsQuickfixProvider { + private readonly registry: QuickfixRegistry; + + constructor(services: SafeDsServices) { + this.registry = { + [CODE_ARGUMENT_POSITIONAL]: [makeArgumentsAssignedToOptionalParametersNamed(services)], + }; + } + + createQuickfixes(diagnostic: Diagnostic, document: LangiumDocument, acceptor: CodeActionAcceptor) { + if (!diagnostic.code) { + /* c8 ignore next 2 */ + return; + } + + const quickfixes = this.registry[diagnostic.code] ?? []; + for (const quickfix of quickfixes) { + quickfix(diagnostic, document, acceptor); + } + } +} + +type QuickfixRegistry = { + [code: string | number]: QuickfixCreator[]; +}; + +type QuickfixCreator = (diagnostic: Diagnostic, document: LangiumDocument, acceptor: CodeActionAcceptor) => void; diff --git a/packages/safe-ds-lang/src/language/codeActions/safe-ds-code-action-provider.ts b/packages/safe-ds-lang/src/language/codeActions/safe-ds-code-action-provider.ts new file mode 100644 index 000000000..759d4ce13 --- /dev/null +++ b/packages/safe-ds-lang/src/language/codeActions/safe-ds-code-action-provider.ts @@ -0,0 +1,31 @@ +import { CodeActionProvider } from 'langium/lsp'; +import { LangiumDocument, MaybePromise } from 'langium'; +import { CancellationToken, CodeAction, CodeActionParams } from 'vscode-languageserver'; +import { SafeDsServices } from '../safe-ds-module.js'; +import { SafeDsQuickfixProvider } from './quickfixes/safe-ds-quickfix-provider.js'; +import { isEmpty } from '../../helpers/collections.js'; + +export class SafeDsCodeActionProvider implements CodeActionProvider { + private readonly quickfixProvider: SafeDsQuickfixProvider; + + constructor(services: SafeDsServices) { + this.quickfixProvider = services.codeActions.QuickfixProvider; + } + + getCodeActions( + document: LangiumDocument, + params: CodeActionParams, + _cancelToken?: CancellationToken, + ): MaybePromise { + const result: CodeAction[] = []; + const acceptor = (action: CodeAction) => result.push(action); + + for (const diagnostic of params.context.diagnostics) { + this.quickfixProvider.createQuickfixes(diagnostic, document, acceptor); + } + + return isEmpty(result) ? undefined : result; + } +} + +export type CodeActionAcceptor = (action: CodeAction) => void; diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts index 5b6be0596..66b455228 100644 --- a/packages/safe-ds-lang/src/language/safe-ds-module.ts +++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts @@ -57,6 +57,8 @@ import { SafeDsPythonServer } from './runtime/safe-ds-python-server.js'; import { SafeDsSlicer } from './flow/safe-ds-slicer.js'; import { SafeDsSyntheticProperties } from './helpers/safe-ds-synthetic-properties.js'; import { SafeDsLinker } from './scoping/safe-ds-linker.js'; +import { SafeDsCodeActionProvider } from './codeActions/safe-ds-code-action-provider.js'; +import { SafeDsQuickfixProvider } from './codeActions/quickfixes/safe-ds-quickfix-provider.js'; /** * Declaration of custom services - add your own service classes here. @@ -68,6 +70,9 @@ export type SafeDsAddedServices = { Enums: SafeDsEnums; ImpurityReasons: SafeDsImpurityReasons; }; + codeActions: { + QuickfixProvider: SafeDsQuickfixProvider; + }; communication: { MessagingProvider: SafeDsMessagingProvider; }; @@ -139,6 +144,9 @@ export const SafeDsModule: Module new SafeDsEnums(services), ImpurityReasons: (services) => new SafeDsImpurityReasons(services), }, + codeActions: { + QuickfixProvider: (services) => new SafeDsQuickfixProvider(services), + }, communication: { MessagingProvider: (services) => new SafeDsMessagingProvider(services), }, @@ -163,6 +171,7 @@ export const SafeDsModule: Module new SafeDsCallHierarchyProvider(services), + CodeActionProvider: (services) => new SafeDsCodeActionProvider(services), CodeLensProvider: (services) => new SafeDsCodeLensProvider(services), CompletionProvider: (services) => new SafeDsCompletionProvider(services), DocumentSymbolProvider: (services) => new SafeDsDocumentSymbolProvider(services), diff --git a/packages/safe-ds-lang/src/language/validation/other/declarations/annotationCalls.ts b/packages/safe-ds-lang/src/language/validation/other/declarations/annotationCalls.ts index af36262fc..315f3a59d 100644 --- a/packages/safe-ds-lang/src/language/validation/other/declarations/annotationCalls.ts +++ b/packages/safe-ds-lang/src/language/validation/other/declarations/annotationCalls.ts @@ -18,7 +18,7 @@ import { } from '../../../helpers/nodeProperties.js'; import type { SafeDsServices } from '../../../safe-ds-module.js'; -export const CODE_ANNOTATION_CALL_CONSTANT_ARGUMENT = 'annotation-call/constant-argument'; +export const CODE_ANNOTATION_CALL_CONSTANT_ARGUMENT = 'annotation-call/non-constant-argument'; export const CODE_ANNOTATION_CALL_MISSING_ARGUMENT_LIST = 'annotation-call/missing-argument-list'; export const CODE_ANNOTATION_CALL_TARGET_PARAMETER = 'annotation-call/target-parameter'; export const CODE_ANNOTATION_CALL_TARGET_RESULT = 'annotation-call/target-result'; diff --git a/packages/safe-ds-lang/src/language/validation/other/expressions/arguments.ts b/packages/safe-ds-lang/src/language/validation/other/expressions/arguments.ts new file mode 100644 index 000000000..b6954106f --- /dev/null +++ b/packages/safe-ds-lang/src/language/validation/other/expressions/arguments.ts @@ -0,0 +1,38 @@ +import { SafeDsServices } from '../../../safe-ds-module.js'; +import type { SdsArgumentList } from '../../../generated/ast.js'; +import { ValidationAcceptor } from 'langium'; +import { Argument, getArguments, Parameter } from '../../../helpers/nodeProperties.js'; + +export const CODE_ARGUMENT_POSITIONAL = 'argument/positional'; + +export const argumentMustBeNamedIfParameterIsOptional = (services: SafeDsServices) => { + const locator = services.workspace.AstNodeLocator; + const nodeMapper = services.helpers.NodeMapper; + + return (node: SdsArgumentList, accept: ValidationAcceptor) => { + for (const argument of getArguments(node).toReversed()) { + const parameter = nodeMapper.argumentToParameter(argument); + if (!parameter) { + // Still keep going if there are extra arguments. + continue; + } + if (Parameter.isRequired(parameter)) { + // Required parameters must appear before optional parameters. + return; + } + + if (!Argument.isNamed(argument)) { + accept('error', 'Argument must be named if the parameter is optional.', { + node: argument, + property: 'value', + code: CODE_ARGUMENT_POSITIONAL, + data: { path: locator.getAstNodePath(node) }, + }); + + // Only show the error for the last argument. If users added names starting in the middle, we would no + // longer be able to assign the arguments to the correct parameters. + return; + } + } + }; +}; diff --git a/packages/safe-ds-lang/src/language/validation/other/expressions/calls.ts b/packages/safe-ds-lang/src/language/validation/other/expressions/calls.ts index 8aae2d4fe..fb1074771 100644 --- a/packages/safe-ds-lang/src/language/validation/other/expressions/calls.ts +++ b/packages/safe-ds-lang/src/language/validation/other/expressions/calls.ts @@ -3,7 +3,7 @@ import { type SdsCall } from '../../../generated/ast.js'; import { getArguments, Parameter } from '../../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../../safe-ds-module.js'; -export const CODE_CALL_CONSTANT_ARGUMENT = 'call/constant-argument'; +export const CODE_CALL_CONSTANT_ARGUMENT = 'call/non-constant-argument'; export const CODE_CALL_INFINITE_RECURSION = 'call/infinite-recursion'; export const callArgumentMustBeConstantIfParameterIsConstant = (services: SafeDsServices) => { diff --git a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts index cb5bc598d..7c3f7576b 100644 --- a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts +++ b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts @@ -192,6 +192,7 @@ import { outputStatementMustOnlyBeUsedInPipeline, } from './other/statements/outputStatements.js'; import { messageOfConstraintsMustOnlyReferenceConstantParameters } from './other/declarations/constraints.js'; +import { argumentMustBeNamedIfParameterIsOptional } from './other/expressions/arguments.js'; /** * Register custom validation checks. @@ -199,15 +200,6 @@ import { messageOfConstraintsMustOnlyReferenceConstantParameters } from './other export const registerValidationChecks = function (services: SafeDsServices) { const registry = services.validation.ValidationRegistry; const checks: ValidationChecks = { - SdsAssignee: [ - assigneeAssignedResultShouldNotBeDeprecated(services), - assigneeAssignedResultShouldNotBeExperimental(services), - ], - SdsAssignment: [ - assignmentAssigneeMustGetValue(services), - assignmentShouldNotImplicitlyIgnoreResult(services), - assignmentShouldHaveMoreThanWildcardsAsAssignees(services), - ], SdsAbstractCall: [ argumentListMustNotHaveTooManyArguments(services), argumentListMustSetAllRequiredParameters(services), @@ -234,6 +226,16 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsArgumentList: [ argumentListMustNotHavePositionalArgumentsAfterNamedArguments, argumentListMustNotSetParameterMultipleTimes(services), + argumentMustBeNamedIfParameterIsOptional(services), + ], + SdsAssignee: [ + assigneeAssignedResultShouldNotBeDeprecated(services), + assigneeAssignedResultShouldNotBeExperimental(services), + ], + SdsAssignment: [ + assignmentAssigneeMustGetValue(services), + assignmentShouldNotImplicitlyIgnoreResult(services), + assignmentShouldHaveMoreThanWildcardsAsAssignees(services), ], SdsAttribute: [attributeMustHaveTypeHint], SdsBlockLambda: [blockLambdaMustContainUniqueNames], diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub index e40eee3c6..3b7f285d5 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Column.sdsstub @@ -18,7 +18,7 @@ from safeds.data.tabular.typing import DataType @Category(DataScienceCategory.BasicElement) class Column( name: String, - data: List = [] + data: List, ) { /** * Whether the column is numeric. diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/StringCell.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/StringCell.sdsstub index 5425e78ba..03ce1ca3a 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/StringCell.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/StringCell.sdsstub @@ -142,7 +142,7 @@ class StringCell { * @example * pipeline example { * val column = Column("example", ["abc", "def", "ghi"]); - * val result = column.transform((cell) -> cell.str.substring(1, 2)); + * val result = column.transform((cell) -> cell.str.substring(start = 1, length = 2)); * // Column("example", ["bc", "ef", "hi"]) * } */ diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Table.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Table.sdsstub index 8666532ba..af3cf02ec 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Table.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/containers/Table.sdsstub @@ -31,7 +31,7 @@ from safeds.data.tabular.typing import Schema */ @Category(DataScienceCategory.BasicElement) class Table( - data: Map>? = null + data: Map> ) { /** * The names of the columns in the table. diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/transformation/Discretizer.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/transformation/Discretizer.sdsstub index e81babbf0..b2d19f3bb 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/transformation/Discretizer.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/data/tabular/transformation/Discretizer.sdsstub @@ -12,7 +12,7 @@ from safeds.data.tabular.transformation import TableTransformer * @example * pipeline example { * val table = Table({"a": [1, 2, 3, 4]}); - * val discretizer = Discretizer(2, columnNames = "a").fit(table); + * val discretizer = Discretizer(binCount = 2, columnNames = "a").fit(table); * val transformedTable = discretizer.transform(table); * // Table({"a": [0, 0, 1, 1]}) * } diff --git a/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub b/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub index 2229e1e8d..91df1bbf0 100644 --- a/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub +++ b/packages/safe-ds-lang/src/resources/builtins/safeds/lang/coreClasses.sdsstub @@ -126,7 +126,7 @@ class List { * * @example * pipeline example { - * val string = [1, 2, 3].join("-"); // "1-2-3" + * val string = [1, 2, 3].join(separator = "-"); // "1-2-3" * } */ @Pure @@ -141,7 +141,7 @@ class List { * * @example * pipeline example { - * val slice = [1, 2, 3].slice(1, 3); // [2, 3] + * val slice = [1, 2, 3].slice(start = 1, end = 3); // [2, 3] * } */ @Pure @@ -293,7 +293,7 @@ class String { * * @example * pipeline example { - * val substring = "Hello, world!".substring(7, 12); // "world" + * val substring = "Hello, world!".substring(start = 7, end = 12); // "world" * } */ @Pure diff --git a/packages/safe-ds-lang/tests/helpers/diagnostics.ts b/packages/safe-ds-lang/tests/helpers/diagnostics.ts index 23a5b932c..ca16b1b55 100644 --- a/packages/safe-ds-lang/tests/helpers/diagnostics.ts +++ b/packages/safe-ds-lang/tests/helpers/diagnostics.ts @@ -97,15 +97,15 @@ export class SyntaxErrorsInCodeError extends TestDescriptionError { } /** - * The code contains syntax errors. + * The code contains errors. */ export class ErrorsInCodeError extends TestDescriptionError { constructor( readonly errors: Diagnostic[], uri: URI, ) { - const syntaxErrorsAsString = errors.map((e) => ` - ${e.message}`).join(`\n`); + const errorsAsString = errors.map((e) => ` - ${e.message}`).join(`\n`); - super(`Code has errors:\n${syntaxErrorsAsString}`, uri); + super(`Code has errors:\n${errorsAsString}`, uri); } } diff --git a/packages/safe-ds-lang/tests/language/codeActions/creator.ts b/packages/safe-ds-lang/tests/language/codeActions/creator.ts new file mode 100644 index 000000000..58a1a9ba4 --- /dev/null +++ b/packages/safe-ds-lang/tests/language/codeActions/creator.ts @@ -0,0 +1,156 @@ +import { + listTestFilesWithExtensions, + listTestSafeDsFilesGroupedByParentDirectory, + loadDocuments, + uriToShortenedTestResourceName, +} from '../../helpers/testResources.js'; +import path from 'path'; +import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js'; +import { NodeFileSystem } from 'langium/node'; +import { TestDescription, TestDescriptionError } from '../../helpers/testDescription.js'; +import { URI } from 'langium'; +import { createSafeDsServices } from '../../../src/language/index.js'; +import { findTestComments } from '../../helpers/testComments.js'; +import { SAFE_DS_FILE_EXTENSIONS } from '../../../src/language/helpers/fileExtensions.js'; + +const services = (await createSafeDsServices(NodeFileSystem)).SafeDs; +const langiumDocuments = services.shared.workspace.LangiumDocuments; + +const rootResourceName = 'code actions'; +const commentRegex = /\s*apply\s*(?r)?"(?.*)"/gu; + +export const createCodeActionsTests = async (): Promise<CodeActionsTest[]> => { + const filesGroupedByParentDirectory = listTestSafeDsFilesGroupedByParentDirectory(rootResourceName); + const testCases = filesGroupedByParentDirectory.map((entry) => createCodeActionsTest(...entry)); + + return Promise.all(testCases); +}; + +const createCodeActionsTest = async (parentDirectory: URI, inputUris: URI[]): Promise<CodeActionsTest> => { + const outputRoot = URI.file(path.join(parentDirectory.fsPath, 'skip-output')); + const expectedOutputUris = listExpectedOutputFiles(outputRoot); + const inputs: CodeActionsInput[] = []; + + // Load all files, so they get linked + await loadDocuments(services, inputUris, { validation: true }); + + for (const uri of inputUris) { + const document = langiumDocuments.getDocument(uri)!; + const code = document.textDocument.getText(); + + // File must not contain syntax errors + const syntaxErrors = await getSyntaxErrors(services, code); + if (syntaxErrors.length > 0) { + return invalidTest('FILE', new SyntaxErrorsInCodeError(syntaxErrors, uri)); + } + + const testComments = findTestComments(code); + const codeActionTitles: (string | RegExp)[] = []; + + for (const comment of testComments) { + const match = commentRegex.exec(comment); + + // Comment must match the expected format + if (!match) { + return invalidTest('FILE', new InvalidCommentError(comment, uri)); + } + + const title = match.groups!.title!; + const titleIsRegex = match.groups!.titleIsRegex === 'r'; + + codeActionTitles.push(titleIsRegex ? new RegExp(title, 'gu') : title); + } + + inputs.push({ uri, codeActionTitles }); + } + + const shortenedResourceName = uriToShortenedTestResourceName(parentDirectory, rootResourceName); + return { + testName: `[${shortenedResourceName}]`, + inputs, + inputRoot: parentDirectory, + expectedOutputUris, + outputRoot, + }; +}; + +/** + * List all expected output files. + * + * @param outputRoot The directory, where output files are located. + */ +const listExpectedOutputFiles = (outputRoot: URI): URI[] => { + return listTestFilesWithExtensions(uriToShortenedTestResourceName(outputRoot), SAFE_DS_FILE_EXTENSIONS); +}; + +/** + * Report a test that has errors. + * + * @param level Whether a test file or a test suite is invalid. + * @param error The error that occurred. + */ +const invalidTest = (level: 'FILE' | 'SUITE', error: TestDescriptionError): CodeActionsTest => { + const shortenedResourceName = uriToShortenedTestResourceName(error.uri, rootResourceName); + const testName = `INVALID TEST ${level} [${shortenedResourceName}]`; + return { + testName, + inputs: [], + inputRoot: URI.file(''), + expectedOutputUris: [], + outputRoot: URI.file(''), + error, + }; +}; + +/** + * A description of a code actions test. + */ +interface CodeActionsTest extends TestDescription { + /** + * The original code. + */ + inputs: CodeActionsInput[]; + + /** + * The directory, where input files are located. + */ + inputRoot: URI; + + /** + * The expected output files. + */ + expectedOutputUris: URI[]; + + /** + * The directory, where output files are located. + */ + outputRoot: URI; +} + +/** + * A description of the input for code actions. + */ +interface CodeActionsInput { + /** + * The URI of the file. + */ + uri: URI; + + /** + * The titles of the code actions that should be applied. Strings must match exactly, regular expressions must match + * the entire string. + */ + codeActionTitles: (string | RegExp)[]; +} + +/** + * A test comment did not match the expected format. + */ +class InvalidCommentError extends TestDescriptionError { + constructor( + readonly comment: string, + uri: URI, + ) { + super(`Invalid test comment (refer to the documentation for guidance): ${comment}`, uri); + } +} diff --git a/packages/safe-ds-lang/tests/language/codeActions/safe-ds-code-action-provider.test.ts b/packages/safe-ds-lang/tests/language/codeActions/safe-ds-code-action-provider.test.ts new file mode 100644 index 000000000..5653c6a3b --- /dev/null +++ b/packages/safe-ds-lang/tests/language/codeActions/safe-ds-code-action-provider.test.ts @@ -0,0 +1,221 @@ +import { describe, expect, it } from 'vitest'; +import { NodeFileSystem } from 'langium/node'; +import { createCodeActionsTests } from './creator.js'; +import { loadDocuments } from '../../helpers/testResources.js'; +import { createSafeDsServices } from '../../../src/language/index.js'; +import { LangiumDocument, URI, UriUtils } from 'langium'; +import { + CodeAction, + CodeActionParams, + Command, + CreateFile, + DeleteFile, + RenameFile, + TextDocumentEdit, + WorkspaceEdit, +} from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +const services = (await createSafeDsServices(NodeFileSystem)).SafeDs; +const langiumDocuments = services.shared.workspace.LangiumDocuments; +const codeActionProvider = services.lsp.CodeActionProvider!; + +const codeActionTests = createCodeActionsTests(); + +describe('code actions', async () => { + it.each(await codeActionTests)('$testName', async (test) => { + // Test is invalid + if (test.error) { + throw test.error; + } + + // Load all documents + const inputUris = test.inputs.map((input) => input.uri); + await loadDocuments(services, inputUris, { validation: true }); + + // Collect workspace edits + const edits: WorkspaceEdit[] = []; + + for (const input of test.inputs) { + const document = langiumDocuments.getDocument(input.uri)!; + const codeActions = (await getCodeActions(document)) ?? []; + + for (const action of codeActions) { + if (actionShouldBeApplied(action, input.codeActionTitles) && action.edit) { + edits.push(action.edit); + } + } + } + + // Compute actual output + const relevantUris = await applyWorkspaceEdits(edits, inputUris); + const actualOutputs = computeActualOutput(relevantUris, test.inputRoot, test.outputRoot); + + // File contents must match + for (const [uriString, code] of actualOutputs) { + const fsPath = uriString.fsPath; + await expect(code).toMatchFileSnapshot(fsPath); + } + + // File paths must match + const actualOutputPaths = Array.from(actualOutputs.keys()) + .map((uri) => uri.toString()) + .sort(); + const expectedOutputPaths = test.expectedOutputUris.map((uri) => uri.toString()).sort(); + expect(actualOutputPaths).toStrictEqual(expectedOutputPaths); + }); +}); + +const getCodeActions = async function getCodeActions(document: LangiumDocument) { + const params: CodeActionParams = { + textDocument: { + uri: document.textDocument.uri, + }, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + context: { + diagnostics: document.diagnostics ?? [], + }, + }; + + return codeActionProvider.getCodeActions(document, params); +}; + +const actionShouldBeApplied = (action: CodeAction | Command, titles: (string | RegExp)[]): action is CodeAction => { + if (!CodeAction.is(action)) { + return false; + } + + for (const title of titles) { + if (typeof title === 'string' && action.title === title) { + return true; + } else if (title instanceof RegExp && title.test(action.title)) { + return true; + } + } + + return false; +}; + +const applyWorkspaceEdits = async function (edits: WorkspaceEdit[], inputUris: URI[]): Promise<URI[]> { + const uris = [...inputUris]; + + for (const edit of edits) { + if (edit.documentChanges) { + await applyDocumentChanges(edit.documentChanges, uris); + } + } + + return uris; +}; + +const applyDocumentChanges = async function ( + changes: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[], + uris: URI[], +) { + for (const change of changes) { + if (TextDocumentEdit.is(change)) { + await applyTextDocumentEdit(change); + } else if (CreateFile.is(change)) { + await applyCreateFile(change, uris); + } else if (RenameFile.is(change)) { + await applyRenameFile(change, uris); + } else if (DeleteFile.is(change)) { + await applyDeleteFile(change, uris); + } + } +}; + +const applyTextDocumentEdit = async function (change: TextDocumentEdit) { + const uri = URI.parse(change.textDocument.uri); + const document = langiumDocuments.getDocument(uri)!; + + const newCode = TextDocument.applyEdits(document.textDocument, change.edits); + langiumDocuments.deleteDocument(uri); + langiumDocuments.createDocument(uri, newCode); +}; + +const applyCreateFile = async function (change: CreateFile, uris: URI[]) { + const uri = URI.parse(change.uri); + + // Do nothing if the file exists already and should not be overwritten + const exists = langiumDocuments.hasDocument(uri); + if (exists && !change.options?.overwrite) { + return; + } + + // Apply the change + if (exists) { + langiumDocuments.deleteDocument(uri); + } + langiumDocuments.createDocument(uri, ''); + + // Update the list of URIs + if (uris.every((knownUri) => !UriUtils.equals(knownUri, uri))) { + uris.push(uri); + } +}; + +const applyRenameFile = async function (change: RenameFile, uris: URI[]) { + const oldUri = URI.parse(change.oldUri); + const newUri = URI.parse(change.newUri); + + // Do nothing if the old file does not exist + if (!langiumDocuments.hasDocument(oldUri)) { + return; + } + + // Do nothing if the new file exists already and should not be overwritten + const newExists = langiumDocuments.hasDocument(newUri); + if (newExists && !change.options?.overwrite) { + return; + } + + // Apply the change + const oldDocument = langiumDocuments.getDocument(oldUri)!; + langiumDocuments.deleteDocument(oldUri); + if (newExists) { + langiumDocuments.deleteDocument(newUri); + } + langiumDocuments.createDocument(newUri, oldDocument.textDocument.getText()); + + // Update the list of URIs + const index = uris.findIndex((knownUri) => UriUtils.equals(knownUri, oldUri)); + if (index >= 0) { + uris[index] = newUri; + } +}; + +const applyDeleteFile = async function (change: DeleteFile, uris: URI[]) { + const uri = URI.parse(change.uri); + + // Do nothing if the file does not exist + if (!langiumDocuments.hasDocument(uri)) { + return; + } + + // Apply the change + langiumDocuments.deleteDocument(uri); + + // Update the list of URIs + const index = uris.findIndex((knownUri) => UriUtils.equals(knownUri, uri)); + if (index >= 0) { + uris.splice(index, 1); + } +}; + +const computeActualOutput = (uris: URI[], inputRoot: URI, outputRoot: URI): Map<URI, string> => { + const result = new Map<URI, string>(); + + for (const uri of uris) { + const document = langiumDocuments.getDocument(uri)!; + const relativeUri = UriUtils.relative(inputRoot, uri); + const outputUri = UriUtils.resolvePath(outputRoot, relativeUri); + + result.set(outputUri, document.textDocument.getText()); + } + + return result; +}; diff --git a/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/input.sdsdev b/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/input.sdsdev new file mode 100644 index 000000000..b19f54957 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/input.sdsdev @@ -0,0 +1,18 @@ +// $TEST$ apply "Add names to arguments that are assigned to optional parameters." + +package tests.codeActions.quickfixes.makeArgumentsAssignedToOptionalParametersNamed + +@Repeatable +annotation MyAnnotation(required: Int, optional1: Int = 0, optional2: Int = 0) + +@Pure +fun myFunction(required: Int, optional1: Int = 0, optional2: Int = 0) + +@MyAnnotation(1, 2, 3, 4) + +@MyAnnotation(1, 2, optional2 = 3) +pipeline testPipeline { + myFunction(1, 2, 3, 4); + + myFunction(1, 2, optional2 = 3); +} diff --git a/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/skip-output/input.sdsdev b/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/skip-output/input.sdsdev new file mode 100644 index 000000000..ff6a0a202 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/code actions/quickfixes/make arguments assigned to optional parameters named/skip-output/input.sdsdev @@ -0,0 +1,18 @@ +// $TEST$ apply "Add names to arguments that are assigned to optional parameters." + +package tests.codeActions.quickfixes.makeArgumentsAssignedToOptionalParametersNamed + +@Repeatable +annotation MyAnnotation(required: Int, optional1: Int = 0, optional2: Int = 0) + +@Pure +fun myFunction(required: Int, optional1: Int = 0, optional2: Int = 0) + +@MyAnnotation(1, optional1 = 2, optional2 = 3, 4) + +@MyAnnotation(1, optional1 = 2, optional2 = 3) +pipeline testPipeline { + myFunction(1, optional1 = 2, optional2 = 3, 4); + + myFunction(1, optional1 = 2, optional2 = 3); +} diff --git a/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/generated/tests/generator/call/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/generated/tests/generator/call/gen_input.py.map index 461d6edf2..a06b78373 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/generated/tests/generator/call/gen_input.py.map +++ b/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/generated/tests/generator/call/gen_input.py.map @@ -1 +1 @@ -{"version":3,"sources":["input.sdsdev"],"names":["test","f","g","param2","h","i","j","k"],"mappings":"AAAA;;;;;;;;;;;;;;;;AA0BA,IAASA,IAAI;IACTC,CAAC,CAAEC,CAAC,CAAC,CAAC,EArBNC,MAAM,CAqBE,CAAC;IACTF,CAAC,CAAEC,CAAC,CAAsB,CAAC,EAtB3BC,MAAM,CAsBQ,CAAC;IACfF,CAAC,CAAEG,CAAC,CAAC,CAAC,EAlBiBD,OAAM,CAkBrB,CAAC;IACTF,CAAC,CAAEG,CAAC,CAAsB,CAAC,EAnBJD,OAAM,CAmBf,CAAC;IACfF,CAAC,CAAEG,CAAC,CAAU,CAAC;IACf,CAAA,CAAE,KAAK;IACP,CAAA,CAAE,KAAK,KAAE,GAAG;IACZ,CAAA,GAAQ,GAAG,IAAT,IAAI;IAEL,oBAAC,CAAFH,CAAC,UAADA,CAAC,CAAGC,CAAC,CAAC,CAAC,EA9BPC,MAAM,CA8BG,CAAC;IACT,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGC,CAAC,CAAsB,CAAC,EA/B5BC,MAAM,CA+BS,CAAC;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAC,CAAC,EA3BgBD,OAAM,CA2BpB,CAAC;IACT,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAsB,CAAC,EA5BLD,OAAM,CA4Bd,CAAC;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAU,CAAC;IACf,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK;IACP,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK,KAAE,GAAG;IACZ,oBAAC,CAAFC,CAAC,WAAD,GAAS,GAAG,IAAT,IAAI","file":"gen_input.py"} \ No newline at end of file +{"version":3,"sources":["input.sdsdev"],"names":["test","f","g","param2","h","i","j","k"],"mappings":"AAAA;;;;;;;;;;;;;;;;AA0BA,IAASA,IAAI;IACTC,CAAC,CAAEC,CAAC,CAAC,CAAC,EArBNC,MAAM,CAqBW,CAAC;IAClBF,CAAC,CAAEC,CAAC,CAAsB,CAAC,EAtB3BC,MAAM,CAsBQ,CAAC;IACfF,CAAC,CAAEG,CAAC,CAAC,CAAC,EAlBiBD,OAAM,CAkBZ,CAAC;IAClBF,CAAC,CAAEG,CAAC,CAAsB,CAAC,EAnBJD,OAAM,CAmBf,CAAC;IACfF,CAAC,CAAEG,CAAC,CAAU,CAAC;IACf,CAAA,CAAE,KAAK;IACP,CAAA,CAAE,KAAK,KAAE,GAAG;IACZ,CAAA,GAAQ,GAAG,IAAT,IAAI;IAEL,oBAAC,CAAFH,CAAC,UAADA,CAAC,CAAGC,CAAC,CAAC,CAAC,EA9BPC,MAAM,CA8BY,CAAC;IAClB,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGC,CAAC,CAAsB,CAAC,EA/B5BC,MAAM,CA+BS,CAAC;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAC,CAAC,EA3BgBD,OAAM,CA2BX,CAAC;IAClB,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAsB,CAAC,EA5BLD,OAAM,CA4Bd,CAAC;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAGG,CAAC,CAAU,CAAC;IACf,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK;IACP,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK,KAAE,GAAG;IACZ,oBAAC,CAAFC,CAAC,WAAD,GAAS,GAAG,IAAT,IAAI","file":"gen_input.py"} \ No newline at end of file diff --git a/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/input.sdsdev index 2486bb18a..770814d95 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/input.sdsdev +++ b/packages/safe-ds-lang/tests/resources/generation/python/expressions/call/input.sdsdev @@ -25,18 +25,18 @@ fun j(param: Any?, param2: Any?) fun k(param: Any?, param2: Any?) pipeline test { - f((g(1, 2))); + f((g(1, param2 = 2))); f((g(param2 = 1, param1 = 2))); - f((h(1, 2))); + f((h(1, param2 = 2))); f((h(param2 = 1, param1 = 2))); f((h(param1 = 2))); i("abc"); j("abc", 123); k(1.23, 456); - f?((g(1, 2))); + f?((g(1, param2 = 2))); f?((g(param2 = 1, param1 = 2))); - f?((h(1, 2))); + f?((h(1, param2 = 2))); f?((h(param2 = 1, param1 = 2))); f?((h(param1 = 2))); i?("abc"); diff --git a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py index 61b8a1617..1a1c19a03 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py +++ b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py @@ -5,4 +5,4 @@ # Pipelines -------------------------------------------------------------------- def test(): - __gen_placeholder_a = Table() + __gen_placeholder_a = Table({'a': []}) diff --git a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py.map index fc20c1aa4..c83a4da0b 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py.map +++ b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/generated/tests/generator/safeds/gen_input.py.map @@ -1 +1 @@ -{"version":3,"sources":["input.sdsdev"],"names":["test","table"],"mappings":"AAEA;;;;;;AAEA,IAASA,IAAI;IACT,sBAAQC,KAAK","file":"gen_input.py"} \ No newline at end of file +{"version":3,"sources":["input.sdsdev"],"names":["test","table"],"mappings":"AAEA;;;;;;AAEA,IAASA,IAAI;IACT,sBAAQC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE","file":"gen_input.py"} \ No newline at end of file diff --git a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/input.sdsdev index 7344f727a..e4be615f5 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/input.sdsdev +++ b/packages/safe-ds-lang/tests/resources/generation/python/imports/safeds/input.sdsdev @@ -3,5 +3,5 @@ package tests.generator.safeds pipeline test { - val a = Table(); + val a = Table({"a": []}); } diff --git a/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/generated/tests/generation/python/macros/strings/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/generated/tests/generation/python/macros/strings/gen_input.py.map index 132ba6d68..5291eb690 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/generated/tests/generation/python/macros/strings/gen_input.py.map +++ b/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/generated/tests/generation/python/macros/strings/gen_input.py.map @@ -1 +1 @@ -{"version":3,"sources":["input.sdsdev"],"names":["mypipeline"],"mappings":"AAAA;;AAEA,IAASA,UAAU;IACA,mBAAA;IAAf,8BAAe,CAAyB,OAAO;IAChC,mBAAA;IAAf,8BAAe,6BAAyB,QAAQ;IAClC,mBAAA;IAAd,6BAAc,yBAAwB,GAAG;IACvB,mBAAA;IAAlB,iCAAkB,0BAA4B,GAAG;IACpC,mBAAA;IAAb,4BAAa;IACA,mBAAA;IAAb,4BAAa,sBAAuB,CAAC;IACvB,mBAAA;IAAd,6BAAc,4BAAwB,OAAO,IAAE,QAAQ;IAC3C,mBAAA;IAAZ,2BAAY,0BAAsB;IACjB,mBAAA;IAAjB,gCAAiB,+BAA2B,OAAO;IACnC,mBAAA;IAAhB,+BAAgB,oBAA0B,CAAC;IAC1B,oBAAA;IAAjB,gCAAiB;IACL,oBAAA,MAAM;IAAlB,2BAAY,0BAAgB;IAClB,oBAAA,IAAI;IAAd,yBAAU;IACM,oBAAA;IAAhB,+BAAgB;IACA,oBAAA;IAAhB,+BAAgB;IACF,oBAAA;IAAd,6BAAc;IACG,oBAAA;IAAjB,gCAAiB;IACE,oBAAA;IAAnB,kCAAmB","file":"gen_input.py"} \ No newline at end of file +{"version":3,"sources":["input.sdsdev"],"names":["mypipeline"],"mappings":"AAAA;;AAEA,IAASA,UAAU;IACA,mBAAA;IAAf,8BAAe,CAAyB,OAAO;IAChC,mBAAA;IAAf,8BAAe,6BAAyB,QAAQ;IAClC,mBAAA;IAAd,6BAAc,yBAAwB,GAAG;IACvB,mBAAA;IAAlB,iCAAkB,0BAA4B,GAAG;IACpC,mBAAA;IAAb,4BAAa;IACA,mBAAA;IAAb,4BAAa,sBAAuB,CAAC;IACvB,mBAAA;IAAd,6BAAc,4BAAwB,OAAO,IAAE,QAAQ;IAC3C,mBAAA;IAAZ,2BAAY,0BAAsB;IACjB,mBAAA;IAAjB,gCAAiB,+BAA2B,OAAO;IACnC,mBAAA;IAAhB,+BAAgB,oBAAkC,CAAC;IAClC,oBAAA;IAAjB,gCAAiB;IACL,oBAAA,MAAM;IAAlB,2BAAY,0BAAgB;IAClB,oBAAA,IAAI;IAAd,yBAAU;IACM,oBAAA;IAAhB,+BAAgB;IACA,oBAAA;IAAhB,+BAAgB;IACF,oBAAA;IAAd,6BAAc;IACG,oBAAA;IAAjB,gCAAiB;IACE,oBAAA;IAAnB,kCAAmB","file":"gen_input.py"} \ No newline at end of file diff --git a/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/input.sdsdev index 0421bf630..e312e907a 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/input.sdsdev +++ b/packages/safe-ds-lang/tests/resources/generation/python/macros/strings/input.sdsdev @@ -10,7 +10,7 @@ pipeline myPipeline { val replace = "Hello, world!".replace("world", "planet"); val split = "Hello, world!".split(", "); val startsWith = "Hello, world!".startsWith("Hello"); - val substring = "Hello, world!".substring(7); + val substring = "Hello, world!".substring(start = 7); val casefolded = "Hello, world!".toCasefolded(); val float = "3.14".toFloat(); val int = "42".toInt(); diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/generated/tests/generator/runnerIntegration/expressions/calls/main/gen_input.py.map b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/generated/tests/generator/runnerIntegration/expressions/calls/main/gen_input.py.map index 39508492e..da25c9f62 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/generated/tests/generator/runnerIntegration/expressions/calls/main/gen_input.py.map +++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/generated/tests/generator/runnerIntegration/expressions/calls/main/gen_input.py.map @@ -1 +1 @@ -{"version":3,"sources":["input.sdsdev"],"names":["segment_a","a","result","test","f","g","h","i","j","k","readfile","l","m"],"mappings":"AAAA;;;;;;;;;;;;;;;;;AAiEA,IAAQA,SAAS,CAACC,CAAC;IACf,YAAMC,MAAM,GAAG,CAAAD,CAAC,EAAC,CAAC,EAAC,CAAC;IADE,OAAG;;;;AA9B7B,IAASE,IAAI;IACTC,CAAC,CAAE;;QAAAC,CAAC;SAAC,CAAC;SAAD,UAAG,CAAC;;;IACTD,CAAC,CAAE;;QAAAC,CAAC;SAAsB,CAAC;SAAtB,UAAS,CAAC;;;IACfD,CAAC,CAAE;;QAAAE,CAAC;SAAC,CAAC;SAAD,WAAG,CAAC;;;IACTF,CAAC,CAAE;;QAAAE,CAAC;SAAsB,CAAC;SAAtB,WAAS,CAAC;;;IACfF,CAAC,CAAE;;QAAAE,CAAC;SAAU,CAAC;SAAV,WA7BgC,CAAC;;;IA8BtC,CAAA,CAAE,KAAK;IACP,CAAA,CAAE,KAAK,KAAE,GAAG;IACZ,CAAA,GAAQ,GAAG,IAAT,IAAI;IAEL,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAC,CAAC;SAAC,CAAC;SAAD,UAAG,CAAC;;;IACT,oBAAC,CAAFD,CAAC,UAADA,CAAC,CAAG;;QAAAC,CAAC;SAAsB,CAAC;SAAtB,UAAS,CAAC;;;IACf,oBAAC,CAAFD,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAC,CAAC;SAAD,WAAG,CAAC;;;IACT,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAsB,CAAC;SAAtB,WAAS,CAAC;;;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAU,CAAC;SAAV,WAtC+B,CAAC;;;IAuCrC,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK;IACP,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK,KAAE,GAAG;IACZ,oBAAC,CAAFC,CAAC,WAAD,GAAS,GAAG,IAAT,IAAI;IAEPL,CAAC,CAAC;;QAAAM,QAAQ;;;SAAR,iCAAU;;IACR,mBAACT,CAAC;eAAKD,SAAS,CAACC,CAAC;IAAtBG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAACD,SAAS,CAACC,CAAC;IAA1BG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK;;YAAAW,CAAC;aAAC,CAAA,CAAC,EAAC,CAAC,EAACZ,SAAS,CAACC,CAAC;;;;IAA5BG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAAC;;YAAAW,CAAC;aAAC;;gBAAAA,CAAC;iBAAC,CAAA,CAAC,EAAC,CAAC,EAACZ,SAAS,CAACC,CAAC;;;;;;;IAAlCG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAGF,SAAS,CAACC,CAAC;QAA/B,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAG;;YAAAU,CAAC;aAACZ,SAAS,CAACC,CAAC;;;;QAAjC,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAACA,CAAC;IAAhBG,CAAC,CAAC;;QAAAO,CAAC;SAAC;;;;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAG,CAAA,CAAC,EAAC,CAAC,EAAC;;YAAAU,CAAC;aAACX,CAAC;;;;QAA3B,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAAC;;QAAAO,CAAC;SAAC","file":"gen_input.py"} \ No newline at end of file +{"version":3,"sources":["input.sdsdev"],"names":["segment_a","a","result","test","f","g","h","i","j","k","readfile","l","m"],"mappings":"AAAA;;;;;;;;;;;;;;;;;AAiEA,IAAQA,SAAS,CAACC,CAAC;IACf,YAAMC,MAAM,GAAG,CAAAD,CAAC,EAAC,CAAC,EAAC,CAAC;IADE,OAAG;;;;AA9B7B,IAASE,IAAI;IACTC,CAAC,CAAE;;QAAAC,CAAC;SAAC,CAAC;SAAD,UAAY,CAAC;;;IAClBD,CAAC,CAAE;;QAAAC,CAAC;SAAsB,CAAC;SAAtB,UAAS,CAAC;;;IACfD,CAAC,CAAE;;QAAAE,CAAC;SAAC,CAAC;SAAD,WAAY,CAAC;;;IAClBF,CAAC,CAAE;;QAAAE,CAAC;SAAsB,CAAC;SAAtB,WAAS,CAAC;;;IACfF,CAAC,CAAE;;QAAAE,CAAC;SAAU,CAAC;SAAV,WA7BgC,CAAC;;;IA8BtC,CAAA,CAAE,KAAK;IACP,CAAA,CAAE,KAAK,KAAE,GAAG;IACZ,CAAA,GAAQ,GAAG,IAAT,IAAI;IAEL,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAC,CAAC;SAAC,CAAC;SAAD,UAAY,CAAC;;;IAClB,oBAAC,CAAFD,CAAC,UAADA,CAAC,CAAG;;QAAAC,CAAC;SAAsB,CAAC;SAAtB,UAAS,CAAC;;;IACf,oBAAC,CAAFD,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAC,CAAC;SAAD,WAAY,CAAC;;;IAClB,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAsB,CAAC;SAAtB,WAAS,CAAC;;;IACf,oBAAC,CAAFF,CAAC,UAADA,CAAC,CAAG;;QAAAE,CAAC;SAAU,CAAC;SAAV,WAtC+B,CAAC;;;IAuCrC,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK;IACP,oBAAC,CAAFC,CAAC,WAAD,CAAG,KAAK,KAAE,GAAG;IACZ,oBAAC,CAAFC,CAAC,WAAD,GAAS,GAAG,IAAT,IAAI;IAEPL,CAAC,CAAC;;QAAAM,QAAQ;;;SAAR,iCAAU;;IACR,mBAACT,CAAC;eAAKD,SAAS,CAACC,CAAC;IAAtBG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAACD,SAAS,CAACC,CAAC;IAA1BG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK;;YAAAW,CAAC;aAAC,CAAA,CAAC,EAAC,CAAC,EAACZ,SAAS,CAACC,CAAC;;;;IAA5BG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAAC;;YAAAW,CAAC;aAAC;;gBAAAA,CAAC;iBAAC,CAAA,CAAC,EAAC,CAAC,EAACZ,SAAS,CAACC,CAAC;;;;;;;IAAlCG,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAGF,SAAS,CAACC,CAAC;QAA/B,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAG;;YAAAU,CAAC;aAACZ,SAAS,CAACC,CAAC;;;;QAAjC,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAACO,CAAC,CAAC;IACA,mBAACV,CAAC;eAAK,CAAA,CAAC,EAAC,CAAC,EAACA,CAAC;IAAhBG,CAAC,CAAC;;QAAAO,CAAC;SAAC;;;;IACA,mBAACV,CAAC;QAAG,0BAAMC,MAAM,GAAG,CAAA,CAAC,EAAC,CAAC,EAAC;;YAAAU,CAAC;aAACX,CAAC;;;;QAA3B,OAAK,0BAAMC,MAAM;IAArBE,CAAC,CAAC;;QAAAO,CAAC;SAAC","file":"gen_input.py"} \ No newline at end of file diff --git a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/input.sdsdev b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/input.sdsdev index 8b9e72860..0cb1b81f7 100644 --- a/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/input.sdsdev +++ b/packages/safe-ds-lang/tests/resources/generation/python/runner integration/expressions/calls/main/input.sdsdev @@ -34,18 +34,18 @@ fun m(param: Int) -> result: Int fun readFile() -> content: String pipeline test { - f((g(1, 2))); + f((g(1, param2 = 2))); f((g(param2 = 1, param1 = 2))); - f((h(1, 2))); + f((h(1, param2 = 2))); f((h(param2 = 1, param1 = 2))); f((h(param1 = 2))); i("abc"); j("abc", 123); k(1.23, 456); - f?((g(1, 2))); + f?((g(1, param2 = 2))); f?((g(param2 = 1, param1 = 2))); - f?((h(1, 2))); + f?((h(1, param2 = 2))); f?((h(param2 = 1, param1 = 2))); f?((h(param1 = 2))); i?("abc"); diff --git a/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/annotation calls.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/annotation calls.sdsdev new file mode 100644 index 000000000..dbc20298e --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/annotation calls.sdsdev @@ -0,0 +1,26 @@ +package tests.validation.other.expressions.arguments.mustBeNamedIfParameterIsOptional + +@Repeatable +annotation MyAnnotation(required: Int, optional1: Int = 0, optional2: Int = 0) + +// $TEST$ no error "Argument must be named if the parameter is optional." +// $TEST$ error "Argument must be named if the parameter is optional." +@MyAnnotation(»1«, »2«) +// $TEST$ no error "Argument must be named if the parameter is optional." +// $TEST$ no error "Argument must be named if the parameter is optional." +// $TEST$ error "Argument must be named if the parameter is optional." +// $TEST$ no error "Argument must be named if the parameter is optional." +@MyAnnotation(»1«, »2«, »3«, »4«) +// $TEST$ no error "Argument must be named if the parameter is optional." +// $TEST$ error "Argument must be named if the parameter is optional." +// $TEST$ no error "Argument must be named if the parameter is optional." +@MyAnnotation(»1«, »2«, »optional2 = 3«) +// $TEST$ no error "Argument must be named if the parameter is optional." +// $TEST$ no error "Argument must be named if the parameter is optional." +@MyAnnotation(»required = 1«, »optional1 = 2«) + +// $TEST$ no error "Argument must be named if the parameter is optional." +@Unresolved(»1«) +// $TEST$ no error "Argument must be named if the parameter is optional." +@MyAnnotation(unresolved = »1«) +pipeline testPipeline {} diff --git a/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/calls.sdsdev b/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/calls.sdsdev new file mode 100644 index 000000000..1275813a6 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/other/expressions/arguments/must be named if parameter is optional/calls.sdsdev @@ -0,0 +1,26 @@ +package tests.validation.other.expressions.arguments.mustBeNamedIfParameterIsOptional + +@Pure fun myFunction(required: Int, optional1: Int = 0, optional2: Int = 0) + +pipeline testPipeline { + // $TEST$ no error "Argument must be named if the parameter is optional." + // $TEST$ error "Argument must be named if the parameter is optional." + myFunction(»1«, »2«); + // $TEST$ no error "Argument must be named if the parameter is optional." + // $TEST$ no error "Argument must be named if the parameter is optional." + // $TEST$ error "Argument must be named if the parameter is optional." + // $TEST$ no error "Argument must be named if the parameter is optional." + myFunction(»1«, »2«, »3«, »4«); + // $TEST$ no error "Argument must be named if the parameter is optional." + // $TEST$ error "Argument must be named if the parameter is optional." + // $TEST$ no error "Argument must be named if the parameter is optional." + myFunction(»1«, »2«, »optional2 = 3«); + // $TEST$ no error "Argument must be named if the parameter is optional." + // $TEST$ no error "Argument must be named if the parameter is optional." + myFunction(»required = 1«, »optional1 = 2«); + + // $TEST$ no error "Argument must be named if the parameter is optional." + unresolved(»1«); + // $TEST$ no error "Argument must be named if the parameter is optional." + myFunction(unresolved = »1«); +}