diff --git a/cspell.yaml b/cspell.yaml index df8339252f4..ae8c813fdea 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -134,6 +134,7 @@ words: - lzutf - MACVMIMAGE - MACVMIMAGEM + - marshal - mday - mgmt - mgmtplane @@ -257,6 +258,7 @@ words: - Ungroup - uninstantiated - unioned + - unmarshal - unparented - unprefixed - unprojected diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts index ba67b433b56..27a99b09c6f 100644 --- a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecPrototypesDecorators } from "./TypeSpec.Prototypes.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecPrototypesDecorators = $decorators["TypeSpec.Prototypes"]; +const _decs: TypeSpecPrototypesDecorators = $decorators["TypeSpec.Prototypes"]; diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index 12337f14a8a..4e0ce70e3c3 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecDecorators } from "./TypeSpec.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecDecorators = $decorators["TypeSpec"]; +const _decs: TypeSpecDecorators = $decorators["TypeSpec"]; diff --git a/packages/compiler/lib/std/reflection.tsp b/packages/compiler/lib/std/reflection.tsp index f1142b2e738..12bb602c034 100644 --- a/packages/compiler/lib/std/reflection.tsp +++ b/packages/compiler/lib/std/reflection.tsp @@ -11,3 +11,4 @@ model Scalar {} model Union {} model UnionVariant {} model StringTemplate {} +model Type {} diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 906218f99b5..057f3463434 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -13,6 +13,7 @@ import { EnumStatementNode, FileLibraryMetadata, FunctionDeclarationStatementNode, + FunctionImplementations, FunctionParameterNode, InterfaceStatementNode, IntersectionExpressionNode, @@ -133,7 +134,7 @@ export function createBinder(program: Program): Binder { for (const [key, member] of Object.entries(sourceFile.esmExports)) { let name: string; - let kind: "decorator" | "function"; + let kind: "decorator" | "function" | "template"; if (key === "$flags") { const context = getLocationContext(program, sourceFile); if (context.type === "library" || context.type === "project") { @@ -152,6 +153,19 @@ export function createBinder(program: Program): Binder { ); } } + } else if (key === "$functions") { + const value: FunctionImplementations = member as any; + for (const [namespaceName, functions] of Object.entries(value)) { + for (const [functionName, fn] of Object.entries(functions)) { + bindFunctionImplementation( + namespaceName === "" ? [] : namespaceName.split("."), + "function", + functionName, + fn, + sourceFile, + ); + } + } } else if (typeof member === "function") { // lots of 'any' casts here because control flow narrowing `member` to Function // isn't particularly useful it turns out. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3802783dc4f..fa709244706 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -10,12 +10,12 @@ import { createTupleToArrayValueCodeFix, } from "./compiler-code-fixes/convert-to-value.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; -import { compilerAssert, ignoreDiagnostics } from "./diagnostics.js"; +import { compilerAssert, createDiagnosticCollector, ignoreDiagnostics } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getTypeName } from "./helpers/type-name-utils.js"; -import { marshallTypeForJS } from "./js-marshaller.js"; +import { marshalTypeForJs, unmarshalJsToValue } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { NameResolver } from "./name-resolver.js"; import { Numeric } from "./numeric.js"; @@ -57,6 +57,7 @@ import { DecoratorDeclarationStatementNode, DecoratorExpressionNode, Diagnostic, + DiagnosticResult, DiagnosticTarget, DocContent, Entity, @@ -70,6 +71,7 @@ import { FunctionDeclarationStatementNode, FunctionParameter, FunctionParameterNode, + FunctionType, IdentifierKind, IdentifierNode, IndeterminateEntity, @@ -155,6 +157,7 @@ import { UnionVariant, UnionVariantNode, UnknownType, + UnknownValue, UsingStatementNode, Value, ValueWithTemplate, @@ -285,6 +288,9 @@ export interface Checker { /** @internal */ readonly anyType: UnknownType; + /** @internal */ + readonly unknownEntity: IndeterminateEntity; + /** @internal */ stats: CheckerStats; } @@ -349,6 +355,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const unknownType = createAndFinishType({ kind: "Intrinsic", name: "unknown" } as const); const nullType = createAndFinishType({ kind: "Intrinsic", name: "null" } as const); + const unknownEntity: IndeterminateEntity = { + entityKind: "Indeterminate", + type: unknownType, + }; + /** * Set keeping track of node pending type resolution. * Key is the SymId of a node. It can be retrieved with getNodeSymId(node) @@ -378,6 +389,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker errorType, nullType, anyType: unknownType, + unknownEntity, voidType, typePrototype, createType, @@ -670,6 +682,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker switch (type.name) { case "null": return checkNullValue(type as any, constraint, node); + case "unknown": + return checkUnknownValue(type as UnknownType, constraint); } return type; default: @@ -778,6 +792,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // If there were diagnostic reported but we still got a value this means that the value might be invalid. reportCheckerDiagnostics(valueDiagnostics); return result; + } else { + const canBeType = constraint?.constraint.type !== undefined; + // If the node _must_ resolve to a value, we will return it unconstrained, so that we will at least produce + // a value. If it _can_ be a type, we already failed the value constraint, so we return the type as is. + return canBeType ? entity.type : getValueFromIndeterminate(entity.type, undefined, node); } } @@ -862,7 +881,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case SyntaxKind.NeverKeyword: return neverType; case SyntaxKind.UnknownKeyword: - return unknownType; + return unknownEntity; case SyntaxKind.ObjectLiteral: return checkObjectValue(node, mapper, valueConstraint); case SyntaxKind.ArrayLiteral: @@ -1436,13 +1455,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return errorType; } - if (sym.flags & SymbolFlags.Function) { - reportCheckerDiagnostic( - createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: sym }), - ); + // if (sym.flags & SymbolFlags.Function) { + // reportCheckerDiagnostic( + // createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: sym }), + // ); - return errorType; - } + // return errorType; + // } const argumentNodes = node.kind === SyntaxKind.TypeReference ? node.arguments : []; const symbolLinks = getSymbolLinks(sym); @@ -1630,7 +1649,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker args: (Type | Value | IndeterminateEntity)[], source: TypeMapper["source"], parentMapper: TypeMapper | undefined, - instantiateTempalates = true, + instantiateTemplates = true, ): Type { const symbolLinks = templateNode.kind === SyntaxKind.OperationStatement && @@ -1661,7 +1680,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (cached) { return cached; } - if (instantiateTempalates) { + if (instantiateTemplates) { return instantiateTemplate(symbolLinks.instantiations, templateNode, params, mapper); } else { return errorType; @@ -1907,9 +1926,53 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkFunctionDeclaration( node: FunctionDeclarationStatementNode, mapper: TypeMapper | undefined, - ) { - reportCheckerDiagnostic(createDiagnostic({ code: "function-unsupported", target: node })); - return errorType; + ): FunctionType { + const mergedSymbol = getMergedSymbol(node.symbol); + const links = getSymbolLinks(mergedSymbol); + + if (links.declaredType && mapper === undefined) { + // we're not instantiating this operation and we've already checked it + return links.declaredType as FunctionType; + } + + const namespace = getParentNamespaceType(node); + compilerAssert( + namespace, + `Function ${node.id.sv} should have resolved a declared namespace or the global namespace.`, + ); + + const name = node.id.sv; + + if (!(node.modifierFlags & ModifierFlags.Extern)) { + reportCheckerDiagnostic(createDiagnostic({ code: "function-extern", target: node })); + } + + const implementation = mergedSymbol.value; + if (implementation === undefined) { + reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); + } + + const functionType: FunctionType = createType({ + kind: "Function", + name, + namespace, + node, + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, true)), + returnType: node.returnType + ? getParamConstraintEntityForNode(node.returnType, mapper) + : ({ + entityKind: "MixedParameterConstraint", + type: unknownType, + } satisfies MixedParameterConstraint), + implementation: + implementation ?? Object.assign(() => errorType, { isDefaultFunctionImplementation: true }), + }); + + namespace.functionDeclarations.set(name, functionType); + + linkType(links, functionType, mapper); + + return functionType; } function checkFunctionParameter( @@ -4081,6 +4144,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } + function checkUnknownValue( + unknownType: UnknownType, + constraint: CheckValueConstraint | undefined, + ): UnknownValue | null { + return createValue( + { + entityKind: "Value", + + valueKind: "UnknownValue", + type: neverType, + }, + constraint ? constraint.type : neverType, + ); + } + function checkEnumValue( literalType: EnumMember, constraint: CheckValueConstraint | undefined, @@ -4104,19 +4182,24 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkCallExpressionTarget( node: CallExpressionNode, mapper: TypeMapper | undefined, - ): ScalarConstructor | Scalar | null { + ): ScalarConstructor | Scalar | FunctionType | null { const target = checkTypeReference(node.target, mapper); - if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { + if ( + target.kind === "Scalar" || + target.kind === "ScalarConstructor" || + target.kind === "Function" + ) { return target; } else { - reportCheckerDiagnostic( - createDiagnostic({ - code: "non-callable", - format: { type: target.kind }, - target: node.target, - }), - ); + if (!isErrorType(target)) + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-callable", + format: { type: target.kind }, + target: node.target, + }), + ); return null; } } @@ -4260,13 +4343,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkCallExpression( node: CallExpressionNode, mapper: TypeMapper | undefined, - ): Value | null { + ): Type | Value | null { const target = checkCallExpressionTarget(node, mapper); if (target === null) { return null; } if (target.kind === "ScalarConstructor") { return createScalarValue(node, mapper, target); + } else if (target.kind === "Function") { + return checkFunctionCall(node, target, mapper); } if (relation.areScalarsRelated(target, getStdType("string"))) { @@ -4287,6 +4372,282 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } + function checkFunctionCall( + node: CallExpressionNode, + target: FunctionType, + mapper: TypeMapper | undefined, + ): Type | Value | null { + const [satisfied, resolvedArgs] = checkFunctionCallArguments(node.arguments, target, mapper); + + const canCall = satisfied && !(target.implementation as any).isDefaultFunctionImplementation; + + const functionReturn = canCall + ? target.implementation(program, ...resolvedArgs) + : getDefaultFunctionResult(target.returnType); + + const returnIsTypeOrValue = + typeof functionReturn === "object" && + functionReturn !== null && + "entityKind" in functionReturn && + (functionReturn.entityKind === "Type" || functionReturn.entityKind === "Value"); + + const result = returnIsTypeOrValue + ? (functionReturn as Type | Value) + : unmarshalJsToValue(program, functionReturn, function onInvalid(value) { + // TODO: diagnostic for invalid return value + }); + + if (satisfied) checkFunctionReturn(target, result, node); + + return result; + } + + function getDefaultFunctionResult(constraint: MixedParameterConstraint): Type | Value { + if (constraint.valueType) { + return createValue( + { + valueKind: "UnknownValue", + entityKind: "Value", + type: constraint.valueType, + }, + constraint.valueType, + ); + } else { + compilerAssert( + constraint.type, + "Expected function to have a return type when it did not have a value type constraint", + ); + return constraint.type; + } + } + + function checkFunctionCallArguments( + args: Expression[], + target: FunctionType, + mapper: TypeMapper | undefined, + ): [boolean, any[]] { + const minArgs = target.parameters.filter((p) => !p.optional && !p.rest).length; + const maxArgs = target.parameters[target.parameters.length - 1]?.rest + ? undefined + : target.parameters.length; + + if (args.length < minArgs) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + messageId: "atLeast", + format: { actual: args.length.toString(), expected: minArgs.toString() }, + target: target.node!, + }), + ); + return [false, []]; + } else if (maxArgs !== undefined && args.length > maxArgs) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + format: { actual: args.length.toString(), expected: maxArgs.toString() }, + target: target.node!, + }), + ); + } + + const collector = createDiagnosticCollector(); + + const resolvedArgs: any[] = []; + let satisfied = true; + + let idx = 0; + + for (const param of target.parameters) { + if (param.rest) { + const constraint = extractRestParamConstraint(param.type); + + if (!constraint) { + satisfied = false; + continue; + } + + const restArgs = args + .slice(idx) + .map((arg) => getTypeOrValueForNode(arg, mapper, { kind: "argument", constraint })); + + if (restArgs.some((x) => x === null)) { + satisfied = false; + continue; + } + + resolvedArgs.push( + ...restArgs.map((v) => + v !== null && isValue(v) + ? marshalTypeForJs(v, undefined, function onUnknown() { + // TODO: diagnostic for unknown value + }) + : v, + ), + ); + } else { + const arg = args[idx++]; + + if (!arg) { + if (param.optional) { + resolvedArgs.push(undefined); + continue; + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument", + messageId: "default", + // TODO: render constraint + format: { value: "undefined", expected: "TODO" }, + target: target.node!, + }), + ); + satisfied = false; + continue; + } + } + + // Normal param + const checkedArg = getTypeOrValueForNode(arg, mapper, { + kind: "argument", + constraint: param.type, + }); + + if (!checkedArg) { + satisfied = false; + continue; + } + + const resolved = collector.pipe( + checkEntityAssignableToConstraint(checkedArg, param.type, arg), + ); + + satisfied &&= !!resolved; + + resolvedArgs.push( + resolved + ? isValue(resolved) + ? marshalTypeForJs(resolved, undefined, function onUnknown() { + // TODO: diagnostic for unknown value + }) + : resolved + : undefined, + ); + } + } + + reportCheckerDiagnostics(collector.diagnostics); + + return [satisfied, resolvedArgs]; + } + + function checkFunctionReturn(target: FunctionType, result: Type | Value, diagnosticTarget: Node) { + const [_, diagnostics] = checkEntityAssignableToConstraint( + result, + target.returnType, + diagnosticTarget, + ); + + reportCheckerDiagnostics(diagnostics); + } + + function checkEntityAssignableToConstraint( + entity: Type | Value | IndeterminateEntity, + constraint: MixedParameterConstraint, + diagnosticTarget: Node, + ): DiagnosticResult { + const constraintIsValue = !!constraint.valueType; + + const collector = createDiagnosticCollector(); + + if (constraintIsValue) { + const normed = collector.pipe(normalizeValue(entity, constraint, diagnosticTarget)); + + // Error should have been reported in normalizeValue + if (!normed) return collector.wrap(null); + + const assignable = collector.pipe( + relation.isValueOfType(normed, constraint.valueType, diagnosticTarget), + ); + + return collector.wrap(assignable ? normed : null); + } else { + // Constraint is a type + + if (entity.entityKind !== "Type") { + collector.add( + createDiagnostic({ + code: "value-in-type", + format: { name: getTypeName(entity.type) }, + target: diagnosticTarget, + }), + ); + return collector.wrap(null); + } + + compilerAssert( + constraint.type, + "Expected type constraint to be defined when known not to be a value constraint.", + ); + + const assignable = collector.pipe( + relation.isTypeAssignableTo(entity, constraint.type, diagnosticTarget), + ); + + return collector.wrap(assignable ? entity : null); + } + } + + function normalizeValue( + entity: Type | Value | IndeterminateEntity, + constraint: MixedParameterConstraint, + diagnosticTarget: Node, + ): DiagnosticResult { + if (entity.entityKind === "Value") return [entity, []]; + + if (entity.entityKind === "Indeterminate") { + // Coerce to a value + const coerced = getValueFromIndeterminate( + entity.type, + constraint.type && { kind: "argument", type: constraint.type }, + entity.type.node!, + ); + + if (coerced?.entityKind !== "Value") { + return [ + null, + [ + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity.type) }, + target: diagnosticTarget, + }), + ], + ]; + } + + return [coerced, []]; + } + + if (entity.entityKind === "Type") { + return [ + null, + [ + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity) }, + target: diagnosticTarget, + }), + ], + ]; + } + + compilerAssert( + false, + `Unreachable: unexpected entity kind '${(entity satisfies never as Entity).entityKind}'`, + ); + } + function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { const entity = checkNode(node.target, mapper, undefined); if (entity === null) { @@ -4980,12 +5341,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return { value: arg, node: argNode, - jsValue: resolveDecoratorArgJsValue( + jsValue: resolveArgumentJsValue( arg, extractValueOfConstraints({ kind: "argument", constraint: perParamType, }), + argNode, ), }; } else { @@ -5059,13 +5421,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return type.kind === "Model" ? type.indexer?.value : undefined; } - function resolveDecoratorArgJsValue( + function resolveArgumentJsValue( value: Type | Value, valueConstraint: CheckValueConstraint | undefined, + diagnosticTarget: Node, ) { if (valueConstraint !== undefined) { if (isValue(value)) { - return marshallTypeForJS(value, valueConstraint.type); + return marshalTypeForJs(value, valueConstraint.type, function onUnknown() { + reportCheckerDiagnostic( + createDiagnostic({ + code: "unknown-value", + messageId: "in-js-argument", + target: diagnosticTarget, + }), + ); + }); } else { return value; } @@ -5445,6 +5816,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case "EnumValue": case "NullValue": case "ScalarValue": + case "UnknownValue": return value; } } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 6b99a90fd60..2624b058f7d 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -82,6 +82,8 @@ function getValuePreview(value: Value, options?: TypeNameOptions): string { return "null"; case "ScalarValue": return `${getTypeName(value.type, options)}.${value.value.name}(${value.value.args.map((x) => getValuePreview(x, options)).join(", ")}})`; + case "UnknownValue": + return "unknown"; } } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index b6548b8630d..e3b9cad58dc 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,19 +1,24 @@ +import { $ } from "../typekit/index.js"; import { compilerAssert } from "./diagnostics.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; +import { Program } from "./program.js"; import type { ArrayValue, MarshalledValue, NumericValue, ObjectValue, + ObjectValuePropertyDescriptor, Scalar, Type, + UnknownValue, Value, } from "./types.js"; -export function marshallTypeForJS( +export function marshalTypeForJs( value: T, valueConstraint: Type | undefined, + onUnknown: (value: UnknownValue) => void, ): MarshalledValue { switch (value.valueKind) { case "BooleanValue": @@ -22,15 +27,18 @@ export function marshallTypeForJS( case "NumericValue": return numericValueToJs(value, valueConstraint) as any; case "ObjectValue": - return objectValueToJs(value) as any; + return objectValueToJs(value, onUnknown) as any; case "ArrayValue": - return arrayValueToJs(value) as any; + return arrayValueToJs(value, onUnknown) as any; case "EnumValue": return value as any; case "NullValue": return null as any; case "ScalarValue": return value as any; + case "UnknownValue": + onUnknown(value); + return null as any; } } @@ -75,13 +83,109 @@ function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined) return type.value; } -function objectValueToJs(type: ObjectValue) { +function objectValueToJs( + type: ObjectValue, + onUnknown: (value: UnknownValue) => void, +): Record { const result: Record = {}; for (const [key, value] of type.properties) { - result[key] = marshallTypeForJS(value.value, undefined); + result[key] = marshalTypeForJs(value.value, undefined, onUnknown); } return result; } -function arrayValueToJs(type: ArrayValue) { - return type.values.map((x) => marshallTypeForJS(x, undefined)); +function arrayValueToJs(type: ArrayValue, onUnknown: (value: UnknownValue) => void) { + return type.values.map((x) => marshalTypeForJs(x, undefined, onUnknown)); +} + +export function unmarshalJsToValue( + program: Program, + value: unknown, + onInvalid: (value: unknown) => void, +): Value { + if ( + typeof value === "object" && + value !== null && + "entityKind" in value && + value.entityKind === "Value" + ) { + return value as Value; + } + + if (value === null || value === undefined) { + return { + entityKind: "Value", + valueKind: "NullValue", + value: null, + type: program.checker.nullType, + }; + } else if (typeof value === "boolean") { + const boolean = program.checker.getStdType("boolean"); + return { + entityKind: "Value", + valueKind: "BooleanValue", + value, + type: boolean, + scalar: boolean, + }; + } else if (typeof value === "string") { + const string = program.checker.getStdType("string"); + return { + entityKind: "Value", + valueKind: "StringValue", + value, + type: string, + scalar: string, + }; + } else if (typeof value === "number") { + const numeric = Numeric(String(value)); + const numericType = program.checker.getStdType("numeric"); + return { + entityKind: "Value", + valueKind: "NumericValue", + value: numeric, + type: $(program).literal.create(value), + scalar: numericType, + }; + } else if (Array.isArray(value)) { + const values: Value[] = []; + const uniqueTypes = new Set(); + + for (const item of value) { + const itemValue = unmarshalJsToValue(program, item, onInvalid); + values.push(itemValue); + uniqueTypes.add(itemValue.type); + } + + return { + entityKind: "Value", + valueKind: "ArrayValue", + type: $(program).array.create($(program).union.create([...uniqueTypes])), + values, + }; + } else if (typeof value === "object" && !("entityKind" in value)) { + const properties: Map = new Map(); + for (const [key, val] of Object.entries(value)) { + properties.set(key, { name: key, value: unmarshalJsToValue(program, val, onInvalid) }); + } + return { + entityKind: "Value", + valueKind: "ObjectValue", + properties, + type: $(program).model.create({ + properties: Object.fromEntries( + [...properties.entries()].map( + ([k, v]) => + [k, $(program).modelProperty.create({ name: k, type: v.value.type })] as const, + ), + ), + }), + }; + } else { + onInvalid(value); + return { + entityKind: "Value", + valueKind: "UnknownValue", + type: program.checker.neverType, + }; + } } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5c42d275d59..8ffa8f3ab87 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -395,6 +395,7 @@ const diagnostics = { modelExpression: `Is a model expression type, but is being used as a value here. Use #{} to create an object value.`, tuple: `Is a tuple type, but is being used as a value here. Use #[] to create an array value.`, templateConstraint: paramMessage`${"name"} template parameter can be a type but is being used as a value here.`, + functionReturn: paramMessage`Function returned a type, but a value was expected.`, }, }, "non-callable": { @@ -1014,6 +1015,16 @@ const diagnostics = { }, }, + "unknown-value": { + severity: "error", + messages: { + default: "The 'unknown' value cannot be used here.", + "in-json": "The 'unknown' value cannot be serialized to JSON.", + "in-js-argument": + "The 'unknown' value cannot be used as an argument to a function or decorator.", + }, + }, + // #region Visibility "visibility-sealed": { severity: "error", diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 62b1c3d7b23..70261685f0a 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -952,7 +952,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const id = parseIdentifier(); let constraint: Expression | ValueOfExpressionNode | undefined; if (parseOptional(Token.ExtendsKeyword)) { - constraint = parseMixedParameterConstraint(); + constraint = parseMixedConstraint(); } let def: Expression | undefined; if (parseOptional(Token.Equals)) { @@ -971,7 +971,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (token() === Token.ValueOfKeyword) { return parseValueOfExpression(); } else if (parseOptional(Token.OpenParen)) { - const expr = parseMixedParameterConstraint(); + const expr = parseMixedConstraint(); parseExpected(Token.CloseParen); return expr; } @@ -979,7 +979,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return parseIntersectionExpressionOrHigher(); } - function parseMixedParameterConstraint(): Expression | ValueOfExpressionNode { + function parseMixedConstraint(): Expression | ValueOfExpressionNode { const pos = tokenPos(); parseOptional(Token.Bar); const node: Expression = parseValueOfExpressionOrIntersectionOrHigher(); @@ -1227,7 +1227,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const id = parseIdentifier(); const { items: templateParameters, range: templateParametersRange } = parseTemplateParameterList(); + parseExpected(Token.Equals); + const value = parseExpression(); parseExpected(Token.Semicolon); return { @@ -2059,7 +2061,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const { items: parameters } = parseFunctionParameters(); let returnType; if (parseOptional(Token.Colon)) { - returnType = parseExpression(); + returnType = parseMixedConstraint(); } parseExpected(Token.Semicolon); return { @@ -2081,7 +2083,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa let foundOptional = false; for (const [index, item] of parameters.items.entries()) { - if (!item.optional && foundOptional) { + if (!(item.optional || item.rest) && foundOptional) { error({ code: "required-parameter-first", target: item }); continue; } @@ -2108,7 +2110,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const optional = parseOptional(Token.Question); let type; if (parseOptional(Token.Colon)) { - type = parseMixedParameterConstraint(); + type = parseMixedConstraint(); } return { kind: SyntaxKind.FunctionParameter, diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 6342db3d26a..803631db738 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -3,6 +3,7 @@ import { isTemplateDeclaration } from "./type-utils.js"; import { Decorator, Enum, + FunctionType, Interface, ListenerFlow, Model, @@ -201,6 +202,10 @@ function navigateNamespaceType(namespace: Namespace, context: NavigationContext) navigateDecoratorDeclaration(decorator, context); } + for (const func of namespace.functionDeclarations.values()) { + navigateFunctionDeclaration(func, context); + } + context.emit("exitNamespace", namespace); } @@ -394,6 +399,13 @@ function navigateScalarConstructor(type: ScalarConstructor, context: NavigationC if (context.emit("scalarConstructor", type) === ListenerFlow.NoRecursion) return; } +function navigateFunctionDeclaration(type: FunctionType, context: NavigationContext) { + if (checkVisited(context.visited, type)) { + return; + } + if (context.emit("function", type) === ListenerFlow.NoRecursion) return; +} + function navigateTypeInternal(type: Type, context: NavigationContext) { switch (type.kind) { case "Model": @@ -426,6 +438,8 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { return navigateDecoratorDeclaration(type, context); case "ScalarConstructor": return navigateScalarConstructor(type, context); + case "Function": + return navigateFunctionDeclaration(type, context); case "FunctionParameter": case "Boolean": case "EnumMember": diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index dc0e02948ae..ccd47da6b91 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -98,11 +98,9 @@ const ReflectionNameToKind = { Tuple: "Tuple", Union: "Union", UnionVariant: "UnionVariant", -} as const; +} as const satisfies Record; -const _assertReflectionNameToKind: Record = ReflectionNameToKind; - -type ReflectionTypeName = keyof typeof ReflectionNameToKind; +type ReflectionTypeName = keyof typeof ReflectionNameToKind | "Type"; export function createTypeRelationChecker(program: Program, checker: Checker): TypeRelation { return { @@ -510,10 +508,10 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isSimpleTypeAssignableTo(source: Type, target: Type): boolean | undefined { if (isNeverType(source)) return true; - if (isVoidType(target)) return false; + if (isVoidType(target)) return isVoidType(source); if (isUnknownType(target)) return true; if (isReflectionType(target)) { - return source.kind === ReflectionNameToKind[target.name]; + return target.name === "Type" || source.kind === ReflectionNameToKind[target.name]; } if (target.kind === "Scalar") { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 912701f3a82..e13b72e6a9b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -15,6 +15,7 @@ Value extends StringValue ? string : Value extends EnumValue ? EnumMember : Value extends NullValue ? null : Value extends ScalarValue ? Value + : Value extends UnknownValue ? null : Value /** @@ -53,6 +54,10 @@ export interface DecoratorFunction { namespace?: string; } +export interface FunctionImplementation { + (program: Program, ...args: any[]): Type | Value; +} + export interface BaseType { readonly entityKind: "Type"; kind: string; @@ -131,7 +136,8 @@ export type Type = | TemplateParameter | Tuple | Union - | UnionVariant; + | UnionVariant + | FunctionType; export type StdTypes = { // Models @@ -166,7 +172,8 @@ export interface IndeterminateEntity { | BooleanLiteral | EnumMember | UnionVariant - | NullType; + | NullType + | UnknownType; } export interface IntrinsicType extends BaseType { @@ -320,7 +327,8 @@ export type Value = | ObjectValue | ArrayValue | EnumValue - | NullValue; + | NullValue + | UnknownValue; /** @internal */ export type ValueWithTemplate = Value | TemplateValue; @@ -387,6 +395,9 @@ export interface NullValue extends BaseValue { valueKind: "NullValue"; value: null; } +export interface UnknownValue extends BaseValue { + valueKind: "UnknownValue"; +} /** * This is an internal type that represent a value while in a template declaration. @@ -576,6 +587,13 @@ export interface Namespace extends BaseType, DecoratedType { * Order is implementation-defined and may change. */ decoratorDeclarations: Map; + + /** + * The functions declared in the namespace. + * + * Order is implementation-defined and may change. + */ + functionDeclarations: Map; } export type LiteralType = StringLiteral | NumericLiteral | BooleanLiteral; @@ -687,6 +705,16 @@ export interface Decorator extends BaseType { implementation: (...args: unknown[]) => void; } +export interface FunctionType extends BaseType { + kind: "Function"; + node?: FunctionDeclarationStatementNode; + name: string; + namespace?: Namespace; + parameters: MixedFunctionParameter[]; + returnType: MixedParameterConstraint; + implementation: (...args: unknown[]) => unknown; +} + export interface FunctionParameterBase extends BaseType { kind: "FunctionParameter"; node?: FunctionParameterNode; @@ -2309,6 +2337,12 @@ export interface DecoratorImplementations { }; } +export interface FunctionImplementations { + readonly [namespace: string]: { + readonly [name: string]: FunctionImplementation; + }; +} + export interface PackageFlags {} export interface LinterDefinition { @@ -2455,6 +2489,10 @@ export interface DecoratorContext { ): R; } +export interface TemplateContext { + program: Program; +} + export interface EmitContext> { /** * TypeSpec Program. diff --git a/packages/compiler/src/experimental/typekit/index.ts b/packages/compiler/src/experimental/typekit/index.ts index 32408209095..6fd78063a05 100644 --- a/packages/compiler/src/experimental/typekit/index.ts +++ b/packages/compiler/src/experimental/typekit/index.ts @@ -1,4 +1,4 @@ -import { type Typekit, TypekitPrototype } from "../../typekit/define-kit.js"; +import { TypekitPrototype, type Typekit } from "../../typekit/define-kit.js"; import { Realm } from "../realm.js"; /** diff --git a/packages/compiler/src/lib/examples.ts b/packages/compiler/src/lib/examples.ts index e52d845c730..81c6e0d846d 100644 --- a/packages/compiler/src/lib/examples.ts +++ b/packages/compiler/src/lib/examples.ts @@ -1,9 +1,12 @@ import { Temporal } from "temporal-polyfill"; import { ignoreDiagnostics } from "../core/diagnostics.js"; +import { reportDiagnostic } from "../core/messages.js"; import type { Program } from "../core/program.js"; import { getProperty } from "../core/semantic-walker.js"; import { isArrayModelType, isUnknownType } from "../core/type-utils.js"; import { + DiagnosticTarget, + NoTarget, type ObjectValue, type Scalar, type ScalarValue, @@ -21,9 +24,16 @@ export function serializeValueAsJson( value: Value, type: Type, encodeAs?: EncodeData, + diagnosticTarget?: DiagnosticTarget | typeof NoTarget, ): unknown { if (type.kind === "ModelProperty") { - return serializeValueAsJson(program, value, type.type, encodeAs ?? getEncode(program, type)); + return serializeValueAsJson( + program, + value, + type.type, + encodeAs ?? getEncode(program, type), + diagnosticTarget, + ); } switch (value.valueKind) { case "NullValue": @@ -43,12 +53,21 @@ export function serializeValueAsJson( type.kind === "Model" && isArrayModelType(program, type) ? type.indexer.value : program.checker.anyType, + /* encodeAs: */ undefined, + diagnosticTarget, ), ); case "ObjectValue": - return serializeObjectValueAsJson(program, value, type); + return serializeObjectValueAsJson(program, value, type, diagnosticTarget); case "ScalarValue": return serializeScalarValueAsJson(program, value, type, encodeAs); + case "UnknownValue": + reportDiagnostic(program, { + code: "unknown-value", + messageId: "in-json", + target: diagnosticTarget ?? value, + }); + return null; } } @@ -89,6 +108,7 @@ function serializeObjectValueAsJson( program: Program, value: ObjectValue, type: Type, + diagnosticTarget?: DiagnosticTarget | typeof NoTarget, ): Record { type = resolveUnions(program, value, type) ?? type; const obj: Record = {}; @@ -99,7 +119,13 @@ function serializeObjectValueAsJson( definition.kind === "ModelProperty" ? resolveEncodedName(program, definition, "application/json") : propValue.name; - obj[name] = serializeValueAsJson(program, propValue.value, definition); + obj[name] = serializeValueAsJson( + program, + propValue.value, + definition, + /* encodeAs: */ undefined, + propValue.node, + ); } } return obj; diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index f9d181c6686..4f1c099839e 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -9,6 +9,7 @@ import { Decorator, EnumMember, FunctionParameter, + FunctionType, Interface, Model, ModelProperty, @@ -103,6 +104,8 @@ function getTypeSignature(type: Type, options: GetSymbolSignatureOptions): strin return `(union variant)\n${fence(getUnionVariantSignature(type))}`; case "Tuple": return `(tuple)\n[${fence(type.values.map((v) => getTypeSignature(v, options)).join(", "))}]`; + case "Function": + return fence(getFunctionSignature(type)); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); @@ -116,6 +119,13 @@ function getDecoratorSignature(type: Decorator) { return `dec ${ns}${name}(${parameters.join(", ")})`; } +function getFunctionSignature(type: FunctionType) { + const ns = getQualifier(type.namespace); + const parameters = type.parameters.map((p) => getFunctionParameterSignature(p)); + const returnType = getEntityName(type.returnType); + return `fn ${ns}${type.name}(${parameters.join(", ")}): ${returnType}`; +} + function getOperationSignature(type: Operation, includeQualifier: boolean = true) { const parameters = [...type.parameters.properties.values()].map((p) => getModelPropertySignature(p, false /* includeQualifier */), diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts new file mode 100644 index 00000000000..44dbb4be71d --- /dev/null +++ b/packages/compiler/test/checker/functions.test.ts @@ -0,0 +1,271 @@ +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { Diagnostic, ModelProperty, Namespace, Type } from "../../src/core/types.js"; +import { Program, setTypeSpecNamespace } from "../../src/index.js"; +import { + BasicTestRunner, + TestHost, + createTestHost, + createTestWrapper, + expectDiagnosticEmpty, + expectDiagnostics, +} from "../../src/testing/index.js"; +import { $ } from "../../src/typekit/index.js"; + +/** Helper to assert a function declaration was bound to the js implementation */ +function expectFunction(ns: Namespace, name: string, impl: any) { + const fn = ns.functionDeclarations.get(name); + ok(fn, `Expected function ${name} to be declared.`); + strictEqual(fn.implementation, impl); +} + +describe("compiler: checker: functions", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + describe("declaration", () => { + let runner: BasicTestRunner; + let testJs: Record; + let testImpl: any; + beforeEach(() => { + testImpl = (_program: Program) => undefined; + testJs = { testFn: testImpl }; + testHost.addJsFile("test.js", testJs); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + describe("bind implementation to declaration", () => { + it("defined at root via direct export", async () => { + await runner.compile(` + extern fn testFn(); + `); + expectFunction(runner.program.getGlobalNamespaceType(), "testFn", testImpl); + }); + + it("in a namespace via direct export", async () => { + setTypeSpecNamespace("Foo.Bar", testImpl); + await runner.compile(` + namespace Foo.Bar { extern fn testFn(); } + `); + const ns = runner.program + .getGlobalNamespaceType() + .namespaces.get("Foo") + ?.namespaces.get("Bar"); + ok(ns); + expectFunction(ns, "testFn", testImpl); + }); + + it("defined at root via $functions map", async () => { + const impl = (_p: Program) => undefined; + testJs.$functions = { "": { otherFn: impl } }; + await runner.compile(`extern fn otherFn();`); + expectFunction(runner.program.getGlobalNamespaceType(), "otherFn", impl); + }); + + it("in namespace via $functions map", async () => { + const impl = (_p: Program) => undefined; + testJs.$functions = { "Foo.Bar": { nsFn: impl } }; + await runner.compile(`namespace Foo.Bar { extern fn nsFn(); }`); + const ns = runner.program + .getGlobalNamespaceType() + .namespaces.get("Foo") + ?.namespaces.get("Bar"); + ok(ns); + expectFunction(ns, "nsFn", impl); + }); + }); + + it("errors if function is missing extern modifier", async () => { + const diagnostics = await runner.diagnose(`fn testFn();`); + expectDiagnostics(diagnostics, { + code: "function-extern", + message: "A function declaration must be prefixed with the 'extern' modifier.", + }); + }); + + it("errors if extern function is missing implementation", async () => { + const diagnostics = await runner.diagnose(`extern fn missing();`); + expectDiagnostics(diagnostics, { + code: "missing-implementation", + message: "Extern declaration must have an implementation in JS file.", + }); + }); + + it("errors if rest parameter type is not array", async () => { + const diagnostics = await runner.diagnose(`extern fn f(...rest: string);`); + expectDiagnostics(diagnostics, [ + { + code: "missing-implementation", + message: "Extern declaration must have an implementation in JS file.", + }, + { + code: "rest-parameter-array", + message: "A rest parameter must be of an array type.", + }, + ]); + }); + }); + + describe("usage", () => { + let runner: BasicTestRunner; + let calledArgs: any[] | undefined; + beforeEach(() => { + calledArgs = undefined; + testHost.addJsFile("test.js", { + testFn(program: Program, a: any, b: any, ...rest: any[]) { + calledArgs = [program, a, b, ...rest]; + return a; // Return first arg + }, + sum(program: Program, ...addends: number[]) { + return addends.reduce((a, b) => a + b, 0); + }, + valFirst(program: Program, v: any) { + return v; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + function expectCalledWith(...args: any[]) { + ok(calledArgs, "Function was not called."); + strictEqual(calledArgs.length, 1 + args.length); + for (const [i, v] of args.entries()) { + strictEqual(calledArgs[1 + i], v); + } + } + + it("errors if function not declared", async () => { + const diagnostics = await runner.diagnose(`const X = missing();`); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown identifier missing", + }); + }); + + it("calls function with arguments", async () => { + await runner.compile( + `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one", "two", "three");`, + ); + expectCalledWith("one", "two", "three"); // program + args, optional b provided + }); + + it("allows omitting optional param", async () => { + await runner.compile( + `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one");`, + ); + expectCalledWith("one", undefined); + }); + + it("allows zero args for rest-only", async () => { + const diagnostics = await runner.diagnose( + `extern fn sum(...addends: valueof int32[]): valueof int32; const S = sum();`, + ); + expectDiagnostics(diagnostics, []); + }); + + it("errors if not enough args", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: valueof string, b: valueof string): valueof string; const X = testFn("one");`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected at least 2 arguments, but got 1.", + }); + }); + + it("errors if too many args", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: valueof string): valueof string; const X = testFn("one", "two");`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected 1 arguments, but got 2.", + }); + }); + + it("errors if too few with rest", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string, ...rest: string[]); alias X = testFn();`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected at least 1 arguments, but got 0.", + }); + }); + + it("errors if argument type mismatch (value)", async () => { + const diagnostics = await runner.diagnose( + `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(123);`, + ); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + }); + }); + + it("errors if passing type where value expected", async () => { + const diagnostics = await runner.diagnose( + `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(string);`, + ); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "string refers to a type, but is being used as a value here.", + }); + }); + + it("accepts string literal for type param", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string); alias X = testFn("abc");`, + ); + expectDiagnosticEmpty(diagnostics); + }); + + it("accepts arguments matching rest", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string, ...rest: string[]); alias X = testFn("a", "b", "c");`, + ); + expectDiagnosticEmpty(diagnostics); + }); + }); + + describe("referencing result type", () => { + it("can use function result in alias", async () => { + testHost.addJsFile("test.js", { + makeArray(program: Program, t: Type) { + return $(program).array.create(t); + }, + }); + const runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn makeArray(T: Reflection.Type); + + alias X = makeArray(string); + + model M { + @test prop: X; + } + `)) as [{ prop: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + ok(prop.type); + ok($(runner.program).array.is(prop.type)); + + const arrayIndexerType = prop.type.indexer.value; + + ok(arrayIndexerType); + ok($(runner.program).scalar.isString(arrayIndexerType)); + }); + }); +}); diff --git a/packages/events/generated-defs/TypeSpec.Events.ts-test.ts b/packages/events/generated-defs/TypeSpec.Events.ts-test.ts index b49c6638a51..e284767b985 100644 --- a/packages/events/generated-defs/TypeSpec.Events.ts-test.ts +++ b/packages/events/generated-defs/TypeSpec.Events.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecEventsDecorators } from "./TypeSpec.Events.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecEventsDecorators = $decorators["TypeSpec.Events"]; +const _decs: TypeSpecEventsDecorators = $decorators["TypeSpec.Events"]; diff --git a/packages/html-program-viewer/src/react/type-config.ts b/packages/html-program-viewer/src/react/type-config.ts index 1066c137a37..3b5d93f4f53 100644 --- a/packages/html-program-viewer/src/react/type-config.ts +++ b/packages/html-program-viewer/src/react/type-config.ts @@ -57,6 +57,7 @@ export const TypeConfig: TypeGraphConfig = buildConfig({ unions: "nested-items", enums: "nested-items", decoratorDeclarations: "nested-items", + functionDeclarations: "nested-items", }, Interface: { operations: "nested-items", @@ -118,6 +119,11 @@ export const TypeConfig: TypeGraphConfig = buildConfig({ implementation: "skip", target: "ref", }, + Function: { + parameters: "nested-items", + returnType: "ref", + implementation: "skip", + }, ScalarConstructor: { scalar: "parent", parameters: "nested-items", diff --git a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts index b7f2d8fa8a1..66ef18c6a14 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecHttpDecorators } from "./TypeSpec.Http.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecHttpDecorators = $decorators["TypeSpec.Http"]; +const _decs: TypeSpecHttpDecorators = $decorators["TypeSpec.Http"]; diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts index 067cfb7932b..000bbe0594e 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecJsonSchemaDecorators } from "./TypeSpec.JsonSchema.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecJsonSchemaDecorators = $decorators["TypeSpec.JsonSchema"]; +const _decs: TypeSpecJsonSchemaDecorators = $decorators["TypeSpec.JsonSchema"]; diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts index 8f8ecdbf06c..1d30a7518b3 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecOpenAPIDecorators } from "./TypeSpec.OpenAPI.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; +const _decs: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; diff --git a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts index b115637c518..70efc1b2e22 100644 --- a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts +++ b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecOpenAPIDecorators } from "./TypeSpec.OpenAPI.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; +const _decs: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; diff --git a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts index 8439d354b21..6c62a82e7c1 100644 --- a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts +++ b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecProtobufDecorators } from "./TypeSpec.Protobuf.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecProtobufDecorators = $decorators["TypeSpec.Protobuf"]; +const _decs: TypeSpecProtobufDecorators = $decorators["TypeSpec.Protobuf"]; diff --git a/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts b/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts index ab1192be9f6..d9238050e6c 100644 --- a/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts +++ b/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecRestDecorators } from "./TypeSpec.Rest.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecRestDecorators = $decorators["TypeSpec.Rest"]; +const _decs: TypeSpecRestDecorators = $decorators["TypeSpec.Rest"]; diff --git a/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts b/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts index 515f6e257c9..cc404423ef3 100644 --- a/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts +++ b/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecSpectorDecorators } from "./TypeSpec.Spector.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecSpectorDecorators = $decorators["TypeSpec.Spector"]; +const _decs: TypeSpecSpectorDecorators = $decorators["TypeSpec.Spector"]; diff --git a/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts b/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts index dc02b5a7be3..ffd24790e3e 100644 --- a/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts +++ b/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecSSEDecorators } from "./TypeSpec.SSE.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecSSEDecorators = $decorators["TypeSpec.SSE"]; +const _decs: TypeSpecSSEDecorators = $decorators["TypeSpec.SSE"]; diff --git a/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts b/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts index db99152fa8d..672d8f183b8 100644 --- a/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts +++ b/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecStreamsDecorators } from "./TypeSpec.Streams.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecStreamsDecorators = $decorators["TypeSpec.Streams"]; +const _decs: TypeSpecStreamsDecorators = $decorators["TypeSpec.Streams"]; diff --git a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx deleted file mode 100644 index 8f40feac957..00000000000 --- a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Refkey } from "@alloy-js/core"; -import * as ts from "@alloy-js/typescript"; - -export interface DecoratorSignatureTests { - namespaceName: string; - dollarDecoratorRefKey: Refkey; - dollarDecoratorsTypeRefKey: Refkey; -} - -export function DecoratorSignatureTests({ - namespaceName, - dollarDecoratorRefKey, - dollarDecoratorsTypeRefKey, -}: Readonly) { - return ( - <> - - - - {dollarDecoratorRefKey} - {`["${namespaceName}"]`} - - - ); -} diff --git a/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx b/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx new file mode 100644 index 00000000000..4f686d02343 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx @@ -0,0 +1,32 @@ +import { For, Refkey } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { FunctionSignature } from "../types.js"; + +export interface DollarFunctionsTypeProps { + namespaceName: string; + functions: FunctionSignature[]; + refkey: Refkey; +} + +/** Type for the $functions variable for the given namespace */ +export function DollarFunctionsType(props: Readonly) { + return ( + + + + {(signature) => { + return ; + }} + + + + ); +} + +function getFunctionsRecordForNamespaceName(namespaceName: string) { + return `${namespaceName.replaceAll(".", "")}Functions`; +} diff --git a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx new file mode 100644 index 00000000000..939b289db50 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx @@ -0,0 +1,53 @@ +import { Refkey, Show } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { EntitySignature } from "../types.js"; + +export interface EntitySignatureTests { + namespaceName: string; + entities: EntitySignature[]; + dollarDecoratorRefKey: Refkey; + dollarDecoratorsTypeRefKey: Refkey; + dollarFunctionsRefKey: Refkey; + dollarFunctionsTypeRefKey: Refkey; +} + +export function EntitySignatureTests({ + namespaceName, + entities, + dollarDecoratorRefKey, + dollarDecoratorsTypeRefKey, + dollarFunctionsRefKey, + dollarFunctionsTypeRefKey, +}: Readonly) { + const hasDecorators = entities.some((e) => e.kind === "Decorator"); + const hasFunctions = entities.some((e) => e.kind === "Function"); + + return ( + <> + + + + + {dollarDecoratorRefKey} + {`["${namespaceName}"]`} + + + + + + + {dollarFunctionsRefKey} + {`["${namespaceName}"]`} + + + + ); +} diff --git a/packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx similarity index 52% rename from packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx rename to packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx index 13991a4f9e6..17dfceaba31 100644 --- a/packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx @@ -5,48 +5,68 @@ import { Refkey, refkey, render, + Show, StatementList, } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { Program } from "@typespec/compiler"; import { typespecCompiler } from "../external-packages/compiler.js"; -import { DecoratorSignature } from "../types.js"; -import { DecoratorSignatureTests } from "./decorator-signature-tests.jsx"; -import { - DecoratorSignatureType, - ValueOfModelTsInterfaceBody, -} from "./decorator-signature-type.jsx"; -import { DollarDecoratorsType } from "./dollar-decorators-type.jsx"; +import { DecoratorSignature, EntitySignature, FunctionSignature } from "../types.js"; +import { DecoratorSignatureType, ValueOfModelTsInterfaceBody } from "./decorator-signature-type.js"; +import { DollarDecoratorsType } from "./dollar-decorators-type.js"; +import { DollarFunctionsType } from "./dollar-functions-type.jsx"; +import { EntitySignatureTests } from "./entity-signature-tests.jsx"; +import { FunctionSignatureType } from "./function-signature-type.jsx"; import { createTspdContext, TspdContext, useTspd } from "./tspd-context.js"; -export interface DecoratorSignaturesProps { - decorators: DecoratorSignature[]; +export interface EntitySignaturesProps { + entities: EntitySignature[]; namespaceName: string; dollarDecoratorsRefKey: Refkey; + dollarFunctionsRefKey: Refkey; } -export function DecoratorSignatures({ +export function EntitySignatures({ namespaceName, - decorators, + entities, dollarDecoratorsRefKey: dollarDecoratorsRefkey, -}: DecoratorSignaturesProps) { + dollarFunctionsRefKey: dollarFunctionsRefkey, +}: EntitySignaturesProps) { + const decorators = entities.filter((e): e is DecoratorSignature => e.kind === "Decorator"); + + const functions = entities.filter((e): e is FunctionSignature => e.kind === "Function"); + return ( - - - - {(signature) => { - return ; - }} - - - - + 0}> + + + + {(signature) => } + + + + + + 0}> + + + + {(signature) => } + + + + + ); } @@ -70,19 +90,20 @@ export function LocalTypes() { export function generateSignatures( program: Program, - decorators: DecoratorSignature[], + entities: EntitySignature[], libraryName: string, namespaceName: string, ): OutputDirectory { const context = createTspdContext(program); const base = namespaceName === "" ? "__global__" : namespaceName; const $decoratorsRef = refkey(); + const $functionsRef = refkey(); const userLib = ts.createPackage({ name: libraryName, version: "0.0.0", descriptor: { ".": { - named: ["$decorators"], + named: ["$decorators", "$functions"], }, }, }); @@ -91,10 +112,11 @@ export function generateSignatures( - {!base.includes(".Private") && ( @@ -102,10 +124,13 @@ export function generateSignatures( path={`${base}.ts-test.ts`} headerComment="An error in the imports would mean that the decorator is not exported or doesn't have the right name." > - )} diff --git a/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx new file mode 100644 index 00000000000..00ae0a8b782 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx @@ -0,0 +1,357 @@ +import { For, join, List, refkey } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { + getSourceLocation, + IntrinsicScalarName, + isArrayModelType, + MixedParameterConstraint, + Model, + Program, + Scalar, + type Type, +} from "@typespec/compiler"; +import { DocTag, SyntaxKind } from "@typespec/compiler/ast"; +import { typespecCompiler } from "../external-packages/compiler.js"; +import { FunctionSignature } from "../types.js"; +import { useTspd } from "./tspd-context.js"; + +export interface FunctionSignatureProps { + signature: FunctionSignature; +} + +/** Render the type of function implementation */ +export function FunctionSignatureType(props: Readonly) { + const { program } = useTspd(); + const func = props.signature.tspFunction; + const parameters: ts.ParameterDescriptor[] = [ + { + name: "program", + type: typespecCompiler.Program, + }, + ...func.parameters.map( + (param): ts.ParameterDescriptor => ({ + // https://github.com/alloy-framework/alloy/issues/144 + name: param.rest ? `...${param.name}` : param.name, + type: param.rest ? ( + <> + ( + {param.type ? ( + + ) : undefined} + )[] + + ) : ( + + ), + optional: param.optional, + }), + ), + ]; + + const returnType = ; + + return ( + + + + ); +} + +/** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ +function extractRestParamConstraint( + program: Program, + constraint: MixedParameterConstraint, +): MixedParameterConstraint | undefined { + let valueType: Type | undefined; + let type: Type | undefined; + if (constraint.valueType) { + if (constraint.valueType.kind === "Model" && isArrayModelType(program, constraint.valueType)) { + valueType = constraint.valueType.indexer.value; + } else { + return undefined; + } + } + if (constraint.type) { + if (constraint.type.kind === "Model" && isArrayModelType(program, constraint.type)) { + type = constraint.type.indexer.value; + } else { + return undefined; + } + } + + return { + entityKind: "MixedParameterConstraint", + type, + valueType, + }; +} + +export interface ParameterTsTypeProps { + constraint: MixedParameterConstraint; +} +export function ParameterTsType({ constraint }: ParameterTsTypeProps) { + if (constraint.type && constraint.valueType) { + return ( + <> + + {" | "} + + + ); + } + if (constraint.valueType) { + return ; + } else if (constraint.type) { + return ; + } + + return typespecCompiler.Type; +} + +function TypeConstraintTSType({ type }: { type: Type }) { + if (type.kind === "Model" && isReflectionType(type)) { + return (typespecCompiler as any)[type.name]; + } else if (type.kind === "Union") { + const variants = [...type.variants.values()].map((x) => x.type); + + if (variants.every((x) => isReflectionType(x))) { + return join( + [...new Set(variants)].map((x) => getCompilerType((x as Model).name)), + { + joiner: " | ", + }, + ); + } else { + return typespecCompiler.Type; + } + } + return typespecCompiler.Type; +} + +function getCompilerType(name: string) { + return (typespecCompiler as any)[name]; +} + +function ValueTsType({ type }: { type: Type }) { + const { program } = useTspd(); + switch (type.kind) { + case "Boolean": + return `${type.value}`; + case "String": + return `"${type.value}"`; + case "Number": + return `${type.value}`; + case "Scalar": + return ; + case "Union": + return join( + [...type.variants.values()].map((x) => ), + { joiner: " | " }, + ); + case "Model": + if (isArrayModelType(program, type)) { + return ( + <> + readonly ( + )[] + + ); + } else if (isReflectionType(type)) { + return getValueOfReflectionType(type); + } else { + // If its exactly the record type use Record instead of the model name. + if (type.indexer && type.name === "Record" && type.namespace?.name === "TypeSpec") { + return ( + <> + Record{" + {">"} + + ); + } + if (type.name) { + return ; + } else { + return ; + } + } + } + return "unknown"; +} + +function LocalTypeReference({ type }: { type: Model }) { + const { addLocalType } = useTspd(); + addLocalType(type); + return ; +} +function ValueOfModelTsType({ model }: { model: Model }) { + return ( + + + + ); +} + +export function ValueOfModelTsInterfaceBody({ model }: { model: Model }) { + return ( + + {model.indexer?.value && ( + } + /> + )} + + {(x) => ( + } + /> + )} + + + ); +} + +function ScalarTsType({ scalar }: { scalar: Scalar }) { + const { program } = useTspd(); + const isStd = program.checker.isStdType(scalar); + if (isStd) { + return getStdScalarTSType(scalar); + } else if (scalar.baseScalar) { + return ; + } else { + return "unknown"; + } +} + +function getStdScalarTSType(scalar: Scalar & { name: IntrinsicScalarName }) { + switch (scalar.name) { + case "numeric": + case "decimal": + case "decimal128": + case "float": + case "integer": + case "int64": + case "uint64": + return typespecCompiler.Numeric; + case "int8": + case "int16": + case "int32": + case "safeint": + case "uint8": + case "uint16": + case "uint32": + case "float64": + case "float32": + return "number"; + case "string": + case "url": + return "string"; + case "boolean": + return "boolean"; + case "plainDate": + case "utcDateTime": + case "offsetDateTime": + case "plainTime": + case "duration": + case "bytes": + return "unknown"; + default: + const _assertNever: never = scalar.name; + return "unknown"; + } +} + +function isReflectionType(type: Type): type is Model & { namespace: { name: "Reflection" } } { + return ( + type.kind === "Model" && + type.namespace?.name === "Reflection" && + type.namespace?.namespace?.name === "TypeSpec" + ); +} + +function getValueOfReflectionType(type: Model) { + switch (type.name) { + case "EnumMember": + case "Enum": + return typespecCompiler.EnumValue; + case "Model": + return "Record"; + default: + return "unknown"; + } +} + +function getDocComment(type: Type): string { + const docs = type.node?.docs; + if (docs === undefined || docs.length === 0) { + return ""; + } + + const mainContentLines: string[] = []; + const tagLines = []; + for (const doc of docs) { + for (const content of doc.content) { + for (const line of content.text.split("\n")) { + mainContentLines.push(line); + } + } + for (const tag of doc.tags) { + tagLines.push(); + + let first = true; + const hasContentFirstLine = checkIfTagHasDocOnSameLine(tag); + const tagStart = + tag.kind === SyntaxKind.DocParamTag || tag.kind === SyntaxKind.DocTemplateTag + ? `@${tag.tagName.sv} ${tag.paramName.sv}` + : `@${tag.tagName.sv}`; + for (const content of tag.content) { + for (const line of content.text.split("\n")) { + const cleaned = sanitizeDocComment(line); + if (first) { + if (hasContentFirstLine) { + tagLines.push(`${tagStart} ${cleaned}`); + } else { + tagLines.push(tagStart, cleaned); + } + + first = false; + } else { + tagLines.push(cleaned); + } + } + } + } + } + + const docLines = [...mainContentLines, ...(tagLines.length > 0 ? [""] : []), ...tagLines]; + return docLines.join("\n"); +} + +function sanitizeDocComment(doc: string): string { + // Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679 + return doc.replaceAll("@internal", `@_internal`); +} + +function checkIfTagHasDocOnSameLine(tag: DocTag): boolean { + const start = tag.content[0]?.pos; + const end = tag.content[0]?.end; + const file = getSourceLocation(tag.content[0]).file; + + let hasFirstLine = false; + for (let i = start; i < end; i++) { + const ch = file.text[i]; + if (ch === "\n") { + break; + } + // Todo reuse compiler whitespace logic or have a way to get this info from the parser. + if (ch !== " ") { + hasFirstLine = true; + } + } + return hasFirstLine; +} diff --git a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts index 2c8789f48e8..05ee8da2416 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -6,6 +6,7 @@ export const typespecCompiler = createPackage({ descriptor: { ".": { named: [ + "Program", "DecoratorContext", "Type", "Namespace", diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 8e5d57fe102..87d8ec7271b 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -19,9 +19,10 @@ import { resolvePath, } from "@typespec/compiler"; import prettier from "prettier"; +import { FunctionType } from "../../../compiler/src/core/types.js"; import { createDiagnostic } from "../ref-doc/lib.js"; -import { generateSignatures } from "./components/decorators-signatures.js"; -import { DecoratorSignature } from "./types.js"; +import { generateSignatures } from "./components/entity-signatures.js"; +import { DecoratorSignature, EntitySignature, FunctionSignature } from "./types.js"; function createSourceLocation(path: string): SourceLocation { return { file: createSourceFile("", path), pos: 0, end: 0 }; @@ -108,8 +109,7 @@ export async function generateExternDecorators( packageName: string, options?: GenerateExternDecoratorOptions, ): Promise> { - const decorators = new Map(); - + const entities = new Map(); const listener: SemanticNodeListener = { decorator(dec) { if ( @@ -119,12 +119,28 @@ export async function generateExternDecorators( return; } const namespaceName = getTypeName(dec.namespace); - let decoratorForNamespace = decorators.get(namespaceName); - if (!decoratorForNamespace) { - decoratorForNamespace = []; - decorators.set(namespaceName, decoratorForNamespace); + let entitiesForNamespace = entities.get(namespaceName); + if (!entitiesForNamespace) { + entitiesForNamespace = []; + entities.set(namespaceName, entitiesForNamespace); } - decoratorForNamespace.push(resolveDecoratorSignature(dec)); + entitiesForNamespace.push(resolveDecoratorSignature(dec)); + }, + function(func) { + if ( + (packageName !== "@typespec/compiler" && + getLocationContext(program, func).type !== "project") || + func.namespace === undefined + ) { + return; + } + const namespaceName = getTypeName(func.namespace); + let entitiesForNamespace = entities.get(namespaceName); + if (!entitiesForNamespace) { + entitiesForNamespace = []; + entities.set(namespaceName, entitiesForNamespace); + } + entitiesForNamespace.push(resolveFunctionSignature(func)); }, }; if (options?.namespaces) { @@ -150,8 +166,8 @@ export async function generateExternDecorators( } const files: Record = {}; - for (const [ns, nsDecorators] of decorators.entries()) { - const output = generateSignatures(program, nsDecorators, packageName, ns); + for (const [ns, nsEntities] of entities.entries()) { + const output = generateSignatures(program, nsEntities, packageName, ns); const rawFiles: OutputFile[] = []; traverseOutput(output, { visitDirectory: () => {}, @@ -169,9 +185,19 @@ export async function generateExternDecorators( function resolveDecoratorSignature(decorator: Decorator): DecoratorSignature { return { + kind: "Decorator", decorator, name: decorator.name, jsName: "$" + decorator.name.slice(1), typeName: decorator.name[1].toUpperCase() + decorator.name.slice(2) + "Decorator", }; } + +function resolveFunctionSignature(func: FunctionType): FunctionSignature { + return { + kind: "Function", + tspFunction: func, + name: func.name, + typeName: func.name[0].toUpperCase() + func.name.slice(1) + "FunctionImplementation", + }; +} diff --git a/packages/tspd/src/gen-extern-signatures/types.ts b/packages/tspd/src/gen-extern-signatures/types.ts index f40343d7e01..9e35b1fe901 100644 --- a/packages/tspd/src/gen-extern-signatures/types.ts +++ b/packages/tspd/src/gen-extern-signatures/types.ts @@ -1,6 +1,10 @@ -import type { Decorator } from "../../../compiler/src/core/types.js"; +import type { Decorator, FunctionType } from "../../../compiler/src/core/types.js"; + +export type EntitySignature = DecoratorSignature | FunctionSignature; export interface DecoratorSignature { + kind: Decorator["kind"]; + /** Decorator name ()`@example `@foo`) */ name: string; @@ -12,3 +16,15 @@ export interface DecoratorSignature { decorator: Decorator; } + +export interface FunctionSignature { + kind: FunctionType["kind"]; + + /** Function name */ + name: string; + + /** TypeScript type name (@example `FooFunction`) */ + typeName: string; + + tspFunction: FunctionType; +} diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index cd4e42984c0..eb30d76aba7 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -14,6 +14,7 @@ import { UnionVariant, } from "@typespec/compiler"; import { TemplateParameterDeclarationNode } from "@typespec/compiler/ast"; +import { FunctionType } from "../../../../compiler/src/core/types.js"; /** @internal */ export function getTypeSignature(type: Type): string { @@ -59,6 +60,8 @@ export function getTypeSignature(type: Type): string { return `(union variant) ${getUnionVariantSignature(type)}`; case "Tuple": return `(tuple) [${type.values.map(getTypeSignature).join(", ")}]`; + case "Function": + return getFunctionSignature(type); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); @@ -84,6 +87,12 @@ function getDecoratorSignature(type: Decorator) { return signature; } +function getFunctionSignature(type: FunctionType) { + const ns = getQualifier(type.namespace); + const parameters = [...type.parameters].map((x) => getFunctionParameterSignature(x)); + return `fn ${ns}${type.name}(${parameters.join(", ")}): ${getEntityName(type.returnType)}`; +} + function getInterfaceSignature(type: Interface) { const ns = getQualifier(type.namespace); diff --git a/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts b/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts index dd682efbc45..5e7d70dad0a 100644 --- a/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts +++ b/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecVersioningDecorators } from "./TypeSpec.Versioning.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecVersioningDecorators = $decorators["TypeSpec.Versioning"]; +const _decs: TypeSpecVersioningDecorators = $decorators["TypeSpec.Versioning"]; diff --git a/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts b/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts index 73d1eb64388..445937659a8 100644 --- a/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts +++ b/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecXmlDecorators } from "./TypeSpec.Xml.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecXmlDecorators = $decorators["TypeSpec.Xml"]; +const _decs: TypeSpecXmlDecorators = $decorators["TypeSpec.Xml"];