Skip to content

Commit be2fcd5

Browse files
committed
implement requested changes from review
1 parent 7d4dbe8 commit be2fcd5

File tree

9 files changed

+394
-166
lines changed

9 files changed

+394
-166
lines changed

packages/schema/src/language-server/validator/attribute-application-validator.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AliasDecl,
23
ArrayExpr,
34
Attribute,
45
AttributeArg,
@@ -300,14 +301,9 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
300301
}
301302
}
302303

303-
// alias expression is compared to corresponding expression resolved shape
304+
// Handle alias expressions by comparing to their resolved shape
304305
if (isAliasDecl(arg.$resolvedType?.decl)) {
305-
// TODO: what is context type? Passed to true to avoid error, to be fixed later
306-
if (dstType === 'ContextType') return true;
307-
308-
const alias = arg.$resolvedType.decl;
309-
const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type);
310-
return dstType === mappedAliasResolvedType || dstType === 'Any' || mappedAliasResolvedType === 'Any';
306+
return isAliasAssignableToType(arg.$resolvedType.decl, dstType ?? 'Any', attr);
311307
}
312308

313309
// destination is field reference or transitive field reference, check if
@@ -422,6 +418,34 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField)
422418
return allowed;
423419
}
424420

421+
function isAliasAssignableToType(alias: AliasDecl, dstType: string, attr: AttributeApplication): boolean {
422+
const effectiveDstType = resolveEffectiveDestinationType(dstType, attr);
423+
if (effectiveDstType === null) {
424+
return false;
425+
}
426+
427+
const mappedAliasResolvedType = mappedRawExpressionTypeToResolvedShape(alias.expression.$type);
428+
return (
429+
effectiveDstType === mappedAliasResolvedType || effectiveDstType === 'Any' || mappedAliasResolvedType === 'Any'
430+
);
431+
}
432+
433+
function resolveEffectiveDestinationType(dstType: string, attr: AttributeApplication): string | null {
434+
if (dstType !== 'ContextType') {
435+
return dstType;
436+
}
437+
438+
// ContextType is inferred from the attribute's container's type
439+
if (isDataModelField(attr.$container)) {
440+
if (!attr.$container?.type?.type) {
441+
return null;
442+
}
443+
return mapBuiltinTypeToExpressionType(attr.$container.type.type);
444+
}
445+
446+
return 'Any';
447+
}
448+
425449
export function validateAttributeApplication(attr: AttributeApplication, accept: ValidationAcceptor) {
426450
new AttributeApplicationValidator().validate(attr, accept);
427451
}

packages/schema/src/language-server/validator/function-invocation-validator.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,6 @@ export default class FunctionInvocationValidator implements AstValidator<Express
110110

111111
// first argument must refer to a model field
112112
const firstArg = expr.args?.[0]?.value;
113-
const callableDecl = expr.function.ref;
114-
if (!callableDecl) {
115-
accept('error', 'function or rule cannot be resolved', { node: expr });
116-
return;
117-
}
118-
119113
if (firstArg) {
120114
if (!getFieldReference(firstArg)) {
121115
accept('error', 'first argument must be a field reference', { node: firstArg });

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,22 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
282282
const globalScope = this.getGlobalScope(referenceType, context);
283283
const node = context.container as MemberAccessExpr;
284284

285+
// Handle auth() invocation in alias
286+
if (isInvocationExpr(node.operand)) {
287+
if (isAuthInvocation(node.operand)) {
288+
return this.createScopeForAuth(node, globalScope);
289+
}
290+
if (isFutureInvocation(node.operand)) {
291+
return this.createScopeForContainingModel(node, globalScope);
292+
}
293+
return EMPTY_SCOPE;
294+
}
295+
296+
// Handle this expression in alias
297+
if (isThisExpr(node.operand)) {
298+
return this.createScopeForContainingModel(node, globalScope);
299+
}
300+
285301
// For member access in aliases, we need to check all possible contexts
286302
if (isReferenceExpr(node.operand)) {
287303
const operandName = node.operand.$cstNode?.text;
@@ -333,6 +349,17 @@ export class ZModelScopeProvider extends DefaultScopeProvider {
333349
}
334350
}
335351

352+
// Handle nested member access (e.g., some.nested.field)
353+
if (isMemberAccessExpr(node.operand)) {
354+
const ref = node.operand.member.ref;
355+
if (isDataModelField(ref) && !ref.type.array) {
356+
return this.createScopeForContainer(ref.type.reference?.ref, globalScope, true);
357+
}
358+
if (isTypeDefField(ref) && !ref.type.array) {
359+
return this.createScopeForContainer(ref.type.reference?.ref, globalScope, true);
360+
}
361+
}
362+
336363
return EMPTY_SCOPE;
337364
}
338365

packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ import {
3232
} from '@zenstackhq/sdk/ast';
3333
import { P, match } from 'ts-pattern';
3434
import { name } from '..';
35-
import { isCheckInvocation } from '../../../utils/ast-utils';
35+
import {
36+
getAliasDeclaration,
37+
getFieldsFromAliasExpression,
38+
isAliasInvocation,
39+
isCheckInvocation,
40+
} from '../../../utils/ast-utils';
3641

3742
/**
3843
* Options for {@link ConstraintTransformer}.
@@ -381,6 +386,19 @@ export class ConstraintTransformer {
381386
if (isMemberAccessExpr(expr)) {
382387
return isThisExpr(expr.operand) ? { name: expr.member.$refText } : undefined;
383388
}
389+
if (isAliasInvocation(expr)) {
390+
// Resolve alias to actual fields - for constraint transformation,
391+
// we need to return a single field name representation
392+
const aliasDecl = getAliasDeclaration(expr);
393+
if (aliasDecl) {
394+
const fields = getFieldsFromAliasExpression(aliasDecl);
395+
if (fields.length > 0) {
396+
// For constraint purposes, use the alias name itself as the field identifier
397+
// The actual field resolution will be handled by the expression transformer
398+
return { name: aliasDecl.name };
399+
}
400+
}
401+
}
384402
return undefined;
385403
}
386404

packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers';
3636
import { streamAst } from 'langium';
3737
import path from 'path';
3838
import { FunctionDeclarationStructure, OptionalKind, Project, SourceFile, VariableDeclarationKind } from 'ts-morph';
39-
import { isCheckInvocation } from '../../../utils/ast-utils';
39+
import {
40+
getAliasDeclaration,
41+
getFieldsFromAliasExpression,
42+
isAliasInvocation,
43+
isCheckInvocation,
44+
} from '../../../utils/ast-utils';
4045
import { ConstraintTransformer } from './constraint-transformer';
4146
import {
4247
generateConstantQueryGuardFunction,
@@ -224,6 +229,25 @@ export class PolicyGenerator {
224229
}
225230
}
226231

232+
// Handle alias invocations
233+
if (isAliasInvocation(expr)) {
234+
const aliasDecl = getAliasDeclaration(expr);
235+
if (aliasDecl) {
236+
const referencedFields = getFieldsFromAliasExpression(aliasDecl);
237+
// Check if any referenced field would prevent create input checking
238+
for (const field of referencedFields) {
239+
if (field.$container === model && hasAttribute(field, '@default')) {
240+
// Alias references field with default value
241+
return false;
242+
}
243+
if (isForeignKeyField(field)) {
244+
// Alias references foreign key field
245+
return false;
246+
}
247+
}
248+
}
249+
}
250+
227251
return true;
228252
});
229253
});

packages/schema/src/plugins/enhancer/policy/utils.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import { getContainerOfType, streamAllContents, streamAst, streamContents } from
4444
import { FunctionDeclarationStructure, OptionalKind } from 'ts-morph';
4545
import { name } from '..';
4646
import {
47+
getAliasDeclaration,
48+
getFieldsFromAliasExpression,
4749
isAliasInvocation,
4850
isCheckInvocation,
4951
isCollectionPredicate,
@@ -318,8 +320,8 @@ export function generateQueryGuardFunction(
318320
isFutureExpr(child) ||
319321
// field reference
320322
(isReferenceExpr(child) && isDataModelField(child.target.ref)) ||
321-
// TODO: field access from alias expression
322-
isAliasInvocation(child)
323+
// field access from alias expression - resolve to actual fields
324+
(isAliasInvocation(child) && isExpression(child) && hasFieldAccessInAlias(child))
323325
)
324326
);
325327

@@ -598,3 +600,17 @@ function getSourceModelOfFieldAccess(expr: Expression) {
598600

599601
return undefined;
600602
}
603+
604+
/**
605+
* Checks if an alias invocation resolves to actual field accesses
606+
*/
607+
function hasFieldAccessInAlias(aliasInvocation: Expression): boolean {
608+
const aliasDecl = getAliasDeclaration(aliasInvocation);
609+
if (!aliasDecl) {
610+
return false;
611+
}
612+
613+
// Get all fields referenced in the alias expression
614+
const fields = getFieldsFromAliasExpression(aliasDecl);
615+
return fields.length > 0;
616+
}

packages/schema/src/utils/ast-utils.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
isDataModel,
1010
isDataModelField,
1111
isInvocationExpr,
12+
isMemberAccessExpr,
1213
isModel,
1314
isReferenceExpr,
15+
isThisExpr,
1416
isAliasDecl,
1517
isTypeDef,
1618
Model,
@@ -166,11 +168,63 @@ export function isCheckInvocation(node: AstNode) {
166168
}
167169

168170
export function isAliasInvocation(node: AstNode) {
169-
// check if a matching alias exists
171+
if (!isInvocationExpr(node)) {
172+
return false;
173+
}
174+
175+
// Check if the resolved reference is an alias declaration
176+
if (node.function.ref && isAliasDecl(node.function.ref)) {
177+
return true;
178+
}
179+
180+
// Fallback: check by name in the current model
170181
const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? [];
171-
// const aliasDecls = getAllLoadedAlias(this.langiumDocuments());
172-
return isInvocationExpr(node) && allAlias.some((alias) => alias.name === node.function.$refText);
173-
// (!node.function.ref || !isFromStdlib(node.function.ref)) /* && isAliasDecl(node.function.ref) */
182+
return allAlias.some((alias) => alias.name === node.function.$refText);
183+
}
184+
185+
/**
186+
* Gets the alias declaration for a given alias invocation
187+
*/
188+
export function getAliasDeclaration(node: AstNode): AliasDecl | undefined {
189+
if (!isInvocationExpr(node)) {
190+
return undefined;
191+
}
192+
193+
const allAlias = getContainerOfType(node, isModel)?.declarations.filter(isAliasDecl) ?? [];
194+
return allAlias.find((alias) => alias.name === node.function.$refText);
195+
}
196+
197+
/**
198+
* Extracts all DataModelField references from an alias expression
199+
*/
200+
export function getFieldsFromAliasExpression(alias: AliasDecl): DataModelField[] {
201+
const fields: DataModelField[] = [];
202+
203+
function extractFields(expr: Expression): void {
204+
if (isReferenceExpr(expr) && isDataModelField(expr.target.ref)) {
205+
fields.push(expr.target.ref);
206+
} else if (isMemberAccessExpr(expr)) {
207+
// Handle this.fieldName
208+
if (isThisExpr(expr.operand) && expr.member.ref && isDataModelField(expr.member.ref)) {
209+
fields.push(expr.member.ref);
210+
}
211+
} else if (isInvocationExpr(expr)) {
212+
// Handle nested alias invocations
213+
const nestedAlias = getAliasDeclaration(expr);
214+
if (nestedAlias) {
215+
fields.push(...getFieldsFromAliasExpression(nestedAlias));
216+
}
217+
// Also check arguments for field references
218+
expr.args.forEach((arg) => extractFields(arg.value));
219+
} else if (isBinaryExpr(expr)) {
220+
extractFields(expr.left);
221+
extractFields(expr.right);
222+
}
223+
// Add more expression types as needed
224+
}
225+
226+
extractFields(alias.expression);
227+
return [...new Set(fields)]; // Remove duplicates
174228
}
175229

176230
export function resolveImportUri(imp: ModelImport): URI | undefined {

packages/sdk/src/typescript-expression-transformer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ export class TypeScriptExpressionTransformer {
8989
return this.this(expr as ThisExpr);
9090

9191
case ReferenceExpr:
92-
// TODO: ensure referenceExpr from alias is resolved
9392
return this.reference(expr as ReferenceExpr);
9493

9594
case InvocationExpr:
@@ -147,7 +146,10 @@ export class TypeScriptExpressionTransformer {
147146

148147
if (isAlias) {
149148
// if the function invocation comes from an alias, we transform its expression
150-
return this.transform(expr.function.ref.expression!, normalizeUndefined);
149+
if (!expr.function.ref.expression) {
150+
throw new TypeScriptExpressionTransformerError(`Unresolved alias expression`);
151+
}
152+
return this.transform(expr.function.ref.expression, normalizeUndefined);
151153
}
152154

153155
if (!isStdFunc) {

0 commit comments

Comments
 (0)