From a698a6aff42c6344bcf104f452718b2d0237562e Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 11 Nov 2023 16:34:16 +0100 Subject: [PATCH] feat: ensure an overriding member matches the overridden one (#758) Closes #639 ### Summary of Changes Show an error if an overriding member does not match the overridden one. --- .../generation/safe-ds-python-generator.ts | 56 ++-- .../src/language/helpers/nodeProperties.ts | 48 ++-- .../scoping/safe-ds-scope-provider.ts | 6 +- .../safe-ds-lang/src/language/typing/model.ts | 7 +- .../typing/safe-ds-class-hierarchy.ts | 33 ++- .../language/typing/safe-ds-type-checker.ts | 9 +- .../validation/builtins/deprecated.ts | 6 +- .../language/validation/builtins/expert.ts | 4 +- .../src/language/validation/inheritance.ts | 39 ++- .../src/language/validation/names.ts | 20 +- .../validation/other/argumentLists.ts | 10 +- .../other/declarations/annotationCalls.ts | 8 +- .../other/declarations/parameterLists.ts | 5 +- .../other/declarations/parameters.ts | 6 +- .../validation/other/expressions/calls.ts | 6 +- .../validation/other/types/callableTypes.ts | 6 +- .../language/validation/safe-ds-validator.ts | 7 +- .../src/language/validation/style.ts | 10 +- .../tests/language/typing/model.test.ts | 41 +-- .../typing/safe-ds-class-hierarchy.test.ts | 244 +++++++++++++++++- .../typing/safe-ds-type-checker.test.ts | 10 + .../validation/safe-ds-validator.test.ts | 12 +- .../main.sdstest | 75 ++++++ .../missing type parameter/main.sdstest | 4 +- packages/safe-ds-vscode/snippets/safe-ds.json | 2 +- 25 files changed, 543 insertions(+), 131 deletions(-) create mode 100644 packages/safe-ds-lang/tests/resources/validation/inheritance/overriding method must match overridden method/main.sdstest diff --git a/packages/safe-ds-lang/src/language/generation/safe-ds-python-generator.ts b/packages/safe-ds-lang/src/language/generation/safe-ds-python-generator.ts index 361adc87a..6098d187a 100644 --- a/packages/safe-ds-lang/src/language/generation/safe-ds-python-generator.ts +++ b/packages/safe-ds-lang/src/language/generation/safe-ds-python-generator.ts @@ -1,4 +1,26 @@ -import { SafeDsServices } from '../safe-ds-module.js'; +import { + CompositeGeneratorNode, + expandToNode, + expandTracedToNode, + findRootNode, + getContainerOfType, + getDocument, + joinToNode, + joinTracedToNode, + LangiumDocument, + NL, + streamAllContents, + toStringAndTrace, + TraceRegion, + traceToNode, + TreeStreamImpl, + URI, +} from 'langium'; +import path from 'path'; +import { SourceMapGenerator, StartOfSourceMap } from 'source-map'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { groupBy } from '../../helpers/collectionUtils.js'; +import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; import { isSdsAbstractResult, isSdsAssignment, @@ -46,25 +68,7 @@ import { SdsStatement, } from '../generated/ast.js'; import { isInStubFile, isStubFile } from '../helpers/fileExtensions.js'; -import path from 'path'; -import { - CompositeGeneratorNode, - expandToNode, - expandTracedToNode, - findRootNode, - getContainerOfType, - getDocument, - joinToNode, - joinTracedToNode, - LangiumDocument, - NL, - streamAllContents, - toStringAndTrace, - TraceRegion, - traceToNode, - TreeStreamImpl, - URI, -} from 'langium'; +import { IdManager } from '../helpers/idManager.js'; import { getAbstractResults, getAssignees, @@ -72,10 +76,10 @@ import { getImports, getModuleMembers, getStatements, - isRequiredParameter, + Parameter, streamBlockLambdaResults, } from '../helpers/nodeProperties.js'; -import { groupBy } from '../../helpers/collectionUtils.js'; +import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; import { BooleanConstant, FloatConstant, @@ -83,12 +87,8 @@ import { NullConstant, StringConstant, } from '../partialEvaluation/model.js'; -import { IdManager } from '../helpers/idManager.js'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js'; -import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js'; -import { SourceMapGenerator, StartOfSourceMap } from 'source-map'; +import { SafeDsServices } from '../safe-ds-module.js'; export const CODEGEN_PREFIX = '__gen_'; const BLOCK_LAMBDA_PREFIX = `${CODEGEN_PREFIX}block_lambda_`; @@ -685,7 +685,7 @@ export class SafeDsPythonGenerator { private generateArgument(argument: SdsArgument, frame: GenerationInfoFrame): CompositeGeneratorNode { const parameter = this.nodeMapper.argumentToParameter(argument); return expandTracedToNode(argument)`${ - parameter !== undefined && !isRequiredParameter(parameter) + parameter !== undefined && !Parameter.isRequired(parameter) ? expandToNode`${this.generateParameter(parameter, frame, false)}=` : '' }${this.generateExpression(argument.value, frame)}`; diff --git a/packages/safe-ds-lang/src/language/helpers/nodeProperties.ts b/packages/safe-ds-lang/src/language/helpers/nodeProperties.ts index 8f1498a8c..76e7a8f9e 100644 --- a/packages/safe-ds-lang/src/language/helpers/nodeProperties.ts +++ b/packages/safe-ds-lang/src/language/helpers/nodeProperties.ts @@ -16,6 +16,7 @@ import { isSdsLambda, isSdsModule, isSdsModuleMember, + isSdsParameter, isSdsPlaceholder, isSdsSegment, isSdsTypeParameterList, @@ -85,28 +86,30 @@ export const isPositionalArgument = (node: SdsArgument): boolean => { return !node.parameter; }; -export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => { - return Boolean(node.typeParameter); -}; +export namespace Parameter { + export const isConstant = (node: SdsParameter | undefined): boolean => { + if (!node) { + return false; + } -export const isConstantParameter = (node: SdsParameter | undefined): boolean => { - if (!node) { - return false; - } + const containingCallable = getContainerOfType(node, isSdsCallable); - const containingCallable = getContainerOfType(node, isSdsCallable); + // In those cases, the const modifier is not applicable + if (isSdsCallableType(containingCallable) || isSdsLambda(containingCallable)) { + return false; + } - // In those cases, the const modifier is not applicable - if (isSdsCallableType(containingCallable) || isSdsLambda(containingCallable)) { - return false; - } + return isSdsAnnotation(containingCallable) || node.isConstant; + }; - return isSdsAnnotation(containingCallable) || node.isConstant; -}; + export const isOptional = (node: SdsParameter | undefined): boolean => { + return Boolean(node?.defaultValue); + }; -export const isRequiredParameter = (node: SdsParameter): boolean => { - return !node.defaultValue; -}; + export const isRequired = (node: SdsParameter | undefined): boolean => { + return isSdsParameter(node) && !node.defaultValue; + }; +} export const isStatic = (node: SdsClassMember): boolean => { if (isSdsClass(node) || isSdsEnum(node)) { @@ -121,6 +124,10 @@ export const isStatic = (node: SdsClassMember): boolean => { } }; +export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => { + return Boolean(node.typeParameter); +}; + // ------------------------------------------------------------------------------------------------- // Accessors for list elements // ------------------------------------------------------------------------------------------------- @@ -190,11 +197,8 @@ export const streamBlockLambdaResults = (node: SdsBlockLambda | undefined): Stre .filter(isSdsBlockLambdaResult); }; -export const getMatchingClassMembers = ( - node: SdsClass | undefined, - filterFunction: (member: SdsClassMember) => boolean = () => true, -): SdsClassMember[] => { - return node?.body?.members?.filter(filterFunction) ?? []; +export const getClassMembers = (node: SdsClass | undefined): SdsClassMember[] => { + return node?.body?.members ?? []; }; export const getColumns = (node: SdsSchema | undefined): SdsColumn[] => { diff --git a/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts b/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts index 0da66db24..4f45832a4 100644 --- a/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts +++ b/packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts @@ -57,10 +57,10 @@ import { getAbstractResults, getAnnotationCallTarget, getAssignees, + getClassMembers, getEnumVariants, getImportedDeclarations, getImports, - getMatchingClassMembers, getPackageName, getParameters, getResults, @@ -185,7 +185,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { // Static access const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver); if (isSdsClass(declaration)) { - const ownStaticMembers = getMatchingClassMembers(declaration, isStatic); + const ownStaticMembers = getClassMembers(declaration).filter(isStatic); const superclassStaticMembers = this.classHierarchy.streamSuperclassMembers(declaration).filter(isStatic); return this.createScopeForNodes(ownStaticMembers, this.createScopeForNodes(superclassStaticMembers)); @@ -215,7 +215,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider { } if (receiverType instanceof ClassType) { - const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it)); + const ownInstanceMembers = getClassMembers(receiverType.declaration).filter((it) => !isStatic(it)); const superclassInstanceMembers = this.classHierarchy .streamSuperclassMembers(receiverType.declaration) .filter((it) => !isStatic(it)); diff --git a/packages/safe-ds-lang/src/language/typing/model.ts b/packages/safe-ds-lang/src/language/typing/model.ts index ca63f39ce..34f1df359 100644 --- a/packages/safe-ds-lang/src/language/typing/model.ts +++ b/packages/safe-ds-lang/src/language/typing/model.ts @@ -8,6 +8,7 @@ import { SdsEnumVariant, SdsParameter, } from '../generated/ast.js'; +import { Parameter } from '../helpers/nodeProperties.js'; import { Constant, NullConstant } from '../partialEvaluation/model.js'; /** @@ -73,7 +74,11 @@ export class CallableType extends Type { } override toString(): string { - return `${this.inputType} -> ${this.outputType}`; + const inputTypeString = this.inputType.entries + .map((it) => `${it.name}${Parameter.isOptional(it.declaration) ? '?' : ''}: ${it.type}`) + .join(', '); + + return `(${inputTypeString}) -> ${this.outputType}`; } override unwrap(): CallableType { diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts index ca7bca798..d6494a48d 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts @@ -1,7 +1,7 @@ -import { EMPTY_STREAM, stream, Stream } from 'langium'; +import { EMPTY_STREAM, getContainerOfType, stream, Stream } from 'langium'; import { SafeDsClasses } from '../builtins/safe-ds-classes.js'; import { isSdsClass, isSdsNamedType, SdsClass, type SdsClassMember } from '../generated/ast.js'; -import { getMatchingClassMembers, getParentTypes } from '../helpers/nodeProperties.js'; +import { getClassMembers, getParentTypes, isStatic } from '../helpers/nodeProperties.js'; import { SafeDsServices } from '../safe-ds-module.js'; export class SafeDsClassHierarchy { @@ -61,7 +61,7 @@ export class SafeDsClassHierarchy { return EMPTY_STREAM; } - return this.streamSuperclasses(node).flatMap(getMatchingClassMembers); + return this.streamSuperclasses(node).flatMap(getClassMembers); } /** @@ -79,4 +79,31 @@ export class SafeDsClassHierarchy { return undefined; } + + /** + * Returns the member that is overridden by the given member, or `undefined` if the member does not override + * anything. + */ + getOverriddenMember(node: SdsClassMember | undefined): SdsClassMember | undefined { + // Static members cannot override anything + if (!node || isStatic(node)) { + return undefined; + } + + // Don't consider members with the same name as a previous member + const containingClass = getContainerOfType(node, isSdsClass); + if (!containingClass) { + return undefined; + } + const firstMemberWithSameName = getClassMembers(containingClass).find( + (it) => !isStatic(it) && it.name === node.name, + ); + if (firstMemberWithSameName !== node) { + return undefined; + } + + return this.streamSuperclassMembers(containingClass) + .filter((it) => !isStatic(it) && it.name === node.name) + .head(); + } } diff --git a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts index 9dd319b84..1a683a014 100644 --- a/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts +++ b/packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts @@ -1,7 +1,7 @@ import { getContainerOfType } from 'langium'; import type { SafeDsClasses } from '../builtins/safe-ds-classes.js'; import { isSdsEnum, type SdsAbstractResult, SdsDeclaration } from '../generated/ast.js'; -import { getParameters } from '../helpers/nodeProperties.js'; +import { getParameters, Parameter } from '../helpers/nodeProperties.js'; import { Constant } from '../partialEvaluation/model.js'; import { SafeDsServices } from '../safe-ds-module.js'; import { @@ -84,6 +84,11 @@ export class SafeDsTypeChecker { return false; } + // Optionality must match (all but required to optional is OK) + if (Parameter.isRequired(typeEntry.declaration) && Parameter.isOptional(otherEntry.declaration)) { + return false; + } + // Types must be contravariant if (!this.isAssignableTo(otherEntry.type, typeEntry.type)) { return false; @@ -93,7 +98,7 @@ export class SafeDsTypeChecker { // Additional parameters must be optional for (let i = other.inputType.length; i < type.inputType.length; i++) { const typeEntry = type.inputType.entries[i]!; - if (!typeEntry.declaration?.defaultValue) { + if (!Parameter.isOptional(typeEntry.declaration)) { return false; } } diff --git a/packages/safe-ds-lang/src/language/validation/builtins/deprecated.ts b/packages/safe-ds-lang/src/language/validation/builtins/deprecated.ts index 7c0cb97db..5b28a0cd7 100644 --- a/packages/safe-ds-lang/src/language/validation/builtins/deprecated.ts +++ b/packages/safe-ds-lang/src/language/validation/builtins/deprecated.ts @@ -1,4 +1,5 @@ import { ValidationAcceptor } from 'langium'; +import { DiagnosticTag } from 'vscode-languageserver'; import { isSdsParameter, isSdsResult, @@ -10,10 +11,9 @@ import { SdsParameter, SdsReference, } from '../../generated/ast.js'; +import { Parameter } from '../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../safe-ds-module.js'; -import { isRequiredParameter } from '../../helpers/nodeProperties.js'; import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js'; -import { DiagnosticTag } from 'vscode-languageserver'; export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result'; export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation'; @@ -108,7 +108,7 @@ export const referenceTargetShouldNotBeDeprecated = export const requiredParameterMustNotBeDeprecated = (services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => { - if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) { + if (Parameter.isRequired(node) && parameterCanBeAnnotated(node)) { if (services.builtins.Annotations.isDeprecated(node)) { accept('error', 'A deprecated parameter must be optional.', { node, diff --git a/packages/safe-ds-lang/src/language/validation/builtins/expert.ts b/packages/safe-ds-lang/src/language/validation/builtins/expert.ts index df8995508..eaff4a368 100644 --- a/packages/safe-ds-lang/src/language/validation/builtins/expert.ts +++ b/packages/safe-ds-lang/src/language/validation/builtins/expert.ts @@ -1,14 +1,14 @@ import { ValidationAcceptor } from 'langium'; import { SdsParameter } from '../../generated/ast.js'; +import { Parameter } from '../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../safe-ds-module.js'; -import { isRequiredParameter } from '../../helpers/nodeProperties.js'; import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js'; export const CODE_EXPERT_TARGET_PARAMETER = 'expert/target-parameter'; export const requiredParameterMustNotBeExpert = (services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => { - if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) { + if (Parameter.isRequired(node) && parameterCanBeAnnotated(node)) { if (services.builtins.Annotations.isExpert(node)) { accept('error', 'An expert parameter must be optional.', { node, diff --git a/packages/safe-ds-lang/src/language/validation/inheritance.ts b/packages/safe-ds-lang/src/language/validation/inheritance.ts index fac801601..a6523bc5e 100644 --- a/packages/safe-ds-lang/src/language/validation/inheritance.ts +++ b/packages/safe-ds-lang/src/language/validation/inheritance.ts @@ -1,14 +1,47 @@ -import { ValidationAcceptor } from 'langium'; -import { SdsClass } from '../generated/ast.js'; +import { expandToStringWithNL, ValidationAcceptor } from 'langium'; +import { isEmpty } from '../../helpers/collectionUtils.js'; +import { SdsClass, type SdsClassMember } from '../generated/ast.js'; import { getParentTypes } from '../helpers/nodeProperties.js'; import { SafeDsServices } from '../safe-ds-module.js'; import { ClassType, UnknownType } from '../typing/model.js'; -import { isEmpty } from '../../helpers/collectionUtils.js'; export const CODE_INHERITANCE_CYCLE = 'inheritance/cycle'; export const CODE_INHERITANCE_MULTIPLE_INHERITANCE = 'inheritance/multiple-inheritance'; +export const CODE_INHERITANCE_MUST_MATCH_OVERRIDDEN_MEMBER = 'inheritance/must-match-overridden-member'; export const CODE_INHERITANCE_NOT_A_CLASS = 'inheritance/not-a-class'; +export const classMemberMustMatchOverriddenMember = (services: SafeDsServices) => { + const classHierarchy = services.types.ClassHierarchy; + const typeChecker = services.types.TypeChecker; + const typeComputer = services.types.TypeComputer; + + return (node: SdsClassMember, accept: ValidationAcceptor): void => { + const overriddenMember = classHierarchy.getOverriddenMember(node); + if (!overriddenMember) { + return; + } + + const ownMemberType = typeComputer.computeType(node); + const overriddenMemberType = typeComputer.computeType(overriddenMember); + + if (!typeChecker.isAssignableTo(ownMemberType, overriddenMemberType)) { + accept( + 'error', + expandToStringWithNL` + Overriding member does not match the overridden member: + - Expected type: ${overriddenMemberType} + - Actual type: ${ownMemberType} + `, + { + node, + property: 'name', + code: CODE_INHERITANCE_MUST_MATCH_OVERRIDDEN_MEMBER, + }, + ); + } + }; +}; + export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => { const typeComputer = services.types.TypeComputer; const computeType = typeComputer.computeType.bind(typeComputer); diff --git a/packages/safe-ds-lang/src/language/validation/names.ts b/packages/safe-ds-lang/src/language/validation/names.ts index 5e373f27b..5a9d43e6a 100644 --- a/packages/safe-ds-lang/src/language/validation/names.ts +++ b/packages/safe-ds-lang/src/language/validation/names.ts @@ -1,3 +1,7 @@ +import { AstNodeDescription, getDocument, ValidationAcceptor } from 'langium'; +import { duplicatesBy } from '../../helpers/collectionUtils.js'; +import { listBuiltinFiles } from '../builtins/fileFinder.js'; +import { BUILTINS_ROOT_PACKAGE } from '../builtins/packageNames.js'; import { isSdsQualifiedImport, SdsAnnotation, @@ -21,13 +25,14 @@ import { SdsSegment, SdsTypeParameter, } from '../generated/ast.js'; -import { AstNodeDescription, getDocument, ValidationAcceptor } from 'langium'; +import { CODEGEN_PREFIX } from '../generation/safe-ds-python-generator.js'; +import { isInPipelineFile, isInStubFile, isInTestFile } from '../helpers/fileExtensions.js'; import { + getClassMembers, getColumns, getEnumVariants, getImportedDeclarations, getImports, - getMatchingClassMembers, getModuleMembers, getPackageName, getParameters, @@ -37,13 +42,8 @@ import { streamBlockLambdaResults, streamPlaceholders, } from '../helpers/nodeProperties.js'; -import { duplicatesBy } from '../../helpers/collectionUtils.js'; -import { isInPipelineFile, isInStubFile, isInTestFile } from '../helpers/fileExtensions.js'; -import { declarationIsAllowedInPipelineFile, declarationIsAllowedInStubFile } from './other/modules.js'; import { SafeDsServices } from '../safe-ds-module.js'; -import { listBuiltinFiles } from '../builtins/fileFinder.js'; -import { BUILTINS_ROOT_PACKAGE } from '../builtins/packageNames.js'; -import { CODEGEN_PREFIX } from '../generation/safe-ds-python-generator.js'; +import { declarationIsAllowedInPipelineFile, declarationIsAllowedInStubFile } from './other/modules.js'; export const CODE_NAME_CODEGEN_PREFIX = 'name/codegen-prefix'; export const CODE_NAME_CASING = 'name/casing'; @@ -184,10 +184,10 @@ export const classMustContainUniqueNames = (node: SdsClass, accept: ValidationAc accept, ); - const instanceMembers = getMatchingClassMembers(node, (it) => !isStatic(it)); + const instanceMembers = getClassMembers(node).filter((it) => !isStatic(it)); namesMustBeUnique(instanceMembers, (name) => `An instance member with name '${name}' exists already.`, accept); - const staticMembers = getMatchingClassMembers(node, isStatic); + const staticMembers = getClassMembers(node).filter(isStatic); namesMustBeUnique(staticMembers, (name) => `A static member with name '${name}' exists already.`, accept); }; diff --git a/packages/safe-ds-lang/src/language/validation/other/argumentLists.ts b/packages/safe-ds-lang/src/language/validation/other/argumentLists.ts index f30a28ad1..3766e21d8 100644 --- a/packages/safe-ds-lang/src/language/validation/other/argumentLists.ts +++ b/packages/safe-ds-lang/src/language/validation/other/argumentLists.ts @@ -1,9 +1,9 @@ -import { isSdsAnnotation, isSdsCall, SdsAbstractCall, SdsArgumentList } from '../../generated/ast.js'; import { getContainerOfType, ValidationAcceptor } from 'langium'; -import { SafeDsServices } from '../../safe-ds-module.js'; -import { getArguments, isRequiredParameter, getParameters } from '../../helpers/nodeProperties.js'; import { duplicatesBy, isEmpty } from '../../../helpers/collectionUtils.js'; import { pluralize } from '../../../helpers/stringUtils.js'; +import { isSdsAnnotation, isSdsCall, SdsAbstractCall, SdsArgumentList } from '../../generated/ast.js'; +import { getArguments, getParameters, Parameter } from '../../helpers/nodeProperties.js'; +import { SafeDsServices } from '../../safe-ds-module.js'; export const CODE_ARGUMENT_LIST_DUPLICATE_PARAMETER = 'argument-list/duplicate-parameter'; export const CODE_ARGUMENT_LIST_MISSING_REQUIRED_PARAMETER = 'argument-list/missing-required-parameter'; @@ -52,7 +52,7 @@ export const argumentListMustNotHaveTooManyArguments = (services: SafeDsServices return; } - const minArgumentCount = parameters.filter((it) => isRequiredParameter(it)).length; + const minArgumentCount = parameters.filter(Parameter.isRequired).length; const kind = pluralize(Math.max(minArgumentCount, maxArgumentCount), 'argument'); if (minArgumentCount === maxArgumentCount) { accept('error', `Expected exactly ${minArgumentCount} ${kind} but got ${actualArgumentCount}.`, { @@ -114,7 +114,7 @@ export const argumentListMustSetAllRequiredParameters = (services: SafeDsService return; } - const expectedParameters = getParameters(callable).filter((it) => isRequiredParameter(it)); + const expectedParameters = getParameters(callable).filter(Parameter.isRequired); if (isEmpty(expectedParameters)) { return; } 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 b33e3845c..7a30eb15b 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 @@ -1,3 +1,5 @@ +import { getContainerOfType, ValidationAcceptor } from 'langium'; +import { isEmpty } from '../../../../helpers/collectionUtils.js'; import { isSdsCallable, isSdsCallableType, @@ -7,16 +9,14 @@ import { SdsLambda, SdsParameter, } from '../../../generated/ast.js'; -import { getContainerOfType, ValidationAcceptor } from 'langium'; import { getAnnotationCalls, getArguments, - isRequiredParameter, getParameters, getResults, + Parameter, } from '../../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../../safe-ds-module.js'; -import { isEmpty } from '../../../../helpers/collectionUtils.js'; export const CODE_ANNOTATION_CALL_CONSTANT_ARGUMENT = 'annotation-call/constant-argument'; export const CODE_ANNOTATION_CALL_MISSING_ARGUMENT_LIST = 'annotation-call/missing-argument-list'; @@ -46,7 +46,7 @@ export const annotationCallMustNotLackArgumentList = (node: SdsAnnotationCall, a return; } - const requiredParameters = getParameters(node.annotation?.ref).filter(isRequiredParameter); + const requiredParameters = getParameters(node.annotation?.ref).filter(Parameter.isRequired); if (!isEmpty(requiredParameters)) { accept( 'error', diff --git a/packages/safe-ds-lang/src/language/validation/other/declarations/parameterLists.ts b/packages/safe-ds-lang/src/language/validation/other/declarations/parameterLists.ts index 662120353..0dfdfc586 100644 --- a/packages/safe-ds-lang/src/language/validation/other/declarations/parameterLists.ts +++ b/packages/safe-ds-lang/src/language/validation/other/declarations/parameterLists.ts @@ -1,5 +1,6 @@ -import { SdsParameterList } from '../../../generated/ast.js'; import { ValidationAcceptor } from 'langium'; +import { SdsParameterList } from '../../../generated/ast.js'; +import { Parameter } from '../../../helpers/nodeProperties.js'; export const CODE_PARAMETER_LIST_REQUIRED_AFTER_OPTIONAL = 'parameter-list/required-after-optional'; @@ -9,7 +10,7 @@ export const parameterListMustNotHaveRequiredParametersAfterOptionalParameters = ) => { let foundOptional = false; for (const parameter of node.parameters) { - if (parameter.defaultValue) { + if (Parameter.isOptional(parameter)) { foundOptional = true; } else if (foundOptional) { accept('error', 'After the first optional parameter all parameters must be optional.', { diff --git a/packages/safe-ds-lang/src/language/validation/other/declarations/parameters.ts b/packages/safe-ds-lang/src/language/validation/other/declarations/parameters.ts index 583e40db5..44eefc6d3 100644 --- a/packages/safe-ds-lang/src/language/validation/other/declarations/parameters.ts +++ b/packages/safe-ds-lang/src/language/validation/other/declarations/parameters.ts @@ -1,6 +1,6 @@ -import { isSdsAnnotation, isSdsCallable, SdsParameter } from '../../../generated/ast.js'; import { getContainerOfType, ValidationAcceptor } from 'langium'; -import { isConstantParameter } from '../../../helpers/nodeProperties.js'; +import { isSdsAnnotation, isSdsCallable, SdsParameter } from '../../../generated/ast.js'; +import { Parameter } from '../../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../../safe-ds-module.js'; export const CODE_PARAMETER_CONSTANT_DEFAULT_VALUE = 'parameter/constant-default-value'; @@ -9,7 +9,7 @@ export const constantParameterMustHaveConstantDefaultValue = (services: SafeDsSe const partialEvaluator = services.evaluation.PartialEvaluator; return (node: SdsParameter, accept: ValidationAcceptor) => { - if (!isConstantParameter(node) || !node.defaultValue) return; + if (!Parameter.isConstant(node) || !node.defaultValue) return; const evaluatedDefaultValue = partialEvaluator.evaluate(node.defaultValue); if (!evaluatedDefaultValue.isFullyEvaluated) { 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 e0f02a66b..463419ab7 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 @@ -1,6 +1,6 @@ -import { SdsCall } from '../../../generated/ast.js'; import { ValidationAcceptor } from 'langium'; -import { getArguments, isConstantParameter } from '../../../helpers/nodeProperties.js'; +import { 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'; @@ -12,7 +12,7 @@ export const callArgumentsMustBeConstantIfParameterIsConstant = (services: SafeD return (node: SdsCall, accept: ValidationAcceptor) => { for (const argument of getArguments(node)) { const parameter = nodeMapper.argumentToParameter(argument); - if (!isConstantParameter(parameter)) continue; + if (!Parameter.isConstant(parameter)) continue; const evaluatedArgumentValue = partialEvaluator.evaluate(argument.value); if (!evaluatedArgumentValue.isFullyEvaluated) { diff --git a/packages/safe-ds-lang/src/language/validation/other/types/callableTypes.ts b/packages/safe-ds-lang/src/language/validation/other/types/callableTypes.ts index 141a58310..e90312541 100644 --- a/packages/safe-ds-lang/src/language/validation/other/types/callableTypes.ts +++ b/packages/safe-ds-lang/src/language/validation/other/types/callableTypes.ts @@ -1,7 +1,7 @@ -import { SdsCallableType } from '../../../generated/ast.js'; import { ValidationAcceptor } from 'langium'; +import { SdsCallableType } from '../../../generated/ast.js'; -import { getParameters } from '../../../helpers/nodeProperties.js'; +import { getParameters, Parameter } from '../../../helpers/nodeProperties.js'; export const CODE_CALLABLE_TYPE_CONST_MODIFIER = 'callable-type/const-modifier'; export const CODE_CALLABLE_TYPE_NO_OPTIONAL_PARAMETERS = 'callable-type/no-optional-parameters'; @@ -23,7 +23,7 @@ export const callableTypeParameterMustNotHaveConstModifier = ( export const callableTypeMustNotHaveOptionalParameters = (node: SdsCallableType, accept: ValidationAcceptor): void => { for (const parameter of getParameters(node)) { - if (parameter.defaultValue) { + if (Parameter.isOptional(parameter)) { accept('error', 'A callable type must not have optional parameters.', { node: parameter, property: 'defaultValue', 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 bce28974e..ec2f87b50 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 @@ -35,7 +35,11 @@ import { typeParameterListsShouldBeUsedWithCaution, unionTypesShouldBeUsedWithCaution, } from './experimentalLanguageFeatures.js'; -import { classMustNotInheritItself, classMustOnlyInheritASingleClass } from './inheritance.js'; +import { + classMemberMustMatchOverriddenMember, + classMustNotInheritItself, + classMustOnlyInheritASingleClass, +} from './inheritance.js'; import { annotationMustContainUniqueNames, blockLambdaMustContainUniqueNames, @@ -224,6 +228,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { classMustNotInheritItself(services), ], SdsClassBody: [classBodyShouldNotBeEmpty], + SdsClassMember: [classMemberMustMatchOverriddenMember(services)], SdsConstraintList: [constraintListsShouldBeUsedWithCaution, constraintListShouldNotBeEmpty], SdsDeclaration: [ nameMustNotStartWithCodegenPrefix, diff --git a/packages/safe-ds-lang/src/language/validation/style.ts b/packages/safe-ds-lang/src/language/validation/style.ts index 239944844..dbd7a4a14 100644 --- a/packages/safe-ds-lang/src/language/validation/style.ts +++ b/packages/safe-ds-lang/src/language/validation/style.ts @@ -1,3 +1,5 @@ +import { ValidationAcceptor } from 'langium'; +import { isEmpty } from '../../helpers/collectionUtils.js'; import { isSdsEnumVariant, isSdsWildcard, @@ -18,12 +20,10 @@ import { SdsTypeParameterList, SdsUnionType, } from '../generated/ast.js'; -import { ValidationAcceptor } from 'langium'; -import { getParameters, getTypeParameters, isRequiredParameter } from '../helpers/nodeProperties.js'; +import { getParameters, getTypeParameters, Parameter } from '../helpers/nodeProperties.js'; +import { NullConstant } from '../partialEvaluation/model.js'; import { SafeDsServices } from '../safe-ds-module.js'; import { UnknownType } from '../typing/model.js'; -import { NullConstant } from '../partialEvaluation/model.js'; -import { isEmpty } from '../../helpers/collectionUtils.js'; export const CODE_STYLE_UNNECESSARY_ASSIGNMENT = 'style/unnecessary-assignment'; export const CODE_STYLE_UNNECESSARY_ARGUMENT_LIST = 'style/unnecessary-argument-list'; @@ -55,7 +55,7 @@ export const annotationCallArgumentListShouldBeNeeded = (node: SdsAnnotationCall return; } - const hasRequiredParameters = getParameters(annotation).some(isRequiredParameter); + const hasRequiredParameters = getParameters(annotation).some(Parameter.isRequired); if (!hasRequiredParameters) { accept('info', 'This argument list can be removed.', { node: argumentList, diff --git a/packages/safe-ds-lang/tests/language/typing/model.test.ts b/packages/safe-ds-lang/tests/language/typing/model.test.ts index 23bab2baf..58fdf0a8d 100644 --- a/packages/safe-ds-lang/tests/language/typing/model.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/model.test.ts @@ -21,7 +21,7 @@ import { getNodeOfType } from '../../helpers/nodeFinder.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; const code = ` - fun f1(p) -> r + fun f1(p1, p2: Int = 0) -> r fun f2(), class C1 @@ -34,7 +34,8 @@ const code = ` enum MyEnum2 {} `; const callable1 = await getNodeOfType(services, code, isSdsFunction, 0); -const parameter = getParameters(callable1)[0]!; +const parameter1 = getParameters(callable1)[0]!; +const parameter2 = getParameters(callable1)[1]!; const result = getResults(callable1.resultList)[0]!; const callable2 = await getNodeOfType(services, code, isSdsFunction, 1); const class1 = await getNodeOfType(services, code, isSdsClass, 0); @@ -50,7 +51,7 @@ describe('type model', async () => { type: () => new CallableType( callable1, - new NamedTupleType(new NamedTupleEntry(parameter, 'p', UnknownType)), + new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', UnknownType)), new NamedTupleType(), ), unequalTypeOfSameType: () => new CallableType(callable2, new NamedTupleType(), new NamedTupleType()), @@ -62,7 +63,7 @@ describe('type model', async () => { typeOfOtherType: () => UnknownType, }, { - type: () => new NamedTupleType(new NamedTupleEntry(parameter, 'p', UnknownType)), + type: () => new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', UnknownType)), unequalTypeOfSameType: () => new NamedTupleType(), typeOfOtherType: () => UnknownType, }, @@ -121,18 +122,26 @@ describe('type model', async () => { { type: new CallableType( callable1, - new NamedTupleType(new NamedTupleEntry(parameter, 'p', UnknownType)), + new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', UnknownType)), new NamedTupleType(), ), - expectedString: '(p: ?) -> ()', + expectedString: '(p1: ?) -> ()', + }, + { + type: new CallableType( + callable1, + new NamedTupleType(new NamedTupleEntry(parameter2, 'p2', UnknownType)), + new NamedTupleType(), + ), + expectedString: '(p2?: ?) -> ()', }, { type: new LiteralType(new BooleanConstant(true)), expectedString: 'literal', }, { - type: new NamedTupleType(new NamedTupleEntry(parameter, 'p', UnknownType)), - expectedString: '(p: ?)', + type: new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', UnknownType)), + expectedString: '(p1: ?)', }, { type: new ClassType(class1, true), @@ -173,12 +182,12 @@ describe('type model', async () => { { type: new CallableType( callable1, - new NamedTupleType(new NamedTupleEntry(parameter, 'p', new UnionType(UnknownType))), + new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', new UnionType(UnknownType))), new NamedTupleType(new NamedTupleEntry(result, 'r', new UnionType(UnknownType))), ), expectedType: new CallableType( callable1, - new NamedTupleType(new NamedTupleEntry(parameter, 'p', UnknownType)), + new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', UnknownType)), new NamedTupleType(new NamedTupleEntry(result, 'r', UnknownType)), ), }, @@ -192,12 +201,12 @@ describe('type model', async () => { }, { type: new NamedTupleType( - new NamedTupleEntry(parameter, 'p', new UnionType(UnknownType)), - new NamedTupleEntry(parameter, 'p', new UnionType(UnknownType)), + new NamedTupleEntry(parameter1, 'p1', new UnionType(UnknownType)), + new NamedTupleEntry(parameter1, 'p1', new UnionType(UnknownType)), ), expectedType: new NamedTupleType( - new NamedTupleEntry(parameter, 'p', UnknownType), - new NamedTupleEntry(parameter, 'p', UnknownType), + new NamedTupleEntry(parameter1, 'p1', UnknownType), + new NamedTupleEntry(parameter1, 'p1', UnknownType), ), }, { @@ -381,7 +390,7 @@ describe('type model', async () => { { type: new CallableType( callable1, - new NamedTupleType(new NamedTupleEntry(parameter, 'p', new ClassType(class1, false))), + new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', new ClassType(class1, false))), new NamedTupleType(), ), index: 0, @@ -402,7 +411,7 @@ describe('type model', async () => { expectedType: UnknownType, }, { - type: new NamedTupleType(new NamedTupleEntry(parameter, 'p', new ClassType(class1, false))), + type: new NamedTupleType(new NamedTupleEntry(parameter1, 'p1', new ClassType(class1, false))), index: 0, expectedType: new ClassType(class1, false), }, diff --git a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts index f2cfebbe4..0e5453704 100644 --- a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts @@ -1,10 +1,16 @@ import { NodeFileSystem } from 'langium/node'; import { clearDocuments } from 'langium/test'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js'; +import { + isSdsAttribute, + isSdsClass, + isSdsFunction, + SdsClass, + type SdsClassMember, +} from '../../../src/language/generated/ast.js'; +import { getClassMembers } from '../../../src/language/helpers/nodeProperties.js'; import { createSafeDsServices } from '../../../src/language/index.js'; import { getNodeOfType } from '../../helpers/nodeFinder.js'; -import { getMatchingClassMembers } from '../../../src/language/helpers/nodeProperties.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; const builtinClasses = services.builtins.Classes; @@ -198,8 +204,240 @@ describe('SafeDsClassHierarchy', async () => { it.each(testCases)('$testName', async ({ code, index, expected }) => { const firstClass = await getNodeOfType(services, code, isSdsClass, index); - const anyMembers = getMatchingClassMembers(builtinClasses.Any).map((member) => member.name); + const anyMembers = getClassMembers(builtinClasses.Any).map((member) => member.name); expect(superclassMemberNames(firstClass)).toStrictEqual(expected.concat(anyMembers)); }); }); + + describe('getOverriddenMember', () => { + const isUndefined = (result: unknown) => result === undefined; + + const testCases: GetOverriddenMemberTest[] = [ + { + testName: 'global function', + code: ` + fun f() + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'attribute, no superclass', + code: ` + class A { + attr a: Int + } + `, + memberPredicate: isSdsAttribute, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'method, no superclass', + code: ` + class A { + fun f() + } + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'attribute, superclass, no match', + code: ` + class A sub B { + attr a: Int + } + + class B { + attr b: Int + } + `, + memberPredicate: isSdsAttribute, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'method, superclass, no match', + code: ` + class A sub B { + fun f() + } + + class B { + fun g() + } + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'static attribute', + code: ` + class A sub B { + static attr a: Int + } + + class B { + static attr a: Int + } + `, + memberPredicate: isSdsAttribute, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'static method', + code: ` + class A sub B { + static fun f() + } + + class B { + static fun f() + } + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'attribute, superclass, match but static', + code: ` + class A sub B { + attr a: Int + } + + class B { + static attr a: Int + } + `, + memberPredicate: isSdsAttribute, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'method, superclass, match but static', + code: ` + class A sub B { + fun f() + } + + class B { + static fun f() + } + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isUndefined, + }, + { + testName: 'attribute, superclass, match', + code: ` + class A sub B { + attr a: Int + } + + class B { + attr a: Int + } + `, + memberPredicate: isSdsAttribute, + index: 0, + expectedResultPredicate: isSdsAttribute, + }, + { + testName: 'method, superclass, match', + code: ` + class A sub B { + fun f() + } + + class B { + fun f() + } + `, + memberPredicate: isSdsFunction, + index: 0, + expectedResultPredicate: isSdsFunction, + }, + { + testName: 'attribute, previous member with same name', + code: ` + class A sub B { + attr a: Int + attr a: Int + } + + class B { + attr a: Int + } + `, + memberPredicate: isSdsAttribute, + index: 1, + expectedResultPredicate: isUndefined, + }, + { + testName: 'method, previous member with same name', + code: ` + class A sub B { + fun f() + fun f() + } + + class B { + fun f() + } + `, + memberPredicate: isSdsFunction, + index: 1, + expectedResultPredicate: isUndefined, + }, + ]; + + it.each(testCases)( + 'should return the overridden member or undefined ($testName)', + async ({ code, memberPredicate, index, expectedResultPredicate }) => { + const member = await getNodeOfType(services, code, memberPredicate, index); + expect(classHierarchy.getOverriddenMember(member)).toSatisfy(expectedResultPredicate); + }, + ); + + it('should return undefined if passed undefined', () => { + expect(classHierarchy.getOverriddenMember(undefined)).toBeUndefined(); + }); + }); }); + +/** + * A test case for {@link ClassHierarchy.getOverriddenMember}. + */ +interface GetOverriddenMemberTest { + /** + * A short description of the test case. + */ + testName: string; + + /** + * The code to parse. + */ + code: string; + + /** + * A predicate that matches the member that should be used as input. + */ + memberPredicate: (value: unknown) => value is SdsClassMember; + + /** + * The index of the member to use as input. + */ + index: number; + + /** + * A predicate that matches the expected result. + */ + expectedResultPredicate: (value: unknown) => boolean; +} diff --git a/packages/safe-ds-lang/tests/language/typing/safe-ds-type-checker.test.ts b/packages/safe-ds-lang/tests/language/typing/safe-ds-type-checker.test.ts index 8fd522e2b..6fb57ef7f 100644 --- a/packages/safe-ds-lang/tests/language/typing/safe-ds-type-checker.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/safe-ds-type-checker.test.ts @@ -112,6 +112,16 @@ describe('SafeDsTypeChecker', async () => { type2: callableType2, expected: false, }, + { + type1: callableType2, + type2: callableType3, + expected: true, + }, + { + type1: callableType3, + type2: callableType2, + expected: false, + }, { type1: callableType3, type2: callableType1, diff --git a/packages/safe-ds-lang/tests/language/validation/safe-ds-validator.test.ts b/packages/safe-ds-lang/tests/language/validation/safe-ds-validator.test.ts index 8d77d0d0d..54b039884 100644 --- a/packages/safe-ds-lang/tests/language/validation/safe-ds-validator.test.ts +++ b/packages/safe-ds-lang/tests/language/validation/safe-ds-validator.test.ts @@ -1,12 +1,12 @@ -import { afterEach, beforeEach, describe, it } from 'vitest'; -import { createSafeDsServices } from '../../../src/language/index.js'; -import { NodeFileSystem } from 'langium/node'; -import { createValidationTests, ExpectedIssue } from './creator.js'; -import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'; import { AssertionError } from 'assert'; +import { NodeFileSystem } from 'langium/node'; import { clearDocuments, isRangeEqual } from 'langium/test'; +import { afterEach, beforeEach, describe, it } from 'vitest'; +import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; import { locationToString } from '../../helpers/location.js'; import { loadDocuments } from '../../helpers/testResources.js'; +import { createValidationTests, ExpectedIssue } from './creator.js'; const services = createSafeDsServices(NodeFileSystem).SafeDs; @@ -94,7 +94,7 @@ const getMatchingActualIssues = (expectedIssue: ExpectedIssue): Diagnostic[] => // Filter by message if (expectedIssue.message) { if (expectedIssue.messageIsRegex) { - const regex = new RegExp(expectedIssue.message, 'gu'); + const regex = new RegExp(expectedIssue.message, 'u'); result = result.filter((d) => regex.test(d.message)); } else { result = result.filter((d) => d.message === expectedIssue.message); diff --git a/packages/safe-ds-lang/tests/resources/validation/inheritance/overriding method must match overridden method/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/inheritance/overriding method must match overridden method/main.sdstest new file mode 100644 index 000000000..29fe514fc --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/inheritance/overriding method must match overridden method/main.sdstest @@ -0,0 +1,75 @@ +package tests.validation.inheritance.overridingMethodMustMatchOverriddenMethod + +class MySuperClass { + attr myInstanceAttribute: Int + static attr myStaticAttribute: Int + + fun myInstanceMethod(a: Int = 0) -> r: Int + static fun myStaticMethod(a: Int = 0) -> r: Int +} + +class MyClass1 sub MySuperClass { + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myInstanceAttribute«: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myStaticAttribute«: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myInstanceAttribute«: String + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myStaticAttribute«: String + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myOwnInstanceAttribute«: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myOwnStaticAttribute«: Int + + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myInstanceMethod«(a: Int = 0) -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myStaticMethod«(a: Int = 0) -> r: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myInstanceMethod«() -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myStaticMethod«() -> r: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myOwnInstanceMethod«(a: Int = 0) -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myOwnStaticMethod«(a: Int = 0) -> r: Int +} + +class MyClass2 sub MySuperClass { + // $TEST$ error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myInstanceAttribute«: String + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myStaticAttribute«: String + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myInstanceAttribute«: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myStaticAttribute«: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + attr »myOwnInstanceAttribute«: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static attr »myOwnStaticAttribute«: Int + + + // $TEST$ error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myInstanceMethod«() -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myStaticMethod«() -> r: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myInstanceMethod«(a: Int = 0) -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myStaticMethod«(a: Int = 0) -> r: Int + + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + fun »myOwnInstanceMethod«(a: Int = 0) -> r: Int + // $TEST$ no error r"Overriding member does not match the overridden member:[\s\S]*" + static fun »myOwnStaticMethod«(a: Int = 0) -> r: Int +} diff --git a/packages/safe-ds-lang/tests/resources/validation/types/named types/missing type parameter/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/types/named types/missing type parameter/main.sdstest index 6af03c3e6..809e61396 100644 --- a/packages/safe-ds-lang/tests/resources/validation/types/named types/missing type parameter/main.sdstest +++ b/packages/safe-ds-lang/tests/resources/validation/types/named types/missing type parameter/main.sdstest @@ -30,7 +30,7 @@ fun myFunction( p7: MyClassWithTwoTypeParameters»<>«, // $TEST$ error "The type parameter 'V' must be set here." p8: MyClassWithTwoTypeParameters»«, - // $TEST$ no error r"The type parameters? .* must be set here\." + // $TEST$ error "The type parameters 'K', 'V' must be set here." p9: MyClassWithTwoTypeParameters»«, @@ -52,7 +52,7 @@ fun myFunction( q7: MyEnum.MyEnumVariantWithTwoTypeParameters»<>«, // $TEST$ error "The type parameter 'V' must be set here." q8: MyEnum.MyEnumVariantWithTwoTypeParameters»«, - // $TEST$ no error r"The type parameters? .* must be set here\." + // $TEST$ error "The type parameters 'K', 'V' must be set here." q9: MyEnum.MyEnumVariantWithTwoTypeParameters»«, diff --git a/packages/safe-ds-vscode/snippets/safe-ds.json b/packages/safe-ds-vscode/snippets/safe-ds.json index 0679b88a7..c49ee0dc7 100644 --- a/packages/safe-ds-vscode/snippets/safe-ds.json +++ b/packages/safe-ds-vscode/snippets/safe-ds.json @@ -35,7 +35,7 @@ "description": "An enumeration." }, "Minimal Function": { - "prefix": ["minimal-function"], + "prefix": ["minimal-function", "minimal-method"], "body": ["fun ${1:myFunction}($0)"], "description": "A minimal function." },