diff --git a/.chronus/changes/fix-functions-in-op-is-2026-6-2-2-35-0.md b/.chronus/changes/fix-functions-in-op-is-2026-6-2-2-35-0.md new file mode 100644 index 00000000000..92320af1d3c --- /dev/null +++ b/.chronus/changes/fix-functions-in-op-is-2026-6-2-2-35-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Allow using function calls in `op is` expressions. Previously, writing `op test is myFunction(args)` would produce a "A value cannot be used as a type" error, requiring an intermediate alias as a workaround. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 58fbf7579a4..e41e5a19145 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2723,7 +2723,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkOperationIs( ctx: CheckContext, operation: OperationStatementNode, - opReference: TypeReferenceNode | undefined, + opReference: TypeReferenceNode | CallExpressionNode | undefined, ): Operation | undefined { if (!opReference) return undefined; // Ensure that we don't end up with a circular reference to the same operation @@ -2732,21 +2732,23 @@ export function createChecker(program: Program, resolver: NameResolver): Checker pendingResolutions.start(opSymId, ResolutionKind.BaseType); } - const target = resolver.getNodeLinks(opReference).resolvedSymbol; + if (opReference.kind === SyntaxKind.TypeReference) { + const target = resolver.getNodeLinks(opReference).resolvedSymbol; - // Did we encounter a circular operation reference? - if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { - if (ctx.mapper === undefined) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "circular-op-signature", - format: { typeName: target.name }, - target: opReference, - }), - ); - } + // Did we encounter a circular operation reference? + if (target && pendingResolutions.has(target, ResolutionKind.BaseType)) { + if (ctx.mapper === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "circular-op-signature", + format: { typeName: target.name }, + target: opReference, + }), + ); + } - return undefined; + return undefined; + } } // Resolve the base operation type diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 0d56166706d..5ea0d6b4280 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -835,22 +835,24 @@ export function createResolver(program: Program): NameResolver { targetTable.set("parameters", sym); } } else { - const { finalSymbol: sig } = resolveTypeReference(node.signature.baseOperation); - if (sig) { - const sigTable = getAugmentedSymbolTable(sig.metatypeMembers!); - const sigParameterSym = sigTable.get("parameters")!; - if (sigParameterSym !== undefined) { - const parametersSym = createSymbol( - sigParameterSym.node, - "parameters", - SymbolFlags.Model & SymbolFlags.MemberContainer, - ); - getAugmentedSymbolTable(parametersSym.members!).include( - getAugmentedSymbolTable(sigParameterSym.members!), - parametersSym, - ); - targetTable.set("parameters", parametersSym); - targetTable.set("returnType", sigTable.get("returnType")!); + if (node.signature.baseOperation.kind === SyntaxKind.TypeReference) { + const { finalSymbol: sig } = resolveTypeReference(node.signature.baseOperation); + if (sig) { + const sigTable = getAugmentedSymbolTable(sig.metatypeMembers!); + const sigParameterSym = sigTable.get("parameters")!; + if (sigParameterSym !== undefined) { + const parametersSym = createSymbol( + sigParameterSym.node, + "parameters", + SymbolFlags.Model & SymbolFlags.MemberContainer, + ); + getAugmentedSymbolTable(parametersSym.members!).include( + getAugmentedSymbolTable(sigParameterSym.members!), + parametersSym, + ); + targetTable.set("parameters", parametersSym); + targetTable.set("returnType", sigTable.get("returnType")!); + } } } } @@ -1383,6 +1385,10 @@ export function createResolver(program: Program): NameResolver { operationPrototype.set("returnType", (baseSym) => { let node = baseSym.declarations[0] as OperationStatementNode; while (node.signature.kind === SyntaxKind.OperationSignatureReference) { + if (node.signature.baseOperation.kind !== SyntaxKind.TypeReference) { + // Function call expressions cannot be statically resolved + return failedResult(ResolutionResultFlags.ResolutionFailed); + } const baseResult = resolveTypeReference(node.signature.baseOperation); if (baseResult.resolutionResult & ResolutionResultFlags.Resolved) { node = baseResult.resolvedSymbol!.declarations[0] as OperationStatementNode; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 63ed0076f13..65c3eaa3af9 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -851,7 +851,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } else { parseExpected(Token.IsKeyword); - const opReference = parseReferenceExpression(); + const opReference = parseCallOrReferenceExpression(); signature = { kind: SyntaxKind.OperationSignatureReference, diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 60ee8493360..8bc774d31b0 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -1547,7 +1547,7 @@ export interface OperationSignatureDeclarationNode extends BaseNode { export interface OperationSignatureReferenceNode extends BaseNode { readonly kind: SyntaxKind.OperationSignatureReference; - readonly baseOperation: TypeReferenceNode; + readonly baseOperation: TypeReferenceNode | CallExpressionNode; } export type OperationSignature = diff --git a/packages/compiler/test/checker/operations.test.ts b/packages/compiler/test/checker/operations.test.ts index ee6207eab0a..48a38dace9b 100644 --- a/packages/compiler/test/checker/operations.test.ts +++ b/packages/compiler/test/checker/operations.test.ts @@ -1,8 +1,8 @@ import { deepStrictEqual, notStrictEqual, ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; -import { DecoratorContext, IntrinsicType, Type } from "../../src/core/types.js"; +import { DecoratorContext, FunctionContext, IntrinsicType, Operation, Type } from "../../src/core/types.js"; import { getDoc } from "../../src/index.js"; -import { expectDiagnostics, mockFile, t } from "../../src/testing/index.js"; +import { expectDiagnosticEmpty, expectDiagnostics, mockFile, t } from "../../src/testing/index.js"; import { Tester } from "../tester.js"; describe("compiler: operations", () => { @@ -420,3 +420,39 @@ describe("ensure the parameters are fully resolved before marking the operation expect(myOp.parameters.properties.has("prop")).toBe(true); }); }); + +describe("op is with function call", () => { + function identityImpl(_ctx: FunctionContext, operation: Operation) { + return operation; + } + + const FnTester = Tester.files({ + "identity.js": mockFile.js({ + $functions: { + "": { + identity: identityImpl, + }, + }, + }), + }) + .import("./identity.js") + .using("TypeSpec.Reflection"); + + it("can use a function call returning an Operation in op is", async () => { + const [{ test }, diagnostics] = await FnTester.compileAndDiagnose(t.code` + extern fn identity(target: Operation): Operation; + + op base(a: string, b: int32): void; + + op ${t.op("test")} is identity(base); + `); + + const filtered = diagnostics.filter((d) => d.code !== "experimental-feature"); + expectDiagnosticEmpty(filtered); + + strictEqual(test.kind, "Operation"); + strictEqual(test.parameters.properties.size, 2); + ok(test.parameters.properties.has("a")); + ok(test.parameters.properties.has("b")); + }); +});