Skip to content

Commit 3b07a1e

Browse files
authored
fix: resolve to the correct enum in field attribute when there's ambiguity (#513)
1 parent 0ea071b commit 3b07a1e

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

packages/schema/src/language-server/zmodel-linker.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DataModel,
77
DataModelField,
88
DataModelFieldType,
9+
Enum,
910
EnumField,
1011
Expression,
1112
FunctionDecl,
@@ -16,6 +17,7 @@ import {
1617
isDataModel,
1718
isDataModelField,
1819
isDataModelFieldType,
20+
isEnum,
1921
isReferenceExpr,
2022
LiteralExpr,
2123
MemberAccessExpr,
@@ -164,6 +166,10 @@ export class ZModelLinker extends DefaultLinker {
164166
this.resolveDataModel(node as DataModel, document, extraScopes);
165167
break;
166168

169+
case DataModelField:
170+
this.resolveDataModelField(node as DataModelField, document, extraScopes);
171+
break;
172+
167173
default:
168174
this.resolveDefault(node, document, extraScopes);
169175
break;
@@ -451,6 +457,49 @@ export class ZModelLinker extends DefaultLinker {
451457
return this.resolveDefault(node, document, extraScopes);
452458
}
453459

460+
private resolveDataModelField(
461+
node: DataModelField,
462+
document: LangiumDocument<AstNode>,
463+
extraScopes: ScopeProvider[]
464+
) {
465+
// Field declaration may contain enum references, and enum fields are pushed to the global
466+
// scope, so if there're enums with fields with the same name, an arbitrary one will be
467+
// used as resolution target. The correct behavior is to resolve to the enum that's used
468+
// as the declaration type of the field:
469+
//
470+
// enum FirstEnum {
471+
// E1
472+
// E2
473+
// }
474+
475+
// enum SecondEnum {
476+
// E1
477+
// E3
478+
// E4
479+
// }
480+
481+
// model M {
482+
// id Int @id
483+
// first SecondEnum @default(E1) <- should resolve to SecondEnum
484+
// second FirstEnum @default(E1) <- should resolve to FirstEnum
485+
// }
486+
//
487+
488+
// make sure type is resolved first
489+
this.resolve(node.type, document, extraScopes);
490+
491+
let scopes = extraScopes;
492+
493+
// if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes
494+
if (node.type.reference?.ref && isEnum(node.type.reference.ref)) {
495+
const contextEnum = node.type.reference.ref as Enum;
496+
const enumScope: ScopeProvider = (name) => contextEnum.fields.find((f) => f.name === name);
497+
scopes = [enumScope, ...scopes];
498+
}
499+
500+
this.resolveDefault(node, document, scopes);
501+
}
502+
454503
private resolveDefault(node: AstNode, document: LangiumDocument<AstNode>, extraScopes: ScopeProvider[]) {
455504
for (const [property, value] of Object.entries(node)) {
456505
if (!property.startsWith('$')) {

packages/schema/tests/schema/parser.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('Parsing Tests', () => {
4040
expect((ds.fields[1].value as InvocationExpr).args[0].value.$type).toBe(LiteralExpr);
4141
});
4242

43-
it('enum', async () => {
43+
it('enum simple', async () => {
4444
const content = `
4545
enum UserRole {
4646
USER
@@ -64,6 +64,39 @@ describe('Parsing Tests', () => {
6464
expect((attrVal.value as ReferenceExpr).target.ref?.name).toBe('USER');
6565
});
6666

67+
it('enum dup name resolve', async () => {
68+
const content = `
69+
datasource db {
70+
provider = "postgresql"
71+
url = env("DATABASE_URL")
72+
}
73+
74+
enum FirstEnum {
75+
E1 // used in both ENUMs
76+
E2
77+
}
78+
79+
enum SecondEnum {
80+
E1 // used in both ENUMs
81+
E3
82+
E4
83+
}
84+
85+
model M {
86+
id Int @id
87+
first SecondEnum @default(E1)
88+
second FirstEnum @default(E1)
89+
}
90+
`;
91+
92+
const doc = await loadModel(content);
93+
const firstEnum = doc.declarations.find((d) => d.name === 'FirstEnum');
94+
const secondEnum = doc.declarations.find((d) => d.name === 'SecondEnum');
95+
const m = doc.declarations.find((d) => d.name === 'M') as DataModel;
96+
expect(m.fields[1].attributes[0].args[0].value.$resolvedType?.decl).toBe(secondEnum);
97+
expect(m.fields[2].attributes[0].args[0].value.$resolvedType?.decl).toBe(firstEnum);
98+
});
99+
67100
it('model field types', async () => {
68101
const content = `
69102
model User {

0 commit comments

Comments
 (0)