-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: various checks related to inheritance (#633)
Closes partially #543 ### Summary of Changes Show an error if a class * inherits from multiple types, * inherits from something that is not a class, or * inherits from itself. --------- Co-authored-by: megalinter-bot <[email protected]>
- Loading branch information
1 parent
b72768c
commit 7ec746a
Showing
15 changed files
with
322 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { SafeDsServices } from '../safe-ds-module.js'; | ||
import { SafeDsClasses } from '../builtins/safe-ds-classes.js'; | ||
import { SdsClass } from '../generated/ast.js'; | ||
import { stream, Stream } from 'langium'; | ||
import { parentTypesOrEmpty } from '../helpers/nodeProperties.js'; | ||
import { SafeDsTypeComputer } from './safe-ds-type-computer.js'; | ||
import { ClassType } from './model.js'; | ||
|
||
export class SafeDsClassHierarchy { | ||
private readonly builtinClasses: SafeDsClasses; | ||
private readonly typeComputer: SafeDsTypeComputer; | ||
|
||
constructor(services: SafeDsServices) { | ||
this.builtinClasses = services.builtins.Classes; | ||
this.typeComputer = services.types.TypeComputer; | ||
} | ||
|
||
/** | ||
* Returns a stream of all superclasses of the given class. The class itself is not included in the stream unless | ||
* there is a cycle in the inheritance hierarchy. Direct ancestors are returned first, followed by their ancestors | ||
* and so on. | ||
*/ | ||
streamSuperclasses(node: SdsClass | undefined): Stream<SdsClass> { | ||
if (!node) { | ||
return stream(); | ||
} | ||
|
||
const capturedThis = this; | ||
const generator = function* () { | ||
const visited = new Set<SdsClass>(); | ||
let current = capturedThis.parentClassOrUndefined(node); | ||
while (current && !visited.has(current)) { | ||
yield current; | ||
visited.add(current); | ||
current = capturedThis.parentClassOrUndefined(current); | ||
} | ||
|
||
const anyClass = capturedThis.builtinClasses.Any; | ||
if (anyClass && node !== anyClass && !visited.has(anyClass)) { | ||
yield anyClass; | ||
} | ||
}; | ||
|
||
return stream(generator()); | ||
} | ||
|
||
/** | ||
* Returns the parent class of the given class, or undefined if there is no parent class. Only the first parent | ||
* type is considered, i.e. multiple inheritance is not supported. | ||
*/ | ||
private parentClassOrUndefined(node: SdsClass | undefined): SdsClass | undefined { | ||
const [firstParentType] = parentTypesOrEmpty(node); | ||
const computedType = this.typeComputer.computeType(firstParentType); | ||
if (computedType instanceof ClassType) { | ||
return computedType.sdsClass; | ||
} | ||
|
||
return undefined; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { ValidationAcceptor } from 'langium'; | ||
import { SdsClass } from '../generated/ast.js'; | ||
import { parentTypesOrEmpty } from '../helpers/nodeProperties.js'; | ||
import { isEmpty } from 'radash'; | ||
import { SafeDsServices } from '../safe-ds-module.js'; | ||
import { ClassType, UnknownType } from '../typing/model.js'; | ||
|
||
export const CODE_INHERITANCE_CYCLE = 'inheritance/cycle'; | ||
export const CODE_INHERITANCE_MULTIPLE_INHERITANCE = 'inheritance/multiple-inheritance'; | ||
export const CODE_INHERITANCE_NOT_A_CLASS = 'inheritance/not-a-class'; | ||
|
||
export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => { | ||
const typeComputer = services.types.TypeComputer; | ||
const computeType = typeComputer.computeType.bind(typeComputer); | ||
|
||
return (node: SdsClass, accept: ValidationAcceptor): void => { | ||
const parentTypes = parentTypesOrEmpty(node); | ||
if (isEmpty(parentTypes)) { | ||
return; | ||
} | ||
|
||
const [firstParentType, ...otherParentTypes] = parentTypes; | ||
|
||
// First parent type must be a class | ||
const computedType = computeType(firstParentType); | ||
if (computedType !== UnknownType && !(computedType instanceof ClassType)) { | ||
accept('error', 'A class must only inherit classes.', { | ||
node: firstParentType, | ||
code: CODE_INHERITANCE_NOT_A_CLASS, | ||
}); | ||
} | ||
|
||
// Must have only one parent type | ||
for (const parentType of otherParentTypes) { | ||
accept('error', 'Multiple inheritance is not supported. Only the first parent type will be considered.', { | ||
node: parentType, | ||
code: CODE_INHERITANCE_MULTIPLE_INHERITANCE, | ||
}); | ||
} | ||
}; | ||
}; | ||
|
||
export const classMustNotInheritItself = (services: SafeDsServices) => { | ||
const classHierarchy = services.types.ClassHierarchy; | ||
|
||
return (node: SdsClass, accept: ValidationAcceptor): void => { | ||
const superClasses = classHierarchy.streamSuperclasses(node); | ||
if (superClasses.includes(node)) { | ||
accept('error', 'A class must not directly or indirectly be a subtype of itself.', { | ||
node: parentTypesOrEmpty(node)[0], | ||
code: CODE_INHERITANCE_CYCLE, | ||
}); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; | ||
import { NodeFileSystem } from 'langium/node'; | ||
import { clearDocuments } from 'langium/test'; | ||
import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js'; | ||
import { getNodeOfType } from '../../helpers/nodeFinder.js'; | ||
|
||
const services = createSafeDsServices(NodeFileSystem).SafeDs; | ||
const classHierarchy = services.types.ClassHierarchy; | ||
|
||
describe('SafeDsClassHierarchy', async () => { | ||
beforeEach(async () => { | ||
// Load the builtin library | ||
await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); | ||
}); | ||
|
||
afterEach(async () => { | ||
await clearDocuments(services); | ||
}); | ||
|
||
describe('streamSuperclasses', () => { | ||
const superclassNames = (node: SdsClass | undefined) => | ||
classHierarchy | ||
.streamSuperclasses(node) | ||
.map((clazz) => clazz.name) | ||
.toArray(); | ||
|
||
it('should return an empty stream if passed undefined', () => { | ||
expect(superclassNames(undefined)).toStrictEqual([]); | ||
}); | ||
|
||
const testCases = [ | ||
{ | ||
testName: 'should return "Any" if the class has no parent types', | ||
code: ` | ||
class A | ||
`, | ||
expected: ['Any'], | ||
}, | ||
{ | ||
testName: 'should return "Any" if the first parent type is not a class', | ||
code: ` | ||
class A sub E | ||
enum E | ||
`, | ||
expected: ['Any'], | ||
}, | ||
{ | ||
testName: 'should return the superclasses of a class (no cycle, implicit any)', | ||
code: ` | ||
class A sub B | ||
class B | ||
`, | ||
expected: ['B', 'Any'], | ||
}, | ||
{ | ||
testName: 'should return the superclasses of a class (no cycle, explicit any)', | ||
code: ` | ||
class A sub Any | ||
`, | ||
expected: ['Any'], | ||
}, | ||
{ | ||
testName: 'should return the superclasses of a class (cycle)', | ||
code: ` | ||
class A sub B | ||
class B sub C | ||
class C sub A | ||
`, | ||
expected: ['B', 'C', 'A', 'Any'], | ||
}, | ||
{ | ||
testName: 'should only consider the first parent type', | ||
code: ` | ||
class A sub B, C | ||
class B | ||
class C | ||
`, | ||
expected: ['B', 'Any'], | ||
}, | ||
]; | ||
|
||
it.each(testCases)('$testName', async ({ code, expected }) => { | ||
const firstClass = await getNodeOfType(services, code, isSdsClass); | ||
expect(superclassNames(firstClass)).toStrictEqual(expected); | ||
}); | ||
}); | ||
}); |
22 changes: 22 additions & 0 deletions
22
tests/resources/validation/inheritance/must be acyclic/main.sdstest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package tests.validation.inheritance.mustBeAcyclic | ||
|
||
// $TEST$ error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass1 sub »MyClass3« | ||
// $TEST$ error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass2 sub »MyClass1« | ||
// $TEST$ error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass3 sub »MyClass2« | ||
|
||
class MyClass4 | ||
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass5 sub »MyClass4« | ||
|
||
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass6 sub »MyClass7« | ||
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass7 sub Any, »MyClass6« | ||
|
||
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass8 sub »Unresolved« | ||
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." | ||
class MyClass9 sub »MyClass8« |
Oops, something went wrong.