diff --git a/packages/compiler/lib/main.tsp b/packages/compiler/lib/main.tsp index 8d1a997664..c563bf5754 100644 --- a/packages/compiler/lib/main.tsp +++ b/packages/compiler/lib/main.tsp @@ -2,3 +2,4 @@ import "./lib.tsp"; import "./decorators.tsp"; import "./reflection.tsp"; import "./projected-names.tsp"; +import "./methods.tsp"; diff --git a/packages/compiler/lib/methods.tsp b/packages/compiler/lib/methods.tsp new file mode 100644 index 0000000000..529f5fa2a9 --- /dev/null +++ b/packages/compiler/lib/methods.tsp @@ -0,0 +1,45 @@ +namespace TypeSpec.ValueMethods; + +interface Array { + someOf(cb: Private.BooleanReturnCb): boolean; + allOf(cb: Private.BooleanReturnCb): boolean; + noneOf(cb: Private.BooleanReturnCb): boolean; + find(cb: Private.BooleanReturnCb): TElementType | void; + contains(value: TElementType): boolean; + first(n: numeric): TElementType[]; + last(n: numeric): TElementType[]; + sum(cb?: Private.NumericReturnCb): numeric; + + /** + * Return the minimum value in the array. Optionally pass a callback to select the value to compare. + */ + min(cb?: Private.NumericReturnCb): numeric; + + /** + * Return the maximum value in the array. Optionally pass a callback to select the value to compare. + */ + max(cb?: Private.NumericReturnCb): numeric; + distinct(): TElementType[]; + length(): numeric; +} + +interface String { + contains(value: string): boolean; + startsWith(value: string): boolean; + endsWith(value: string): boolean; + slice(start: numeric, end?: numeric, unit?: StringUnit = StringUnit.utf16CodeUnit): string; + concat(value: string): string; + length(unit?: StringUnit = StringUnit.utf16CodeUnit): numeric; +} + +enum StringUnit { + codePoint, + utf16CodeUnit, + utf8CodeUnit, + graphemeCluster, +} + +namespace Private { + op BooleanReturnCb(value: T): boolean; + op NumericReturnCb(value: T): numeric; +} diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index ce61c8ebd5..40313eda94 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -62,6 +62,7 @@ import { MemberExpressionNode, MemberNode, MemberType, + MetaMemberKey, Model, ModelExpressionNode, ModelIndexer, @@ -302,8 +303,6 @@ export function createChecker(program: Program): Checker { const nullType = createType({ kind: "Intrinsic", name: "null" } as const); const nullSym = createSymbol(undefined, "null", SymbolFlags.None); - const sharedMetaProperties = createSharedMetaProperties(); - const projectionsByTypeKind = new Map([ ["Model", []], ["ModelProperty", []], @@ -351,6 +350,7 @@ export function createChecker(program: Program): Checker { } } + const sharedMetaInterfaces: Partial> = createSharedMetaInterfaces(); let evalContext: EvalContext | undefined = undefined; const checker: Checker = { @@ -414,6 +414,21 @@ export function createChecker(program: Program): Checker { getSymbolLinks(nullSym).type = nullType; } + function createSharedMetaInterfaces(): Partial> { + if (!typespecNamespaceBinding) return {}; + + const interfaces: Partial> = {}; + + const vmns = typespecNamespaceBinding.exports!.get("ValueMethods")!; + for (const [name, sym] of vmns.exports!) { + if (sym.flags & SymbolFlags.Interface) { + interfaces[name as MetaMemberKey] = sym; + } + } + + return interfaces; + } + function getStdType(name: T): StdTypes[T] { const type = stdTypes[name]; if (type !== undefined) { @@ -944,7 +959,6 @@ export function createChecker(program: Program): Checker { } if (tooFew) { - throw new Error("TOO FEW"); reportCheckerDiagnostic( createDiagnostic({ code: "invalid-template-args", @@ -1940,9 +1954,9 @@ export function createChecker(program: Program): Checker { } else { addCompletions(base.metatypeMembers); const type = base.type ?? getTypeForNode(base.declarations[0], undefined); - const members = sharedMetaProperties[metaMemberKey(type)]; + const members = getTableForMetaMembers(type, undefined); if (members) { - for (const sym of Object.values(members).map((m: any) => m.symbol)) { + for (const sym of members.values()) { addCompletion(sym.name, sym); } } @@ -2290,13 +2304,42 @@ export function createChecker(program: Program): Checker { ? base.type! : checkTypeReferenceSymbol(base, node, mapper); - const metaMembers = sharedMetaProperties[metaMemberKey(baseType)]; - if (!metaMembers) return undefined; - const metaProp = metaMembers[node.id.sv]; - return metaProp.symbol; + const table = getTableForMetaMembers(baseType, mapper); + if (!table) return undefined; + + return table.get(node.id.sv); + } + + function getTableForMetaMembers(baseType: Type, mapper: TypeMapper | undefined) { + const key = metaMemberKey(baseType); + const ifaceSym = sharedMetaInterfaces[key]; + if (!ifaceSym) { + return undefined; + } + + if (key === "Array") { + // Array needs instantiated. + const ifaceNode = ifaceSym.declarations[0] as InterfaceStatementNode; + const param: TemplateParameter = getTypeForNode(ifaceNode.templateParameters[0]) as any; + const ifaceType = getOrInstantiateTemplate( + ifaceNode, + [param], + [(baseType as Model).indexer!.value], + mapper + ) as Interface; + lateBindMemberContainer(ifaceType); + lateBindMembers(ifaceType, ifaceType.symbol!); + return getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); + } else { + const links = getSymbolLinks(ifaceSym); + const ifaceType = links.declaredType as Interface; // should be checked by now. + lateBindMemberContainer(ifaceType); + lateBindMembers(ifaceType, ifaceType.symbol!); + return getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); + } } - function metaMemberKey(baseType: Type) { + function metaMemberKey(baseType: Type): MetaMemberKey { return baseType.kind === "Model" && isArrayModelType(program, baseType) ? ("Array" as const) : baseType.kind === "Scalar" && isRelatedToScalar(baseType, getStdType("string")) @@ -3471,12 +3514,76 @@ export function createChecker(program: Program): Checker { case SyntaxKind.ProjectionCallExpression: { const target = checkLogicExpression(node.target, mapper); if (!target) return; + + if (target.type.kind !== "Operation") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "type-expected", + format: { + actual: target.type.kind, + expected: "Operation", + }, + target: node, + }) + ); + return; + } + const expectedArgTypes = [...target.type.parameters.properties.values()]; + + const minArgs = expectedArgTypes.filter((x) => !x.optional).length; + const maxArgs = expectedArgTypes.length; + + if (node.arguments.length < minArgs) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-function-args", + messageId: "tooFew", + format: { + actual: String(node.arguments.length), + expected: String(minArgs), + }, + target: node, + }) + ); + return; + } else if (node.arguments.length > maxArgs) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-function-args", + messageId: "tooMany", + format: { + actual: String(node.arguments.length), + expected: String(maxArgs), + }, + target: node, + }) + ); + return; + } + const args = []; - for (const arg of node.arguments) { - const argResult = checkLogicExpression(arg, mapper); - if (!argResult) return; - args.push(argResult); + for (let i = 0; i < node.arguments.length; i++) { + const expectedType = expectedArgTypes[i]; + const argType = checkLogicExpression(node.arguments[i], mapper); + if (!argType) return; + if (!isTypeAssignableTo(argType.type, expectedType.type, argType.type)[0]) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-function-args", + messageId: "incorrect", + format: { + name: expectedType.name, + actual: getTypeName(argType.type), + expected: getTypeName(expectedType.type), + }, + target: node, + }) + ); + return; + } + args.push(argType); } + return { logic: { kind: "CallExpression", @@ -5569,6 +5676,9 @@ export function createChecker(program: Program): Checker { return isAssignableToUnion(source, target, diagnosticTarget); } else if (target.kind === "Enum") { return isAssignableToEnum(source, target, diagnosticTarget); + } else if (target.kind === "Operation" && source.kind === "Operation") { + // todo: check if the operation is assignable + return [true, []]; } return [false, [createUnassignableDiagnostic(source, target, diagnosticTarget)]]; @@ -5885,41 +5995,6 @@ export function createChecker(program: Program): Checker { if (type.kind === "Model") return stdType === undefined || stdType === type.name; return false; } - - function createSharedMetaProperties(): Partial> { - function createSharedMetaProperty(scope: string, name: string) { - const type = createAndFinishType({ kind: "Intrinsic", name: scope + "::" + name }); - const symbol = createSymbol(undefined, name, SymbolFlags.LateBound); - mutate(symbol).type = type as any; // intrinsics have a set name, need to fix this - return { - type, - symbol, - }; - } - - return { - Array: { - someOf: createSharedMetaProperty("Array", "someOf"), - allOf: createSharedMetaProperty("Array", "allOf"), - noneOf: createSharedMetaProperty("Array", "noneOf"), - find: createSharedMetaProperty("Array", "find"), - contains: createSharedMetaProperty("Array", "contains"), - first: createSharedMetaProperty("Array", "first"), - last: createSharedMetaProperty("Array", "last"), - sum: createSharedMetaProperty("Array", "sum"), - min: createSharedMetaProperty("Array", "min"), - max: createSharedMetaProperty("Array", "max"), - distinct: createSharedMetaProperty("Array", "distinct"), - }, - String: { - startsWith: createSharedMetaProperty("String", "startsWith"), - endsWith: createSharedMetaProperty("String", "endsWith"), - contains: createSharedMetaProperty("String", "contains"), - slice: createSharedMetaProperty("String", "slice"), - concat: createSharedMetaProperty("String", "concat"), - }, - }; - } } function isAnonymous(type: Type) { diff --git a/packages/compiler/src/core/helpers/operation-utils.ts b/packages/compiler/src/core/helpers/operation-utils.ts index 7cbd4016ce..d5d5db34d5 100644 --- a/packages/compiler/src/core/helpers/operation-utils.ts +++ b/packages/compiler/src/core/helpers/operation-utils.ts @@ -20,6 +20,8 @@ export function listOperationsIn( ): Operation[] { const operations: Operation[] = []; + const globalNamespace = container.kind === "Namespace" && container.name === ""; + function addOperations(current: Namespace | Interface) { if (current.kind === "Interface" && isTemplateDeclaration(current)) { // Skip template interface operations @@ -42,6 +44,9 @@ export function listOperationsIn( ]; for (const child of children) { + if (globalNamespace && child.name === "TypeSpec" && child.namespace === container) { + continue; + } addOperations(child); } } diff --git a/packages/compiler/src/core/helpers/usage-resolver.ts b/packages/compiler/src/core/helpers/usage-resolver.ts index 4d4db46a5f..c24c1126b7 100644 --- a/packages/compiler/src/core/helpers/usage-resolver.ts +++ b/packages/compiler/src/core/helpers/usage-resolver.ts @@ -68,7 +68,12 @@ function trackUsage( } function addUsagesInNamespace(namespace: Namespace, usages: Map): void { + const globalNamespace = namespace.name === ""; + for (const subNamespace of namespace.namespaces.values()) { + if (globalNamespace && subNamespace.name === "TypeSpec") { + continue; + } addUsagesInNamespace(subNamespace, usages); } for (const Interface of namespace.interfaces.values()) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index afc9687aee..0ecc86334a 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -486,6 +486,10 @@ const diagnostics = { default: paramMessage`Shadowing parent template parmaeter with the same name "${"name"}"`, }, }, + + /** + * Validation clause + */ "type-expected": { severity: "error", messages: { @@ -494,6 +498,15 @@ const diagnostics = { }, }, + "invalid-function-args": { + severity: "error", + messages: { + default: paramMessage`Invalid arguments for function "${"name"}".`, + tooFew: paramMessage`Too few arguments. Expected at least ${"expected"} argument(s) but got ${"actual"}.`, + tooMany: paramMessage`Too many arguments. Expected at most ${"expected"} argument(s) but got ${"actual"}.`, + incorrect: paramMessage`Argument '${"name"}' has incorrect type. Expected ${"expected"} but got ${"actual"}.`, + }, + }, /** * Configuration */ diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 43c629b052..e25b137e06 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -208,6 +208,8 @@ export type IntrinsicScalarName = | "boolean" | "url"; +export type MetaMemberKey = Type["kind"] | "Array" | "String"; + export type NeverIndexer = { key: NeverType; value: undefined }; export type ModelIndexer = { key: Scalar; diff --git a/packages/compiler/test/checker/validate.test.ts b/packages/compiler/test/checker/validate.test.ts index f2602799a4..38bbca0b90 100644 --- a/packages/compiler/test/checker/validate.test.ts +++ b/packages/compiler/test/checker/validate.test.ts @@ -1,8 +1,8 @@ import { notStrictEqual, strictEqual } from "assert"; -import { IntrinsicType, LogicCallExpression, Model, Scalar } from "../../src/core/types.js"; +import { LogicCallExpression, Model, Operation, Scalar } from "../../src/core/types.js"; import { TestHost, createTestHost, expectDiagnostics } from "../../src/testing/index.js"; -describe.only("compiler: validate", () => { +describe("compiler: validate", () => { let testHost: TestHost; beforeEach(async () => { @@ -289,11 +289,11 @@ describe.only("compiler: validate", () => { }; const checkItems = M.validates.get("checkItems")!.logic as LogicCallExpression; - strictEqual((checkItems.target.type as IntrinsicType).name, "Array::someOf"); + strictEqual((checkItems.target.type as Operation).name, "someOf"); strictEqual(checkItems.arguments[0].kind, "LambdaExpression"); const validateStr = M.validates.get("checkStr")!.logic as LogicCallExpression; - strictEqual((validateStr.target.type as IntrinsicType).name, "String::startsWith"); + strictEqual((validateStr.target.type as Operation).name, "startsWith"); strictEqual(validateStr.arguments[0].kind, "StringLiteral"); }); @@ -400,6 +400,80 @@ describe.only("compiler: validate", () => { ); await testHost.compile("main.tsp"); + + // TODO: VALIDATE + }); + + it("emits diagnostics for incorrect function call target", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test model M { + prop: string[]; + + validate check: prop(12); + } + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, [ + { code: "type-expected", message: /Expected type of Operation but got Model/ }, + ]); + }); + + it("emits diagnostics for too few or too many function call parameters", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test model M { + prop: numeric[]; + + validate check: prop::contains(); + validate check2: prop::contains(1, 2); + validate check3: prop::sum(); // ok + validate check4: prop::sum((v) => { v; }); // ok + validate check5: prop::sum((v) => { v; }, 1); // error + } + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, [ + { + code: "invalid-function-args", + message: /Too few arguments. Expected at least 1 argument\(s\) but got 0./, + }, + { + code: "invalid-function-args", + message: /Too many arguments. Expected at most 1 argument\(s\) but got 2./, + }, + { + code: "invalid-function-args", + message: /Too many arguments. Expected at most 1 argument\(s\) but got 2./, + }, + ]); + }); + + it("emits diagnostics for incorrect function call parameters", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test model M { + prop: string[]; + + validate check: prop::contains(12); + } + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, [ + { + code: "invalid-function-args", + message: /Argument 'value' has incorrect type. Expected string but got numeric./, + }, + ]); }); it.skip("doesn't allow references decorators", async () => { diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index bb02612f4c..e7a34b8ce1 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -948,14 +948,22 @@ describe("compiler: server: completion", () => { { label: "min", insertText: "min", - kind: CompletionItemKind.Function, - documentation: undefined, + kind: CompletionItemKind.Method, + documentation: { + kind: "markdown", + value: + "```typespec\nop TypeSpec.ValueMethods.min(cb: TypeSpec.ValueMethods.Private.NumericReturnCb): numeric\n```", + }, }, { label: "max", insertText: "max", - kind: CompletionItemKind.Function, - documentation: undefined, + kind: CompletionItemKind.Method, + documentation: { + kind: "markdown", + value: + "```typespec\nop TypeSpec.ValueMethods.max(cb: TypeSpec.ValueMethods.Private.NumericReturnCb): numeric\n```", + }, }, ]); }); @@ -980,7 +988,7 @@ describe("compiler: server: completion", () => { ]); }); - it.only("Completes members of the type of model property references inside validates clauses", async () => { + it("Completes members of the type of model property references inside validates clauses", async () => { const completions = await complete( ` model Foo {