Skip to content

Commit a698a6a

Browse files
authored
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.
1 parent fd6f432 commit a698a6a

File tree

25 files changed

+543
-131
lines changed

25 files changed

+543
-131
lines changed

packages/safe-ds-lang/src/language/generation/safe-ds-python-generator.ts

+28-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1-
import { SafeDsServices } from '../safe-ds-module.js';
1+
import {
2+
CompositeGeneratorNode,
3+
expandToNode,
4+
expandTracedToNode,
5+
findRootNode,
6+
getContainerOfType,
7+
getDocument,
8+
joinToNode,
9+
joinTracedToNode,
10+
LangiumDocument,
11+
NL,
12+
streamAllContents,
13+
toStringAndTrace,
14+
TraceRegion,
15+
traceToNode,
16+
TreeStreamImpl,
17+
URI,
18+
} from 'langium';
19+
import path from 'path';
20+
import { SourceMapGenerator, StartOfSourceMap } from 'source-map';
21+
import { TextDocument } from 'vscode-languageserver-textdocument';
22+
import { groupBy } from '../../helpers/collectionUtils.js';
23+
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
224
import {
325
isSdsAbstractResult,
426
isSdsAssignment,
@@ -46,49 +68,27 @@ import {
4668
SdsStatement,
4769
} from '../generated/ast.js';
4870
import { isInStubFile, isStubFile } from '../helpers/fileExtensions.js';
49-
import path from 'path';
50-
import {
51-
CompositeGeneratorNode,
52-
expandToNode,
53-
expandTracedToNode,
54-
findRootNode,
55-
getContainerOfType,
56-
getDocument,
57-
joinToNode,
58-
joinTracedToNode,
59-
LangiumDocument,
60-
NL,
61-
streamAllContents,
62-
toStringAndTrace,
63-
TraceRegion,
64-
traceToNode,
65-
TreeStreamImpl,
66-
URI,
67-
} from 'langium';
71+
import { IdManager } from '../helpers/idManager.js';
6872
import {
6973
getAbstractResults,
7074
getAssignees,
7175
getImportedDeclarations,
7276
getImports,
7377
getModuleMembers,
7478
getStatements,
75-
isRequiredParameter,
79+
Parameter,
7680
streamBlockLambdaResults,
7781
} from '../helpers/nodeProperties.js';
78-
import { groupBy } from '../../helpers/collectionUtils.js';
82+
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
7983
import {
8084
BooleanConstant,
8185
FloatConstant,
8286
IntConstant,
8387
NullConstant,
8488
StringConstant,
8589
} from '../partialEvaluation/model.js';
86-
import { IdManager } from '../helpers/idManager.js';
87-
import { TextDocument } from 'vscode-languageserver-textdocument';
88-
import { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
89-
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
9090
import { SafeDsPartialEvaluator } from '../partialEvaluation/safe-ds-partial-evaluator.js';
91-
import { SourceMapGenerator, StartOfSourceMap } from 'source-map';
91+
import { SafeDsServices } from '../safe-ds-module.js';
9292

9393
export const CODEGEN_PREFIX = '__gen_';
9494
const BLOCK_LAMBDA_PREFIX = `${CODEGEN_PREFIX}block_lambda_`;
@@ -685,7 +685,7 @@ export class SafeDsPythonGenerator {
685685
private generateArgument(argument: SdsArgument, frame: GenerationInfoFrame): CompositeGeneratorNode {
686686
const parameter = this.nodeMapper.argumentToParameter(argument);
687687
return expandTracedToNode(argument)`${
688-
parameter !== undefined && !isRequiredParameter(parameter)
688+
parameter !== undefined && !Parameter.isRequired(parameter)
689689
? expandToNode`${this.generateParameter(parameter, frame, false)}=`
690690
: ''
691691
}${this.generateExpression(argument.value, frame)}`;

packages/safe-ds-lang/src/language/helpers/nodeProperties.ts

+26-22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isSdsLambda,
1717
isSdsModule,
1818
isSdsModuleMember,
19+
isSdsParameter,
1920
isSdsPlaceholder,
2021
isSdsSegment,
2122
isSdsTypeParameterList,
@@ -85,28 +86,30 @@ export const isPositionalArgument = (node: SdsArgument): boolean => {
8586
return !node.parameter;
8687
};
8788

88-
export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => {
89-
return Boolean(node.typeParameter);
90-
};
89+
export namespace Parameter {
90+
export const isConstant = (node: SdsParameter | undefined): boolean => {
91+
if (!node) {
92+
return false;
93+
}
9194

92-
export const isConstantParameter = (node: SdsParameter | undefined): boolean => {
93-
if (!node) {
94-
return false;
95-
}
95+
const containingCallable = getContainerOfType(node, isSdsCallable);
9696

97-
const containingCallable = getContainerOfType(node, isSdsCallable);
97+
// In those cases, the const modifier is not applicable
98+
if (isSdsCallableType(containingCallable) || isSdsLambda(containingCallable)) {
99+
return false;
100+
}
98101

99-
// In those cases, the const modifier is not applicable
100-
if (isSdsCallableType(containingCallable) || isSdsLambda(containingCallable)) {
101-
return false;
102-
}
102+
return isSdsAnnotation(containingCallable) || node.isConstant;
103+
};
103104

104-
return isSdsAnnotation(containingCallable) || node.isConstant;
105-
};
105+
export const isOptional = (node: SdsParameter | undefined): boolean => {
106+
return Boolean(node?.defaultValue);
107+
};
106108

107-
export const isRequiredParameter = (node: SdsParameter): boolean => {
108-
return !node.defaultValue;
109-
};
109+
export const isRequired = (node: SdsParameter | undefined): boolean => {
110+
return isSdsParameter(node) && !node.defaultValue;
111+
};
112+
}
110113

111114
export const isStatic = (node: SdsClassMember): boolean => {
112115
if (isSdsClass(node) || isSdsEnum(node)) {
@@ -121,6 +124,10 @@ export const isStatic = (node: SdsClassMember): boolean => {
121124
}
122125
};
123126

127+
export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => {
128+
return Boolean(node.typeParameter);
129+
};
130+
124131
// -------------------------------------------------------------------------------------------------
125132
// Accessors for list elements
126133
// -------------------------------------------------------------------------------------------------
@@ -190,11 +197,8 @@ export const streamBlockLambdaResults = (node: SdsBlockLambda | undefined): Stre
190197
.filter(isSdsBlockLambdaResult);
191198
};
192199

193-
export const getMatchingClassMembers = (
194-
node: SdsClass | undefined,
195-
filterFunction: (member: SdsClassMember) => boolean = () => true,
196-
): SdsClassMember[] => {
197-
return node?.body?.members?.filter(filterFunction) ?? [];
200+
export const getClassMembers = (node: SdsClass | undefined): SdsClassMember[] => {
201+
return node?.body?.members ?? [];
198202
};
199203

200204
export const getColumns = (node: SdsSchema | undefined): SdsColumn[] => {

packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ import {
5757
getAbstractResults,
5858
getAnnotationCallTarget,
5959
getAssignees,
60+
getClassMembers,
6061
getEnumVariants,
6162
getImportedDeclarations,
6263
getImports,
63-
getMatchingClassMembers,
6464
getPackageName,
6565
getParameters,
6666
getResults,
@@ -185,7 +185,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
185185
// Static access
186186
const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver);
187187
if (isSdsClass(declaration)) {
188-
const ownStaticMembers = getMatchingClassMembers(declaration, isStatic);
188+
const ownStaticMembers = getClassMembers(declaration).filter(isStatic);
189189
const superclassStaticMembers = this.classHierarchy.streamSuperclassMembers(declaration).filter(isStatic);
190190

191191
return this.createScopeForNodes(ownStaticMembers, this.createScopeForNodes(superclassStaticMembers));
@@ -215,7 +215,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
215215
}
216216

217217
if (receiverType instanceof ClassType) {
218-
const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it));
218+
const ownInstanceMembers = getClassMembers(receiverType.declaration).filter((it) => !isStatic(it));
219219
const superclassInstanceMembers = this.classHierarchy
220220
.streamSuperclassMembers(receiverType.declaration)
221221
.filter((it) => !isStatic(it));

packages/safe-ds-lang/src/language/typing/model.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
SdsEnumVariant,
99
SdsParameter,
1010
} from '../generated/ast.js';
11+
import { Parameter } from '../helpers/nodeProperties.js';
1112
import { Constant, NullConstant } from '../partialEvaluation/model.js';
1213

1314
/**
@@ -73,7 +74,11 @@ export class CallableType extends Type {
7374
}
7475

7576
override toString(): string {
76-
return `${this.inputType} -> ${this.outputType}`;
77+
const inputTypeString = this.inputType.entries
78+
.map((it) => `${it.name}${Parameter.isOptional(it.declaration) ? '?' : ''}: ${it.type}`)
79+
.join(', ');
80+
81+
return `(${inputTypeString}) -> ${this.outputType}`;
7782
}
7883

7984
override unwrap(): CallableType {

packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { EMPTY_STREAM, stream, Stream } from 'langium';
1+
import { EMPTY_STREAM, getContainerOfType, stream, Stream } from 'langium';
22
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
33
import { isSdsClass, isSdsNamedType, SdsClass, type SdsClassMember } from '../generated/ast.js';
4-
import { getMatchingClassMembers, getParentTypes } from '../helpers/nodeProperties.js';
4+
import { getClassMembers, getParentTypes, isStatic } from '../helpers/nodeProperties.js';
55
import { SafeDsServices } from '../safe-ds-module.js';
66

77
export class SafeDsClassHierarchy {
@@ -61,7 +61,7 @@ export class SafeDsClassHierarchy {
6161
return EMPTY_STREAM;
6262
}
6363

64-
return this.streamSuperclasses(node).flatMap(getMatchingClassMembers);
64+
return this.streamSuperclasses(node).flatMap(getClassMembers);
6565
}
6666

6767
/**
@@ -79,4 +79,31 @@ export class SafeDsClassHierarchy {
7979

8080
return undefined;
8181
}
82+
83+
/**
84+
* Returns the member that is overridden by the given member, or `undefined` if the member does not override
85+
* anything.
86+
*/
87+
getOverriddenMember(node: SdsClassMember | undefined): SdsClassMember | undefined {
88+
// Static members cannot override anything
89+
if (!node || isStatic(node)) {
90+
return undefined;
91+
}
92+
93+
// Don't consider members with the same name as a previous member
94+
const containingClass = getContainerOfType(node, isSdsClass);
95+
if (!containingClass) {
96+
return undefined;
97+
}
98+
const firstMemberWithSameName = getClassMembers(containingClass).find(
99+
(it) => !isStatic(it) && it.name === node.name,
100+
);
101+
if (firstMemberWithSameName !== node) {
102+
return undefined;
103+
}
104+
105+
return this.streamSuperclassMembers(containingClass)
106+
.filter((it) => !isStatic(it) && it.name === node.name)
107+
.head();
108+
}
82109
}

packages/safe-ds-lang/src/language/typing/safe-ds-type-checker.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getContainerOfType } from 'langium';
22
import type { SafeDsClasses } from '../builtins/safe-ds-classes.js';
33
import { isSdsEnum, type SdsAbstractResult, SdsDeclaration } from '../generated/ast.js';
4-
import { getParameters } from '../helpers/nodeProperties.js';
4+
import { getParameters, Parameter } from '../helpers/nodeProperties.js';
55
import { Constant } from '../partialEvaluation/model.js';
66
import { SafeDsServices } from '../safe-ds-module.js';
77
import {
@@ -84,6 +84,11 @@ export class SafeDsTypeChecker {
8484
return false;
8585
}
8686

87+
// Optionality must match (all but required to optional is OK)
88+
if (Parameter.isRequired(typeEntry.declaration) && Parameter.isOptional(otherEntry.declaration)) {
89+
return false;
90+
}
91+
8792
// Types must be contravariant
8893
if (!this.isAssignableTo(otherEntry.type, typeEntry.type)) {
8994
return false;
@@ -93,7 +98,7 @@ export class SafeDsTypeChecker {
9398
// Additional parameters must be optional
9499
for (let i = other.inputType.length; i < type.inputType.length; i++) {
95100
const typeEntry = type.inputType.entries[i]!;
96-
if (!typeEntry.declaration?.defaultValue) {
101+
if (!Parameter.isOptional(typeEntry.declaration)) {
97102
return false;
98103
}
99104
}

packages/safe-ds-lang/src/language/validation/builtins/deprecated.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ValidationAcceptor } from 'langium';
2+
import { DiagnosticTag } from 'vscode-languageserver';
23
import {
34
isSdsParameter,
45
isSdsResult,
@@ -10,10 +11,9 @@ import {
1011
SdsParameter,
1112
SdsReference,
1213
} from '../../generated/ast.js';
14+
import { Parameter } from '../../helpers/nodeProperties.js';
1315
import { SafeDsServices } from '../../safe-ds-module.js';
14-
import { isRequiredParameter } from '../../helpers/nodeProperties.js';
1516
import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js';
16-
import { DiagnosticTag } from 'vscode-languageserver';
1717

1818
export const CODE_DEPRECATED_ASSIGNED_RESULT = 'deprecated/assigned-result';
1919
export const CODE_DEPRECATED_CALLED_ANNOTATION = 'deprecated/called-annotation';
@@ -108,7 +108,7 @@ export const referenceTargetShouldNotBeDeprecated =
108108

109109
export const requiredParameterMustNotBeDeprecated =
110110
(services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => {
111-
if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) {
111+
if (Parameter.isRequired(node) && parameterCanBeAnnotated(node)) {
112112
if (services.builtins.Annotations.isDeprecated(node)) {
113113
accept('error', 'A deprecated parameter must be optional.', {
114114
node,

packages/safe-ds-lang/src/language/validation/builtins/expert.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { ValidationAcceptor } from 'langium';
22
import { SdsParameter } from '../../generated/ast.js';
3+
import { Parameter } from '../../helpers/nodeProperties.js';
34
import { SafeDsServices } from '../../safe-ds-module.js';
4-
import { isRequiredParameter } from '../../helpers/nodeProperties.js';
55
import { parameterCanBeAnnotated } from '../other/declarations/annotationCalls.js';
66

77
export const CODE_EXPERT_TARGET_PARAMETER = 'expert/target-parameter';
88

99
export const requiredParameterMustNotBeExpert =
1010
(services: SafeDsServices) => (node: SdsParameter, accept: ValidationAcceptor) => {
11-
if (isRequiredParameter(node) && parameterCanBeAnnotated(node)) {
11+
if (Parameter.isRequired(node) && parameterCanBeAnnotated(node)) {
1212
if (services.builtins.Annotations.isExpert(node)) {
1313
accept('error', 'An expert parameter must be optional.', {
1414
node,

packages/safe-ds-lang/src/language/validation/inheritance.ts

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
1-
import { ValidationAcceptor } from 'langium';
2-
import { SdsClass } from '../generated/ast.js';
1+
import { expandToStringWithNL, ValidationAcceptor } from 'langium';
2+
import { isEmpty } from '../../helpers/collectionUtils.js';
3+
import { SdsClass, type SdsClassMember } from '../generated/ast.js';
34
import { getParentTypes } from '../helpers/nodeProperties.js';
45
import { SafeDsServices } from '../safe-ds-module.js';
56
import { ClassType, UnknownType } from '../typing/model.js';
6-
import { isEmpty } from '../../helpers/collectionUtils.js';
77

88
export const CODE_INHERITANCE_CYCLE = 'inheritance/cycle';
99
export const CODE_INHERITANCE_MULTIPLE_INHERITANCE = 'inheritance/multiple-inheritance';
10+
export const CODE_INHERITANCE_MUST_MATCH_OVERRIDDEN_MEMBER = 'inheritance/must-match-overridden-member';
1011
export const CODE_INHERITANCE_NOT_A_CLASS = 'inheritance/not-a-class';
1112

13+
export const classMemberMustMatchOverriddenMember = (services: SafeDsServices) => {
14+
const classHierarchy = services.types.ClassHierarchy;
15+
const typeChecker = services.types.TypeChecker;
16+
const typeComputer = services.types.TypeComputer;
17+
18+
return (node: SdsClassMember, accept: ValidationAcceptor): void => {
19+
const overriddenMember = classHierarchy.getOverriddenMember(node);
20+
if (!overriddenMember) {
21+
return;
22+
}
23+
24+
const ownMemberType = typeComputer.computeType(node);
25+
const overriddenMemberType = typeComputer.computeType(overriddenMember);
26+
27+
if (!typeChecker.isAssignableTo(ownMemberType, overriddenMemberType)) {
28+
accept(
29+
'error',
30+
expandToStringWithNL`
31+
Overriding member does not match the overridden member:
32+
- Expected type: ${overriddenMemberType}
33+
- Actual type: ${ownMemberType}
34+
`,
35+
{
36+
node,
37+
property: 'name',
38+
code: CODE_INHERITANCE_MUST_MATCH_OVERRIDDEN_MEMBER,
39+
},
40+
);
41+
}
42+
};
43+
};
44+
1245
export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => {
1346
const typeComputer = services.types.TypeComputer;
1447
const computeType = typeComputer.computeType.bind(typeComputer);

0 commit comments

Comments
 (0)