Skip to content

Commit 465956a

Browse files
committed
fix: add alias scope
1 parent 20087f6 commit 465956a

File tree

3 files changed

+236
-55
lines changed

3 files changed

+236
-55
lines changed

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

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
UnaryExpr,
3131
isAliasDecl,
3232
isArrayExpr,
33-
isBinaryExpr,
3433
isBooleanLiteral,
3534
isDataModel,
3635
isDataModelField,
@@ -41,7 +40,6 @@ import {
4140
isReferenceExpr,
4241
isStringLiteral,
4342
isTypeDefField,
44-
isUnaryExpr,
4543
} from '@zenstackhq/language/ast';
4644
import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk';
4745
import {
@@ -171,7 +169,6 @@ export class ZModelLinker extends DefaultLinker {
171169
break;
172170

173171
case MemberAccessExpr:
174-
// TODO: check from alias ?
175172
this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes);
176173
break;
177174

@@ -207,6 +204,10 @@ export class ZModelLinker extends DefaultLinker {
207204
this.resolveDataModelField(node as DataModelField, document, extraScopes);
208205
break;
209206

207+
case AliasDecl:
208+
// Don't resolve alias declarations - they will be resolved when used
209+
break;
210+
210211
default:
211212
this.resolveDefault(node, document, extraScopes);
212213
break;
@@ -267,18 +268,13 @@ export class ZModelLinker extends DefaultLinker {
267268
}
268269

269270
private resolveReference(node: ReferenceExpr, document: LangiumDocument<AstNode>, extraScopes: ScopeProvider[]) {
270-
// If the reference comes from an alias, we resolve it against the first matching data model
271-
if (getContainerOfType(node, isAliasDecl)) {
272-
this.resolveAliasExpr(node as ReferenceExpr, document);
273-
}
274271
this.resolveDefault(node, document, extraScopes);
275272

276273
if (node.target.ref) {
277274
// resolve type
278275
if (node.target.ref.$type === EnumField) {
279276
this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container);
280277
} else {
281-
// TODO: if the reference is from an alias, we should resolve it against the first matching data model
282278
this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type);
283279
}
284280
}
@@ -329,6 +325,18 @@ export class ZModelLinker extends DefaultLinker {
329325

330326
if (matchingAlias) {
331327
node.$resolvedType = { decl: matchingAlias, nullable: false };
328+
329+
// Resolve the alias expression in the context of the containing model
330+
const containingModel = getContainingDataModel(node);
331+
if (containingModel && matchingAlias.expression) {
332+
const scopeProvider = (name: string) =>
333+
getModelFieldsWithBases(containingModel).find((field) => field.name === name);
334+
335+
// Ensure the alias expression is fully resolved in the current context
336+
this.resolveExpressionInContext(matchingAlias.expression, document, containingModel, [
337+
scopeProvider,
338+
]);
339+
}
332340
}
333341
} else {
334342
this.resolveToDeclaredType(node, (funcDecl as FunctionDecl).returnType);
@@ -464,44 +472,14 @@ export class ZModelLinker extends DefaultLinker {
464472
node.$resolvedType = node.value.$resolvedType;
465473
}
466474

467-
private resolveAliasExpr(node: AstNode, document: LangiumDocument<AstNode>) {
468-
const container = getContainerOfType(node, isAliasDecl);
469-
if (!container) {
470-
return;
471-
}
472-
const model = getContainerOfType(node, isModel);
473-
const models = model?.declarations.filter(isDataModel) ?? [];
474-
// Find the first model that has the alias reference as a field
475-
const matchingModel = models.find((model) => model.fields.some((f) => f.name === node.$cstNode?.text));
476-
if (!matchingModel) {
477-
return;
478-
}
479-
480-
const scopeProvider = (name: string) =>
481-
getModelFieldsWithBases(matchingModel).find((field) => field.name === name);
482-
483-
const visitExpr = (node: Expression) => {
484-
if (isReferenceExpr(node)) {
485-
// enums in alias expressions are already resolved
486-
if (isEnum(node.target.ref?.$container)) {
487-
return;
488-
}
489-
490-
const resolved = this.resolveFromScopeProviders(node, 'target', document, [scopeProvider]);
491-
if (resolved) {
492-
this.resolveToDeclaredType(node, (resolved as DataModelField).type);
493-
} else {
494-
this.unresolvableRefExpr(node);
495-
}
496-
} else if (isBinaryExpr(node)) {
497-
visitExpr(node.left);
498-
visitExpr(node.right);
499-
} else if (isUnaryExpr(node)) {
500-
visitExpr(node.operand);
501-
}
502-
};
503-
504-
visitExpr(container.expression);
475+
private resolveExpressionInContext(
476+
expr: Expression,
477+
document: LangiumDocument<AstNode>,
478+
contextModel: DataModel,
479+
extraScopes: ScopeProvider[]
480+
) {
481+
// Resolve the expression with the model context scope
482+
this.resolve(expr, document, extraScopes);
505483
}
506484

507485
private unresolvableRefExpr(item: ReferenceExpr) {

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
BinaryExpr,
33
MemberAccessExpr,
4+
isAliasDecl,
45
isDataModel,
56
isDataModelField,
67
isEnumField,
@@ -117,6 +118,11 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
117118

118119
override getScope(context: ReferenceInfo): Scope {
119120
if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') {
121+
// Check if we're inside an alias first
122+
const aliasDecl = getContainerOfType(context.container, isAliasDecl);
123+
if (aliasDecl) {
124+
return this.getAliasMemberAccessScope(context);
125+
}
120126
return this.getMemberAccessScope(context);
121127
}
122128

@@ -126,6 +132,12 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
126132
if (containerCollectionPredicate) {
127133
return this.getCollectionPredicateScope(context, containerCollectionPredicate);
128134
}
135+
136+
// Check if we're inside an alias declaration - if so, get scope from containing model
137+
const aliasDecl = getContainerOfType(context.container, isAliasDecl);
138+
if (aliasDecl) {
139+
return this.getAliasScope(context);
140+
}
129141
}
130142

131143
return super.getScope(context);
@@ -243,6 +255,100 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
243255
return EMPTY_SCOPE;
244256
}
245257
}
258+
259+
private getAliasScope(context: ReferenceInfo): Scope {
260+
const referenceType = this.reflection.getReferenceType(context);
261+
const globalScope = this.getGlobalScope(referenceType, context);
262+
263+
// In aliases, we want to resolve references against all possible models
264+
const model = getContainerOfType(context.container, isModel);
265+
if (!model) {
266+
return globalScope;
267+
}
268+
269+
// Collect all fields from all models
270+
const allFields: AstNode[] = [];
271+
for (const decl of model.declarations) {
272+
if (isDataModel(decl)) {
273+
allFields.push(...getModelFieldsWithBases(decl));
274+
}
275+
}
276+
277+
return this.createScopeForNodes(allFields, globalScope);
278+
}
279+
280+
private getAliasMemberAccessScope(context: ReferenceInfo): Scope {
281+
const referenceType = this.reflection.getReferenceType(context);
282+
const globalScope = this.getGlobalScope(referenceType, context);
283+
const node = context.container as MemberAccessExpr;
284+
285+
// For member access in aliases, we need to check all possible contexts
286+
if (isReferenceExpr(node.operand)) {
287+
const operandName = node.operand.$cstNode?.text;
288+
if (!operandName) {
289+
return EMPTY_SCOPE;
290+
}
291+
292+
// Check if this is used in an invocation context
293+
let invocationContext: AstNode | undefined = node.$container;
294+
while (invocationContext && !isInvocationExpr(invocationContext)) {
295+
invocationContext = invocationContext.$container;
296+
}
297+
298+
if (invocationContext && isInvocationExpr(invocationContext)) {
299+
// Find the model where this invocation is used
300+
const containingModel = getContainerOfType(invocationContext, isDataModel);
301+
if (containingModel) {
302+
const field = getModelFieldsWithBases(containingModel).find(
303+
(f) => f.name === operandName
304+
);
305+
if (field && field.type.reference?.ref) {
306+
return this.createScopeForContainer(field.type.reference.ref, globalScope);
307+
}
308+
}
309+
}
310+
311+
// Otherwise, check all models for possible matches
312+
const model = getContainerOfType(context.container, isModel);
313+
if (!model) {
314+
return EMPTY_SCOPE;
315+
}
316+
317+
// Collect all possible scopes from all models
318+
const allScopes: Scope[] = [];
319+
for (const decl of model.declarations) {
320+
if (isDataModel(decl)) {
321+
const field = getModelFieldsWithBases(decl).find(
322+
(f) => f.name === operandName
323+
);
324+
if (field && field.type.reference?.ref) {
325+
allScopes.push(this.createScopeForContainer(field.type.reference.ref, globalScope));
326+
}
327+
}
328+
}
329+
330+
// Combine all scopes
331+
if (allScopes.length > 0) {
332+
return this.combineScopes(allScopes);
333+
}
334+
}
335+
336+
return EMPTY_SCOPE;
337+
}
338+
339+
private combineScopes(scopes: Scope[]): Scope {
340+
const allElements: AstNodeDescription[] = [];
341+
for (const scope of scopes) {
342+
const elements = scope.getAllElements();
343+
for (const element of elements) {
344+
// Avoid duplicates
345+
if (!allElements.some(e => e.name === element.name && e.type === element.type)) {
346+
allElements.push(element);
347+
}
348+
}
349+
}
350+
return new StreamScope(stream(allElements));
351+
}
246352
}
247353

248354
function getCollectionPredicateContext(node: AstNode) {

0 commit comments

Comments
 (0)