diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 9054c82c6..630bf0085 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -33,6 +33,10 @@ export default class DataModelValidator implements AstValidator { validateDuplicatedDeclarations(dm, getModelFieldsWithBases(dm), accept); this.validateAttributes(dm, accept); this.validateFields(dm, accept); + + if (dm.superTypes.length > 0) { + this.validateInheritance(dm, accept); + } } private validateFields(dm: DataModel, accept: ValidationAcceptor) { @@ -407,6 +411,26 @@ export default class DataModelValidator implements AstValidator { }); } } + + private validateInheritance(dm: DataModel, accept: ValidationAcceptor) { + const seen = [dm]; + const todo: DataModel[] = dm.superTypes.map((superType) => superType.ref!); + while (todo.length > 0) { + const current = todo.shift()!; + if (seen.includes(current)) { + accept( + 'error', + `Circular inheritance detected: ${seen.map((m) => m.name).join(' -> ')} -> ${current.name}`, + { + node: dm, + } + ); + return; + } + seen.push(current); + todo.push(...current.superTypes.map((superType) => superType.ref!)); + } + } } export interface MissingOppositeRelationData { diff --git a/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts b/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts new file mode 100644 index 000000000..494dad2be --- /dev/null +++ b/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts @@ -0,0 +1,39 @@ +import { loadModelWithError } from '../../utils'; + +describe('Cyclic inheritance', () => { + it('abstract inheritance', async () => { + const errors = await loadModelWithError( + ` + abstract model A extends B {} + abstract model B extends A {} + model C extends B { + id Int @id + } + ` + ); + expect(errors).toContain('Circular inheritance detected: A -> B -> A'); + expect(errors).toContain('Circular inheritance detected: B -> A -> B'); + expect(errors).toContain('Circular inheritance detected: C -> B -> A -> B'); + }); + + it('delegate inheritance', async () => { + const errors = await loadModelWithError( + ` + model A extends B { + typeA String + @@delegate(typeA) + } + model B extends A { + typeB String + @@delegate(typeB) + } + model C extends B { + id Int @id + } + ` + ); + expect(errors).toContain('Circular inheritance detected: A -> B -> A'); + expect(errors).toContain('Circular inheritance detected: B -> A -> B'); + expect(errors).toContain('Circular inheritance detected: C -> B -> A -> B'); + }); +}); diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 46c2a82c1..ecb6895eb 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -544,8 +544,16 @@ export function getModelFieldsWithBases(model: DataModel, includeDelegate = true } } -export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] { +export function getRecursiveBases( + dataModel: DataModel, + includeDelegate = true, + seen = new Set() +): DataModel[] { const result: DataModel[] = []; + if (seen.has(dataModel)) { + return result; + } + seen.add(dataModel); dataModel.superTypes.forEach((superType) => { const baseDecl = superType.ref; if (baseDecl) { @@ -553,7 +561,7 @@ export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): return; } result.push(baseDecl); - result.push(...getRecursiveBases(baseDecl, includeDelegate)); + result.push(...getRecursiveBases(baseDecl, includeDelegate, seen)); } }); return result;