From 51075bb6f6160a4c965db1d1e3eed85e873a8b9d Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 21 Aug 2023 15:44:08 -0700 Subject: [PATCH 1/4] Use operations for value methods --- packages/compiler/lib/main.tsp | 1 + packages/compiler/lib/methods.tsp | 45 ++++++++++++++++ packages/compiler/src/core/checker.ts | 53 +++++++++++++++++-- packages/compiler/src/core/types.ts | 2 + .../compiler/test/checker/validate.test.ts | 8 +-- 5 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/lib/methods.tsp 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..89a4bc77d1 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, @@ -351,6 +352,7 @@ export function createChecker(program: Program): Checker { } } + const sharedMetaInterfaces: Partial> = createSharedMetaInterfaces(); let evalContext: EvalContext | undefined = undefined; const checker: Checker = { @@ -414,6 +416,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) { @@ -2290,13 +2307,39 @@ 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 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!); + const table = getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); + const member = table.get(node.id.sv); + return member; + } else { + const links = getSymbolLinks(ifaceSym); + const ifaceType = links.declaredType as Interface; // should be checked by now. + lateBindMemberContainer(ifaceType); + lateBindMembers(ifaceType, ifaceType.symbol!); + const table = getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); + const member = table.get(node.id.sv); + return member; + } } - 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")) 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..ced75dde5f 100644 --- a/packages/compiler/test/checker/validate.test.ts +++ b/packages/compiler/test/checker/validate.test.ts @@ -1,5 +1,5 @@ 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", () => { @@ -271,7 +271,7 @@ describe.only("compiler: validate", () => { ]); }); - it("can resolve built-in functions", async () => { + it.only("can resolve built-in functions", async () => { testHost.addTypeSpecFile( "main.tsp", ` @@ -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"); }); From 30363be9c6c4c4d8876df3bdc436b14f69133169 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 21 Aug 2023 16:26:26 -0700 Subject: [PATCH 2/4] Fix tests --- packages/compiler/src/core/checker.ts | 57 ++++--------------- .../src/core/helpers/operation-utils.ts | 5 ++ .../src/core/helpers/usage-resolver.ts | 5 ++ .../compiler/test/checker/validate.test.ts | 4 +- .../compiler/test/server/completion.test.ts | 18 ++++-- 5 files changed, 36 insertions(+), 53 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 89a4bc77d1..3e79db7c7d 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -303,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", []], @@ -961,7 +959,6 @@ export function createChecker(program: Program): Checker { } if (tooFew) { - throw new Error("TOO FEW"); reportCheckerDiagnostic( createDiagnostic({ code: "invalid-template-args", @@ -1957,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); } } @@ -2307,6 +2304,13 @@ export function createChecker(program: Program): Checker { ? base.type! : checkTypeReferenceSymbol(base, node, mapper); + 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) { @@ -2325,17 +2329,13 @@ export function createChecker(program: Program): Checker { ) as Interface; lateBindMemberContainer(ifaceType); lateBindMembers(ifaceType, ifaceType.symbol!); - const table = getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); - const member = table.get(node.id.sv); - return member; + 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!); - const table = getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); - const member = table.get(node.id.sv); - return member; + return getOrCreateAugmentedSymbolTable(ifaceType.symbol!.members!); } } @@ -5928,41 +5928,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/test/checker/validate.test.ts b/packages/compiler/test/checker/validate.test.ts index ced75dde5f..a3659cfe10 100644 --- a/packages/compiler/test/checker/validate.test.ts +++ b/packages/compiler/test/checker/validate.test.ts @@ -2,7 +2,7 @@ import { notStrictEqual, strictEqual } from "assert"; 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 () => { @@ -271,7 +271,7 @@ describe.only("compiler: validate", () => { ]); }); - it.only("can resolve built-in functions", async () => { + it("can resolve built-in functions", async () => { testHost.addTypeSpecFile( "main.tsp", ` 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 { From 7041a4c509784e6c0a2e38f3a66469f8ac21db35 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 21 Aug 2023 17:03:36 -0700 Subject: [PATCH 3/4] Check function arguments --- packages/compiler/src/core/checker.ts | 69 +++++++++++++++++-- packages/compiler/src/core/messages.ts | 13 ++++ .../compiler/test/checker/validate.test.ts | 67 ++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3e79db7c7d..2f1469b3c7 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3514,12 +3514,73 @@ 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; + } + + if (target.type.parameters.properties.size < node.arguments.length) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-function-args", + messageId: "tooFew", + format: { + actual: String(node.arguments.length), + expected: String(target.type.parameters.properties.size), + }, + target: node, + }) + ); + return; + } else if (target.type.parameters.properties.size > node.arguments.length) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-function-args", + messageId: "tooMany", + format: { + actual: String(node.arguments.length), + expected: String(target.type.parameters.properties.size), + }, + target: node, + }) + ); + return; + } + const args = []; - for (const arg of node.arguments) { - const argResult = checkLogicExpression(arg, mapper); - if (!argResult) return; - args.push(argResult); + const expectedArgTypes = [...target.type.parameters.properties.values()]; + 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", diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index afc9687aee..7281e5dfb4 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"} arguments but got ${"actual"}.`, + tooMany: paramMessage`Too many arguments. Expected at most ${"expected"} arguments but got ${"actual"}.`, + incorrect: paramMessage`Argument '${"name"}' has incorrect type. Expected ${"expected"} but got ${"actual"}.`, + }, + }, /** * Configuration */ diff --git a/packages/compiler/test/checker/validate.test.ts b/packages/compiler/test/checker/validate.test.ts index a3659cfe10..6c73363ba2 100644 --- a/packages/compiler/test/checker/validate.test.ts +++ b/packages/compiler/test/checker/validate.test.ts @@ -400,6 +400,73 @@ describe("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: string[]; + + validate check: prop::contains(); + validate check2: prop::contains("hi", "hi"); + } + ` + ); + + const diagnostics = await testHost.diagnose("main.tsp"); + expectDiagnostics(diagnostics, [ + { + code: "invalid-function-args", + message: /Too many arguments. Expected at most 1 arguments but got 0./, + }, + { + code: "invalid-function-args", + message: /Too few arguments. Expected at least 1 arguments 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 () => { From 67311b67e9ce9b2d382e1de6a434c246a39d6415 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Mon, 21 Aug 2023 17:39:09 -0700 Subject: [PATCH 4/4] Fix checking function arguments --- packages/compiler/src/core/checker.ts | 16 +++++++++++----- packages/compiler/src/core/messages.ts | 4 ++-- packages/compiler/test/checker/validate.test.ts | 15 +++++++++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 2f1469b3c7..40313eda94 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3528,28 +3528,32 @@ export function createChecker(program: Program): Checker { ); return; } + const expectedArgTypes = [...target.type.parameters.properties.values()]; + + const minArgs = expectedArgTypes.filter((x) => !x.optional).length; + const maxArgs = expectedArgTypes.length; - if (target.type.parameters.properties.size < node.arguments.length) { + if (node.arguments.length < minArgs) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-function-args", messageId: "tooFew", format: { actual: String(node.arguments.length), - expected: String(target.type.parameters.properties.size), + expected: String(minArgs), }, target: node, }) ); return; - } else if (target.type.parameters.properties.size > node.arguments.length) { + } else if (node.arguments.length > maxArgs) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-function-args", messageId: "tooMany", format: { actual: String(node.arguments.length), - expected: String(target.type.parameters.properties.size), + expected: String(maxArgs), }, target: node, }) @@ -3558,7 +3562,6 @@ export function createChecker(program: Program): Checker { } const args = []; - const expectedArgTypes = [...target.type.parameters.properties.values()]; for (let i = 0; i < node.arguments.length; i++) { const expectedType = expectedArgTypes[i]; const argType = checkLogicExpression(node.arguments[i], mapper); @@ -5673,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)]]; diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 7281e5dfb4..0ecc86334a 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -502,8 +502,8 @@ const diagnostics = { severity: "error", messages: { default: paramMessage`Invalid arguments for function "${"name"}".`, - tooFew: paramMessage`Too few arguments. Expected at least ${"expected"} arguments but got ${"actual"}.`, - tooMany: paramMessage`Too many arguments. Expected at most ${"expected"} arguments but got ${"actual"}.`, + 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"}.`, }, }, diff --git a/packages/compiler/test/checker/validate.test.ts b/packages/compiler/test/checker/validate.test.ts index 6c73363ba2..38bbca0b90 100644 --- a/packages/compiler/test/checker/validate.test.ts +++ b/packages/compiler/test/checker/validate.test.ts @@ -427,10 +427,13 @@ describe("compiler: validate", () => { "main.tsp", ` @test model M { - prop: string[]; + prop: numeric[]; validate check: prop::contains(); - validate check2: prop::contains("hi", "hi"); + 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 } ` ); @@ -439,11 +442,15 @@ describe("compiler: validate", () => { expectDiagnostics(diagnostics, [ { code: "invalid-function-args", - message: /Too many arguments. Expected at most 1 arguments but got 0./, + 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 few arguments. Expected at least 1 arguments but got 2./, + message: /Too many arguments. Expected at most 1 argument\(s\) but got 2./, }, ]); });