Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/fix-functions-in-op-is-2026-6-2-2-35-0.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 16 additions & 14 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
38 changes: 22 additions & 16 deletions packages/compiler/src/core/name-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")!);
}
}
}
}
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
40 changes: 38 additions & 2 deletions packages/compiler/test/checker/operations.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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"));
});
});
Loading